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) { |
|
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() { |
||
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
|
|||
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); |
||
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')) { |
|
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 |
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.