Passed
Push — master ( f13f78...5c1b24 )
by Ismayil
04:22
created

engine/classes/Elgg/Application.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Elgg;
4
5
use Elgg\Database\DbConfig;
6
use Elgg\Database\SiteSecret;
7
use Elgg\Di\ServiceProvider;
8
use Elgg\Filesystem\Directory;
9
use Elgg\Http\Request;
10
use Elgg\Filesystem\Directory\Local;
11
use ConfigurationException;
12
use Elgg\Project\Paths;
13
use Exception;
14
use InstallationException;
15
16
/**
17
 * Load, boot, and implement a front controller for an Elgg application
18
 *
19
 * To run as PHP CLI server:
20
 * <code>php -S localhost:8888 /full/path/to/elgg/index.php</code>
21
 *
22
 * The full path is necessary to work around this: https://bugs.php.net/bug.php?id=55726
23
 *
24
 * @since 2.0.0
25
 *
26
 * @property-read \Elgg\Menu\Service $menus
27
 * @property-read \Elgg\Views\TableColumn\ColumnFactory $table_columns
28
 */
29
class Application {
30
31
	const DEFAULT_LANG = 'en';
32
	const DEFAULT_LIMIT = 10;
33
34
	/**
35
	 * @var ServiceProvider
36
	 *
37
	 * @internal DO NOT USE
38
	 */
39
	public $_services;
40
41
	/**
42
	 * @var bool
43
	 */
44
	private static $testing_app;
45
46
	/**
47
	 * Property names of the service provider to be exposed via __get()
48
	 *
49
	 * E.g. the presence of `'foo' => true` in the list would allow _elgg_services()->foo to
50
	 * be accessed via elgg()->foo.
51
	 *
52
	 * @var string[]
53
	 */
54
	private static $public_services = [
55
		//'config' => true,
56
		'menus' => true,
57
		'table_columns' => true,
58
	];
59
60
	/**
61
	 * Reference to the loaded Application returned by elgg()
62
	 *
63
	 * @internal Do not use this. use elgg() to access the application
64
	 * @access private
65
	 * @var Application
66
	 */
67
	public static $_instance;
68
69
	/**
70
	 * Get the global Application instance. If not set, it's auto-created and wired to $CONFIG.
71
	 *
72
	 * @return Application|null
73
	 */
74
	public static function getInstance() {
75
		if (self::$_instance === null) {
76
			self::$_instance = self::factory();
77
			self::setGlobalConfig(self::$_instance);
78
		}
79
		return self::$_instance;
80
	}
81
82
	/**
83
	 * Set the global Application instance
84
	 *
85
	 * @param Application $application Global application
86
	 * @return void
87
	 */
88 197
	public static function setInstance(Application $application) {
89 197
		self::$_instance = $application;
90 197
	}
91
92
	/**
93
	 * Constructor
94
	 *
95
	 * Upon construction, no actions are taken to load or boot Elgg.
96
	 *
97
	 * @param ServiceProvider $services Elgg services provider
98
	 * @throws ConfigurationException
99
	 */
100 199
	public function __construct(ServiceProvider $services) {
101 199
		$this->_services = $services;
102
103 199
		$this->initConfig();
104 199
	}
105
106
	/**
107
	 * Validate, normalize, fill in missing values, and lock some
108
	 *
109
	 * @return void
110
	 * @throws ConfigurationException
111
	 */
112 199
	private function initConfig() {
113 199
		$config = $this->_services->config;
114
115 199
		if ($config->elgg_config_locks === null) {
116 198
			$config->elgg_config_locks = true;
117
		}
118
119 199
		if ($config->elgg_config_locks) {
120
			$lock = function ($name) use ($config) {
121 199
				$config->lock($name);
122 199
			};
123
		} else {
124
			// the installer needs to build an application with defaults then update
125
			// them after they're validated, so we don't want to lock them.
126
			$lock = function () {
127
			};
128
		}
129
130 199
		$this->_services->timer->begin([]);
131
132
		// Until DB loads, let's log problems
133 199
		if ($config->debug === null) {
134 198
			$config->debug = 'NOTICE';
135
		}
136
137 199
		if ($config->dataroot) {
138 199
			$config->dataroot = rtrim($config->dataroot, '\\/') . DIRECTORY_SEPARATOR;
139
		} else {
140
			if (!$config->installer_running) {
141
				throw new ConfigurationException('Config value "dataroot" is required.');
142
			}
143
		}
144 199
		$lock('dataroot');
145
146 199
		if ($config->cacheroot) {
147 199
			$config->cacheroot = rtrim($config->cacheroot, '\\/') . DIRECTORY_SEPARATOR;
148
		} else {
149
			$config->cacheroot = $config->dataroot;
150
		}
151 199
		$lock('cacheroot');
152
153 199
		if ($config->wwwroot) {
154 199
			$config->wwwroot = rtrim($config->wwwroot, '/') . '/';
155
		} else {
156
			$config->wwwroot = $this->_services->request->sniffElggUrl();
157
		}
158 199
		$lock('wwwroot');
159
160 199
		if (!$config->language) {
161 198
			$config->language = self::DEFAULT_LANG;
162
		}
163
164 199
		if ($config->default_limit) {
165 1
			$lock('default_limit');
166
		} else {
167 198
			$config->default_limit = self::DEFAULT_LIMIT;
168
		}
169
170
		$locked_props = [
171 199
			'site_guid' => 1,
172 199
			'path' => Paths::project(),
173 199
			'plugins_path' => Paths::project() . "mod/",
174 199
			'pluginspath' => Paths::project() . "mod/",
175 199
			'url' => $config->wwwroot,
176
		];
177 199
		foreach ($locked_props as $name => $value) {
178 199
			$config->$name = $value;
179 199
			$lock($name);
180
		}
181
182
		// move sensitive credentials into isolated services
183 199
		$this->_services->dbConfig;
184
185
		// If the site secret is in the settings file, let's move it to that component
186
		// right away to keep this value out of config.
187 199
		$secret = SiteSecret::fromConfig($config);
188 199
		if ($secret) {
189
			$this->_services->setValue('siteSecret', $secret);
190
			$config->elgg_config_set_secret = true;
191
		}
192
193 199
		$config->boot_complete = false;
194 199
	}
195
196
	/**
197
	 * Get the DB credentials.
198
	 *
199
	 * We no longer leave DB credentials in the config in case it gets accidentally dumped.
200
	 *
201
	 * @return \Elgg\Database\DbConfig
202
	 */
203
	public function getDbConfig() {
204
		return $this->_services->dbConfig;
205
	}
206
207
	/**
208
	 * Define all Elgg global functions and constants, wire up boot events, but don't boot
209
	 *
210
	 * This includes all the .php files in engine/lib (not upgrades). If a script returns a function,
211
	 * it is queued and executed at the end.
212
	 *
213
	 * @return void
214
	 * @access private
215
	 * @internal
216
	 * @throws \InstallationException
217
	 */
218 197
	public function loadCore() {
219 197
		if (self::isCoreLoaded()) {
220 197
			return;
221
		}
222
223
		$setups = [];
224
		$path = Paths::elgg() . 'engine/lib';
225
226
		// include library files, capturing setup functions
227
		foreach (self::getEngineLibs() as $file) {
228
			$return = Includer::includeFile("$path/$file");
229
			if (!$return) {
230
				throw new \InstallationException("Elgg lib file failed include: engine/lib/$file");
231
			}
232
			if ($return instanceof \Closure) {
233
				$setups[$file] = $return;
234
			}
235
		}
236
237
		$hooks = $this->_services->hooks;
238
		$events = $hooks->getEvents();
239
240
		// run setups
241
		foreach ($setups as $func) {
242
			$func($events, $hooks);
243
		}
244
	}
245
246
	/**
247
	 * Start and boot the core
248
	 *
249
	 * @return self
250
	 */
251
	public static function start() {
252
		$app = self::getInstance();
253
		$app->bootCore();
254
		return $app;
255
	}
256
257
	/**
258
	 * Are Elgg's global functions loaded?
259
	 *
260
	 * @return bool
261
	 */
262 197
	public static function isCoreLoaded() {
263 197
		return function_exists('elgg');
264
	}
265
266
	/**
267
	 * Bootstrap the Elgg engine, loads plugins, and calls initial system events
268
	 *
269
	 * This method loads the full Elgg engine, checks the installation
270
	 * state, and triggers a series of events to finish booting Elgg:
271
	 * 	- {@elgg_event boot system}
272
	 * 	- {@elgg_event init system}
273
	 * 	- {@elgg_event ready system}
274
	 *
275
	 * If Elgg is not fully installed, the browser will be redirected to an installation page.
276
	 *
277
	 * @return void
278
	 */
279
	public function bootCore() {
280
		$config = $this->_services->config;
281
282
		if ($this->isTestingApplication()) {
283
			throw new \RuntimeException('Unit tests should not call ' . __METHOD__);
284
		}
285
286
		if ($config->boot_complete) {
287
			return;
288
		}
289
290
		// in case not loaded already
291
		$this->loadCore();
292
293
		if (!$this->_services->db) {
294
			// no database boot!
295
			elgg_views_boot();
296
			$this->_services->session->start();
297
			$this->_services->translator->loadTranslations();
298
299
			actions_init();
300
			_elgg_init();
301
			_elgg_input_init();
302
			_elgg_nav_init();
303
304
			$config->boot_complete = true;
305
			$config->lock('boot_complete');
306
			return;
307
		}
308
309
		// Connect to database, load language files, load configuration, init session
310
		$this->_services->boot->boot($this->_services);
311
312
		elgg_views_boot();
313
314
		// Load the plugins that are active
315
		$this->_services->plugins->load();
316
317
		if (Paths::project() != Paths::elgg()) {
318
			// Elgg is installed as a composer dep, so try to treat the root directory
319
			// as a custom plugin that is always loaded last and can't be disabled...
320
			if (!$config->system_cache_loaded) {
321
				// configure view locations for the custom plugin (not Elgg core)
322
				$viewsFile = Paths::project() . 'views.php';
323 View Code Duplication
				if (is_file($viewsFile)) {
324
					$viewsSpec = Includer::includeFile($viewsFile);
325
					if (is_array($viewsSpec)) {
326
						$this->_services->views->mergeViewsSpec($viewsSpec);
327
					}
328
				}
329
330
				// find views for the custom plugin (not Elgg core)
331
				$this->_services->views->registerPluginViews(Paths::project());
332
			}
333
334
			if (!$config->i18n_loaded_from_cache) {
335
				$this->_services->translator->registerPluginTranslations(Paths::project());
336
			}
337
338
			// This is root directory start.php
339
			$root_start = Paths::project() . "start.php";
340
			if (is_file($root_start)) {
341
				require $root_start;
342
			}
343
		}
344
345
		// after plugins are started we know which viewtypes are populated
346
		$this->_services->views->clampViewtypeToPopulatedViews();
347
348
		$this->allowPathRewrite();
349
350
		$events = $this->_services->hooks->getEvents();
351
352
		// Allows registering handlers strictly before all init, system handlers
353
		$events->trigger('plugins_boot', 'system');
354
355
		// Complete the boot process for both engine and plugins
356
		$events->trigger('init', 'system');
357
358
		$config->boot_complete = true;
359
		$config->lock('boot_complete');
360
361
		// System loaded and ready
362
		$events->trigger('ready', 'system');
363
	}
364
365
	/**
366
	 * Get a Database wrapper for performing queries without booting Elgg
367
	 *
368
	 * If settings has not been loaded, it will be loaded to configure the DB connection.
369
	 *
370
	 * @note Before boot, the Database instance will not yet be bound to a Logger.
371
	 *
372
	 * @return \Elgg\Application\Database
373
	 */
374
	public function getDb() {
375
		return $this->_services->publicDb;
376
	}
377
378
	/**
379
	 * Get an undefined property
380
	 *
381
	 * @param string $name The property name accessed
382
	 *
383
	 * @return mixed
384
	 */
385
	public function __get($name) {
386
		if (isset(self::$public_services[$name])) {
387
			return $this->_services->{$name};
388
		}
389
		trigger_error("Undefined property: " . __CLASS__ . ":\${$name}");
390
	}
391
392
	/**
393
	 * Make the global $CONFIG a reference to this application's config service
394
	 *
395
	 * @param Application $application The Application
396
	 * @return void
397
	 */
398
	public static function setGlobalConfig(Application $application) {
399
		global $CONFIG;
400
		$CONFIG = $application->_services->config;
401
	}
402
403
	/**
404
	 * Create a new application.
405
	 *
406
	 * @warning You generally want to use getInstance().
407
	 *
408
	 * For normal operation, you must use setInstance() and optionally setGlobalConfig() to wire the
409
	 * application to Elgg's global API.
410
	 *
411
	 * @param array $spec Specification for initial call.
412
	 * @return self
413
	 * @throws ConfigurationException
414
	 */
415 1
	public static function factory(array $spec = []) {
416
		$defaults = [
417 1
			'config' => null,
418
			'handle_exceptions' => true,
419
			'handle_shutdown' => true,
420
			'request' => null,
421
			'service_provider' => null,
422
			'set_start_time' => true,
423
			'settings_path' => null,
424
		];
425 1
		$spec = array_merge($defaults, $spec);
426
427 1
		if ($spec['set_start_time']) {
428
			/**
429
			 * The time with microseconds when the Elgg engine was started.
430
			 *
431
			 * @global float
432
			 */
433 1
			if (!isset($GLOBALS['START_MICROTIME'])) {
434 1
				$GLOBALS['START_MICROTIME'] = microtime(true);
435
			}
436
		}
437
438 1
		if (!$spec['service_provider']) {
439 1
			if (!$spec['config']) {
440
				$spec['config'] = Config::factory($spec['settings_path']);
441
			}
442 1
			$spec['service_provider'] = new ServiceProvider($spec['config']);
443
		}
444
445 1
		if ($spec['request']) {
446
			if ($spec['request'] instanceof Request) {
447
				$spec['service_provider']->setValue('request', $spec['request']);
448
			} else {
449
				throw new \InvalidArgumentException("Given request is not a " . Request::class);
450
			}
451
		}
452
453 1
		$app = new self($spec['service_provider']);
454
455 1
		if ($spec['handle_exceptions']) {
456
			set_error_handler([$app, 'handleErrors']);
457
			set_exception_handler([$app, 'handleExceptions']);
458
		}
459
460 1
		if ($spec['handle_shutdown']) {
461
			// we need to register for shutdown before Symfony registers the
462
			// session_write_close() function. https://github.com/Elgg/Elgg/issues/9243
463
			register_shutdown_function(function () {
464
				// There are cases where we may exit before this function is defined
465
				if (function_exists('_elgg_shutdown_hook')) {
466
					_elgg_shutdown_hook();
467
				}
468
			});
469
		}
470
471 1
		return $app;
472
	}
473
474
	/**
475
	 * Elgg's front controller. Handles basically all incoming URL requests.
476
	 *
477
	 * @return bool True if Elgg will handle the request, false if the server should (PHP-CLI server)
478
	 */
479
	public static function index() {
480
		$req = Request::createFromGlobals();
481
		/** @var Request $req */
482
483
		if ($req->isRewriteCheck()) {
484
			echo Request::REWRITE_TEST_OUTPUT;
485
			return true;
486
		}
487
488
		$app = self::factory(['request' => $req]);
489
		self::setGlobalConfig($app);
490
		self::setInstance($app);
491
492
		return $app->run();
493
	}
494
495
	/**
496
	 * Routes the request, booting core if not yet booted
497
	 *
498
	 * @return bool False if Elgg wants the PHP CLI server to handle the request
499
	 */
500
	public function run() {
501
		$config = $this->_services->config;
502
		$request = $this->_services->request;
503
504
		if ($request->isCliServer()) {
505
			if ($request->isCliServable(Paths::project())) {
506
				return false;
507
			}
508
509
			// overwrite value from settings
510
			$www_root = rtrim($request->getSchemeAndHttpHost() . $request->getBaseUrl(), '/') . '/';
511
			$config->wwwroot = $www_root;
512
			$config->wwwroot_cli_server = $www_root;
1 ignored issue
show
The property wwwroot_cli_server does not seem to exist. Did you mean wwwroot?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
513
		}
514
515
		if (0 === strpos($request->getElggPath(), '/cache/')) {
516
			$this->_services->cacheHandler->handleRequest($request, $this)->prepare($request)->send();
517
			return true;
518
		}
519
520
		if (0 === strpos($request->getElggPath(), '/serve-file/')) {
521
			$this->_services->serveFileHandler->getResponse($request)->send();
522
			return true;
523
		}
524
525
		$this->bootCore();
526
527
		// TODO use formal Response object instead
528
		// This is to set the charset to UTF-8.
529
		header("Content-Type: text/html;charset=utf-8", true);
530
531
		// re-fetch new request from services in case it was replaced by route:rewrite
532
		$request = $this->_services->request;
533
534
		if (!$this->_services->router->route($request)) {
535
			forward('', '404');
536
		}
537
	}
538
539
	/**
540
	 * Returns a directory that points to the root of Elgg, but not necessarily
541
	 * the install root. See `self::root()` for that.
542
	 *
543
	 * @return Directory
544
	 */
545 1
	public static function elggDir() {
546 1
		return Local::elggRoot();
547
	}
548
549
	/**
550
	 * Returns a directory that points to the project root, where composer is installed.
551
	 *
552
	 * @return Directory
553
	 */
554
	public static function projectDir() {
555
		return Local::projectRoot();
556
	}
557
558
	/**
559
	 * Renders a web UI for installing Elgg.
560
	 *
561
	 * @return void
562
	 */
563
	public static function install() {
564
		ini_set('display_errors', 1);
565
		$installer = new \ElggInstaller();
566
		$installer->run();
567
	}
568
569
	/**
570
	 * Elgg upgrade script.
571
	 *
572
	 * This script triggers any necessary upgrades. If the site has been upgraded
573
	 * to the most recent version of the code, no upgrades are run but the caches
574
	 * are flushed.
575
	 *
576
	 * Upgrades use a table {db_prefix}upgrade_lock as a mutex to prevent concurrent upgrades.
577
	 *
578
	 * The URL to forward to after upgrades are complete can be specified by setting $_GET['forward']
579
	 * to a relative URL.
580
	 *
581
	 * @return void
582
	 */
583
	public static function upgrade() {
584
		// we want to know if an error occurs
585
		ini_set('display_errors', 1);
586
		$is_cli = (php_sapi_name() === 'cli');
587
588
		$forward = function ($url) use ($is_cli) {
589
			if ($is_cli) {
590
				fwrite(STDOUT, "Open $url in your browser to continue." . PHP_EOL);
591
				return;
592
			}
593
594
			forward($url);
595
		};
596
597
		define('UPGRADING', 'upgrading');
598
599
		self::migrate();
600
		self::start();
601
602
		// check security settings
603
		if (!$is_cli && _elgg_config()->security_protect_upgrade && !elgg_is_admin_logged_in()) {
604
			// only admin's or users with a valid token can run upgrade.php
605
			elgg_signed_request_gatekeeper();
606
		}
607
608
		$site_url = _elgg_config()->url;
609
		$site_host = parse_url($site_url, PHP_URL_HOST) . '/';
610
611
		// turn any full in-site URLs into absolute paths
612
		$forward_url = get_input('forward', '/admin', false);
613
		$forward_url = str_replace([$site_url, $site_host], '/', $forward_url);
614
615
		if (strpos($forward_url, '/') !== 0) {
616
			$forward_url = '/' . $forward_url;
617
		}
618
619
		if ($is_cli || (get_input('upgrade') == 'upgrade')) {
620
			$upgrader = _elgg_services()->upgrades;
621
			$result = $upgrader->run();
622
623
			if ($result['failure'] == true) {
624
				register_error($result['reason']);
625
				$forward($forward_url);
626
			}
627
628
			// Find unprocessed batch upgrade classes and save them as ElggUpgrade objects
629
			$core_upgrades = (require self::elggDir()->getPath('engine/lib/upgrades/async-upgrades.php'));
630
			$has_pending_upgrades = _elgg_services()->upgradeLocator->run($core_upgrades);
631
632
			if ($has_pending_upgrades) {
633
				// Forward to the list of pending upgrades
634
				$forward_url = '/admin/upgrades';
635
			}
636
		} else {
637
			$rewriteTester = new \ElggRewriteTester();
638
			$url = elgg_get_site_url() . "__testing_rewrite?__testing_rewrite=1";
639
			if (!$rewriteTester->runRewriteTest($url)) {
640
				// see if there is a problem accessing the site at all
641
				// due to ip restrictions for example
642
				if (!$rewriteTester->runLocalhostAccessTest()) {
643
					// note: translation may not be available until after upgrade
644
					$msg = elgg_echo("installation:htaccess:localhost:connectionfailed");
645
					if ($msg === "installation:htaccess:localhost:connectionfailed") {
646
						$msg = "Elgg cannot connect to itself to test rewrite rules properly. Check "
647
								. "that curl is working and there are no IP restrictions preventing "
648
								. "localhost connections.";
649
					}
650
					echo $msg;
651
					exit;
652
				}
653
654
				// note: translation may not be available until after upgrade
655
				$msg = elgg_echo("installation:htaccess:needs_upgrade");
656
				if ($msg === "installation:htaccess:needs_upgrade") {
657
					$msg = "You must update your .htaccess file (use install/config/htaccess.dist as a guide).";
658
				}
659
				echo $msg;
660
				exit;
661
			}
662
663
			$vars = [
664
				'forward' => $forward_url
665
			];
666
667
			// reset cache to have latest translations available during upgrade
668
			elgg_reset_system_cache();
669
670
			echo elgg_view_page(elgg_echo('upgrading'), '', 'upgrade', $vars);
671
			exit;
672
		}
673
674
		$forward($forward_url);
675
	}
676
677
	/**
678
	 * Runs database migrations
679
	 *
680
	 * @throws InstallationException
681
	 * @return bool
682
	 */
683
	public static function migrate() {
684
		$conf = self::elggDir()->getPath('engine/schema/settings.php');
685
		if (!$conf) {
686
			throw new Exception('Settings file is required to run database migrations.');
687
		}
688
689
		$app = new \Phinx\Console\PhinxApplication();
690
		$wrapper = new \Phinx\Wrapper\TextWrapper($app, [
691
			'configuration' => $conf,
692
		]);
693
		$log = $wrapper->getMigrate();
694
		error_log($log);
695
696
		return true;
697
	}
698
699
	/**
700
	 * Returns configuration array for database migrations
701
	 * @return array
702
	 */
703
	public static function getMigrationSettings() {
704
705
		$config = Config::factory();
706
		$db_config = DbConfig::fromElggConfig($config);
707
		if ($db_config->isDatabaseSplit()) {
708
			$conn = $db_config->getConnectionConfig(DbConfig::WRITE);
709
		} else {
710
			$conn = $db_config->getConnectionConfig();
711
		}
712
713
		return [
714
			"paths" => [
715
				"migrations" => Paths::elgg() . 'engine/schema/migrations/',
716
			],
717
			"environments" => [
718
				"default_migration_table" => "{$conn['prefix']}migrations",
719
				"default_database" => "prod",
720
				"prod" => [
721
					"adapter" => "mysql",
722
					"host" => $conn['host'],
723
					"name" => $conn['database'],
724
					"user" => $conn['user'],
725
					"pass" => $conn['password'],
726
					"charset" => $conn['encoding'],
727
					"table_prefix" => $conn['prefix'],
728
				],
729
			],
730
		];
731
	}
732
733
	/**
734
	 * Allow plugins to rewrite the path.
735
	 *
736
	 * @return void
737
	 */
738
	private function allowPathRewrite() {
739
		$request = $this->_services->request;
740
		$new = $this->_services->router->allowRewrite($request);
741
		if ($new === $request) {
742
			return;
743
		}
744
745
		$this->_services->setValue('request', $new);
746
		$this->_services->context->initialize($new);
747
	}
748
749
	/**
750
	 * Flag this application as running for testing (PHPUnit)
751
	 *
752
	 * @param bool $testing Is testing application
753
	 * @return void
754
	 */
755 197
	public static function setTestingApplication($testing = true) {
756 197
		self::$testing_app = $testing;
757 197
	}
758
759
	/**
760
	 * Checks if the application is running in PHPUnit
761
	 * @return bool
762
	 */
763 1
	public static function isTestingApplication() {
764 1
		return (bool) self::$testing_app;
765
	}
766
767
	/**
768
	 * Intercepts, logs, and displays uncaught exceptions.
769
	 *
770
	 * To use a viewtype other than failsafe, create the views:
771
	 *  <viewtype>/messages/exceptions/admin_exception
772
	 *  <viewtype>/messages/exceptions/exception
773
	 * See the json viewtype for an example.
774
	 *
775
	 * @warning This function should never be called directly.
776
	 *
777
	 * @see http://www.php.net/set-exception-handler
778
	 *
779
	 * @param \Exception|\Error $exception The exception/error being handled
780
	 *
781
	 * @return void
782
	 * @access private
783
	 */
784
	public function handleExceptions($exception) {
785
		$timestamp = time();
786
		error_log("Exception at time $timestamp: $exception");
787
788
		// Wipe any existing output buffer
789
		ob_end_clean();
790
791
		// make sure the error isn't cached
792
		header("Cache-Control: no-cache, must-revalidate", true);
793
		header('Expires: Fri, 05 Feb 1982 00:00:00 -0500', true);
794
795
		if ($exception instanceof \InstallationException) {
796
			forward('/install.php');
797
		}
798
799
		if (!self::isCoreLoaded()) {
800
			http_response_code(500);
801
			echo "Exception loading Elgg core. Check log at time $timestamp";
802
			return;
803
		}
804
805
		try {
806
			// allow custom scripts to trigger on exception
807
			// value in settings.php should be a system path to a file to include
808
			$exception_include = $this->_services->config->exception_include;
809
810
			if ($exception_include && is_file($exception_include)) {
811
				ob_start();
812
813
				// don't isolate, these scripts may use the local $exception var.
814
				include $exception_include;
815
816
				$exception_output = ob_get_clean();
817
818
				// if content is returned from the custom handler we will output
819
				// that instead of our default failsafe view
820
				if (!empty($exception_output)) {
821
					echo $exception_output;
822
					exit;
823
				}
824
			}
825
826
			if (elgg_is_xhr()) {
827
				elgg_set_viewtype('json');
828
				$response = new \Symfony\Component\HttpFoundation\JsonResponse(null, 500);
829
			} else {
830
				elgg_set_viewtype('failsafe');
831
				$response = new \Symfony\Component\HttpFoundation\Response('', 500);
832
			}
833
834
			if (elgg_is_admin_logged_in()) {
835
				$body = elgg_view("messages/exceptions/admin_exception", [
836
					'object' => $exception,
837
					'ts' => $timestamp
838
				]);
839
			} else {
840
				$body = elgg_view("messages/exceptions/exception", [
841
					'object' => $exception,
842
					'ts' => $timestamp
843
				]);
844
			}
845
846
			$response->setContent(elgg_view_page(elgg_echo('exception:title'), $body));
847
			$response->send();
848
		} catch (\Exception $e) {
849
			$timestamp = time();
850
			$message = $e->getMessage();
851
			http_response_code(500);
852
			echo "Fatal error in exception handler. Check log for Exception at time $timestamp";
853
			error_log("Exception at time $timestamp : fatal error in exception handler : $message");
854
		}
855
	}
856
857
	/**
858
	 * Intercepts catchable PHP errors.
859
	 *
860
	 * @warning This function should never be called directly.
861
	 *
862
	 * @internal
863
	 * For catchable fatal errors, throws an Exception with the error.
864
	 *
865
	 * For non-fatal errors, depending upon the debug settings, either
866
	 * log the error or ignore it.
867
	 *
868
	 * @see http://www.php.net/set-error-handler
869
	 *
870
	 * @param int    $errno    The level of the error raised
871
	 * @param string $errmsg   The error message
872
	 * @param string $filename The filename the error was raised in
873
	 * @param int    $linenum  The line number the error was raised at
874
	 * @param array  $vars     An array that points to the active symbol table where error occurred
875
	 *
876
	 * @return true
877
	 * @throws \Exception
878
	 * @access private
879
	 */
880
	public function handleErrors($errno, $errmsg, $filename, $linenum, $vars) {
881
		$error = date("Y-m-d H:i:s (T)") . ": \"$errmsg\" in file $filename (line $linenum)";
882
883
		$log = function ($message, $level) {
884
			if (self::isCoreLoaded()) {
885
				return elgg_log($message, $level);
886
			}
887
888
			return false;
889
		};
890
891
		switch ($errno) {
892
			case E_USER_ERROR:
893
				if (!$log("PHP: $error", 'ERROR')) {
894
					error_log("PHP ERROR: $error");
895
				}
896
				if (self::isCoreLoaded()) {
897
					register_error("ERROR: $error");
898
				}
899
900
				// Since this is a fatal error, we want to stop any further execution but do so gracefully.
901
				throw new \Exception($error);
902
				break;
903
904
			case E_WARNING :
905
			case E_USER_WARNING :
906
			case E_RECOVERABLE_ERROR: // (e.g. type hint violation)
907
908
				// check if the error wasn't suppressed by the error control operator (@)
909
				if (error_reporting() && !$log("PHP: $error", 'WARNING')) {
910
					error_log("PHP WARNING: $error");
911
				}
912
				break;
913
914
			default:
915
				if (function_exists('_elgg_config')) {
916
					$debug = _elgg_config()->debug;
917
				} else {
918
					$debug = isset($GLOBALS['CONFIG']->debug) ? $GLOBALS['CONFIG']->debug : null;
919
				}
920
				if ($debug !== 'NOTICE') {
921
					return true;
922
				}
923
924
				if (!$log("PHP (errno $errno): $error", 'NOTICE')) {
925
					error_log("PHP NOTICE: $error");
926
				}
927
		}
928
929
		return true;
930
	}
931
932
	/**
933
	 * Does nothing.
934
	 *
935
	 * @return void
936
	 * @deprecated
937
	 */
938
	public function loadSettings() {
939
		trigger_error(__METHOD__ . ' is no longer needed and will be removed.');
940
	}
941
942
	/**
943
	 * Get all engine/lib library filenames
944
	 *
945
	 * @note We can't just pull in all directory files because some users leave old files in place.
946
	 *
947
	 * @return string[]
948
	 */
949
	private static function getEngineLibs() {
950
		return [
951
			'access.php',
952
			'actions.php',
953
			'admin.php',
954
			'annotations.php',
955
			'cache.php',
956
			'comments.php',
957
			'configuration.php',
958
			'constants.php',
959
			'cron.php',
960
			'database.php',
961
			'deprecated-3.0.php',
962
			'elgglib.php',
963
			'entities.php',
964
			'filestore.php',
965
			'group.php',
966
			'input.php',
967
			'languages.php',
968
			'mb_wrapper.php',
969
			'memcache.php',
970
			'metadata.php',
971
			'metastrings.php',
972
			'navigation.php',
973
			'notification.php',
974
			'output.php',
975
			'pagehandler.php',
976
			'pageowner.php',
977
			'pam.php',
978
			'plugins.php',
979
			'private_settings.php',
980
			'relationships.php',
981
			'river.php',
982
			'sessions.php',
983
			'statistics.php',
984
			'system_log.php',
985
			'tags.php',
986
			'upgrade.php',
987
			'user_settings.php',
988
			'users.php',
989
			'views.php',
990
			'widgets.php',
991
		];
992
	}
993
}
994