Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/Elgg/Application.php (5 issues)

1
<?php
2
3
namespace Elgg;
4
5
use ConfigurationException;
6
use Doctrine\DBAL\Connection;
7
use Elgg\Database\DbConfig;
8
use Elgg\Di\ServiceProvider;
9
use Elgg\Filesystem\Directory;
10
use Elgg\Filesystem\Directory\Local;
11
use Elgg\Http\ErrorResponse;
12
use Elgg\Http\Request;
13
use Elgg\Project\Paths;
14
use InstallationException;
15
use InvalidArgumentException;
16
use InvalidParameterException;
17
use SecurityException;
18
19
/**
20
 * Load, boot, and implement a front controller for an Elgg application
21
 *
22
 * To run as PHP CLI server:
23
 * <code>php -S localhost:8888 /full/path/to/elgg/index.php</code>
24
 *
25
 * The full path is necessary to work around this: https://bugs.php.net/bug.php?id=55726
26
 *
27
 * @since 2.0.0
28
 *
29
 * @property-read \Elgg\Menu\Service $menus
30
 * @property-read \Elgg\Views\TableColumn\ColumnFactory $table_columns
31
 */
32
class Application {
33
34
	const DEFAULT_LANG = 'en';
35
	const DEFAULT_LIMIT = 10;
36
37
	/**
38
	 * @var ServiceProvider
39
	 *
40
	 * @internal DO NOT USE
41
	 */
42
	public $_services;
43
44
	/**
45
	 * @var \Closure[]
46
	 */
47
	private static $_setups = [];
48
49
	/**
50
	 * Property names of the service provider to be exposed via __get()
51
	 *
52
	 * E.g. the presence of `'foo' => true` in the list would allow _elgg_services()->foo to
53
	 * be accessed via elgg()->foo.
54
	 *
55
	 * @var string[]
56
	 */
57
	private static $public_services = [
58
		//'config' => true,
59
		'menus' => true,
60
		'table_columns' => true,
61
	];
62
63
	/**
64
	 * Reference to the loaded Application returned by elgg()
65
	 *
66
	 * @internal Do not use this. use elgg() to access the application
67
	 * @access private
68
	 * @var Application
69
	 */
70
	public static $_instance;
71
72
	/**
73
	 * Get the global Application instance. If not set, it's auto-created and wired to $CONFIG.
74
	 *
75
	 * @return Application|null
76
	 */
77 625
	public static function getInstance() {
78 625
		if (self::$_instance === null) {
79
			self::$_instance = self::factory();
80
			self::setGlobalConfig(self::$_instance);
81
		}
82 625
		return self::$_instance;
83
	}
84
85
	/**
86
	 * Set the global Application instance
87
	 *
88
	 * @param Application $application Global application
89
	 * @return void
90
	 */
91 5389
	public static function setInstance(Application $application = null) {
92 5389
		self::$_instance = $application;
93 5389
	}
94
95
	/**
96
	 * Constructor
97
	 *
98
	 * Upon construction, no actions are taken to load or boot Elgg.
99
	 *
100
	 * @param ServiceProvider $services Elgg services provider
101
	 * @throws ConfigurationException
102
	 */
103 4417
	public function __construct(ServiceProvider $services) {
104 4417
		$this->_services = $services;
105 4417
	}
106
107
	/**
108
	 * Define all Elgg global functions and constants, wire up boot events, but don't boot
109
	 *
110
	 * This includes all the .php files in engine/lib (not upgrades). If a script returns a function,
111
	 * it is queued and executed at the end.
112
	 *
113
	 * @return void
114
	 * @access private
115
	 * @internal
116
	 * @throws \InstallationException
117
	 */
118 4417
	public static function loadCore() {
119 4417
		if (self::isCoreLoaded()) {
120 4417
			return;
121
		}
122
123
		$path = Paths::elgg() . 'engine/lib';
124
125
		// include library files, capturing setup functions
126
		foreach (self::getEngineLibs() as $file) {
127
			try {
128
				self::requireSetupFileOnce("$path/$file");
129
			} catch (\Error $e) {
130
				throw new \InstallationException("Elgg lib file failed include: engine/lib/$file");
131
			}
132
		}
133
	}
134
135
	/**
136
	 * Require a library/plugin file once and capture returned anonymous functions
137
	 *
138
	 * @param string $file File to require
139
	 * @return mixed
140
	 * @internal
141
	 * @access private
142
	 */
143 95
	public static function requireSetupFileOnce($file) {
144 95
		$return = Includer::requireFileOnce($file);
145 95
		if ($return instanceof \Closure) {
146
			self::$_setups[] = $return;
147
		}
148 95
		return $return;
149
	}
150
151
	/**
152
	 * Start and boot the core
153
	 *
154
	 * @return self
155
	 */
156
	public static function start() {
157
		$app = self::getInstance();
158
		$app->bootCore();
159
		return $app;
160
	}
161
162
	/**
163
	 * Are Elgg's global functions loaded?
164
	 *
165
	 * @return bool
166
	 */
167 4417
	public static function isCoreLoaded() {
168 4417
		return function_exists('elgg');
169
	}
170
171
	/**
172
	 * Bootstrap the Elgg engine, loads plugins, and calls initial system events
173
	 *
174
	 * This method loads the full Elgg engine, checks the installation
175
	 * state, and triggers a series of events to finish booting Elgg:
176
	 * 	- {@elgg_event boot system}
177
	 * 	- {@elgg_event init system}
178
	 * 	- {@elgg_event ready system}
179
	 *
180
	 * If Elgg is not fully installed, the browser will be redirected to an installation page.
181
	 *
182
	 * @return void
183
	 * @throws InstallationException
184
	 */
185 18
	public function bootCore() {
186 18
		$config = $this->_services->config;
187
188 18
		if ($config->boot_complete) {
189
			return;
190
		}
191
192
		// in case not loaded already
193 18
		$this->loadCore();
194
195 18
		$hooks = $this->_services->hooks;
196 18
		$events = $hooks->getEvents();
197
198 18
		foreach (self::$_setups as $setup) {
199 18
			$setup($events, $hooks);
200
		}
201
202 18
		if (!$this->_services->db) {
203
			// no database boot!
204
			elgg_views_boot();
205
			$this->_services->session->start();
206
			$this->_services->translator->loadTranslations();
207
208
			actions_init();
0 ignored issues
show
The function actions_init was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

208
			/** @scrutinizer ignore-call */ 
209
   actions_init();
Loading history...
209
			_elgg_init();
210
			_elgg_input_init();
211
			_elgg_nav_init();
212
213
			$config->boot_complete = true;
214
			$config->lock('boot_complete');
215
			return;
216
		}
217
218
		// Connect to database, load language files, load configuration, init session
219 18
		$this->_services->boot->boot($this->_services);
220
221 18
		elgg_views_boot();
222
223
		// Load the plugins that are active
224 18
		$this->_services->plugins->load();
225
226 18
		if (Paths::project() != Paths::elgg()) {
227
			// Elgg is installed as a composer dep, so try to treat the root directory
228
			// as a custom plugin that is always loaded last and can't be disabled...
229
			if (!$config->system_cache_loaded) {
230
				// configure view locations for the custom plugin (not Elgg core)
231
				$viewsFile = Paths::project() . 'views.php';
232
				if (is_file($viewsFile)) {
233
					$viewsSpec = Includer::includeFile($viewsFile);
234
					if (is_array($viewsSpec)) {
235
						$this->_services->views->mergeViewsSpec($viewsSpec);
236
					}
237
				}
238
239
				// find views for the custom plugin (not Elgg core)
240
				$this->_services->views->registerPluginViews(Paths::project());
241
			}
242
243
			if (!$config->i18n_loaded_from_cache) {
244
				$this->_services->translator->registerPluginTranslations(Paths::project());
245
			}
246
247
			// This is root directory start.php
248
			$root_start = Paths::project() . "start.php";
249
			if (is_file($root_start)) {
250
				require $root_start;
251
			}
252
		}
253
254
		// after plugins are started we know which viewtypes are populated
255 18
		$this->_services->views->clampViewtypeToPopulatedViews();
256
257 18
		$this->allowPathRewrite();
258
259
		// Allows registering handlers strictly before all init, system handlers
260 18
		$events->trigger('plugins_boot', 'system');
261
262
		// Complete the boot process for both engine and plugins
263 18
		$events->trigger('init', 'system');
264
265 18
		$config->boot_complete = true;
266 18
		$config->lock('boot_complete');
267
268
		// System loaded and ready
269 18
		$events->trigger('ready', 'system');
270 18
	}
271
272
	/**
273
	 * Get the DB credentials.
274
	 *
275
	 * We no longer leave DB credentials in the config in case it gets accidentally dumped.
276
	 *
277
	 * @return \Elgg\Database\DbConfig
278
	 */
279 1
	public function getDbConfig() {
280 1
		return $this->_services->dbConfig;
281
	}
282
283
	/**
284
	 * Get a Database wrapper for performing queries without booting Elgg
285
	 *
286
	 * If settings has not been loaded, it will be loaded to configure the DB connection.
287
	 *
288
	 * @note Before boot, the Database instance will not yet be bound to a Logger.
289
	 *
290
	 * @return \Elgg\Application\Database
291
	 */
292 13
	public function getDb() {
293 13
		return $this->_services->publicDb;
294
	}
295
296
	/**
297
	 * Get database connection
298
	 *
299
	 * @param string $type Connection type
300
	 * @return Connection|false
301
	 *
302
	 * @access private
303
	 */
304 13
	public function getDbConnection($type = 'readwrite') {
305
		try {
306 13
			return $this->getDb()->getConnection($type);
307
		} catch (\DatabaseException $e) {
308
			return false;
309
		}
310
	}
311
312
	/**
313
	 * Get an undefined property
314
	 *
315
	 * @param string $name The property name accessed
316
	 *
317
	 * @return mixed
318
	 */
319
	public function __get($name) {
320
		if (isset(self::$public_services[$name])) {
321
			return $this->_services->{$name};
322
		}
323
		trigger_error("Undefined property: " . __CLASS__ . ":\${$name}");
324
	}
325
326
	/**
327
	 * Make the global $CONFIG a reference to this application's config service
328
	 *
329
	 * @param Application $application The Application
330
	 * @return void
331
	 */
332
	public static function setGlobalConfig(Application $application) {
333
		global $CONFIG;
334
		$CONFIG = $application->_services->config;
335
	}
336
337
	/**
338
	 * Create a new application.
339
	 *
340
	 * @warning You generally want to use getInstance().
341
	 *
342
	 * For normal operation, you must use setInstance() and optionally setGlobalConfig() to wire the
343
	 * application to Elgg's global API.
344
	 *
345
	 * @param array $spec Specification for initial call.
346
	 * @return self
347
	 * @throws ConfigurationException
348
	 * @throws InvalidArgumentException
349
	 */
350 4417
	public static function factory(array $spec = []) {
351 4417
		self::loadCore();
352
353
		$defaults = [
354 4417
			'config' => null,
355
			'handle_exceptions' => true,
356
			'handle_shutdown' => true,
357
			'request' => null,
358
			'service_provider' => null,
359
			'set_start_time' => true,
360
			'settings_path' => null,
361
		];
362 4417
		$spec = array_merge($defaults, $spec);
363
364 4417
		if ($spec['set_start_time']) {
365
			/**
366
			 * The time with microseconds when the Elgg engine was started.
367
			 *
368
			 * @global float
369
			 */
370 10
			if (!isset($GLOBALS['START_MICROTIME'])) {
371 1
				$GLOBALS['START_MICROTIME'] = microtime(true);
372
			}
373
		}
374
375 4417
		if (!$spec['service_provider']) {
376 1
			if (!$spec['config']) {
377
				$spec['config'] = Config::factory($spec['settings_path']);
378
			}
379 1
			$spec['service_provider'] = new ServiceProvider($spec['config']);
380
		}
381
382 4417
		if ($spec['request']) {
383
			if ($spec['request'] instanceof Request) {
384
				$spec['service_provider']->setValue('request', $spec['request']);
385
			} else {
386
				throw new InvalidArgumentException("Given request is not a " . Request::class);
387
			}
388
		}
389
390 4417
		$app = new self($spec['service_provider']);
391
392 4417
		if ($spec['handle_exceptions']) {
393
			set_error_handler([$app, 'handleErrors']);
394
			set_exception_handler([$app, 'handleExceptions']);
395
		}
396
397 4417
		if ($spec['handle_shutdown']) {
398
			register_shutdown_function('_elgg_db_run_delayed_queries');
399
			register_shutdown_function('_elgg_db_log_profiling_data');
400
401
			// we need to register for shutdown before Symfony registers the
402
			// session_write_close() function. https://github.com/Elgg/Elgg/issues/9243
403
			register_shutdown_function(function () {
404
				// There are cases where we may exit before this function is defined
405
				if (function_exists('_elgg_shutdown_hook')) {
406
					_elgg_shutdown_hook();
407
				}
408
			});
409
		}
410
411 4417
		return $app;
412
	}
413
414
	/**
415
	 * Elgg's front controller. Handles basically all incoming URL requests.
416
	 *
417
	 * @return bool True if Elgg will handle the request, false if the server should (PHP-CLI server)
418
	 * @throws ConfigurationException
419
	 */
420
	public static function index() {
421
		$req = Request::createFromGlobals();
422
		/** @var Request $req */
423
424
		if ($req->isRewriteCheck()) {
425
			echo Request::REWRITE_TEST_OUTPUT;
426
			return true;
427
		}
428
429
		try {
430
			$app = self::factory([
431
				'request' => $req,
432
			]);
433
		} catch (ConfigurationException $ex) {
434
			return self::install();
435
		}
436
437
		self::setGlobalConfig($app);
438
		self::setInstance($app);
439
440
		return $app->run();
441
	}
442
443
	/**
444
	 * Routes the request, booting core if not yet booted
445
	 *
446
	 * @return bool False if Elgg wants the PHP CLI server to handle the request
447
	 * @throws InstallationException
448
	 * @throws InvalidParameterException
449
	 * @throws SecurityException
450
	 */
451
	public function run() {
452
		try {
453
			$config = $this->_services->config;
454
			$request = $this->_services->request;
455
456
			if ($request->isCliServer()) {
457
				if ($request->isCliServable(Paths::project())) {
458
					return false;
459
				}
460
461
				// overwrite value from settings
462
				$www_root = rtrim($request->getSchemeAndHttpHost() . $request->getBaseUrl(), '/') . '/';
463
				$config->wwwroot = $www_root;
464
				$config->wwwroot_cli_server = $www_root;
465
			}
466
467
			if (0 === strpos($request->getElggPath(), '/cache/')) {
468
				$this->_services->cacheHandler->handleRequest($request, $this)->prepare($request)->send();
469
470
				return true;
471
			}
472
473
			if (0 === strpos($request->getElggPath(), '/serve-file/')) {
474
				$this->_services->serveFileHandler->getResponse($request)->send();
475
476
				return true;
477
			}
478
479
			$this->bootCore();
480
481
			// re-fetch new request from services in case it was replaced by route:rewrite
482
			$request = $this->_services->request;
483
484
			if (!$this->_services->router->route($request)) {
485
				throw new PageNotFoundException();
486
			}
487
		} catch (HttpException $ex) {
488
			$forward_url = REFERRER;
489
			if ($ex instanceof GatekeeperException) {
490
				$forward_url = elgg_is_logged_in() ? '' : '/login';
491
			}
492
493
			$hook_params = [
494
				'exception' => $ex,
495
			];
496
			
497
			$this->_services->hooks->trigger('forward', $ex->getCode(), $hook_params, $forward_url);
498
499
			$response = new ErrorResponse($ex->getMessage(), $ex->getCode(), $forward_url);
500
			$this->_services->responseFactory->respond($response);
501
		}
502
503
		return true;
504
	}
505
506
	/**
507
	 * Returns a directory that points to the root of Elgg, but not necessarily
508
	 * the install root. See `self::root()` for that.
509
	 *
510
	 * @return Directory
511
	 */
512 9
	public static function elggDir() {
513 9
		return Local::elggRoot();
514
	}
515
516
	/**
517
	 * Returns a directory that points to the project root, where composer is installed.
518
	 *
519
	 * @return Directory
520
	 */
521
	public static function projectDir() {
522
		return Local::projectRoot();
523
	}
524
525
	/**
526
	 * Renders a web UI for installing Elgg.
527
	 *
528
	 * @return bool
529
	 * @throws InstallationException
530
	 */
531
	public static function install() {
532
		ini_set('display_errors', 1);
533
534
		$installer = new \ElggInstaller();
535
		$response = $installer->run();
536
		try {
537
			// we won't trust server configuration but specify utf-8
538
			elgg_set_http_header('Content-type: text/html; charset=utf-8');
539
540
			// turn off browser caching
541
			elgg_set_http_header('Pragma: public', true);
542
			elgg_set_http_header("Cache-Control: no-cache, must-revalidate", true);
543
			elgg_set_http_header('Expires: Fri, 05 Feb 1982 00:00:00 -0500', true);
544
545
			_elgg_services()->responseFactory->respond($response);
546
			return headers_sent();
547
		} catch (InvalidParameterException $ex) {
548
			throw new InstallationException($ex->getMessage());
549
		}
550
	}
551
552
	/**
553
	 * Elgg upgrade script.
554
	 *
555
	 * This script triggers any necessary upgrades. If the site has been upgraded
556
	 * to the most recent version of the code, no upgrades are run but the caches
557
	 * are flushed.
558
	 *
559
	 * Upgrades use a table {db_prefix}upgrade_lock as a mutex to prevent concurrent upgrades.
560
	 *
561
	 * The URL to forward to after upgrades are complete can be specified by setting $_GET['forward']
562
	 * to a relative URL.
563
	 *
564
	 * @return void
565
	 * @throws InstallationException
566
	 */
567
	public static function upgrade() {
568
		// we want to know if an error occurs
569
		ini_set('display_errors', 1);
570
		$is_cli = (php_sapi_name() === 'cli');
571
572
		$forward = function ($url) use ($is_cli) {
573
			if ($is_cli) {
574
				fwrite(STDOUT, "Open $url in your browser to continue." . PHP_EOL);
575
				return;
576
			}
577
578
			forward($url);
579
		};
580
581
		define('UPGRADING', 'upgrading');
582
583
		self::migrate();
584
		self::start();
585
586
		// clear autoload cache so plugin classes can be reregistered and used during upgrade
587
		_elgg_services()->autoloadManager->deleteCache();
588
589
		// check security settings
590
		if (!$is_cli && _elgg_config()->security_protect_upgrade && !elgg_is_admin_logged_in()) {
591
			// only admin's or users with a valid token can run upgrade.php
592
			elgg_signed_request_gatekeeper();
593
		}
594
595
		$site_url = _elgg_config()->url;
596
		$site_host = parse_url($site_url, PHP_URL_HOST) . '/';
597
598
		// turn any full in-site URLs into absolute paths
599
		$forward_url = get_input('forward', '/admin', false);
600
		$forward_url = str_replace([$site_url, $site_host], '/', $forward_url);
601
602
		if (strpos($forward_url, '/') !== 0) {
603
			$forward_url = '/' . $forward_url;
604
		}
605
606
		if ($is_cli || (get_input('upgrade') == 'upgrade')) {
607
			$upgrader = _elgg_services()->upgrades;
608
			$result = $upgrader->run();
609
610
			if ($result['failure'] == true) {
611
				register_error($result['reason']);
612
				$forward($forward_url);
613
			}
614
615
			// Find unprocessed batch upgrade classes and save them as ElggUpgrade objects
616
			$core_upgrades = (require self::elggDir()->getPath('engine/lib/upgrades/async-upgrades.php'));
617
			$has_pending_upgrades = _elgg_services()->upgradeLocator->run($core_upgrades);
618
619
			if ($has_pending_upgrades) {
620
				// Forward to the list of pending upgrades
621
				$forward_url = '/admin/upgrades';
622
			}
623
		} else {
624
			$rewriteTester = new \ElggRewriteTester();
625
			$url = elgg_get_site_url() . "__testing_rewrite?__testing_rewrite=1";
626
			if (!$rewriteTester->runRewriteTest($url)) {
627
				// see if there is a problem accessing the site at all
628
				// due to ip restrictions for example
629
				if (!$rewriteTester->runLocalhostAccessTest()) {
630
					// note: translation may not be available until after upgrade
631
					$msg = elgg_echo("installation:htaccess:localhost:connectionfailed");
632
					if ($msg === "installation:htaccess:localhost:connectionfailed") {
633
						$msg = "Elgg cannot connect to itself to test rewrite rules properly. Check "
634
								. "that curl is working and there are no IP restrictions preventing "
635
								. "localhost connections.";
636
					}
637
					echo $msg;
638
					exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
639
				}
640
641
				// note: translation may not be available until after upgrade
642
				$msg = elgg_echo("installation:htaccess:needs_upgrade");
643
				if ($msg === "installation:htaccess:needs_upgrade") {
644
					$msg = "You must update your .htaccess file (use install/config/htaccess.dist as a guide).";
645
				}
646
				echo $msg;
647
				exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
648
			}
649
650
			$vars = [
651
				'forward' => $forward_url
652
			];
653
654
			// reset cache to have latest translations available during upgrade
655
			elgg_reset_system_cache();
656
657
			echo elgg_view_page(elgg_echo('upgrading'), '', 'upgrade', $vars);
658
			exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
659
		}
660
661
		$forward($forward_url);
662
	}
663
664
	/**
665
	 * Runs database migrations
666
	 *
667
	 * @throws InstallationException
668
	 * @return bool
669
	 */
670 2
	public static function migrate() {
671 2
		$conf = self::elggDir()->getPath('engine/conf/migrations.php');
672 2
		if (!$conf) {
673
			throw new InstallationException('Settings file is required to run database migrations.');
674
		}
675
676 2
		$app = new \Phinx\Console\PhinxApplication();
677 2
		$wrapper = new \Phinx\Wrapper\TextWrapper($app, [
678 2
			'configuration' => $conf,
679
		]);
680 2
		$log = $wrapper->getMigrate();
681
682 2
		if (!empty($_SERVER['argv']) && in_array('--verbose', $_SERVER['argv'])) {
683
			error_log($log);
684
		}
685
686 2
		return true;
687
	}
688
689
	/**
690
	 * Returns configuration array for database migrations
691
	 * @return array
692
	 */
693 2
	public static function getMigrationSettings() {
694
695 2
		$config = Config::factory();
696 2
		$db_config = DbConfig::fromElggConfig($config);
697
698 2
		if ($db_config->isDatabaseSplit()) {
699
			$conn = $db_config->getConnectionConfig(DbConfig::WRITE);
700
		} else {
701 2
			$conn = $db_config->getConnectionConfig();
702
		}
703
704
		return [
705
			"paths" => [
706 2
				"migrations" => Paths::elgg() . 'engine/schema/migrations/',
707
			],
708
			"environments" => [
709 2
				"default_migration_table" => "{$conn['prefix']}migrations",
710 2
				"default_database" => "prod",
711
				"prod" => [
712 2
					"adapter" => "mysql",
713 2
					"host" => $conn['host'],
714 2
					"name" => $conn['database'],
715 2
					"user" => $conn['user'],
716 2
					"pass" => $conn['password'],
717 2
					"charset" => $conn['encoding'],
718 2
					"table_prefix" => $conn['prefix'],
719
				],
720
			],
721
		];
722
	}
723
724
	/**
725
	 * Allow plugins to rewrite the path.
726
	 *
727
	 * @return void
728
	 */
729 18
	private function allowPathRewrite() {
730 18
		$request = $this->_services->request;
731 18
		$new = $this->_services->router->allowRewrite($request);
732 18
		if ($new === $request) {
733 18
			return;
734
		}
735
736
		$this->_services->setValue('request', $new);
737
		$this->_services->context->initialize($new);
738
	}
739
740
	/**
741
	 * Intercepts, logs, and displays uncaught exceptions.
742
	 *
743
	 * To use a viewtype other than failsafe, create the views:
744
	 *  <viewtype>/messages/exceptions/admin_exception
745
	 *  <viewtype>/messages/exceptions/exception
746
	 * See the json viewtype for an example.
747
	 *
748
	 * @warning This function should never be called directly.
749
	 *
750
	 * @see http://www.php.net/set-exception-handler
751
	 *
752
	 * @param \Exception|\Error $exception The exception/error being handled
753
	 *
754
	 * @return void
755
	 * @access private
756
	 */
757
	public function handleExceptions($exception) {
758
		$timestamp = time();
759
		error_log("Exception at time $timestamp: $exception");
760
761
		// Wipe any existing output buffer
762
		ob_end_clean();
763
764
		// make sure the error isn't cached
765
		header("Cache-Control: no-cache, must-revalidate", true);
766
		header('Expires: Fri, 05 Feb 1982 00:00:00 -0500', true);
767
768
		if ($exception instanceof \InstallationException) {
769
			forward('/install.php');
770
		}
771
772
		if (!self::isCoreLoaded()) {
773
			http_response_code(500);
774
			echo "Exception loading Elgg core. Check log at time $timestamp";
775
			return;
776
		}
777
778
		try {
779
			// allow custom scripts to trigger on exception
780
			// value in settings.php should be a system path to a file to include
781
			$exception_include = $this->_services->config->exception_include;
782
783
			if ($exception_include && is_file($exception_include)) {
784
				ob_start();
785
786
				// don't isolate, these scripts may use the local $exception var.
787
				include $exception_include;
788
789
				$exception_output = ob_get_clean();
790
791
				// if content is returned from the custom handler we will output
792
				// that instead of our default failsafe view
793
				if (!empty($exception_output)) {
794
					echo $exception_output;
795
					exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
796
				}
797
			}
798
799
			if (elgg_is_xhr()) {
800
				elgg_set_viewtype('json');
801
				$response = new \Symfony\Component\HttpFoundation\JsonResponse(null, 500);
802
			} else {
803
				elgg_set_viewtype('failsafe');
804
				$response = new \Symfony\Component\HttpFoundation\Response('', 500);
805
			}
806
807
			if (elgg_is_admin_logged_in()) {
808
				$body = elgg_view("messages/exceptions/admin_exception", [
809
					'object' => $exception,
810
					'ts' => $timestamp
811
				]);
812
			} else {
813
				$body = elgg_view("messages/exceptions/exception", [
814
					'object' => $exception,
815
					'ts' => $timestamp
816
				]);
817
			}
818
819
			$response->setContent(elgg_view_page(elgg_echo('exception:title'), $body));
820
			$response->send();
821
		} catch (\Exception $e) {
822
			$timestamp = time();
823
			$message = $e->getMessage();
824
			http_response_code(500);
825
			echo "Fatal error in exception handler. Check log for Exception at time $timestamp";
826
			error_log("Exception at time $timestamp : fatal error in exception handler : $message");
827
		}
828
	}
829
830
	/**
831
	 * Intercepts catchable PHP errors.
832
	 *
833
	 * @warning This function should never be called directly.
834
	 *
835
	 * @internal
836
	 * For catchable fatal errors, throws an Exception with the error.
837
	 *
838
	 * For non-fatal errors, depending upon the debug settings, either
839
	 * log the error or ignore it.
840
	 *
841
	 * @see http://www.php.net/set-error-handler
842
	 *
843
	 * @param int    $errno    The level of the error raised
844
	 * @param string $errmsg   The error message
845
	 * @param string $filename The filename the error was raised in
846
	 * @param int    $linenum  The line number the error was raised at
847
	 * @param array  $vars     An array that points to the active symbol table where error occurred
848
	 *
849
	 * @return true
850
	 * @throws \Exception
851
	 * @access private
852
	 */
853
	public function handleErrors($errno, $errmsg, $filename, $linenum, $vars) {
854
		$error = date("Y-m-d H:i:s (T)") . ": \"$errmsg\" in file $filename (line $linenum)";
855
856
		$log = function ($message, $level) {
857
			if (!self::isCoreLoaded()) {
858
				return false;
859
			}
860
861
			if (!self::$_instance) {
862
				// can occur during tests
863
				return false;
864
			}
865
866
			return elgg_log($message, $level);
867
		};
868
869
		switch ($errno) {
870
			case E_USER_ERROR:
871
				if (!$log("PHP: $error", 'ERROR')) {
872
					error_log("PHP ERROR: $error");
873
				}
874
				if (self::isCoreLoaded()) {
875
					register_error("ERROR: $error");
876
				}
877
878
				// Since this is a fatal error, we want to stop any further execution but do so gracefully.
879
				throw new \Exception($error);
880
881
			case E_WARNING :
882
			case E_USER_WARNING :
883
			case E_RECOVERABLE_ERROR: // (e.g. type hint violation)
884
885
				// check if the error wasn't suppressed by the error control operator (@)
886
				if (error_reporting() && !$log("PHP: $error", 'WARNING')) {
887
					error_log("PHP WARNING: $error");
888
				}
889
				break;
890
891
			default:
892
				if (function_exists('_elgg_config')) {
893
					$debug = _elgg_config()->debug;
894
				} else {
895
					$debug = isset($GLOBALS['CONFIG']->debug) ? $GLOBALS['CONFIG']->debug : null;
896
				}
897
				if ($debug !== 'NOTICE') {
898
					return true;
899
				}
900
901
				if (!$log("PHP (errno $errno): $error", 'NOTICE')) {
902
					error_log("PHP NOTICE: $error");
903
				}
904
		}
905
906
		return true;
907
	}
908
909
	/**
910
	 * Get all engine/lib library filenames
911
	 *
912
	 * @note We can't just pull in all directory files because some users leave old files in place.
913
	 *
914
	 * @return string[]
915
	 */
916
	private static function getEngineLibs() {
917
		return [
918
			'elgglib.php',
919
			'access.php',
920
			'actions.php',
921
			'admin.php',
922
			'annotations.php',
923
			'cache.php',
924
			'comments.php',
925
			'configuration.php',
926
			'constants.php',
927
			'cron.php',
928
			'database.php',
929
			'deprecated-2.3.php',
930
			'deprecated-3.0.php',
931
			'entities.php',
932
			'filestore.php',
933
			'group.php',
934
			'input.php',
935
			'languages.php',
936
			'mb_wrapper.php',
937
			'metadata.php',
938
			'metastrings.php',
939
			'navigation.php',
940
			'notification.php',
941
			'output.php',
942
			'pagehandler.php',
943
			'pageowner.php',
944
			'pam.php',
945
			'plugins.php',
946
			'private_settings.php',
947
			'relationships.php',
948
			'river.php',
949
			'search.php',
950
			'sessions.php',
951
			'statistics.php',
952
			'tags.php',
953
			'upgrade.php',
954
			'user_settings.php',
955
			'users.php',
956
			'views.php',
957
			'widgets.php',
958
		];
959
	}
960
}
961