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(); |
||
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
|
|||
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
|
|||
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
|
|||
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; |
||
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 |
In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.