Elgg /
Elgg
| 1 | <?php |
||
| 2 | |||
| 3 | namespace Elgg; |
||
| 4 | |||
| 5 | use ConfigurationException; |
||
| 6 | use Doctrine\DBAL\Connection; |
||
| 7 | use Elgg\Database\DbConfig; |
||
| 8 | use Elgg\Di\ServiceProvider; |
||
| 9 | use Elgg\Filesystem\Directory; |
||
| 10 | use Elgg\Filesystem\Directory\Local; |
||
| 11 | use Elgg\Http\ErrorResponse; |
||
| 12 | use Elgg\Http\Request; |
||
| 13 | use Elgg\Project\Paths; |
||
| 14 | use InstallationException; |
||
| 15 | use InvalidArgumentException; |
||
| 16 | use InvalidParameterException; |
||
| 17 | use SecurityException; |
||
| 18 | |||
| 19 | /** |
||
| 20 | * Load, boot, and implement a front controller for an Elgg application |
||
| 21 | * |
||
| 22 | * To run as PHP CLI server: |
||
| 23 | * <code>php -S localhost:8888 /full/path/to/elgg/index.php</code> |
||
| 24 | * |
||
| 25 | * The full path is necessary to work around this: https://bugs.php.net/bug.php?id=55726 |
||
| 26 | * |
||
| 27 | * @since 2.0.0 |
||
| 28 | * |
||
| 29 | * @property-read \Elgg\Menu\Service $menus |
||
| 30 | * @property-read \Elgg\Views\TableColumn\ColumnFactory $table_columns |
||
| 31 | */ |
||
| 32 | class Application { |
||
| 33 | |||
| 34 | const DEFAULT_LANG = 'en'; |
||
| 35 | const DEFAULT_LIMIT = 10; |
||
| 36 | |||
| 37 | /** |
||
| 38 | * @var ServiceProvider |
||
| 39 | * |
||
| 40 | * @internal DO NOT USE |
||
| 41 | */ |
||
| 42 | public $_services; |
||
| 43 | |||
| 44 | /** |
||
| 45 | * @var \Closure[] |
||
| 46 | */ |
||
| 47 | private static $_setups = []; |
||
| 48 | |||
| 49 | /** |
||
| 50 | * Property names of the service provider to be exposed via __get() |
||
| 51 | * |
||
| 52 | * E.g. the presence of `'foo' => true` in the list would allow _elgg_services()->foo to |
||
| 53 | * be accessed via elgg()->foo. |
||
| 54 | * |
||
| 55 | * @var string[] |
||
| 56 | */ |
||
| 57 | private static $public_services = [ |
||
| 58 | //'config' => true, |
||
| 59 | 'menus' => true, |
||
| 60 | 'table_columns' => true, |
||
| 61 | ]; |
||
| 62 | |||
| 63 | /** |
||
| 64 | * Reference to the loaded Application returned by elgg() |
||
| 65 | * |
||
| 66 | * @internal Do not use this. use elgg() to access the application |
||
| 67 | * @access private |
||
| 68 | * @var Application |
||
| 69 | */ |
||
| 70 | public static $_instance; |
||
| 71 | |||
| 72 | /** |
||
| 73 | * Get the global Application instance. If not set, it's auto-created and wired to $CONFIG. |
||
| 74 | * |
||
| 75 | * @return Application|null |
||
| 76 | */ |
||
| 77 | 625 | public static function getInstance() { |
|
| 78 | 625 | if (self::$_instance === null) { |
|
| 79 | self::$_instance = self::factory(); |
||
| 80 | self::setGlobalConfig(self::$_instance); |
||
| 81 | } |
||
| 82 | 625 | return self::$_instance; |
|
| 83 | } |
||
| 84 | |||
| 85 | /** |
||
| 86 | * Set the global Application instance |
||
| 87 | * |
||
| 88 | * @param Application $application Global application |
||
| 89 | * @return void |
||
| 90 | */ |
||
| 91 | 5389 | public static function setInstance(Application $application = null) { |
|
| 92 | 5389 | self::$_instance = $application; |
|
| 93 | 5389 | } |
|
| 94 | |||
| 95 | /** |
||
| 96 | * Constructor |
||
| 97 | * |
||
| 98 | * Upon construction, no actions are taken to load or boot Elgg. |
||
| 99 | * |
||
| 100 | * @param ServiceProvider $services Elgg services provider |
||
| 101 | * @throws ConfigurationException |
||
| 102 | */ |
||
| 103 | 4417 | public function __construct(ServiceProvider $services) { |
|
| 104 | 4417 | $this->_services = $services; |
|
| 105 | 4417 | } |
|
| 106 | |||
| 107 | /** |
||
| 108 | * Define all Elgg global functions and constants, wire up boot events, but don't boot |
||
| 109 | * |
||
| 110 | * This includes all the .php files in engine/lib (not upgrades). If a script returns a function, |
||
| 111 | * it is queued and executed at the end. |
||
| 112 | * |
||
| 113 | * @return void |
||
| 114 | * @access private |
||
| 115 | * @internal |
||
| 116 | * @throws \InstallationException |
||
| 117 | */ |
||
| 118 | 4417 | public static function loadCore() { |
|
| 119 | 4417 | if (self::isCoreLoaded()) { |
|
| 120 | 4417 | return; |
|
| 121 | } |
||
| 122 | |||
| 123 | $path = Paths::elgg() . 'engine/lib'; |
||
| 124 | |||
| 125 | // include library files, capturing setup functions |
||
| 126 | foreach (self::getEngineLibs() as $file) { |
||
| 127 | try { |
||
| 128 | self::requireSetupFileOnce("$path/$file"); |
||
| 129 | } catch (\Error $e) { |
||
| 130 | throw new \InstallationException("Elgg lib file failed include: engine/lib/$file"); |
||
| 131 | } |
||
| 132 | } |
||
| 133 | } |
||
| 134 | |||
| 135 | /** |
||
| 136 | * Require a library/plugin file once and capture returned anonymous functions |
||
| 137 | * |
||
| 138 | * @param string $file File to require |
||
| 139 | * @return mixed |
||
| 140 | * @internal |
||
| 141 | * @access private |
||
| 142 | */ |
||
| 143 | 95 | public static function requireSetupFileOnce($file) { |
|
| 144 | 95 | $return = Includer::requireFileOnce($file); |
|
| 145 | 95 | if ($return instanceof \Closure) { |
|
| 146 | self::$_setups[] = $return; |
||
| 147 | } |
||
| 148 | 95 | return $return; |
|
| 149 | } |
||
| 150 | |||
| 151 | /** |
||
| 152 | * Start and boot the core |
||
| 153 | * |
||
| 154 | * @return self |
||
| 155 | */ |
||
| 156 | public static function start() { |
||
| 157 | $app = self::getInstance(); |
||
| 158 | $app->bootCore(); |
||
| 159 | return $app; |
||
| 160 | } |
||
| 161 | |||
| 162 | /** |
||
| 163 | * Are Elgg's global functions loaded? |
||
| 164 | * |
||
| 165 | * @return bool |
||
| 166 | */ |
||
| 167 | 4417 | public static function isCoreLoaded() { |
|
| 168 | 4417 | return function_exists('elgg'); |
|
| 169 | } |
||
| 170 | |||
| 171 | /** |
||
| 172 | * Bootstrap the Elgg engine, loads plugins, and calls initial system events |
||
| 173 | * |
||
| 174 | * This method loads the full Elgg engine, checks the installation |
||
| 175 | * state, and triggers a series of events to finish booting Elgg: |
||
| 176 | * - {@elgg_event boot system} |
||
| 177 | * - {@elgg_event init system} |
||
| 178 | * - {@elgg_event ready system} |
||
| 179 | * |
||
| 180 | * If Elgg is not fully installed, the browser will be redirected to an installation page. |
||
| 181 | * |
||
| 182 | * @return void |
||
| 183 | * @throws InstallationException |
||
| 184 | */ |
||
| 185 | 18 | public function bootCore() { |
|
| 186 | 18 | $config = $this->_services->config; |
|
| 187 | |||
| 188 | 18 | if ($config->boot_complete) { |
|
| 189 | return; |
||
| 190 | } |
||
| 191 | |||
| 192 | // in case not loaded already |
||
| 193 | 18 | $this->loadCore(); |
|
| 194 | |||
| 195 | 18 | $hooks = $this->_services->hooks; |
|
| 196 | 18 | $events = $hooks->getEvents(); |
|
| 197 | |||
| 198 | 18 | foreach (self::$_setups as $setup) { |
|
| 199 | 18 | $setup($events, $hooks); |
|
| 200 | } |
||
| 201 | |||
| 202 | 18 | if (!$this->_services->db) { |
|
| 203 | // no database boot! |
||
| 204 | elgg_views_boot(); |
||
| 205 | $this->_services->session->start(); |
||
| 206 | $this->_services->translator->loadTranslations(); |
||
| 207 | |||
| 208 | actions_init(); |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 209 | _elgg_init(); |
||
| 210 | _elgg_input_init(); |
||
| 211 | _elgg_nav_init(); |
||
| 212 | |||
| 213 | $config->boot_complete = true; |
||
| 214 | $config->lock('boot_complete'); |
||
| 215 | return; |
||
| 216 | } |
||
| 217 | |||
| 218 | // Connect to database, load language files, load configuration, init session |
||
| 219 | 18 | $this->_services->boot->boot($this->_services); |
|
| 220 | |||
| 221 | 18 | elgg_views_boot(); |
|
| 222 | |||
| 223 | // Load the plugins that are active |
||
| 224 | 18 | $this->_services->plugins->load(); |
|
| 225 | |||
| 226 | 18 | if (Paths::project() != Paths::elgg()) { |
|
| 227 | // Elgg is installed as a composer dep, so try to treat the root directory |
||
| 228 | // as a custom plugin that is always loaded last and can't be disabled... |
||
| 229 | if (!$config->system_cache_loaded) { |
||
| 230 | // configure view locations for the custom plugin (not Elgg core) |
||
| 231 | $viewsFile = Paths::project() . 'views.php'; |
||
| 232 | if (is_file($viewsFile)) { |
||
| 233 | $viewsSpec = Includer::includeFile($viewsFile); |
||
| 234 | if (is_array($viewsSpec)) { |
||
| 235 | $this->_services->views->mergeViewsSpec($viewsSpec); |
||
| 236 | } |
||
| 237 | } |
||
| 238 | |||
| 239 | // find views for the custom plugin (not Elgg core) |
||
| 240 | $this->_services->views->registerPluginViews(Paths::project()); |
||
| 241 | } |
||
| 242 | |||
| 243 | if (!$config->i18n_loaded_from_cache) { |
||
| 244 | $this->_services->translator->registerPluginTranslations(Paths::project()); |
||
| 245 | } |
||
| 246 | |||
| 247 | // This is root directory start.php |
||
| 248 | $root_start = Paths::project() . "start.php"; |
||
| 249 | if (is_file($root_start)) { |
||
| 250 | require $root_start; |
||
| 251 | } |
||
| 252 | } |
||
| 253 | |||
| 254 | // after plugins are started we know which viewtypes are populated |
||
| 255 | 18 | $this->_services->views->clampViewtypeToPopulatedViews(); |
|
| 256 | |||
| 257 | 18 | $this->allowPathRewrite(); |
|
| 258 | |||
| 259 | // Allows registering handlers strictly before all init, system handlers |
||
| 260 | 18 | $events->trigger('plugins_boot', 'system'); |
|
| 261 | |||
| 262 | // Complete the boot process for both engine and plugins |
||
| 263 | 18 | $events->trigger('init', 'system'); |
|
| 264 | |||
| 265 | 18 | $config->boot_complete = true; |
|
| 266 | 18 | $config->lock('boot_complete'); |
|
| 267 | |||
| 268 | // System loaded and ready |
||
| 269 | 18 | $events->trigger('ready', 'system'); |
|
| 270 | 18 | } |
|
| 271 | |||
| 272 | /** |
||
| 273 | * Get the DB credentials. |
||
| 274 | * |
||
| 275 | * We no longer leave DB credentials in the config in case it gets accidentally dumped. |
||
| 276 | * |
||
| 277 | * @return \Elgg\Database\DbConfig |
||
| 278 | */ |
||
| 279 | 1 | public function getDbConfig() { |
|
| 280 | 1 | return $this->_services->dbConfig; |
|
| 281 | } |
||
| 282 | |||
| 283 | /** |
||
| 284 | * Get a Database wrapper for performing queries without booting Elgg |
||
| 285 | * |
||
| 286 | * If settings has not been loaded, it will be loaded to configure the DB connection. |
||
| 287 | * |
||
| 288 | * @note Before boot, the Database instance will not yet be bound to a Logger. |
||
| 289 | * |
||
| 290 | * @return \Elgg\Application\Database |
||
| 291 | */ |
||
| 292 | 13 | public function getDb() { |
|
| 293 | 13 | return $this->_services->publicDb; |
|
| 294 | } |
||
| 295 | |||
| 296 | /** |
||
| 297 | * Get database connection |
||
| 298 | * |
||
| 299 | * @param string $type Connection type |
||
| 300 | * @return Connection|false |
||
| 301 | * |
||
| 302 | * @access private |
||
| 303 | */ |
||
| 304 | 13 | public function getDbConnection($type = 'readwrite') { |
|
| 305 | try { |
||
| 306 | 13 | return $this->getDb()->getConnection($type); |
|
| 307 | } catch (\DatabaseException $e) { |
||
| 308 | return false; |
||
| 309 | } |
||
| 310 | } |
||
| 311 | |||
| 312 | /** |
||
| 313 | * Get an undefined property |
||
| 314 | * |
||
| 315 | * @param string $name The property name accessed |
||
| 316 | * |
||
| 317 | * @return mixed |
||
| 318 | */ |
||
| 319 | public function __get($name) { |
||
| 320 | if (isset(self::$public_services[$name])) { |
||
| 321 | return $this->_services->{$name}; |
||
| 322 | } |
||
| 323 | trigger_error("Undefined property: " . __CLASS__ . ":\${$name}"); |
||
| 324 | } |
||
| 325 | |||
| 326 | /** |
||
| 327 | * Make the global $CONFIG a reference to this application's config service |
||
| 328 | * |
||
| 329 | * @param Application $application The Application |
||
| 330 | * @return void |
||
| 331 | */ |
||
| 332 | public static function setGlobalConfig(Application $application) { |
||
| 333 | global $CONFIG; |
||
| 334 | $CONFIG = $application->_services->config; |
||
| 335 | } |
||
| 336 | |||
| 337 | /** |
||
| 338 | * Create a new application. |
||
| 339 | * |
||
| 340 | * @warning You generally want to use getInstance(). |
||
| 341 | * |
||
| 342 | * For normal operation, you must use setInstance() and optionally setGlobalConfig() to wire the |
||
| 343 | * application to Elgg's global API. |
||
| 344 | * |
||
| 345 | * @param array $spec Specification for initial call. |
||
| 346 | * @return self |
||
| 347 | * @throws ConfigurationException |
||
| 348 | * @throws InvalidArgumentException |
||
| 349 | */ |
||
| 350 | 4417 | public static function factory(array $spec = []) { |
|
| 351 | 4417 | self::loadCore(); |
|
| 352 | |||
| 353 | $defaults = [ |
||
| 354 | 4417 | 'config' => null, |
|
| 355 | 'handle_exceptions' => true, |
||
| 356 | 'handle_shutdown' => true, |
||
| 357 | 'request' => null, |
||
| 358 | 'service_provider' => null, |
||
| 359 | 'set_start_time' => true, |
||
| 360 | 'settings_path' => null, |
||
| 361 | ]; |
||
| 362 | 4417 | $spec = array_merge($defaults, $spec); |
|
| 363 | |||
| 364 | 4417 | if ($spec['set_start_time']) { |
|
| 365 | /** |
||
| 366 | * The time with microseconds when the Elgg engine was started. |
||
| 367 | * |
||
| 368 | * @global float |
||
| 369 | */ |
||
| 370 | 10 | if (!isset($GLOBALS['START_MICROTIME'])) { |
|
| 371 | 1 | $GLOBALS['START_MICROTIME'] = microtime(true); |
|
| 372 | } |
||
| 373 | } |
||
| 374 | |||
| 375 | 4417 | if (!$spec['service_provider']) { |
|
| 376 | 1 | if (!$spec['config']) { |
|
| 377 | $spec['config'] = Config::factory($spec['settings_path']); |
||
| 378 | } |
||
| 379 | 1 | $spec['service_provider'] = new ServiceProvider($spec['config']); |
|
| 380 | } |
||
| 381 | |||
| 382 | 4417 | if ($spec['request']) { |
|
| 383 | if ($spec['request'] instanceof Request) { |
||
| 384 | $spec['service_provider']->setValue('request', $spec['request']); |
||
| 385 | } else { |
||
| 386 | throw new InvalidArgumentException("Given request is not a " . Request::class); |
||
| 387 | } |
||
| 388 | } |
||
| 389 | |||
| 390 | 4417 | $app = new self($spec['service_provider']); |
|
| 391 | |||
| 392 | 4417 | if ($spec['handle_exceptions']) { |
|
| 393 | set_error_handler([$app, 'handleErrors']); |
||
| 394 | set_exception_handler([$app, 'handleExceptions']); |
||
| 395 | } |
||
| 396 | |||
| 397 | 4417 | if ($spec['handle_shutdown']) { |
|
| 398 | register_shutdown_function('_elgg_db_run_delayed_queries'); |
||
| 399 | register_shutdown_function('_elgg_db_log_profiling_data'); |
||
| 400 | |||
| 401 | // we need to register for shutdown before Symfony registers the |
||
| 402 | // session_write_close() function. https://github.com/Elgg/Elgg/issues/9243 |
||
| 403 | register_shutdown_function(function () { |
||
| 404 | // There are cases where we may exit before this function is defined |
||
| 405 | if (function_exists('_elgg_shutdown_hook')) { |
||
| 406 | _elgg_shutdown_hook(); |
||
| 407 | } |
||
| 408 | }); |
||
| 409 | } |
||
| 410 | |||
| 411 | 4417 | return $app; |
|
| 412 | } |
||
| 413 | |||
| 414 | /** |
||
| 415 | * Elgg's front controller. Handles basically all incoming URL requests. |
||
| 416 | * |
||
| 417 | * @return bool True if Elgg will handle the request, false if the server should (PHP-CLI server) |
||
| 418 | * @throws ConfigurationException |
||
| 419 | */ |
||
| 420 | public static function index() { |
||
| 421 | $req = Request::createFromGlobals(); |
||
| 422 | /** @var Request $req */ |
||
| 423 | |||
| 424 | if ($req->isRewriteCheck()) { |
||
| 425 | echo Request::REWRITE_TEST_OUTPUT; |
||
| 426 | return true; |
||
| 427 | } |
||
| 428 | |||
| 429 | try { |
||
| 430 | $app = self::factory([ |
||
| 431 | 'request' => $req, |
||
| 432 | ]); |
||
| 433 | } catch (ConfigurationException $ex) { |
||
| 434 | return self::install(); |
||
| 435 | } |
||
| 436 | |||
| 437 | self::setGlobalConfig($app); |
||
| 438 | self::setInstance($app); |
||
| 439 | |||
| 440 | return $app->run(); |
||
| 441 | } |
||
| 442 | |||
| 443 | /** |
||
| 444 | * Routes the request, booting core if not yet booted |
||
| 445 | * |
||
| 446 | * @return bool False if Elgg wants the PHP CLI server to handle the request |
||
| 447 | * @throws InstallationException |
||
| 448 | * @throws InvalidParameterException |
||
| 449 | * @throws SecurityException |
||
| 450 | */ |
||
| 451 | public function run() { |
||
| 452 | try { |
||
| 453 | $config = $this->_services->config; |
||
| 454 | $request = $this->_services->request; |
||
| 455 | |||
| 456 | if ($request->isCliServer()) { |
||
| 457 | if ($request->isCliServable(Paths::project())) { |
||
| 458 | return false; |
||
| 459 | } |
||
| 460 | |||
| 461 | // overwrite value from settings |
||
| 462 | $www_root = rtrim($request->getSchemeAndHttpHost() . $request->getBaseUrl(), '/') . '/'; |
||
| 463 | $config->wwwroot = $www_root; |
||
| 464 | $config->wwwroot_cli_server = $www_root; |
||
| 465 | } |
||
| 466 | |||
| 467 | if (0 === strpos($request->getElggPath(), '/cache/')) { |
||
| 468 | $this->_services->cacheHandler->handleRequest($request, $this)->prepare($request)->send(); |
||
| 469 | |||
| 470 | return true; |
||
| 471 | } |
||
| 472 | |||
| 473 | if (0 === strpos($request->getElggPath(), '/serve-file/')) { |
||
| 474 | $this->_services->serveFileHandler->getResponse($request)->send(); |
||
| 475 | |||
| 476 | return true; |
||
| 477 | } |
||
| 478 | |||
| 479 | $this->bootCore(); |
||
| 480 | |||
| 481 | // re-fetch new request from services in case it was replaced by route:rewrite |
||
| 482 | $request = $this->_services->request; |
||
| 483 | |||
| 484 | if (!$this->_services->router->route($request)) { |
||
| 485 | throw new PageNotFoundException(); |
||
| 486 | } |
||
| 487 | } catch (HttpException $ex) { |
||
| 488 | $forward_url = REFERRER; |
||
| 489 | if ($ex instanceof GatekeeperException) { |
||
| 490 | $forward_url = elgg_is_logged_in() ? '' : '/login'; |
||
| 491 | } |
||
| 492 | |||
| 493 | $hook_params = [ |
||
| 494 | 'exception' => $ex, |
||
| 495 | ]; |
||
| 496 | |||
| 497 | $this->_services->hooks->trigger('forward', $ex->getCode(), $hook_params, $forward_url); |
||
| 498 | |||
| 499 | $response = new ErrorResponse($ex->getMessage(), $ex->getCode(), $forward_url); |
||
| 500 | $this->_services->responseFactory->respond($response); |
||
| 501 | } |
||
| 502 | |||
| 503 | return true; |
||
| 504 | } |
||
| 505 | |||
| 506 | /** |
||
| 507 | * Returns a directory that points to the root of Elgg, but not necessarily |
||
| 508 | * the install root. See `self::root()` for that. |
||
| 509 | * |
||
| 510 | * @return Directory |
||
| 511 | */ |
||
| 512 | 9 | public static function elggDir() { |
|
| 513 | 9 | return Local::elggRoot(); |
|
| 514 | } |
||
| 515 | |||
| 516 | /** |
||
| 517 | * Returns a directory that points to the project root, where composer is installed. |
||
| 518 | * |
||
| 519 | * @return Directory |
||
| 520 | */ |
||
| 521 | public static function projectDir() { |
||
| 522 | return Local::projectRoot(); |
||
| 523 | } |
||
| 524 | |||
| 525 | /** |
||
| 526 | * Renders a web UI for installing Elgg. |
||
| 527 | * |
||
| 528 | * @return bool |
||
| 529 | * @throws InstallationException |
||
| 530 | */ |
||
| 531 | public static function install() { |
||
| 532 | ini_set('display_errors', 1); |
||
| 533 | |||
| 534 | $installer = new \ElggInstaller(); |
||
| 535 | $response = $installer->run(); |
||
| 536 | try { |
||
| 537 | // we won't trust server configuration but specify utf-8 |
||
| 538 | elgg_set_http_header('Content-type: text/html; charset=utf-8'); |
||
| 539 | |||
| 540 | // turn off browser caching |
||
| 541 | elgg_set_http_header('Pragma: public', true); |
||
| 542 | elgg_set_http_header("Cache-Control: no-cache, must-revalidate", true); |
||
| 543 | elgg_set_http_header('Expires: Fri, 05 Feb 1982 00:00:00 -0500', true); |
||
| 544 | |||
| 545 | _elgg_services()->responseFactory->respond($response); |
||
| 546 | return headers_sent(); |
||
| 547 | } catch (InvalidParameterException $ex) { |
||
| 548 | throw new InstallationException($ex->getMessage()); |
||
| 549 | } |
||
| 550 | } |
||
| 551 | |||
| 552 | /** |
||
| 553 | * Elgg upgrade script. |
||
| 554 | * |
||
| 555 | * This script triggers any necessary upgrades. If the site has been upgraded |
||
| 556 | * to the most recent version of the code, no upgrades are run but the caches |
||
| 557 | * are flushed. |
||
| 558 | * |
||
| 559 | * Upgrades use a table {db_prefix}upgrade_lock as a mutex to prevent concurrent upgrades. |
||
| 560 | * |
||
| 561 | * The URL to forward to after upgrades are complete can be specified by setting $_GET['forward'] |
||
| 562 | * to a relative URL. |
||
| 563 | * |
||
| 564 | * @return void |
||
| 565 | * @throws InstallationException |
||
| 566 | */ |
||
| 567 | public static function upgrade() { |
||
| 568 | // we want to know if an error occurs |
||
| 569 | ini_set('display_errors', 1); |
||
| 570 | $is_cli = (php_sapi_name() === 'cli'); |
||
| 571 | |||
| 572 | $forward = function ($url) use ($is_cli) { |
||
| 573 | if ($is_cli) { |
||
| 574 | fwrite(STDOUT, "Open $url in your browser to continue." . PHP_EOL); |
||
| 575 | return; |
||
| 576 | } |
||
| 577 | |||
| 578 | forward($url); |
||
| 579 | }; |
||
| 580 | |||
| 581 | define('UPGRADING', 'upgrading'); |
||
| 582 | |||
| 583 | self::migrate(); |
||
| 584 | self::start(); |
||
| 585 | |||
| 586 | // clear autoload cache so plugin classes can be reregistered and used during upgrade |
||
| 587 | _elgg_services()->autoloadManager->deleteCache(); |
||
| 588 | |||
| 589 | // check security settings |
||
| 590 | if (!$is_cli && _elgg_config()->security_protect_upgrade && !elgg_is_admin_logged_in()) { |
||
| 591 | // only admin's or users with a valid token can run upgrade.php |
||
| 592 | elgg_signed_request_gatekeeper(); |
||
| 593 | } |
||
| 594 | |||
| 595 | $site_url = _elgg_config()->url; |
||
| 596 | $site_host = parse_url($site_url, PHP_URL_HOST) . '/'; |
||
| 597 | |||
| 598 | // turn any full in-site URLs into absolute paths |
||
| 599 | $forward_url = get_input('forward', '/admin', false); |
||
| 600 | $forward_url = str_replace([$site_url, $site_host], '/', $forward_url); |
||
| 601 | |||
| 602 | if (strpos($forward_url, '/') !== 0) { |
||
| 603 | $forward_url = '/' . $forward_url; |
||
| 604 | } |
||
| 605 | |||
| 606 | if ($is_cli || (get_input('upgrade') == 'upgrade')) { |
||
| 607 | $upgrader = _elgg_services()->upgrades; |
||
| 608 | $result = $upgrader->run(); |
||
| 609 | |||
| 610 | if ($result['failure'] == true) { |
||
| 611 | register_error($result['reason']); |
||
| 612 | $forward($forward_url); |
||
| 613 | } |
||
| 614 | |||
| 615 | // Find unprocessed batch upgrade classes and save them as ElggUpgrade objects |
||
| 616 | $core_upgrades = (require self::elggDir()->getPath('engine/lib/upgrades/async-upgrades.php')); |
||
| 617 | $has_pending_upgrades = _elgg_services()->upgradeLocator->run($core_upgrades); |
||
| 618 | |||
| 619 | if ($has_pending_upgrades) { |
||
| 620 | // Forward to the list of pending upgrades |
||
| 621 | $forward_url = '/admin/upgrades'; |
||
| 622 | } |
||
| 623 | } else { |
||
| 624 | $rewriteTester = new \ElggRewriteTester(); |
||
| 625 | $url = elgg_get_site_url() . "__testing_rewrite?__testing_rewrite=1"; |
||
| 626 | if (!$rewriteTester->runRewriteTest($url)) { |
||
| 627 | // see if there is a problem accessing the site at all |
||
| 628 | // due to ip restrictions for example |
||
| 629 | if (!$rewriteTester->runLocalhostAccessTest()) { |
||
| 630 | // note: translation may not be available until after upgrade |
||
| 631 | $msg = elgg_echo("installation:htaccess:localhost:connectionfailed"); |
||
| 632 | if ($msg === "installation:htaccess:localhost:connectionfailed") { |
||
| 633 | $msg = "Elgg cannot connect to itself to test rewrite rules properly. Check " |
||
| 634 | . "that curl is working and there are no IP restrictions preventing " |
||
| 635 | . "localhost connections."; |
||
| 636 | } |
||
| 637 | echo $msg; |
||
| 638 | exit; |
||
| 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; |
||
| 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; |
||
| 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 |