Test Failed
Push — master ( 8c47c2...3acf9f )
by Steve
12:37
created

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

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\Di\ServiceProvider;
6
use Elgg\Filesystem\Directory;
7
8
/**
9
 * Load, boot, and implement a front controller for an Elgg application
10
 *
11
 * To run as PHP CLI server:
12
 * <code>php -S localhost:8888 /full/path/to/elgg/index.php</code>
13
 *
14
 * The full path is necessary to work around this: https://bugs.php.net/bug.php?id=55726
15
 *
16
 * @since 2.0.0
17
 *
18
 * @property-read \Elgg\Menu\Service $menus
19
 * @property-read \Elgg\Views\TableColumn\ColumnFactory $table_columns
20
 */
21
class Application {
22
23
	const REWRITE_TEST_TOKEN = '__testing_rewrite';
24
	const REWRITE_TEST_OUTPUT = 'success';
25
26
	/**
27
	 * @var ServiceProvider
28
	 */
29
	private $services;
30
31
	/**
32
	 * @var string
33
	 */
34
	private $engine_dir;
35
36
	/**
37
	 * @var bool
38
	 */
39
	private static $testing_app;
40
41
	/**
42
	 * Property names of the service provider to be exposed via __get()
43
	 *
44
	 * E.g. the presence of `'foo' => true` in the list would allow _elgg_services()->foo to
45
	 * be accessed via elgg()->foo.
46
	 *
47
	 * @var string[]
48
	 */
49
	private static $public_services = [
50
		//'config' => true,
51
		'menus' => true,
52
		'table_columns' => true,
53
	];
54
55
	/**
56
	 * Reference to the loaded Application returned by elgg()
57
	 *
58
	 * @internal Do not use this. use elgg() to access the application
59
	 * @access private
60
	 * @var Application
61
	 */
62
	public static $_instance;
63
64
	/**
65
	 * Constructor
66
	 *
67
	 * Upon construction, no actions are taken to load or boot Elgg.
68
	 *
69
	 * @param ServiceProvider $services Elgg services provider
70
	 */
71 198
	public function __construct(ServiceProvider $services) {
0 ignored issues
show
__construct uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
72 198
		$this->services = $services;
73
74
		/**
75
		 * The time with microseconds when the Elgg engine was started.
76
		 *
77
		 * @global float
78
		 */
79 198
		if (!isset($GLOBALS['START_MICROTIME'])) {
80 1
			$GLOBALS['START_MICROTIME'] = microtime(true);
81
		}
82
83 198
		$services->timer->begin([]);
84
85
		/**
86
		 * This was introduced in 2.0 in order to remove all internal non-API state from $CONFIG. This will
87
		 * be a breaking change, but frees us to refactor in 2.x without fear of plugins depending on
88
		 * $CONFIG.
89
		 *
90
		 * @access private
91
		 */
92 198
		if (!isset($GLOBALS['_ELGG'])) {
93
			$GLOBALS['_ELGG'] = new \stdClass();
94
		}
95
96 198
		$this->engine_dir = dirname(dirname(__DIR__));
97 198
	}
98
99
	/**
100
	 * Load settings.php
101
	 *
102
	 * This is done automatically during the boot process or before requesting a database object
103
	 *
104
	 * @see Config::loadSettingsFile
105
	 * @return void
106
	 */
107
	public function loadSettings() {
108
		$this->services->config->loadSettingsFile();
109
	}
110
111
	/**
112
	 * Load all Elgg procedural code and wire up boot events, but don't boot
113
	 *
114
	 * This is used for internal testing purposes
115
	 *
116
	 * @return void
117
	 * @access private
118
	 * @internal
119
	 */
120 196
	public function loadCore() {
121 196
		if (function_exists('elgg')) {
122 196
			return;
123
		}
124
125
		$lib_dir = self::elggDir()->chroot("engine/lib");
126
127
		// load the rest of the library files from engine/lib/
128
		// All on separate lines to make diffs easy to read + make it apparent how much
129
		// we're actually loading on every page (Hint: it's too much).
130
		$lib_files = [
131
			// Needs to be loaded first to correctly bootstrap
132
			'autoloader.php',
133
			'elgglib.php',
134
135
			// The order of these doesn't matter, so keep them alphabetical
136
			'access.php',
137
			'actions.php',
138
			'admin.php',
139
			'annotations.php',
140
			'cache.php',
141
			'comments.php',
142
			'configuration.php',
143
			'cron.php',
144
			'database.php',
145
			'entities.php',
146
			'filestore.php',
147
			'group.php',
148
			'input.php',
149
			'languages.php',
150
			'mb_wrapper.php',
151
			'memcache.php',
152
			'metadata.php',
153
			'metastrings.php',
154
			'navigation.php',
155
			'notification.php',
156
			'objects.php',
157
			'output.php',
158
			'pagehandler.php',
159
			'pageowner.php',
160
			'pam.php',
161
			'plugins.php',
162
			'private_settings.php',
163
			'relationships.php',
164
			'river.php',
165
			'sessions.php',
166
			'sites.php',
167
			'statistics.php',
168
			'system_log.php',
169
			'tags.php',
170
			'user_settings.php',
171
			'users.php',
172
			'upgrade.php',
173
			'views.php',
174
			'widgets.php',
175
176
			// backward compatibility
177
			'deprecated-3.0.php',
178
		];
179
180
		// isolate global scope
181
		call_user_func(function () use ($lib_dir, $lib_files) {
182
183
			$setups = [];
184
185
			// include library files, capturing setup functions
186
			foreach ($lib_files as $file) {
187
				$setup = (require_once $lib_dir->getPath($file));
188
189
				if ($setup instanceof \Closure) {
190
					$setups[$file] = $setup;
191
				}
192
			}
193
194
			// store instance to be returned by elgg()
195
			self::$_instance = $this;
196
197
			// set up autoloading and DIC
198
			_elgg_services($this->services);
199
200
			$events = $this->services->events;
201
			$hooks = $this->services->hooks;
202
203
			// run setups
204
			foreach ($setups as $func) {
205
				$func($events, $hooks);
206
			}
207
		});
208
	}
209
210
	/**
211
	 * Start and boot the core
212
	 *
213
	 * @return self
214
	 */
215
	public static function start() {
216
		$app = self::create();
217
		$app->bootCore();
218
		return $app;
219
	}
220
221
	/**
222
	 * Bootstrap the Elgg engine, loads plugins, and calls initial system events
223
	 *
224
	 * This method loads the full Elgg engine, checks the installation
225
	 * state, and triggers a series of events to finish booting Elgg:
226
	 * 	- {@elgg_event boot system}
227
	 * 	- {@elgg_event init system}
228
	 * 	- {@elgg_event ready system}
229
	 *
230
	 * If Elgg is not fully installed, the browser will be redirected to an installation page.
231
	 *
232
	 * @return void
233
	 */
234
	public function bootCore() {
235
236
		$config = $this->services->config;
237
238
		if ($this->isTestingApplication()) {
239
			throw new \RuntimeException('Unit tests should not call ' . __METHOD__);
240
		}
241
242
		if ($config->getVolatile('boot_complete')) {
243
			return;
244
		}
245
246
		$this->loadSettings();
247
		$this->resolveWebRoot();
248
249
		$config->set('boot_complete', false);
250
251
		// This will be overridden by the DB value but may be needed before the upgrade script can be run.
252
		$config->set('default_limit', 10);
253
254
		// in case not loaded already
255
		$this->loadCore();
256
257
		$events = $this->services->events;
258
259
		// Connect to database, load language files, load configuration, init session
260
		$this->services->boot->boot();
261
		elgg_views_boot();
262
263
		// Load the plugins that are active
264
		$this->services->plugins->load();
265
266
		$root = Directory\Local::root();
267
		if ($root->getPath() != self::elggDir()->getPath()) {
268
			// Elgg is installed as a composer dep, so try to treat the root directory
269
			// as a custom plugin that is always loaded last and can't be disabled...
270
			if (!elgg_get_config('system_cache_loaded')) {
271
				// configure view locations for the custom plugin (not Elgg core)
272
				$viewsFile = $root->getFile('views.php');
273
				if ($viewsFile->exists()) {
274
					$viewsSpec = $viewsFile->includeFile();
275
					if (is_array($viewsSpec)) {
276
						_elgg_services()->views->mergeViewsSpec($viewsSpec);
277
					}
278
				}
279
280
				// find views for the custom plugin (not Elgg core)
281
				_elgg_services()->views->registerPluginViews($root->getPath());
282
			}
283
284
			if (!elgg_get_config('i18n_loaded_from_cache')) {
285
				_elgg_services()->translator->registerPluginTranslations($root->getPath());
286
			}
287
288
			// This is root directory start.php
289
			$root_start = $root->getPath("start.php");
290
			if (is_file($root_start)) {
291
				require $root_start;
292
			}
293
		}
294
295
		// after plugins are started we know which viewtypes are populated
296
		$this->services->views->clampViewtypeToPopulatedViews();
297
298
		$this->allowPathRewrite();
299
300
		// Allows registering handlers strictly before all init, system handlers
301
		$events->trigger('plugins_boot', 'system');
302
303
		// Complete the boot process for both engine and plugins
304
		$events->trigger('init', 'system');
305
306
		$config->set('boot_complete', true);
307
308
		// System loaded and ready
309
		$events->trigger('ready', 'system');
310
	}
311
312
	/**
313
	 * Get a Database wrapper for performing queries without booting Elgg
314
	 *
315
	 * If settings.php has not been loaded, it will be loaded to configure the DB connection.
316
	 *
317
	 * @note Before boot, the Database instance will not yet be bound to a Logger.
318
	 *
319
	 * @return \Elgg\Application\Database
320
	 */
321
	public function getDb() {
322
		$this->loadSettings();
323
		return $this->services->publicDb;
324
	}
325
326
	/**
327
	 * Get an undefined property
328
	 *
329
	 * @param string $name The property name accessed
330
	 *
331
	 * @return mixed
332
	 */
333
	public function __get($name) {
334
		if (isset(self::$public_services[$name])) {
335
			return $this->services->{$name};
336
		}
337
		trigger_error("Undefined property: " . __CLASS__ . ":\${$name}");
338
	}
339
340
	/**
341
	 * Creates a new, trivial instance of Elgg\Application and set it as the singleton instance.
342
	 * If the singleton is already set, it's returned.
343
	 *
344
	 * @return self
345
	 */
346
	private static function create() {
347
		if (self::$_instance === null) {
348
			// we need to register for shutdown before Symfony registers the
349
			// session_write_close() function. https://github.com/Elgg/Elgg/issues/9243
350
			register_shutdown_function(function () {
351
				// There are cases where we may exit before this function is defined
352
				if (function_exists('_elgg_shutdown_hook')) {
353
					_elgg_shutdown_hook();
354
				}
355
			});
356
357
			self::$_instance = new self(new Di\ServiceProvider(new Config()));
358
		}
359
360
		return self::$_instance;
361
	}
362
363
	/**
364
	 * Elgg's front controller. Handles basically all incoming URL requests.
365
	 *
366
	 * @return bool True if Elgg will handle the request, false if the server should (PHP-CLI server)
367
	 */
368
	public static function index() {
369
		return self::create()->run();
370
	}
371
372
	/**
373
	 * Routes the request, booting core if not yet booted
374
	 *
375
	 * @return bool False if Elgg wants the PHP CLI server to handle the request
376
	 */
377
	public function run() {
0 ignored issues
show
run uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
run uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
378
		$config = $this->services->config;
379
380
		$request = $this->services->request;
381
		$path = $request->getPathInfo();
382
383
		// allow testing from the upgrade page before the site is upgraded.
384
		if (isset($_GET[self::REWRITE_TEST_TOKEN])) {
385
			if (false !== strpos($path, self::REWRITE_TEST_TOKEN)) {
386
				echo self::REWRITE_TEST_OUTPUT;
387
			}
388
			return true;
389
		}
390
391 View Code Duplication
		if (php_sapi_name() === 'cli-server') {
1 ignored issue
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
392
			// overwrite value from settings
393
			$www_root = rtrim($request->getSchemeAndHttpHost() . $request->getBaseUrl(), '/') . '/';
394
			$config->set('wwwroot', $www_root);
395
		}
396
397
		if (0 === strpos($path, '/cache/')) {
398
			$config->loadSettingsFile();
399
			if ($config->getVolatile('simplecache_enabled') === null) {
400
				// allow the value to be loaded if needed
401
				$config->setConfigTable($this->services->configTable);
402
			}
403
			(new Application\CacheHandler($this, $config, $_SERVER))->handleRequest($path);
0 ignored issues
show
$path is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
404
			return true;
405
		}
406
407
		if (0 === strpos($path, '/serve-file/')) {
408
			$this->services->serveFileHandler->getResponse($request)->send();
409
			return true;
410
		}
411
412
		if ($path === '/rewrite.php') {
413
			require Directory\Local::root()->getPath("install.php");
414
			return true;
415
		}
416
417
		if (php_sapi_name() === 'cli-server') {
418
			// The CLI server routes ALL requests here (even existing files), so we have to check for these.
419
			if ($path !== '/' && Directory\Local::root()->isFile($path)) {
420
				// serve the requested resource as-is.
421
				return false;
422
			}
423
		}
424
425
		$this->bootCore();
426
427
		// TODO use formal Response object instead
428
		header("Content-Type: text/html;charset=utf-8");
429
430
		// fetch new request from services in case it was replaced by route:rewrite
431
		if (!$this->services->router->route($this->services->request)) {
432
			forward('', '404');
433
		}
434
	}
435
436
	/**
437
	 * Get the Elgg data directory with trailing slash
438
	 *
439
	 * @return string
440
	 */
441
	public static function getDataPath() {
442
		return self::create()->services->config->getDataPath();
443
	}
444
445
	/**
446
	 * Returns a directory that points to the root of Elgg, but not necessarily
447
	 * the install root. See `self::root()` for that.
448
	 *
449
	 * @return Directory
450
	 */
451
	public static function elggDir() /*: Directory*/ {
452
		return Directory\Local::fromPath(realpath(__DIR__ . '/../../..'));
453
	}
454
455
	/**
456
	 * Renders a web UI for installing Elgg.
457
	 *
458
	 * @return void
459
	 */
460
	public static function install() {
461
		ini_set('display_errors', 1);
462
		$installer = new \ElggInstaller();
463
		$step = get_input('step', 'welcome');
464
		$installer->run($step);
465
	}
466
467
	/**
468
	 * Elgg upgrade script.
469
	 *
470
	 * This script triggers any necessary upgrades. If the site has been upgraded
471
	 * to the most recent version of the code, no upgrades are run but the caches
472
	 * are flushed.
473
	 *
474
	 * Upgrades use a table {db_prefix}upgrade_lock as a mutex to prevent concurrent upgrades.
475
	 *
476
	 * The URL to forward to after upgrades are complete can be specified by setting $_GET['forward']
477
	 * to a relative URL.
478
	 *
479
	 * @return void
480
	 */
481
	public static function upgrade() {
482
		// we want to know if an error occurs
483
		ini_set('display_errors', 1);
484
		$is_cli = (php_sapi_name() === 'cli');
485
486
		$forward = function ($url) use ($is_cli) {
487
			if ($is_cli) {
488
				echo "Open $url in your browser to continue.";
489
				exit;
490
			}
491
492
			forward($url);
493
		};
494
495
		define('UPGRADING', 'upgrading');
496
497
		self::start();
498
		
499
		// check security settings
500
		if (!$is_cli && elgg_get_config('security_protect_upgrade') && !elgg_is_admin_logged_in()) {
501
			// only admin's or users with a valid token can run upgrade.php
502
			elgg_signed_request_gatekeeper();
503
		}
504
		
505
		$site_url = elgg_get_config('url');
506
		$site_host = parse_url($site_url, PHP_URL_HOST) . '/';
507
508
		// turn any full in-site URLs into absolute paths
509
		$forward_url = get_input('forward', '/admin', false);
510
		$forward_url = str_replace([$site_url, $site_host], '/', $forward_url);
511
512
		if (strpos($forward_url, '/') !== 0) {
513
			$forward_url = '/' . $forward_url;
514
		}
515
516
		if ($is_cli || (get_input('upgrade') == 'upgrade')) {
517
			$upgrader = _elgg_services()->upgrades;
518
			$result = $upgrader->run();
519
520
			if ($result['failure'] == true) {
521
				register_error($result['reason']);
522
				$forward($forward_url);
523
			}
524
525
			// Find unprocessed batch upgrade classes and save them as ElggUpgrade objects
526
			$core_upgrades = (require self::elggDir()->getPath('engine/lib/upgrades/async-upgrades.php'));
527
			$has_pending_upgrades = _elgg_services()->upgradeLocator->run($core_upgrades);
528
529
			if ($has_pending_upgrades) {
530
				// Forward to the list of pending upgrades
531
				$forward_url = '/admin/upgrades';
532
			}
533
		} else {
534
			$rewriteTester = new \ElggRewriteTester();
535
			$url = elgg_get_site_url() . "__testing_rewrite?__testing_rewrite=1";
536
			if (!$rewriteTester->runRewriteTest($url)) {
537
				// see if there is a problem accessing the site at all
538
				// due to ip restrictions for example
539
				if (!$rewriteTester->runLocalhostAccessTest()) {
540
					// note: translation may not be available until after upgrade
541
					$msg = elgg_echo("installation:htaccess:localhost:connectionfailed");
542
					if ($msg === "installation:htaccess:localhost:connectionfailed") {
543
						$msg = "Elgg cannot connect to itself to test rewrite rules properly. Check "
544
								. "that curl is working and there are no IP restrictions preventing "
545
								. "localhost connections.";
546
					}
547
					echo $msg;
548
					exit;
549
				}
550
551
				// note: translation may not be available until after upgrade
552
				$msg = elgg_echo("installation:htaccess:needs_upgrade");
553
				if ($msg === "installation:htaccess:needs_upgrade") {
554
					$msg = "You must update your .htaccess file (use install/config/htaccess.dist as a guide).";
555
				}
556
				echo $msg;
557
				exit;
558
			}
559
560
			$vars = [
561
				'forward' => $forward_url
562
			];
563
564
			// reset cache to have latest translations available during upgrade
565
			elgg_reset_system_cache();
566
567
			echo elgg_view_page(elgg_echo('upgrading'), '', 'upgrade', $vars);
568
			exit;
569
		}
570
571
		$forward($forward_url);
572
	}
573
574
	/**
575
	 * Allow plugins to rewrite the path.
576
	 *
577
	 * @return void
578
	 */
579
	private function allowPathRewrite() {
580
		$request = $this->services->request;
581
		$new = $this->services->router->allowRewrite($request);
582
		if ($new === $request) {
583
			return;
584
		}
585
586
		$this->services->setValue('request', $new);
587
		$this->services->context->initialize($new);
588
	}
589
590
	/**
591
	 * Make sure config has a non-empty wwwroot. Calculate from request if missing.
592
	 *
593
	 * @return void
594
	 */
595
	private function resolveWebRoot() {
596
		$config = $this->services->config;
597
		$request = $this->services->request;
598
599
		$config->loadSettingsFile();
600 View Code Duplication
		if (!$config->getVolatile('wwwroot')) {
1 ignored issue
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
601
			$www_root = rtrim($request->getSchemeAndHttpHost() . $request->getBaseUrl(), '/') . '/';
602 196
			$config->set('wwwroot', $www_root);
603 196
		}
604 196
	}
605
606
	/**
607
	 * Flag this application as running for testing (PHPUnit)
608
	 *
609
	 * @param bool $testing Is testing application
610 1
	 * @return void
611 1
	 */
612
	public static function setTestingApplication($testing = true) {
613
		self::$testing_app = $testing;
614
	}
615
616
	/**
617
	 * Checks if the application is running in PHPUnit
618
	 * @return bool
619
	 */
620
	public static function isTestingApplication() {
621
		return (bool) self::$testing_app;
622
	}
623
}
624