· 6 years ago · Mar 29, 2020, 05:14 PM
1<?php
2/**
3 * @brief Initiates Invision Community constants, autoloader and exception handler
4 * @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
5 * @copyright (c) Invision Power Services, Inc.
6 * @license https://www.invisioncommunity.com/legal/standards/
7 * @package Invision Community
8 * @since 18 Feb 2013
9 */
10
11namespace IPS;
12
13/**
14 * Class to contain Invision Community autoloader and exception handler
15 */
16class IPS
17{
18 /**
19 * @brief Classes that have hooks on
20 */
21 public static $hooks = array();
22
23 /**
24 * @brief Unique key for this suite (used in http requests to defy browser caching)
25 */
26 public static $suiteUniqueKey = NULL;
27
28 /**
29 * @brief Developer Code to be added to all namespaces
30 */
31 private static $inDevCode = '';
32
33 /**
34 * @brief Namespaces developer code has been imported to
35 */
36 private static $inDevCodeImportedTo = array();
37
38 /**
39 * @brief Vendors to use PSR-0 autoloader for
40 */
41 public static $PSR0Namespaces = array();
42
43 /**
44 * @brief Community in the Cloud configuration
45 */
46 public static $cicConfig = array();
47
48 /**
49 * Get default constants
50 *
51 * @return array
52 */
53 public static function defaultConstants()
54 {
55 $storeMethod = 'FileSystem';
56 $storeConfig = '{"path":"{root}/datastore"}';
57 $cacheMethod = 'None';
58 $cacheConfig = '{}';
59 $redisConfig = NULL;
60 $redisEnabled = FALSE;
61 $outputCache = 'Database';
62 $outputCacheConfig = NULL;
63
64 if ( isset( self::$cicConfig['remote-cache'] ) and self::$cicConfig['remote-cache']['mode'] == 'on' )
65 {
66 require( __DIR__. '/conf_global.php' );
67
68 $outputCacheConfig = json_encode( array(
69 'sql_host' => self::$cicConfig['remote-cache']['endpoint'],
70 'sql_database' => $INFO['sql_database'],
71 'sql_user' => $INFO['sql_user'],
72 'sql_pass' => $INFO['sql_pass'],
73 'sql_port' => ( ! empty( $INFO['sql_port'] ) ) ? $INFO['sql_port'] : NULL,
74 'sql_socket' => ( ! empty( $INFO['sql_socket'] ) ) ? $INFO['sql_socket'] : NULL
75 ) );
76
77 $outputCache = 'RemoteDatabase';
78 }
79
80 if ( isset( self::$cicConfig['redis'] ) and self::$cicConfig['redis']['mode'] !== 'off' )
81 {
82 $storeMethod = 'Redis';
83 $cacheMethod = 'Redis';
84 $redisEnabled = TRUE;
85
86 if ( isset( self::$cicConfig['redis']['replicas'] ) and \count( self::$cicConfig['redis']['replicas'] ) and self::$cicConfig['redis']['mode'] == 'replica' )
87 {
88 /* RW separation baby */
89 $redisConfig = array(
90 'write' => array(
91 'server' => self::$cicConfig['redis']['primary'],
92 'port' => 6379
93 ),
94 'read' => array()
95 );
96
97 foreach( self::$cicConfig['redis']['replicas'] as $replica )
98 {
99 $redisConfig['read'][] = array(
100 'server' => $replica,
101 'port' => 6379
102 );
103 }
104 }
105 else
106 {
107 /* Single mode */
108 $redisConfig = array(
109 'server' => self::$cicConfig['redis']['primary'],
110 'port' => 6379
111 );
112 }
113
114 $redisConfig = json_encode( $redisConfig );
115 }
116 else if ( isset( $_SERVER['IPS_CIC'] ) )
117 {
118 if ( isset( $_SERVER['IPS_CLOUD_REDIS425'] ) )
119 {
120 $storeMethod = 'Redis';
121 $storeConfig = json_encode( array(
122 'server' => $_SERVER['IPS_CLOUD_REDIS425'],
123 'port' => 6379
124 ) );
125 $cacheMethod = 'Redis';
126 $cacheConfig = json_encode( array(
127 'server' => $_SERVER['IPS_CLOUD_REDIS425'],
128 'port' => 6379
129 ) );
130 }
131 else
132 {
133 $storeMethod = 'Database';
134 $storeConfig = '{}';
135 $cacheMethod = 'None';
136 $cacheConfig = '{}';
137 }
138 }
139 return array(
140 'CIC' => isset( $_SERVER['IPS_CIC'] ),
141 'CP_DIRECTORY' => 'admin',
142 'IN_DEV' => FALSE,
143 'IN_DEV_STRICT_MODE' => TRUE, // Works in addition to IN_DEV to add more stringent checks such as root namespace functions, method names, non MB str functions and more
144 'DEV_USE_WHOOPS' => TRUE,
145 'DEV_USE_FURL_CACHE' => FALSE,
146 'DEV_USE_MENU_CACHE' => FALSE,
147 'DEV_HIDE_DEV_TOOLS' => FALSE, // Makes IN_DEV not look like IN_DEV for the sake of doing screenshots
148 'DEBUG_JS' => FALSE,
149 'DEV_DEBUG_JS' => TRUE,
150 'DEV_DEBUG_CSS' => FALSE,
151 'DEBUG_TEMPLATES' => FALSE,
152 'IPS_FOLDER_PERMISSION' => 0777,
153 'FOLDER_PERMISSION_NO_WRITE' => 0755,
154 'IPS_FILE_PERMISSION' => 0666,
155 'FILE_PERMISSION_NO_WRITE' => 0644,
156 'ROOT_PATH' => __DIR__,
157 'NO_WRITES' => ( isset( $_SERVER['IPS_CIC'] ) and !isset( $_SERVER['IPS_CIC_ACP'] ) ),
158 'DEBUG_LOG' => FALSE,
159 'LOG_FALLBACK_DIR' => '{root}/uploads/logs',
160 'STORE_METHOD' => $storeMethod,
161 'STORE_CONFIG' => $storeConfig,
162 'CACHE_METHOD' => $cacheMethod,
163 'CACHE_CONFIG' => $cacheConfig,
164 'CACHE_PAGE_TIMEOUT' => ( isset( $_SERVER['IPS_CIC'] ) ? 300 : 30 ),
165 'TEST_CACHING' => FALSE,
166 'EMAIL_DEBUG_PATH' => NULL,
167 'BULK_MAILS_PER_CYCLE' => 50,
168 'JAVA_PATH' => "",
169 'ERROR_PAGE' => 'error.php',
170 'UPGRADING_PAGE' => ( \defined( 'CP_DIRECTORY' ) ) ? CP_DIRECTORY . '/upgrade/upgrading.html' : 'admin/upgrade/upgrading.html', # defined call here just as a sanity check
171 'QUERY_LOG' => FALSE,
172 'CACHING_LOG' => FALSE,
173 'ENFORCE_ACCESS' => FALSE,
174 'THUMBNAIL_SIZE' => '500x500',
175 'PHOTO_THUMBNAIL_SIZE' => 240, // The max we display is 120x120, so this allows for double size for high dpi screens
176 'COOKIE_DOMAIN' => NULL,
177 'COOKIE_PREFIX' => 'ips4_',
178 'COOKIE_PATH' => NULL,
179 'COOKIE_BYPASS_SSLONLY' => FALSE,
180 'CONNECT_NOSYNC_NAMES' => FALSE,
181 'BYPASS_CURL' => FALSE,
182 'FORCE_CURL' => FALSE,
183 'NEXUS_TEST_GATEWAYS' => FALSE,
184 'NEXUS_LKEY_API_DISABLE' => TRUE,
185 'NEXUS_LKEY_API_CHECK_IP' => TRUE,
186 'NEXUS_LKEY_API_ALLOW_IP_OVERRIDE' => FALSE,
187 'UPGRADE_MANUAL_THRESHOLD' => 250000,
188 'UPGRADE_LARGE_TABLE_SIZE' => 100000000, // Roughly 100M...the value we check against is an estimate anyways
189 'SUITE_UNIQUE_KEY' => ( isset( $_SERVER['IPS_CIC'] ) and preg_match( '/^\/var\/www\/html\/(.+?)$/i', __DIR__, $matches ) ) ? str_replace( '/', '', $matches[1] ) : mb_substr( md5( '13_wf' . '$Rev: 3023$'), 10, 10 ),
190 'CACHEBUST_KEY' => mb_substr( md5( '13_wf' . '$Rev: 3023$'), 10, 10 ), // This looks unnecessary but SUITE_UNIQUE_KEY can be set to a constant constant in constants.php whereas we need a version specific constant for cache busting.
191 'TEXT_ENCRYPTION_KEY' => NULL,
192 'CONNECT_MASTER_KEY' => NULL,
193 'USE_DEVELOPMENT_BUILDS' => FALSE,
194 'DEV_WHOOPS_EDITOR' => NULL,
195 'DEFAULT_REQUEST_TIMEOUT' => 10, // In seconds - default for most external connections
196 'LONG_REQUEST_TIMEOUT' => 30, // In seconds - used for specific API-based calls where we expect a slightly longer response time
197 'TEMP_DIRECTORY' => sys_get_temp_dir(),
198 'TEST_DELTA_ZIP' => '',
199 'TEST_DELTA_TEMPLATE_CHANGES' => '',
200 'DELTA_FORCE_FTP' => FALSE,
201 'BYPASS_ACP_IP_CHECK' => FALSE,
202 'RECOVERY_MODE' => FALSE,
203 'DEV_FORCE_MFA' => FALSE,
204 'DISABLE_MFA' => FALSE,
205 'BOT_SEARCH_FLOOD_SECONDS' => 30,
206 'READ_WRITE_SEPARATION' => TRUE,
207 'USE_MYSQL_SEARCH_BASIC_MODE_THRESHOLD' => 0,
208 'UPGRADE_MD5_CHECK' => TRUE,
209 'BYPASS_UPGRADER_LOGIN' => FALSE,
210 'OAUTH_REQUIRES_HTTPS' => TRUE,
211 'REDIS_LOG' => FALSE,
212 'REDIS_ENABLED' => $redisEnabled,
213 'REDIS_ENCRYPT' => TRUE,
214 'REDIS_CONFIG' => $redisConfig,
215 'NOTIFICATIONS_PER_BATCH' => 30,
216 'SHOW_ACP_LINK' => TRUE,
217 'REPORT_EXCEPTIONS' => FALSE,
218 'DEFAULT_THEME_ID' => 1,
219 'REBUILD_SLOW' => 50, // Number of items to be rebuilt per-cycle for routines that take a while
220 'REBUILD_NORMAL' => 250, // Number of items to be rebuilt per-cycle for most routines
221 'REBUILD_QUICK' => 500, // Number of items to be rebuilt per-cycle for routines that are fast
222 'CONVERTERS_DEV_UI' => FALSE, // Use the old converters UI (primarily for development/testing purposes)
223 'OUTPUT_CACHE_METHOD' => $outputCache, // Allows either Redis (when enabled) or Database. Allows Redis to be used for caching, but guest page caching used for something else
224 'DEMO_MODE' => FALSE, // Is this a demo install?
225 'OUTPUT_CACHE_METHOD_CONFIG' => $outputCacheConfig
226 );
227 }
228
229 /**
230 * Initiate Invision Community constants, autoloader and exception handler
231 *
232 * @return void
233 */
234 public static function init()
235 {
236 /* Set timezone */
237 date_default_timezone_set( 'UTC' );
238
239 /* Set default MB internal encoding */
240 mb_internal_encoding('UTF-8');
241
242 /* Define the IN_IPB constant - this needs to be in the global namespace for backwards compatibility */
243 \define( 'IN_IPB', TRUE );
244
245 /* Load constants.php */
246 if( file_exists( __DIR__ . '/constants.php' ) )
247 {
248 @include_once( __DIR__ . '/constants.php' );
249 }
250
251 /* Do we have a CiC config */
252 self::unpackCicConfig();
253
254 /* Import and set defaults */
255 $defaultConstants = static::defaultConstants();
256
257 foreach ( $defaultConstants as $k => $v )
258 {
259 if( \defined( $k ) )
260 {
261 \define( 'IPS\\' . $k, \constant( $k ) );
262 }
263 else
264 {
265 \define( 'IPS\\' . $k, $v );
266 }
267 }
268
269 /* If they have customized the ACP directory but it doesn't exist, throw an error */
270 if( !is_dir( ROOT_PATH . '/' . CP_DIRECTORY ) AND CP_DIRECTORY != $defaultConstants['CP_DIRECTORY'] )
271 {
272 die( "You have defined a custom ACP directory (CP_DIRECTORY) in constants.php, however it is not valid. Please remove or correct this constant definition." );
273 }
274
275 /* Load developer code */
276 if( IN_DEV and IN_DEV_STRICT_MODE and file_exists( ROOT_PATH . '/dev/function_overrides.php' ) )
277 {
278 self::$inDevCode = file_get_contents( ROOT_PATH . '/dev/function_overrides.php' );
279 }
280
281 /* Set autoloader */
282 spl_autoload_register( '\IPS\IPS::autoloader', true, true );
283
284 /* Set error handlers */
285 if ( \IPS\IN_DEV AND \IPS\DEV_USE_WHOOPS and file_exists( ROOT_PATH . '/dev/Whoops/Run.php' ) )
286 {
287 self::$PSR0Namespaces['Whoops'] = ROOT_PATH . '/dev/Whoops';
288 $whoops = new \Whoops\Run;
289 $handler = new \Whoops\Handler\PrettyPageHandler;
290 if ( \IPS\DEV_WHOOPS_EDITOR )
291 {
292 $handler->setEditor( \IPS\DEV_WHOOPS_EDITOR );
293 }
294 $whoops->pushHandler( $handler );
295 $whoops->register();
296 }
297 else
298 {
299 set_error_handler( '\IPS\IPS::errorHandler' );
300 set_exception_handler( '\IPS\IPS::exceptionHandler' );
301 }
302
303 /* Init hooks */
304 if ( file_exists( \IPS\ROOT_PATH . '/plugins/hooks.php' ) )
305 {
306 if ( !( self::$hooks = require( \IPS\ROOT_PATH . '/plugins/hooks.php' ) ) )
307 {
308 self::$hooks = array();
309 }
310 }
311 }
312
313 /**
314 * Unpack the special IPS_CLOUD_CONFIG environment variable
315 *
316 * @return void
317 */
318 public static function unpackCicConfig()
319 {
320 if ( isset( $_SERVER['IPS_CLOUD_CONF'] ) )
321 {
322 $config = json_decode( base64_decode( $_SERVER['IPS_CLOUD_CONF'] ), TRUE );
323
324 if ( \is_array( $config ) and \count( $config ) )
325 {
326 self::$cicConfig = $config;
327 }
328 }
329 }
330
331 /**
332 * Autoloader
333 *
334 * @param string $classname Class to load
335 * @return void
336 */
337 public static function autoloader( $classname )
338 {
339 /* Separate by namespace */
340 $bits = explode( '\\', ltrim( $classname, '\\' ) );
341
342 /* If this doesn't belong to us, try a PSR-0 loader or ignore it */
343 $vendorName = array_shift( $bits );
344 if( $vendorName !== 'IPS' )
345 {
346 if ( isset( self::$PSR0Namespaces[ $vendorName ] ) )
347 {
348 @include_once( self::$PSR0Namespaces[ $vendorName ] . DIRECTORY_SEPARATOR . implode( DIRECTORY_SEPARATOR, $bits ) . '.php' );
349 }
350
351 return;
352 }
353
354 /* Work out what namespace we're in */
355 $class = array_pop( $bits );
356 $namespace = empty( $bits ) ? 'IPS' : ( 'IPS\\' . implode( '\\', $bits ) );
357 $inDevCode = '';
358
359 /* We only need to load the file if we don't have the underscore-prefixed one */
360 if( !class_exists( "{$namespace}\\_{$class}", FALSE ) )
361 {
362 /* Locate file */
363 $path = ROOT_PATH . '/';
364 $sourcesDirSet = FALSE;
365 foreach ( array_merge( $bits, array( $class ) ) as $i => $bit )
366 {
367 if( preg_match( "/^[a-z0-9]/", $bit ) )
368 {
369 if( $i === 0 )
370 {
371 $path .= 'applications/';
372 }
373 else
374 {
375 $sourcesDirSet = TRUE;
376 }
377 }
378 elseif ( $i === 3 and $bit === 'Upgrade' )
379 {
380 $bit = mb_strtolower( $bit );
381 }
382 elseif( $sourcesDirSet === FALSE )
383 {
384 if( $i === 0 )
385 {
386 $path .= 'system/';
387 }
388 elseif ( $i === 1 and $bit === 'Application' )
389 {
390 // do nothing
391 }
392 else
393 {
394 $path .= 'sources/';
395 }
396 $sourcesDirSet = TRUE;
397 }
398
399 $path .= "{$bit}/";
400 }
401
402 /* Load it */
403 $path = \substr( $path, 0, -1 ) . '.php';
404 if( !file_exists( $path ) )
405 {
406 $path = \substr( $path, 0, -4 ) . \substr( $path, \strrpos( $path, '/' ) );
407 if ( !file_exists( $path ) )
408 {
409 return FALSE;
410 }
411 }
412 require_once( $path );
413
414 /* Is it an interface? */
415 if ( interface_exists( "{$namespace}\\{$class}", FALSE ) )
416 {
417 return;
418 }
419
420 /* Is it a trait? The function_exists call is just so PHP 5.3 doesn't throw a fatal error (even though it's required, we want it to fail gracefully) */
421 if ( \function_exists('trait_exists') and trait_exists( "{$namespace}\\{$class}", FALSE ) )
422 {
423 return;
424 }
425
426 /* Doesn't exist? */
427 if( !class_exists( "{$namespace}\\_{$class}", FALSE ) )
428 {
429 trigger_error( "Class {$classname} could not be loaded. Ensure it has been properly prefixed with an underscore and is in the correct namespace.", E_USER_ERROR );
430 }
431
432 /* Stuff for developer mode */
433 if( IN_DEV and IN_DEV_STRICT_MODE )
434 {
435 $reflection = new \ReflectionClass( "{$namespace}\\_{$class}" );
436
437 /* Import our code to override forbidden functions */
438 if( !\in_array( \strtolower( $namespace ), self::$inDevCodeImportedTo ) )
439 {
440 $inDevCode = self::$inDevCode;
441 self::$inDevCodeImportedTo[] = \strtolower( $namespace );
442 }
443
444 /* Any classes which extend a core PHP class are exempt from our rules */
445 $extendsCorePhpClass = FALSE;
446 for ( $workingClass = $reflection; $parent = $workingClass->getParentClass(); $workingClass = $parent )
447 {
448 if ( \substr( $parent->getNamespaceName(), 0, 3 ) !== 'IPS' )
449 {
450 $extendsCorePhpClass = TRUE;
451 break;
452 }
453 }
454 if ( !$extendsCorePhpClass )
455 {
456 /* Make sure it's name follows our standards */
457 if( !preg_match( '/^_[A-Z0-9]+$/i', $reflection->getShortName() ) )
458 {
459 trigger_error( "{$classname} does not follow our naming conventions. Please rename using only alphabetic characters and PascalCase. (PHP Coding Standards: Classes.5)", E_USER_ERROR );
460 }
461
462 /* Loop methods */
463 $hasNonAbstract = FALSE;
464 $hasNonStatic = FALSE;
465 foreach ( $reflection->getMethods() as $method )
466 {
467 if ( \substr( $method->getDeclaringClass()->getName(), 0, 3 ) === 'IPS' )
468 {
469 /* Make sure it's not private */
470 if( $method->isPrivate() )
471 {
472 trigger_error( "{$classname}::{$method->name} is declared as private. In order to ensure that hooks are able to work freely, please use protected instead. (PHP Coding Standards: Functions and Methods.4)", E_USER_ERROR );
473 }
474
475 /* We need to know for later if we have non-abstract methods */
476 if( !$method->isAbstract() )
477 {
478 $hasNonAbstract = TRUE;
479 }
480
481 /* We need to know for later if we have non-static methods */
482 if( !$method->isStatic() )
483 {
484 $hasNonStatic = TRUE;
485 }
486
487 /* Make sure the name follows our conventions */
488 if(
489 !preg_match( '/^_?[a-z][A-Za-z0-9]*$/', $method->name ) // Normal pattern most methods should match
490 and
491 !preg_match( '/^get_/i', $method->name ) // get_* is allowed
492 and
493 !preg_match( '/^set_/i', $method->name ) // set_* is allowed
494 and
495 !preg_match( '/^parse_/i', $method->name ) // parse_* is allowed
496 and
497 !preg_match( '/^setBitwise_/i', $method->name ) // set_Bitiwse_* is allowed
498 and
499 !preg_match( '/^(GET|POST|PUT|DELETE)[a-zA-Z_]+$/', $method->name ) // API methods have a specific naming format
500 and
501 !\in_array( $method->name, array( // PHP's magic methods are allowed (except __sleep and __wakeup as we don't allow serializing)
502 '__construct',
503 '__destruct',
504 '__call',
505 '__callStatic',
506 '__get',
507 '__set',
508 '__isset',
509 '__unset',
510 '__toString',
511 '__invoke',
512 '__set_state',
513 '__clone',
514 '__debugInfo',
515 ) )
516 ) {
517 trigger_error( "{$classname}::{$method->name} does not follow our naming conventions. Please rename using only alphabetic characters and camelCase. (PHP Coding Standards: Functions and Methods.1-3)", E_USER_ERROR );
518 }
519 }
520 }
521
522 /* Loop properties */
523 foreach ( $reflection->getProperties() as $property )
524 {
525 $hasNonAbstract = TRUE;
526
527 /* Make sure it's not private */
528 if( $property->isPrivate() )
529 {
530 trigger_error( "{$classname}::\${$property->name} is declared as private. In order to ensure that hooks are able to work freely, please use protected instead. (PHP Coding Standards: Properties and Variables.3)", E_USER_ERROR );
531 }
532
533 /* Make sure the name follows our conventions */
534 if( !preg_match( '/^_?[a-z][A-Za-z]*$/', $property->name ) )
535 {
536 trigger_error( "{$classname}::\${$property->name} does not follow our naming conventions. Please rename using only alphabetic characters and camelCase. (PHP Coding Standards: Properties and Variables.1-2)", E_USER_ERROR );
537 }
538 }
539
540 /* Check an interface wouldn't be more appropriate */
541 if( !$hasNonAbstract )
542 {
543 trigger_error( "You do not have any non-abstract methods in {$classname}. Please use an interface instead. (PHP Coding Standards: Classes.7)", E_USER_ERROR );
544 }
545
546 /* Check we have at least one non-static method (unless this class is abstract or has a parent) */
547 elseif( !$reflection->isAbstract() and $reflection->getParentClass() === FALSE and !$hasNonStatic and $reflection->getNamespacename() !== 'IPS\Output\Plugin' and !\in_array( 'extensions', $bits ) and !\in_array( 'templateplugins', $bits ) )
548 {
549 trigger_error( "You do not have any methods in {$classname} which are not static. Please refactor. (PHP Coding Standards: Functions and Methods.6)", E_USER_ERROR );
550 }
551 }
552 }
553 }
554
555 /* Monkey Patch */
556 self::monkeyPatch( $namespace, $class, $inDevCode );
557 }
558
559 /**
560 * Monkey Patch
561 *
562 * @param string $namespace The namespace
563 * @param string $finalClass The final class name we want to be able to use (without namespace)
564 * @param string $extraCode Any additonal code to import before the class is defined
565 * @return null
566 */
567 public static function monkeyPatch( $namespace, $finalClass, $extraCode = '' )
568 {
569 $realClass = "_{$finalClass}";
570 if( isset( self::$hooks[ "\\{$namespace}\\{$finalClass}" ] ) AND \IPS\RECOVERY_MODE === FALSE )
571 {
572 foreach ( self::$hooks[ "\\{$namespace}\\{$finalClass}" ] as $id => $data )
573 {
574 if ( file_exists( ROOT_PATH . '/' . $data['file'] ) )
575 {
576 $contents = "namespace {$namespace}; ". str_replace( '_HOOK_CLASS_', $realClass, file_get_contents( ROOT_PATH . '/' . $data['file'] ) );
577 try
578 {
579 if( @eval( $contents ) !== FALSE )
580 {
581 $realClass = $data['class'];
582 }
583 }
584 catch ( \ParseError $e )
585 {
586 /* Show the error if we have development mode enabled */
587 if( \IPS\IN_DEV )
588 {
589 throw $e;
590 }
591 }
592 }
593 }
594 }
595
596 $reflection = new \ReflectionClass( "{$namespace}\\_{$finalClass}" );
597 if( eval( "namespace {$namespace}; ". $extraCode . ( $reflection->isAbstract() ? 'abstract' : '' )." class {$finalClass} extends {$realClass} {}" ) === FALSE )
598 {
599 trigger_error( "There was an error initiating the class {$namespace}\\{$finalClass}.", E_USER_ERROR );
600 }
601 }
602
603 /**
604 * Error Handler
605 *
606 * @param int $errno Error number
607 * @param string $errstr Error message
608 * @param string $errfile File
609 * @param int $errline Line
610 * @param array $trace Backtrace
611 * @return void
612 */
613 public static function errorHandler( $errno, $errstr, $errfile, $errline, $trace=NULL )
614 {
615 /* We don't care about these in production */
616 if ( \in_array( $errno, array( E_WARNING, E_NOTICE, E_STRICT, E_DEPRECATED ) ) )
617 {
618 return;
619 }
620
621 /* This means the error suppressor was used, so we should ignore any non-fatal errors */
622 if ( error_reporting() === 0 )
623 {
624 return false;
625 }
626
627 throw new \ErrorException( $errstr, $errno, 0, $errfile, $errline );
628 }
629
630 /**
631 * Exception Handler
632 *
633 * @param \Throwable $exception Exception class
634 * @return void
635 */
636 public static function exceptionHandler( $exception )
637 {
638 /* Should we show the exception message? */
639 $showMessage = ( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation == 'admin' );
640 if ( method_exists( $exception, 'isServerError' ) and $exception->isServerError() )
641 {
642 $showMessage = TRUE;
643 }
644
645 /* Work out what we'll log - exception classes can provide extra data */
646 $log = '';
647 if ( method_exists( $exception, 'extraLogData' ) )
648 {
649 $log .= $exception->extraLogData() . "\n";
650 }
651 $log .= \get_class( $exception ) . ": " . $exception->getMessage() . " (" . $exception->getCode() . ")\n" . $exception->getTraceAsString();
652
653 /* Log it (unless it's a MySQL server error) */
654 if( ! ( $exception instanceof \IPS\Db\Exception and $exception->isServerError() ) )
655 {
656 \IPS\Log::log( $log, 'uncaught_exception' );
657 }
658
659 /* Report it */
660 try
661 {
662 if (
663 !\IPS\IN_DEV
664 and \IPS\REPORT_EXCEPTIONS === TRUE
665 and \IPS\Settings::i()->diagnostics_reporting
666 and !\IPS\Settings::i()->theme_designers_mode
667 and !self::exceptionWasThrownByThirdParty( $exception )
668 and ( !method_exists( $exception, 'isThirdPartyError' ) or !$exception->isThirdPartyError() )
669 and ( !method_exists( $exception, 'isServerError' ) or !$exception->isServerError() )
670 and !( $exception instanceof \ParseError )
671 )
672 {
673 self::reportExceptionToIPS( $exception );
674 }
675 }
676 catch ( \Exception $e ) { }
677
678 /* Try to display a friendly error page */
679 try
680 {
681 /* If we couldn't connect to the database, don't bother trying to show the friendly page because nope */
682 if( $exception instanceof \IPS\Db\Exception AND $exception->getCode() === 0 )
683 {
684 throw new \RuntimeException;
685 }
686
687 /* If we're in the installer/upgrader, show the raw message */
688 $message = 'generic_error';
689 if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation == 'setup' )
690 {
691 $message = $exception->getMessage();
692 }
693
694 $faultyAppOrHookId = static::exceptionWasThrownByThirdParty( $exception );
695
696 /* Output */
697 \IPS\Output::i()->error( $message, "EX{$exception->getCode()}", 500, NULL, array(), $log, $faultyAppOrHookId );
698 }
699 /* And if *that* fails, show our generic page */
700 catch ( \Exception $e )
701 {
702 static::genericExceptionPage( $showMessage ? $exception->getMessage() : NULL );
703 }
704 catch ( \Throwable $e )
705 {
706 static::genericExceptionPage( $showMessage ? $exception->getMessage() : NULL );
707 }
708
709 exit;
710 }
711
712 /**
713 * Should a given exception be reported to IPS? Filter out 3rd party etc.
714 *
715 * @param \Throwable $exception The exception
716 * @return void
717 */
718 final public static function reportExceptionToIPS( $exception )
719 {
720 $response = \IPS\Http\Url::external('https://invisionpowerdiagnostics.com')->request()->post( array(
721 'version' => \IPS\Application::getAvailableVersion('core'),
722 'class' => \get_class( $exception ),
723 'message' => $exception->getMessage(),
724 'code' => $exception->getCode(),
725 'file' => str_replace( \IPS\ROOT_PATH, '', $exception->getFile() ),
726 'line' => $exception->getLine(),
727 'backtrace' => str_replace( \IPS\ROOT_PATH, '', $exception->getTraceAsString() )
728 ) );
729
730 if ( $response->httpResponseCode == 410 )
731 {
732 \IPS\Settings::i()->changeValues( array( 'diagnostics_reporting' => 0 ) );
733 }
734 }
735
736 /**
737 * Generic exception page
738 *
739 * @param string $message The error message
740 * @return void
741 * @note Abstracted so Theme can call this if templates are in the process of building
742 */
743 public static function genericExceptionPage( $message = NULL )
744 {
745 if( isset( $_SERVER['SERVER_PROTOCOL'] ) and \strstr( $_SERVER['SERVER_PROTOCOL'], '/1.0' ) !== false )
746 {
747 header( "HTTP/1.0 500 Internal Server Error" );
748 }
749 else
750 {
751 header( "HTTP/1.1 500 Internal Server Error" );
752 }
753
754 /* Don't allow error pages to be cached */
755 header( "Cache-Control: no-cache, no-store, must-revalidate" );
756 header( "Pragma: no-cache" );
757 header( "Expires: 0" );
758
759 require \IPS\ROOT_PATH . '/' . \IPS\ERROR_PAGE;
760 exit;
761 }
762
763 /**
764 * Small utility function to check if a class has a trait as PHP doesn't have an operator
765 * for this and the monkey patching means we can't use class_uses() directly
766 *
767 * @param string|object $class The class
768 * @param string $trait Trait name to look for
769 * @return bool
770 */
771 public static function classUsesTrait( $class, $trait )
772 {
773 do
774 {
775 if ( \in_array( $trait, class_uses( $class ) ) )
776 {
777 return TRUE;
778 }
779 }
780 while( $class = get_parent_class( $class ) );
781
782 return FALSE;
783 }
784
785 /**
786 * Get license key data
787 *
788 * @param bool $forceRefresh If TRUE, will get data from server
789 * @return array|NULL
790 */
791 public static function licenseKey( $forceRefresh = FALSE )
792 {
793 /* Do we even have a license key? */
794 if ( !\IPS\Settings::i()->ipb_reg_number )
795 {
796 try
797 {
798 \IPS\core\AdminNotification::send( 'core', 'License', 'missing', FALSE );
799 }
800 /* AdminCP Notifications table may not exist yet if upgrading */
801 catch( \IPS\Db\Exception $e ) {}
802 return NULL;
803 }
804
805 /* Get the cached value */
806 $response = NULL;
807 $cached = NULL;
808 $setFetched = FALSE;
809 if ( isset( \IPS\Data\Store::i()->license_data ) )
810 {
811 $cached = \IPS\Data\Store::i()->license_data;
812
813 /* If it's younger than 21 days, just use that */
814 if ( $cached['fetched'] > ( time() - 1814400 ) and !$forceRefresh )
815 {
816 /* If the license is not expired, return the data */
817 if( !$cached['data']['expires'] OR strtotime( $cached['data']['expires'] ) > time() )
818 {
819 $response = $cached['data'];
820 }
821 /* Otherwise if the license is expired but we've automatically refetched, return the data */
822 else if( $cached['data']['expires'] AND strtotime( $cached['data']['expires'] ) < time() AND isset( $cached['refetched'] ) )
823 {
824 $response = $cached['data'];
825 }
826 /* Otherwise remember to set the 'refetched' flag */
827 else
828 {
829 $setFetched = TRUE;
830 }
831 }
832 }
833
834 /* If we can't use the cache, call the actual server */
835 if ( $response === NULL )
836 {
837 try
838 {
839 /* Prevent a race condition and set the next check cycle to be 10 mins from the 21 day cut off in case this request fails */
840 \IPS\Data\Store::i()->license_data = array( 'fetched' => time() - 1813800, 'data' => NULL );
841
842 /* Actually call the server */
843 $response = \IPS\Http\Url::ips( 'license/' . trim( \IPS\Settings::i()->ipb_reg_number ) )->request()->get();
844 if ( $response->httpResponseCode == 404 )
845 {
846 \IPS\Data\Store::i()->license_data = array( 'fetched' => time() - 1728000, 'data' => NULL );
847 return $cached;
848 }
849 $response = $response->decodeJson();
850
851 /* Update the license info in the store */
852 $licenseData = array( 'fetched' => time(), 'data' => $response );
853 if( $setFetched )
854 {
855 $licenseData['refetched'] = 1;
856 }
857 \IPS\Data\Store::i()->license_data = $licenseData;
858 }
859 catch ( \Exception $e )
860 {
861 /* If we can't access the license server right now, store something in cache to prevent a request on every page load. We
862 set fetched to 20 days ago so that this cache is only good for 1 day instead of 21 days however. */
863 if( $cached === NULL )
864 {
865 \IPS\Data\Store::i()->license_data = array( 'fetched' => time() - 1728000, 'data' => NULL );
866 }
867 else
868 {
869 /* We wipe the data to prevent a race condition, but the license server failed so restore the data and set to try again in 1 day */
870 \IPS\Data\Store::i()->license_data = array( 'fetched' => time() - 1728000, 'data' => ( isset( $cached['data'] ) ? $cached['data'] : NULL ) );
871 }
872
873 /* If the server is offline right now, use the cached value from above */
874 $response = $cached;
875 }
876 }
877
878 /* Check it's good */
879 if ( $response['legacy'] )
880 {
881 try
882 {
883 \IPS\core\AdminNotification::send( 'core', 'License', 'missing', FALSE );
884 }
885 /* AdminCP Notifications table may not exist yet if upgrading */
886 catch( \IPS\Db\Exception $e ) {}
887 $response = NULL;
888 }
889 else
890 {
891 try
892 {
893 \IPS\core\AdminNotification::remove( 'core', 'License', 'missing' );
894
895 if ( !\IPS\CIC )
896 {
897 /* Check it hasn't expired */
898 if ( ( isset( $response['expires'] ) and strtotime( $response['expires'] ) < time() ) or !isset( $response['active'] ) or !$response['active'] )
899 {
900 \IPS\core\AdminNotification::send( 'core', 'License', 'expired', FALSE );
901 \IPS\core\AdminNotification::remove( 'core', 'License', 'expireSoon' );
902 }
903 else
904 {
905 \IPS\core\AdminNotification::remove( 'core', 'License', 'expired' );
906
907 /* Or there's 7 days or less to go */
908 $daysLeft = (int) (new \IPS\DateTime)->diff( \IPS\DateTime::ts( strtotime( $response['expires'] ) ) )->format('%r%a');
909 if( $daysLeft < 0 )
910 {
911 $daysLeft = 0;
912 }
913 if ( $daysLeft <= 7 )
914 {
915 \IPS\core\AdminNotification::send( 'core', 'License', 'expireSoon', FALSE );
916 }
917 else
918 {
919 \IPS\core\AdminNotification::remove( 'core', 'License', 'expireSoon' );
920 }
921 }
922 }
923
924 /* Check the URL is correct */
925 $doUrlCheck = TRUE;
926 $parsed = parse_url( \IPS\Settings::i()->base_url );
927 if ( \IPS\CIC or $parsed['host'] === 'localhost' or mb_substr( $parsed['host'], -4 ) === '.dev' or mb_substr( $parsed['host'], -5 ) === '.test' )
928 {
929 $doUrlCheck = FALSE;
930 }
931 if ( $doUrlCheck )
932 {
933 /* Normalize our URL's. Specifically ignore the www. subdomain. */
934 $validUrls = array();
935 $validUrls[] = rtrim( str_replace( array( 'http://', 'https://', 'www.' ), '', $response['url'] ), '/' );
936 $validUrls[] = rtrim( str_replace( array( 'http://', 'https://', 'www.' ), '', $response['test_url'] ), '/' );
937 $ourUrl = rtrim( str_replace( array( 'http://', 'https://', 'www.' ), '', \IPS\Settings::i()->base_url ), '/' );
938
939 if ( !\in_array( $ourUrl, $validUrls ) )
940 {
941 \IPS\core\AdminNotification::send( 'core', 'License', 'url', FALSE );
942 }
943 else
944 {
945 \IPS\core\AdminNotification::remove( 'core', 'License', 'url' );
946 }
947 }
948 else
949 {
950 \IPS\core\AdminNotification::remove( 'core', 'License', 'url' );
951 }
952 }
953 /* AdminCP Notifications table may not exist yet if upgrading */
954 catch( \IPS\Db\Exception $e ) {}
955 }
956
957 /* Return */
958 return $response;
959 }
960
961 /**
962 * Check license key
963 *
964 * @param string $val The license key
965 * @param string $url The site URL
966 * @return void
967 * @throws \DomainException
968 */
969 public static function checkLicenseKey( $val, $url )
970 {
971 $test = FALSE;
972 if ( mb_substr( $val, -12 ) === '-TESTINSTALL' )
973 {
974 $test = TRUE;
975 $val = mb_substr( $val, 0, -12 );
976 }
977 $urlKey = $test ? 'test_url' : 'url';
978
979 try
980 {
981 $response = \IPS\Http\Url::ips( 'license/' . $val )->setQueryString( $urlKey, $url )->request()->get();
982 switch ( $response->httpResponseCode )
983 {
984 case 200:
985 $response = json_decode( $response, TRUE );
986 if ( $response['legacy'] )
987 {
988 throw new \DomainException( 'license_key_legacy' );
989 }
990
991 if ( !$response[ $urlKey ] )
992 {
993 \IPS\Http\Url::ips( 'license/' . $val )->request()->post( array(
994 $urlKey => $url
995 ) );
996 }
997 elseif ( $response[ $urlKey ] != $url )
998 {
999 if ( rtrim( preg_replace( '/^https?:\/\//', '', $response[ $urlKey ] ), '/' ) == rtrim( preg_replace( '/^https?:\/\//', '', $url ), '/' ) ) // Allow changing if the difference is http/https or just a trailing slash
1000 {
1001 \IPS\Http\Url::ips( 'license/' . $val )->request()->post( array(
1002 $urlKey => $url
1003 ) );
1004 }
1005 else
1006 {
1007 throw new \DomainException( $test ? 'license_key_test_active' : 'license_key_active' );
1008 }
1009 }
1010 break;
1011
1012 case 404:
1013 throw new \DomainException( 'license_key_not_found' );
1014
1015 default:
1016 throw new \DomainException( 'license_generic_error' );
1017 }
1018 }
1019 catch ( \IPS\Http\Request\Exception $e )
1020 {
1021 throw new \DomainException( sprintf( \IPS\Member::loggedIn()->language()->get( 'license_server_error' ), $e->getMessage() ) );
1022 }
1023 }
1024
1025 /**
1026 * Was the exception thrown by a third party app/plugin?
1027 *
1028 * @param \Throwable $exception The exception
1029 * @return string|int|NULL string = application directory, int = hook id, null means that it was probably caused by an application
1030 */
1031 public static function exceptionWasThrownByThirdParty( $exception )
1032 {
1033 $trace = $exception->getTraceAsString();
1034
1035 /* Did it happen in a hook? */
1036 if ( preg_match( '/init\.php\(\d*\) : eval\(\)\'d code$/', $exception->getFile() ) )
1037 {
1038 /* Did it happen inside a plugin hook? */
1039 if ( preg_match( '/hook(\d+)/', $trace, $matches ) )
1040 {
1041 /* Return the hook id, the error method will fetch the plugin name */
1042 return $matches[1];
1043 }
1044 /* Did it happen inside an applications hook? */
1045 else if ( preg_match_all( '/([a-zA-Z]+)_hook/', $trace, $matches ) )
1046 {
1047 foreach ( $matches[1] as $appKey )
1048 {
1049 if ( !\in_array( $appKey, \IPS\Application::$ipsApps ) )
1050 {
1051 return $appKey;
1052 }
1053 }
1054 }
1055
1056 return NULL;
1057 }
1058 /* Exception was thrown by 'normal code', check if it's from a third-party app */
1059 else
1060 {
1061 foreach ( explode( "\n", $trace ) as $line )
1062 {
1063 if ( preg_match( '/' . preg_quote( DIRECTORY_SEPARATOR, '/' ) . 'applications' . preg_quote( DIRECTORY_SEPARATOR, '/' ) . '([a-zA-Z]+)/', str_replace( \IPS\ROOT_PATH, '', $line ), $matches ) )
1064 {
1065 if ( !\in_array( $matches[1], \IPS\Application::$ipsApps ) )
1066 {
1067 return $matches[1];
1068 }
1069 else
1070 {
1071 return NULL;
1072 }
1073 }
1074 }
1075 }
1076
1077 /* Still here? Probably system */
1078 return NULL;
1079 }
1080
1081 /**
1082 * Resync IPS Cloud File System
1083 * Must be called when writing any files to disk on IPS Community in the Cloud
1084 *
1085 * @param string $reason Reason
1086 * @return void
1087 */
1088 public static function resyncIPSCloud( $reason = NULL )
1089 {
1090 if ( \IPS\CIC )
1091 {
1092 if ( preg_match( '/^\/var\/www\/html\/(.+?)(?:\/|$)/i', \IPS\ROOT_PATH, $matches ) )
1093 {
1094 try
1095 {
1096 \IPS\Http\Url::external('http://ips-cic-fileupdate.invisioncic.com/')
1097 ->setQueryString( array( 'user' => $matches[1], 'reason' => $reason ) )
1098 ->request()
1099 ->post();
1100 }
1101 catch ( \Exception $e ) { }
1102 }
1103 }
1104 }
1105}
1106
1107/* Init */
1108IPS::init();
1109
1110/* Custom mb_ucfirst() function - eval'd so we can put into global namespace */
1111eval( '
1112function mb_ucfirst()
1113{
1114 $text = \func_get_arg( 0 );
1115 return mb_strtoupper( mb_substr( $text, 0, 1 ) ) . mb_substr( $text, 1 );
1116}
1117');