1<?php 2 3use Elgg\Database; 4use Elgg\Application; 5use Elgg\Config; 6use Elgg\Database\DbConfig; 7use Elgg\Project\Paths; 8use Elgg\Di\ServiceProvider; 9use Elgg\Http\Request; 10 11/** 12 * Elgg Installer. 13 * Controller for installing Elgg. Supports both web-based on CLI installation. 14 * 15 * This controller steps the user through the install process. The method for 16 * each step handles both the GET and POST requests. There is no XSS/CSRF protection 17 * on the POST processing since the installer is only run once by the administrator. 18 * 19 * The installation process can be resumed by hitting the first page. The installer 20 * will try to figure out where to pick up again. 21 * 22 * All the logic for the installation process is in this class, but it depends on 23 * the core libraries. To do this, we selectively load a subset of the core libraries 24 * for the first few steps and then load the entire engine once the database and 25 * site settings are configured. In addition, this controller does its own session 26 * handling until the database is setup. 27 * 28 * There is an aborted attempt in the code at creating the data directory for 29 * users as a subdirectory of Elgg's root. The idea was to protect this directory 30 * through a .htaccess file. The problem is that a malicious user can upload a 31 * .htaccess of his own that overrides the protection for his user directory. The 32 * best solution is server level configuration that turns off AllowOverride for the 33 * data directory. See ticket #3453 for discussion on this. 34 */ 35class ElggInstaller { 36 37 private $steps = [ 38 'welcome', 39 'requirements', 40 'database', 41 'settings', 42 'admin', 43 'complete', 44 ]; 45 46 private $has_completed = [ 47 'config' => false, 48 'database' => false, 49 'settings' => false, 50 'admin' => false, 51 ]; 52 53 private $is_action = false; 54 55 private $autoLogin = true; 56 57 /** 58 * @var Application 59 */ 60 private $app; 61 62 /** 63 * Dispatches a request to one of the step controllers 64 * 65 * @return \Elgg\Http\ResponseBuilder 66 * @throws InstallationException 67 */ 68 public function run() { 69 $app = $this->getApp(); 70 71 $this->is_action = $app->_services->request->getMethod() === 'POST'; 72 73 $step = get_input('step', 'welcome'); 74 75 if (!in_array($step, $this->getSteps())) { 76 $step = 'welcome'; 77 } 78 79 $this->determineInstallStatus(); 80 81 $response = $this->checkInstallCompletion($step); 82 if ($response) { 83 return $response; 84 } 85 86 // check if this is an install being resumed 87 $response = $this->resumeInstall($step); 88 if ($response) { 89 return $response; 90 } 91 92 $this->finishBootstrapping($step); 93 94 $params = $app->_services->request->request->all(); 95 96 $method = "run" . ucwords($step); 97 98 return $this->$method($params); 99 } 100 101 /** 102 * Build the application needed by the installer 103 * 104 * @return Application 105 * @throws InstallationException 106 */ 107 protected function getApp() { 108 if ($this->app) { 109 return $this->app; 110 } 111 112 try { 113 $config = new Config(); 114 $config->elgg_config_locks = false; 115 $config->installer_running = true; 116 $config->dbencoding = 'utf8mb4'; 117 $config->boot_cache_ttl = 0; 118 $config->system_cache_enabled = false; 119 $config->simplecache_enabled = false; 120 $config->debug = \Psr\Log\LogLevel::WARNING; 121 $config->cacheroot = Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/caches/'; 122 $config->assetroot = Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/assets/'; 123 124 $services = new ServiceProvider($config); 125 126 $app = Application::factory([ 127 'service_provider' => $services, 128 'handle_exceptions' => false, 129 'handle_shutdown' => false, 130 ]); 131 132 // Don't set global $CONFIG, because loading the settings file may require it to write to 133 // it, and it can have array sets (e.g. cookie config) that fail when using a proxy for 134 // the config service. 135 //$app->setGlobalConfig(); 136 137 Application::setInstance($app); 138 $app->loadCore(); 139 $this->app = $app; 140 141 $app->_services->boot->getCache()->disable(); 142 $app->_services->plugins->getCache()->disable(); 143 $app->_services->sessionCache->disable(); 144 $app->_services->dic_cache->getCache()->disable(); 145 $app->_services->dataCache->disable(); 146 $app->_services->autoloadManager->getCache()->disable(); 147 148 $app->_services->setValue('session', \ElggSession::getMock()); 149 $app->_services->views->setViewtype('installation'); 150 $app->_services->views->registerViewtypeFallback('installation'); 151 $app->_services->views->registerPluginViews(Paths::elgg()); 152 $app->_services->translator->registerTranslations(Paths::elgg() . "install/languages/", true); 153 154 return $this->app; 155 } catch (ConfigurationException $ex) { 156 throw new InstallationException($ex->getMessage()); 157 } 158 } 159 160 /** 161 * Set the auto login flag 162 * 163 * @param bool $flag Auto login 164 * 165 * @return void 166 */ 167 public function setAutoLogin($flag) { 168 $this->autoLogin = (bool) $flag; 169 } 170 171 /** 172 * A batch install of Elgg 173 * 174 * All required parameters must be passed in as an associative array. See 175 * $requiredParams for a list of them. This creates the necessary files, 176 * loads the database, configures the site settings, and creates the admin 177 * account. If it fails, an exception is thrown. It does not check any of 178 * the requirements as the multiple step web installer does. 179 * 180 * @param array $params Array of key value pairs 181 * @param bool $create_htaccess Should .htaccess be created 182 * 183 * @return void 184 * @throws InstallationException 185 */ 186 public function batchInstall(array $params, $create_htaccess = false) { 187 $app = $this->getApp(); 188 189 $defaults = [ 190 'dbhost' => 'localhost', 191 'dbport' => '3306', 192 'dbprefix' => 'elgg_', 193 'language' => 'en', 194 'siteaccess' => ACCESS_PUBLIC, 195 ]; 196 $params = array_merge($defaults, $params); 197 198 $required_params = [ 199 'dbuser', 200 'dbpassword', 201 'dbname', 202 'sitename', 203 'wwwroot', 204 'dataroot', 205 'displayname', 206 'email', 207 'username', 208 'password', 209 ]; 210 foreach ($required_params as $key) { 211 if (empty($params[$key])) { 212 $msg = elgg_echo('install:error:requiredfield', [$key]); 213 throw new InstallationException($msg); 214 } 215 } 216 217 // password is passed in once 218 $params['password1'] = $params['password2'] = $params['password']; 219 220 if ($create_htaccess) { 221 $rewrite_tester = new ElggRewriteTester(); 222 if (!$rewrite_tester->createHtaccess($params['wwwroot'])) { 223 throw new InstallationException(elgg_echo('install:error:htaccess')); 224 } 225 } 226 227 if (!_elgg_sane_validate_url($params['wwwroot'])) { 228 throw new InstallationException(elgg_echo('install:error:wwwroot', [$params['wwwroot']])); 229 } 230 231 // sanitize dataroot path 232 $params['dataroot'] = Paths::sanitize($params['dataroot']); 233 234 $this->determineInstallStatus(); 235 236 if (!$this->has_completed['config']) { 237 if (!$this->createSettingsFile($params)) { 238 throw new InstallationException(elgg_echo('install:error:settings')); 239 } 240 } 241 242 $this->loadSettingsFile(); 243 244 // Make sure settings file matches parameters 245 $config = $app->_services->config; 246 $config_keys = [ 247 // param key => config key 248 'dbhost' => 'dbhost', 249 'dbport' => 'dbport', 250 'dbuser' => 'dbuser', 251 'dbpassword' => 'dbpass', 252 'dbname' => 'dbname', 253 'dataroot' => 'dataroot', 254 'dbprefix' => 'dbprefix', 255 ]; 256 foreach ($config_keys as $params_key => $config_key) { 257 if ($params[$params_key] !== $config->$config_key) { 258 throw new InstallationException(elgg_echo('install:error:settings_mismatch', [$config_key]) . $params[$params_key] . ' ' . $config->$config_key); 259 } 260 } 261 262 if (!$this->connectToDatabase()) { 263 throw new InstallationException(elgg_echo('install:error:databasesettings')); 264 } 265 266 if (!$this->has_completed['database']) { 267 if (!$this->installDatabase()) { 268 throw new InstallationException(elgg_echo('install:error:cannotloadtables')); 269 } 270 } 271 272 // load remaining core libraries 273 $this->finishBootstrapping('settings'); 274 275 if (!$this->saveSiteSettings($params)) { 276 throw new InstallationException(elgg_echo('install:error:savesitesettings')); 277 } 278 279 if (!$this->createAdminAccount($params)) { 280 throw new InstallationException(elgg_echo('install:admin:cannot_create')); 281 } 282 } 283 284 /** 285 * Renders the data passed by a controller 286 * 287 * @param string $step The current step 288 * @param array $vars Array of vars to pass to the view 289 * 290 * @return \Elgg\Http\OkResponse 291 */ 292 protected function render($step, $vars = []) { 293 $vars['next_step'] = $this->getNextStep($step); 294 295 $title = elgg_echo("install:$step"); 296 $body = elgg_view("install/pages/$step", $vars); 297 298 $output = elgg_view_page( 299 $title, 300 $body, 301 'default', 302 [ 303 'step' => $step, 304 'steps' => $this->getSteps(), 305 ] 306 ); 307 308 return new \Elgg\Http\OkResponse($output); 309 } 310 311 /** 312 * Step controllers 313 */ 314 315 /** 316 * Welcome controller 317 * 318 * @param array $vars Not used 319 * 320 * @return \Elgg\Http\ResponseBuilder 321 */ 322 protected function runWelcome($vars) { 323 return $this->render('welcome'); 324 } 325 326 /** 327 * Requirements controller 328 * 329 * Checks version of php, libraries, permissions, and rewrite rules 330 * 331 * @param array $vars Vars 332 * 333 * @return \Elgg\Http\ResponseBuilder 334 * @throws InstallationException 335 */ 336 protected function runRequirements($vars) { 337 338 $report = []; 339 340 // check PHP parameters and libraries 341 $this->checkPHP($report); 342 343 // check URL rewriting 344 $this->checkRewriteRules($report); 345 346 // check for existence of settings file 347 if ($this->checkSettingsFile($report) !== true) { 348 // no file, so check permissions on engine directory 349 $this->isInstallDirWritable($report); 350 } 351 352 // check the database later 353 $report['database'] = [ 354 [ 355 'severity' => 'notice', 356 'message' => elgg_echo('install:check:database') 357 ] 358 ]; 359 360 // any failures? 361 $numFailures = $this->countNumConditions($report, 'error'); 362 363 // any warnings 364 $numWarnings = $this->countNumConditions($report, 'warning'); 365 366 367 $params = [ 368 'report' => $report, 369 'num_failures' => $numFailures, 370 'num_warnings' => $numWarnings, 371 ]; 372 373 return $this->render('requirements', $params); 374 } 375 376 /** 377 * Database set up controller 378 * 379 * Creates the settings.php file and creates the database tables 380 * 381 * @param array $submissionVars Submitted form variables 382 * 383 * @return \Elgg\Http\ResponseBuilder 384 * @throws ConfigurationException 385 */ 386 protected function runDatabase($submissionVars) { 387 388 $app = $this->getApp(); 389 390 $formVars = [ 391 'dbuser' => [ 392 'type' => 'text', 393 'value' => '', 394 'required' => true, 395 ], 396 'dbpassword' => [ 397 'type' => 'password', 398 'value' => '', 399 'required' => false, 400 ], 401 'dbname' => [ 402 'type' => 'text', 403 'value' => '', 404 'required' => true, 405 ], 406 'dbhost' => [ 407 'type' => 'text', 408 'value' => 'localhost', 409 'required' => true, 410 ], 411 'dbport' => [ 412 'type' => 'number', 413 'value' => 3306, 414 'required' => true, 415 'min' => 0, 416 'max' => 65535, 417 ], 418 'dbprefix' => [ 419 'type' => 'text', 420 'value' => 'elgg_', 421 'required' => false, 422 ], 423 'dataroot' => [ 424 'type' => 'text', 425 'value' => '', 426 'required' => true, 427 ], 428 'wwwroot' => [ 429 'type' => 'url', 430 'value' => $app->_services->config->wwwroot, 431 'required' => true, 432 ], 433 'timezone' => [ 434 'type' => 'dropdown', 435 'value' => 'UTC', 436 'options' => \DateTimeZone::listIdentifiers(), 437 'required' => true 438 ] 439 ]; 440 441 if ($this->checkSettingsFile()) { 442 // user manually created settings file so we fake out action test 443 $this->is_action = true; 444 } 445 446 if ($this->is_action) { 447 $getResponse = function () use ($app, $submissionVars, $formVars) { 448 // only create settings file if it doesn't exist 449 if (!$this->checkSettingsFile()) { 450 if (!$this->validateDatabaseVars($submissionVars, $formVars)) { 451 // error so we break out of action and serve same page 452 return; 453 } 454 455 if (!$this->createSettingsFile($submissionVars)) { 456 return; 457 } 458 } 459 460 // check db version and connect 461 if (!$this->connectToDatabase()) { 462 return; 463 } 464 465 if (!$this->installDatabase()) { 466 return; 467 } 468 469 $app->_services->systemMessages->addSuccessMessage(elgg_echo('install:success:database')); 470 471 return $this->continueToNextStep('database'); 472 }; 473 474 $response = $getResponse(); 475 if ($response) { 476 return $response; 477 } 478 } 479 480 $formVars = $this->makeFormSticky($formVars, $submissionVars); 481 482 $params = ['variables' => $formVars,]; 483 484 if ($this->checkSettingsFile()) { 485 // settings file exists and we're here so failed to create database 486 $params['failure'] = true; 487 } 488 489 return $this->render('database', $params); 490 } 491 492 /** 493 * Site settings controller 494 * 495 * Sets the site name, URL, data directory, etc. 496 * 497 * @param array $submissionVars Submitted vars 498 * 499 * @return \Elgg\Http\ResponseBuilder 500 */ 501 protected function runSettings($submissionVars) { 502 503 $app = $this->getApp(); 504 505 $formVars = [ 506 'sitename' => [ 507 'type' => 'text', 508 'value' => 'My New Community', 509 'required' => true, 510 ], 511 'siteemail' => [ 512 'type' => 'email', 513 'value' => '', 514 'required' => false, 515 ], 516 'siteaccess' => [ 517 'type' => 'access', 518 'value' => ACCESS_PUBLIC, 519 'required' => true, 520 ], 521 ]; 522 523 if ($this->is_action) { 524 $getResponse = function () use ($app, $submissionVars, $formVars) { 525 526 if (!$this->validateSettingsVars($submissionVars, $formVars)) { 527 return; 528 } 529 530 if (!$this->saveSiteSettings($submissionVars)) { 531 return; 532 } 533 534 $app->_services->systemMessages->addSuccessMessage(elgg_echo('install:success:settings')); 535 536 return $this->continueToNextStep('settings'); 537 }; 538 539 $response = $getResponse(); 540 if ($response) { 541 return $response; 542 } 543 } 544 545 $formVars = $this->makeFormSticky($formVars, $submissionVars); 546 547 return $this->render('settings', ['variables' => $formVars]); 548 } 549 550 /** 551 * Admin account controller 552 * 553 * Creates an admin user account 554 * 555 * @param array $submissionVars Submitted vars 556 * 557 * @return \Elgg\Http\ResponseBuilder 558 * @throws InstallationException 559 */ 560 protected function runAdmin($submissionVars) { 561 $app = $this->getApp(); 562 563 $formVars = [ 564 'displayname' => [ 565 'type' => 'text', 566 'value' => '', 567 'required' => true, 568 ], 569 'email' => [ 570 'type' => 'email', 571 'value' => '', 572 'required' => true, 573 ], 574 'username' => [ 575 'type' => 'text', 576 'value' => '', 577 'required' => true, 578 ], 579 'password1' => [ 580 'type' => 'password', 581 'value' => '', 582 'required' => true, 583 'pattern' => '.{6,}', 584 ], 585 'password2' => [ 586 'type' => 'password', 587 'value' => '', 588 'required' => true, 589 ], 590 ]; 591 592 if ($this->is_action) { 593 $getResponse = function () use ($app, $submissionVars, $formVars) { 594 if (!$this->validateAdminVars($submissionVars, $formVars)) { 595 return; 596 } 597 598 if (!$this->createAdminAccount($submissionVars, $this->autoLogin)) { 599 return; 600 } 601 602 $app->_services->systemMessages->addSuccessMessage(elgg_echo('install:success:admin')); 603 604 return $this->continueToNextStep('admin'); 605 }; 606 607 $response = $getResponse(); 608 if ($response) { 609 return $response; 610 } 611 } 612 613 // Bit of a hack to get the password help to show right number of characters 614 // We burn the value into the stored translation. 615 $app = $this->getApp(); 616 $lang = $app->_services->translator->getCurrentLanguage(); 617 $translations = $app->_services->translator->getLoadedTranslations(); 618 $app->_services->translator->addTranslation($lang, [ 619 'install:admin:help:password1' => sprintf( 620 $translations[$lang]['install:admin:help:password1'], 621 $app->_services->config->min_password_length 622 ), 623 ]); 624 625 $formVars = $this->makeFormSticky($formVars, $submissionVars); 626 627 return $this->render('admin', ['variables' => $formVars]); 628 } 629 630 /** 631 * Controller for last step 632 * 633 * @return \Elgg\Http\ResponseBuilder 634 */ 635 protected function runComplete() { 636 637 // nudge to check out settings 638 $link = elgg_format_element([ 639 '#tag_name' => 'a', 640 '#text' => elgg_echo('install:complete:admin_notice:link_text'), 641 'href' => elgg_normalize_url('admin/site_settings'), 642 ]); 643 $notice = elgg_echo('install:complete:admin_notice', [$link]); 644 elgg_add_admin_notice('fresh_install', $notice); 645 646 $result = $this->render('complete'); 647 648 elgg_delete_directory(Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/'); 649 650 return $result; 651 } 652 653 /** 654 * Step management 655 */ 656 657 /** 658 * Get an array of steps 659 * 660 * @return array 661 */ 662 protected function getSteps() { 663 return $this->steps; 664 } 665 666 /** 667 * Forwards the browser to the next step 668 * 669 * @param string $currentStep Current installation step 670 * 671 * @return \Elgg\Http\RedirectResponse 672 * @throws InstallationException 673 */ 674 protected function continueToNextStep($currentStep) { 675 $this->is_action = false; 676 677 return new \Elgg\Http\RedirectResponse($this->getNextStepUrl($currentStep)); 678 } 679 680 /** 681 * Get the next step as a string 682 * 683 * @param string $currentStep Current installation step 684 * 685 * @return string 686 */ 687 protected function getNextStep($currentStep) { 688 $index = 1 + array_search($currentStep, $this->steps); 689 if (isset($this->steps[$index])) { 690 return $this->steps[$index]; 691 } else { 692 return null; 693 } 694 } 695 696 /** 697 * Get the URL of the next step 698 * 699 * @param string $currentStep Current installation step 700 * 701 * @return string 702 * @throws InstallationException 703 */ 704 protected function getNextStepUrl($currentStep) { 705 $app = $this->getApp(); 706 $nextStep = $this->getNextStep($currentStep); 707 708 return $app->_services->config->wwwroot . "install.php?step=$nextStep"; 709 } 710 711 /** 712 * Updates $this->has_completed according to the current installation 713 * 714 * @return void 715 * @throws InstallationException 716 */ 717 protected function determineInstallStatus() { 718 $app = $this->getApp(); 719 720 $path = Config::resolvePath(); 721 if (!is_file($path) || !is_readable($path)) { 722 return; 723 } 724 725 $this->loadSettingsFile(); 726 727 $this->has_completed['config'] = true; 728 729 // must be able to connect to database to jump install steps 730 $dbSettingsPass = $this->checkDatabaseSettings( 731 $app->_services->config->dbuser, 732 $app->_services->config->dbpass, 733 $app->_services->config->dbname, 734 $app->_services->config->dbhost, 735 $app->_services->config->dbport 736 ); 737 738 if (!$dbSettingsPass) { 739 return; 740 } 741 742 $db = $app->_services->db; 743 744 try { 745 // check that the config table has been created 746 $result = $db->getData("SHOW TABLES"); 747 if (empty($result)) { 748 return; 749 } 750 foreach ($result as $table) { 751 $table = (array) $table; 752 if (in_array("{$db->prefix}config", $table)) { 753 $this->has_completed['database'] = true; 754 } 755 } 756 if ($this->has_completed['database'] == false) { 757 return; 758 } 759 760 // check that the config table has entries 761 $qb = \Elgg\Database\Select::fromTable('config'); 762 $qb->select('COUNT(*) AS total'); 763 764 $result = $db->getDataRow($qb); 765 if (!empty($result) && $result->total > 0) { 766 $this->has_completed['settings'] = true; 767 } else { 768 return; 769 } 770 771 // check that the users entity table has an entry 772 $qb = \Elgg\Database\Select::fromTable('entities', 'e'); 773 $qb->select('COUNT(*) AS total') 774 ->where($qb->compare('type', '=', 'user', ELGG_VALUE_STRING)); 775 776 $result = $db->getDataRow($qb); 777 if (!empty($result) && $result->total > 0) { 778 $this->has_completed['admin'] = true; 779 } else { 780 return; 781 } 782 } catch (DatabaseException $ex) { 783 throw new InstallationException('Elgg can not connect to the database: ' . $ex->getMessage()); 784 } 785 786 return; 787 } 788 789 /** 790 * Security check to ensure the installer cannot be run after installation 791 * has finished. If this is detected, the viewer is sent to the front page. 792 * 793 * @param string $step Installation step to check against 794 * 795 * @return \Elgg\Http\RedirectResponse|null 796 */ 797 protected function checkInstallCompletion($step) { 798 if ($step != 'complete') { 799 if (!in_array(false, $this->has_completed)) { 800 // install complete but someone is trying to view an install page 801 return new \Elgg\Http\RedirectResponse('/'); 802 } 803 } 804 } 805 806 /** 807 * Check if this is a case of a install being resumed and figure 808 * out where to continue from. Returns the best guess on the step. 809 * 810 * @param string $step Installation step to resume from 811 * 812 * @return \Elgg\Http\RedirectResponse|null 813 */ 814 protected function resumeInstall($step) { 815 // only do a resume from the first step 816 if ($step !== 'welcome') { 817 return null; 818 } 819 820 if ($this->has_completed['database'] == false) { 821 return null; 822 } 823 824 if ($this->has_completed['settings'] == false) { 825 return new \Elgg\Http\RedirectResponse("install.php?step=settings"); 826 } 827 828 if ($this->has_completed['admin'] == false) { 829 return new \Elgg\Http\RedirectResponse("install.php?step=admin"); 830 } 831 832 // everything appears to be set up 833 return new \Elgg\Http\RedirectResponse("install.php?step=complete"); 834 } 835 836 /** 837 * Bootstrapping 838 */ 839 840 /** 841 * Load remaining engine libraries and complete bootstrapping 842 * 843 * @param string $step Which step to boot strap for. Required because 844 * boot strapping is different until the DB is populated. 845 * 846 * @return void 847 * @throws InstallationException 848 */ 849 protected function finishBootstrapping($step) { 850 851 $app = $this->getApp(); 852 853 $index_db = array_search('database', $this->getSteps()); 854 $index_settings = array_search('settings', $this->getSteps()); 855 $index_admin = array_search('admin', $this->getSteps()); 856 $index_complete = array_search('complete', $this->getSteps()); 857 $index_step = array_search($step, $this->getSteps()); 858 859 // To log in the user, we need to use the Elgg core session handling. 860 // Otherwise, use default php session handling 861 $use_elgg_session = ($index_step == $index_admin && $this->is_action) || ($index_step == $index_complete); 862 if (!$use_elgg_session) { 863 $this->createSessionFromFile(); 864 } 865 866 if ($index_step > $index_db) { 867 // once the database has been created, load rest of engine 868 869 // dummy site needed to boot 870 $app->_services->config->site = new ElggSite(); 871 872 $app->bootCore(); 873 } 874 } 875 876 /** 877 * Load settings 878 * 879 * @return void 880 * @throws InstallationException 881 */ 882 protected function loadSettingsFile() { 883 try { 884 $app = $this->getApp(); 885 886 $config = Config::fromFile(Config::resolvePath()); 887 $app->_services->setValue('config', $config); 888 889 // in case the DB instance is already captured in services, we re-inject its settings. 890 $app->_services->db->resetConnections(DbConfig::fromElggConfig($config)); 891 } catch (\Exception $e) { 892 $msg = elgg_echo('InstallationException:CannotLoadSettings'); 893 throw new InstallationException($msg, 0, $e); 894 } 895 } 896 897 /** 898 * Action handling methods 899 */ 900 901 /** 902 * If form is reshown, remember previously submitted variables 903 * 904 * @param array $formVars Vars int he form 905 * @param array $submissionVars Submitted vars 906 * 907 * @return array 908 */ 909 protected function makeFormSticky($formVars, $submissionVars) { 910 foreach ($submissionVars as $field => $value) { 911 $formVars[$field]['value'] = $value; 912 } 913 914 return $formVars; 915 } 916 917 /* Requirement checks support methods */ 918 919 /** 920 * Indicates whether the webserver can add settings.php on its own or not. 921 * 922 * @param array $report The requirements report object 923 * 924 * @return bool 925 */ 926 protected function isInstallDirWritable(&$report) { 927 if (!is_writable(Paths::projectConfig())) { 928 $msg = elgg_echo('install:check:installdir', [Paths::PATH_TO_CONFIG]); 929 $report['settings'] = [ 930 [ 931 'severity' => 'error', 932 'message' => $msg, 933 ] 934 ]; 935 936 return false; 937 } 938 939 return true; 940 } 941 942 /** 943 * Check that the settings file exists 944 * 945 * @param array $report The requirements report array 946 * 947 * @return bool 948 */ 949 protected function checkSettingsFile(&$report = []) { 950 if (!is_file(Config::resolvePath())) { 951 return false; 952 } 953 954 if (!is_readable(Config::resolvePath())) { 955 $report['settings'] = [ 956 [ 957 'severity' => 'error', 958 'message' => elgg_echo('install:check:readsettings'), 959 ] 960 ]; 961 } 962 963 return true; 964 } 965 966 /** 967 * Check version of PHP, extensions, and variables 968 * 969 * @param array $report The requirements report array 970 * 971 * @return void 972 */ 973 protected function checkPHP(&$report) { 974 $phpReport = []; 975 976 $min_php_version = '7.1.0'; 977 if (version_compare(PHP_VERSION, $min_php_version, '<')) { 978 $phpReport[] = [ 979 'severity' => 'error', 980 'message' => elgg_echo('install:check:php:version', [$min_php_version, PHP_VERSION]) 981 ]; 982 } 983 984 $this->checkPhpExtensions($phpReport); 985 986 $this->checkPhpDirectives($phpReport); 987 988 if (count($phpReport) == 0) { 989 $phpReport[] = [ 990 'severity' => 'success', 991 'message' => elgg_echo('install:check:php:success') 992 ]; 993 } 994 995 $report['php'] = $phpReport; 996 } 997 998 /** 999 * Check the server's PHP extensions 1000 * 1001 * @param array $phpReport The PHP requirements report array 1002 * 1003 * @return void 1004 */ 1005 protected function checkPhpExtensions(&$phpReport) { 1006 $extensions = get_loaded_extensions(); 1007 $requiredExtensions = [ 1008 'pdo_mysql', 1009 'json', 1010 'xml', 1011 'gd', 1012 ]; 1013 foreach ($requiredExtensions as $extension) { 1014 if (!in_array($extension, $extensions)) { 1015 $phpReport[] = [ 1016 'severity' => 'error', 1017 'message' => elgg_echo('install:check:php:extension', [$extension]) 1018 ]; 1019 } 1020 } 1021 1022 $recommendedExtensions = [ 1023 'mbstring', 1024 ]; 1025 foreach ($recommendedExtensions as $extension) { 1026 if (!in_array($extension, $extensions)) { 1027 $phpReport[] = [ 1028 'severity' => 'warning', 1029 'message' => elgg_echo('install:check:php:extension:recommend', [$extension]) 1030 ]; 1031 } 1032 } 1033 } 1034 1035 /** 1036 * Check PHP parameters 1037 * 1038 * @param array $phpReport The PHP requirements report array 1039 * 1040 * @return void 1041 */ 1042 protected function checkPhpDirectives(&$phpReport) { 1043 if (ini_get('open_basedir')) { 1044 $phpReport[] = [ 1045 'severity' => 'warning', 1046 'message' => elgg_echo("install:check:php:open_basedir") 1047 ]; 1048 } 1049 1050 if (ini_get('safe_mode')) { 1051 $phpReport[] = [ 1052 'severity' => 'warning', 1053 'message' => elgg_echo("install:check:php:safe_mode") 1054 ]; 1055 } 1056 1057 if (ini_get('arg_separator.output') !== '&') { 1058 $separator = htmlspecialchars(ini_get('arg_separator.output')); 1059 $msg = elgg_echo("install:check:php:arg_separator", [$separator]); 1060 $phpReport[] = [ 1061 'severity' => 'error', 1062 'message' => $msg, 1063 ]; 1064 } 1065 1066 if (ini_get('register_globals')) { 1067 $phpReport[] = [ 1068 'severity' => 'error', 1069 'message' => elgg_echo("install:check:php:register_globals") 1070 ]; 1071 } 1072 1073 if (ini_get('session.auto_start')) { 1074 $phpReport[] = [ 1075 'severity' => 'error', 1076 'message' => elgg_echo("install:check:php:session.auto_start") 1077 ]; 1078 } 1079 } 1080 1081 /** 1082 * Confirm that the rewrite rules are firing 1083 * 1084 * @param array $report The requirements report array 1085 * 1086 * @return void 1087 * @throws InstallationException 1088 */ 1089 protected function checkRewriteRules(&$report) { 1090 $app = $this->getApp(); 1091 1092 $tester = new ElggRewriteTester(); 1093 $url = $app->_services->config->wwwroot; 1094 $url .= Request::REWRITE_TEST_TOKEN . '?' . http_build_query([ 1095 Request::REWRITE_TEST_TOKEN => '1', 1096 ]); 1097 $report['rewrite'] = [$tester->run($url)]; 1098 } 1099 1100 /** 1101 * Count the number of failures in the requirements report 1102 * 1103 * @param array $report The requirements report array 1104 * @param string $condition 'failure' or 'warning' 1105 * 1106 * @return int 1107 */ 1108 protected function countNumConditions($report, $condition) { 1109 $count = 0; 1110 foreach ($report as $category => $checks) { 1111 foreach ($checks as $check) { 1112 if ($check['severity'] === $condition) { 1113 $count++; 1114 } 1115 } 1116 } 1117 1118 return $count; 1119 } 1120 1121 1122 /** 1123 * Database support methods 1124 */ 1125 1126 /** 1127 * Validate the variables for the database step 1128 * 1129 * @param array $submissionVars Submitted vars 1130 * @param array $formVars Vars in the form 1131 * 1132 * @return bool 1133 * @throws InstallationException 1134 */ 1135 protected function validateDatabaseVars($submissionVars, $formVars) { 1136 1137 $app = $this->getApp(); 1138 1139 foreach ($formVars as $field => $info) { 1140 if ($info['required'] == true && !$submissionVars[$field]) { 1141 $name = elgg_echo("install:database:label:$field"); 1142 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name])); 1143 1144 return false; 1145 } 1146 } 1147 1148 if (!empty($submissionVars['wwwroot']) && !_elgg_sane_validate_url($submissionVars['wwwroot'])) { 1149 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:wwwroot', [$submissionVars['wwwroot']])); 1150 1151 return false; 1152 } 1153 1154 // check that data root is absolute path 1155 if (stripos(PHP_OS, 'win') === 0) { 1156 if (strpos($submissionVars['dataroot'], ':') !== 1) { 1157 $msg = elgg_echo('install:error:relative_path', [$submissionVars['dataroot']]); 1158 $app->_services->systemMessages->addErrorMessage($msg); 1159 1160 return false; 1161 } 1162 } else { 1163 if (strpos($submissionVars['dataroot'], '/') !== 0) { 1164 $msg = elgg_echo('install:error:relative_path', [$submissionVars['dataroot']]); 1165 $app->_services->systemMessages->addErrorMessage($msg); 1166 1167 return false; 1168 } 1169 } 1170 1171 // check that data root exists 1172 if (!is_dir($submissionVars['dataroot'])) { 1173 $msg = elgg_echo('install:error:datadirectoryexists', [$submissionVars['dataroot']]); 1174 $app->_services->systemMessages->addErrorMessage($msg); 1175 1176 return false; 1177 } 1178 1179 // check that data root is writable 1180 if (!is_writable($submissionVars['dataroot'])) { 1181 $msg = elgg_echo('install:error:writedatadirectory', [$submissionVars['dataroot']]); 1182 $app->_services->systemMessages->addErrorMessage($msg); 1183 1184 return false; 1185 } 1186 1187 if (!$app->_services->config->data_dir_override) { 1188 // check that data root is not subdirectory of Elgg root 1189 if (stripos($submissionVars['dataroot'], $app->_services->config->path) === 0) { 1190 $msg = elgg_echo('install:error:locationdatadirectory', [$submissionVars['dataroot']]); 1191 $app->_services->systemMessages->addErrorMessage($msg); 1192 1193 return false; 1194 } 1195 } 1196 1197 // according to postgres documentation: SQL identifiers and key words must 1198 // begin with a letter (a-z, but also letters with diacritical marks and 1199 // non-Latin letters) or an underscore (_). Subsequent characters in an 1200 // identifier or key word can be letters, underscores, digits (0-9), or dollar signs ($). 1201 // Refs #4994 1202 if (!empty($submissionVars['dbprefix']) && !preg_match("/^[a-zA-Z_][\w]*$/", $submissionVars['dbprefix'])) { 1203 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:database_prefix')); 1204 1205 return false; 1206 } 1207 1208 return $this->checkDatabaseSettings( 1209 $submissionVars['dbuser'], 1210 $submissionVars['dbpassword'], 1211 $submissionVars['dbname'], 1212 $submissionVars['dbhost'], 1213 $submissionVars['dbport'] 1214 ); 1215 } 1216 1217 /** 1218 * Confirm the settings for the database 1219 * 1220 * @param string $user Username 1221 * @param string $password Password 1222 * @param string $dbname Database name 1223 * @param string $host Host 1224 * @param int $port Port 1225 * 1226 * @return bool 1227 */ 1228 protected function checkDatabaseSettings($user, $password, $dbname, $host, $port) { 1229 $app = $this->getApp(); 1230 1231 $config = new DbConfig((object) [ 1232 'dbhost' => $host, 1233 'dbport' => $port, 1234 'dbuser' => $user, 1235 'dbpass' => $password, 1236 'dbname' => $dbname, 1237 'dbencoding' => 'utf8mb4', 1238 ]); 1239 $db = new Database($config, $app->_services->queryCache); 1240 1241 try { 1242 $db->getDataRow("SELECT 1"); 1243 } catch (DatabaseException $e) { 1244 if (0 === strpos($e->getMessage(), "Elgg couldn't connect")) { 1245 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:databasesettings')); 1246 } else { 1247 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:nodatabase', [$dbname])); 1248 } 1249 1250 return false; 1251 } 1252 1253 // check MySQL version 1254 $version = $db->getServerVersion(DbConfig::READ_WRITE); 1255 if (version_compare($version, '5.5.3', '<')) { 1256 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:oldmysql2', [$version])); 1257 1258 return false; 1259 } 1260 1261 return true; 1262 } 1263 1264 /** 1265 * Writes the settings file to the engine directory 1266 * 1267 * @param array $params Array of inputted params from the user 1268 * 1269 * @return bool 1270 * @throws InstallationException 1271 */ 1272 protected function createSettingsFile($params) { 1273 $app = $this->getApp(); 1274 1275 $template = Application::elggDir()->getContents("elgg-config/settings.example.php"); 1276 if (!$template) { 1277 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:readsettingsphp')); 1278 1279 return false; 1280 } 1281 1282 foreach ($params as $k => $v) { 1283 // do some sanitization 1284 switch ($k) { 1285 case 'dataroot': 1286 $v = Paths::sanitize($v); 1287 break; 1288 case 'dbpassword': 1289 $v = addslashes($v); 1290 break; 1291 } 1292 1293 $template = str_replace("{{" . $k . "}}", $v, $template); 1294 } 1295 1296 $result = file_put_contents(Config::resolvePath(), $template); 1297 if ($result === false) { 1298 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:writesettingphp')); 1299 1300 return false; 1301 } 1302 1303 $config = (object) [ 1304 'dbhost' => elgg_extract('dbhost', $params, 'localhost'), 1305 'dbport' => elgg_extract('dbport', $params, 3306), 1306 'dbuser' => elgg_extract('dbuser', $params), 1307 'dbpass' => elgg_extract('dbpassword', $params), 1308 'dbname' => elgg_extract('dbname', $params), 1309 'dbencoding' => elgg_extract('dbencoding', $params, 'utf8mb4'), 1310 'dbprefix' => elgg_extract('dbprefix', $params, 'elgg_'), 1311 ]; 1312 1313 $dbConfig = new DbConfig($config); 1314 $this->getApp()->_services->setValue('dbConfig', $dbConfig); 1315 $this->getApp()->_services->db->resetConnections($dbConfig); 1316 1317 return true; 1318 } 1319 1320 /** 1321 * Bootstrap database connection before entire engine is available 1322 * 1323 * @return bool 1324 * @throws InstallationException 1325 */ 1326 protected function connectToDatabase() { 1327 try { 1328 $app = $this->getApp(); 1329 $app->_services->db->setupConnections(); 1330 } catch (DatabaseException $e) { 1331 $app->_services->systemMessages->addErrorMessage($e->getMessage()); 1332 1333 return false; 1334 } 1335 1336 return true; 1337 } 1338 1339 /** 1340 * Create the database tables 1341 * 1342 * @return bool 1343 */ 1344 protected function installDatabase() { 1345 try { 1346 return $this->getApp()->migrate(); 1347 } catch (\Exception $e) { 1348 return false; 1349 } 1350 } 1351 1352 /** 1353 * Site settings support methods 1354 */ 1355 1356 /** 1357 * Create the data directory if requested 1358 * 1359 * @param array $submissionVars Submitted vars 1360 * @param array $formVars Variables in the form 1361 * 1362 * @return bool 1363 */ 1364 protected function createDataDirectory(&$submissionVars, $formVars) { 1365 // did the user have option of Elgg creating the data directory 1366 if ($formVars['dataroot']['type'] != 'combo') { 1367 return true; 1368 } 1369 1370 // did the user select the option 1371 if ($submissionVars['dataroot'] != 'dataroot-checkbox') { 1372 return true; 1373 } 1374 1375 $dir = \Elgg\Project\Paths::sanitize($submissionVars['path']) . 'data'; 1376 if (file_exists($dir) || mkdir($dir, 0755)) { 1377 $submissionVars['dataroot'] = $dir; 1378 if (!file_exists("$dir/.htaccess")) { 1379 $htaccess = "Order Deny,Allow\nDeny from All\n"; 1380 if (!file_put_contents("$dir/.htaccess", $htaccess)) { 1381 return false; 1382 } 1383 } 1384 1385 return true; 1386 } 1387 1388 return false; 1389 } 1390 1391 /** 1392 * Validate the site settings form variables 1393 * 1394 * @param array $submissionVars Submitted vars 1395 * @param array $formVars Vars in the form 1396 * 1397 * @return bool 1398 */ 1399 protected function validateSettingsVars($submissionVars, $formVars) { 1400 $app = $this->getApp(); 1401 1402 foreach ($formVars as $field => $info) { 1403 $submissionVars[$field] = trim($submissionVars[$field]); 1404 if ($info['required'] == true && $submissionVars[$field] === '') { 1405 $name = elgg_echo("install:settings:label:$field"); 1406 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name])); 1407 1408 return false; 1409 } 1410 } 1411 1412 // check that email address is email address 1413 if ($submissionVars['siteemail'] && !is_email_address($submissionVars['siteemail'])) { 1414 $msg = elgg_echo('install:error:emailaddress', [$submissionVars['siteemail']]); 1415 $app->_services->systemMessages->addErrorMessage($msg); 1416 1417 return false; 1418 } 1419 1420 return true; 1421 } 1422 1423 /** 1424 * Initialize the site including site entity, plugins, and configuration 1425 * 1426 * @param array $submissionVars Submitted vars 1427 * 1428 * @return bool 1429 * @throws InstallationException 1430 */ 1431 protected function saveSiteSettings($submissionVars) { 1432 $app = $this->getApp(); 1433 1434 $site = elgg_get_site_entity(); 1435 1436 if (!$site->guid) { 1437 $site = new ElggSite(); 1438 $site->name = strip_tags($submissionVars['sitename']); 1439 $site->access_id = ACCESS_PUBLIC; 1440 $site->email = $submissionVars['siteemail']; 1441 $site->save(); 1442 } 1443 1444 if ($site->guid !== 1) { 1445 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:createsite')); 1446 1447 return false; 1448 } 1449 1450 $app->_services->config->site = $site; 1451 1452 $sets = [ 1453 'installed' => time(), 1454 'version' => elgg_get_version(), 1455 'simplecache_enabled' => 1, 1456 'system_cache_enabled' => 1, 1457 'simplecache_minify_js' => true, 1458 'simplecache_minify_css' => true, 1459 'lastcache' => time(), 1460 'processed_upgrades' => [], 1461 'language' => 'en', 1462 'default_access' => $submissionVars['siteaccess'], 1463 'allow_registration' => false, 1464 'require_admin_validation' => false, 1465 'walled_garden' => false, 1466 'allow_user_default_access' => '', 1467 'default_limit' => 10, 1468 'security_protect_upgrade' => true, 1469 'security_notify_admins' => true, 1470 'security_notify_user_password' => true, 1471 'security_email_require_password' => true, 1472 'security_email_require_confirmation' => true, 1473 ]; 1474 1475 foreach ($sets as $key => $value) { 1476 elgg_save_config($key, $value); 1477 } 1478 1479 try { 1480 // Plugins hold reference to non-existing DB 1481 $app->_services->reset('plugins'); 1482 1483 _elgg_generate_plugin_entities(); 1484 1485 $plugins = $app->_services->plugins->find('any'); 1486 1487 foreach ($plugins as $plugin) { 1488 $manifest = $plugin->getManifest(); 1489 if (!$manifest instanceof ElggPluginManifest) { 1490 continue; 1491 } 1492 1493 if (!$manifest->getActivateOnInstall()) { 1494 continue; 1495 } 1496 1497 $plugin->activate(); 1498 } 1499 1500 // Wo don't need to run upgrades on new installations 1501 $app->_services->events->unregisterHandler('create', 'object', '_elgg_create_notice_of_pending_upgrade'); 1502 $upgrades = $app->_services->upgradeLocator->locate(); 1503 foreach ($upgrades as $upgrade) { 1504 $upgrade->setCompleted(); 1505 } 1506 } catch (Exception $e) { 1507 $app->_services->logger->log(\Psr\Log\LogLevel::ERROR, $e); 1508 } 1509 1510 return true; 1511 } 1512 1513 /** 1514 * Validate account form variables 1515 * 1516 * @param array $submissionVars Submitted vars 1517 * @param array $formVars Form vars 1518 * 1519 * @return bool 1520 * @throws InstallationException 1521 */ 1522 protected function validateAdminVars($submissionVars, $formVars) { 1523 1524 $app = $this->getApp(); 1525 1526 foreach ($formVars as $field => $info) { 1527 if ($info['required'] == true && !$submissionVars[$field]) { 1528 $name = elgg_echo("install:admin:label:$field"); 1529 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name])); 1530 1531 return false; 1532 } 1533 } 1534 1535 if ($submissionVars['password1'] !== $submissionVars['password2']) { 1536 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:admin:password:mismatch')); 1537 1538 return false; 1539 } 1540 1541 if (trim($submissionVars['password1']) == "") { 1542 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:admin:password:empty')); 1543 1544 return false; 1545 } 1546 1547 $minLength = $app->_services->configTable->get('min_password_length'); 1548 if (strlen($submissionVars['password1']) < $minLength) { 1549 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:admin:password:tooshort')); 1550 1551 return false; 1552 } 1553 1554 // check that email address is email address 1555 if ($submissionVars['email'] && !is_email_address($submissionVars['email'])) { 1556 $msg = elgg_echo('install:error:emailaddress', [$submissionVars['email']]); 1557 $app->_services->systemMessages->addErrorMessage($msg); 1558 1559 return false; 1560 } 1561 1562 return true; 1563 } 1564 1565 /** 1566 * Create a user account for the admin 1567 * 1568 * @param array $submissionVars Submitted vars 1569 * @param bool $login Login in the admin user? 1570 * 1571 * @return bool 1572 * @throws InstallationException 1573 */ 1574 protected function createAdminAccount($submissionVars, $login = false) { 1575 $app = $this->getApp(); 1576 1577 try { 1578 $guid = register_user( 1579 $submissionVars['username'], 1580 $submissionVars['password1'], 1581 $submissionVars['displayname'], 1582 $submissionVars['email'] 1583 ); 1584 } catch (RegistrationException $e) { 1585 $app->_services->systemMessages->addErrorMessage($e->getMessage()); 1586 1587 return false; 1588 } 1589 1590 if ($guid === false) { 1591 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:admin:cannot_create')); 1592 1593 return false; 1594 } 1595 1596 $user = get_entity($guid); 1597 1598 if (!$user instanceof ElggUser) { 1599 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:loadadmin')); 1600 1601 return false; 1602 } 1603 1604 $app = $this->getApp(); 1605 1606 $ia = $app->_services->session->setIgnoreAccess(true); 1607 if (!$user->makeAdmin()) { 1608 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:adminaccess')); 1609 } else { 1610 $app->_services->configTable->set('admin_registered', 1); 1611 } 1612 $app->_services->session->setIgnoreAccess($ia); 1613 1614 // add validation data to satisfy user validation plugins 1615 $user->validated = true; 1616 $user->validated_method = 'admin_user'; 1617 1618 if (!$login) { 1619 return true; 1620 } 1621 1622 $this->createSessionFromDatabase(); 1623 try { 1624 login($user); 1625 } catch (LoginException $ex) { 1626 $app->_services->systemMessages->addErrorMessage(elgg_echo('install:error:adminlogin')); 1627 1628 return false; 1629 } 1630 1631 return true; 1632 } 1633 1634 /** 1635 * Setup session 1636 * 1637 * @return void 1638 * @throws InstallationException 1639 */ 1640 protected function createSessionFromFile() { 1641 $app = $this->getApp(); 1642 $session = ElggSession::fromFiles($app->_services->config); 1643 $session->setName('Elgg_install'); 1644 $app->_services->setValue('session', $session); 1645 } 1646 1647 /** 1648 * Setup session 1649 * 1650 * @return void 1651 * @throws InstallationException 1652 */ 1653 protected function createSessionFromDatabase() { 1654 $app = $this->getApp(); 1655 $session = ElggSession::fromDatabase($app->_services->config, $app->_services->db); 1656 $session->start(); 1657 $app->_services->setValue('session', $session); 1658 } 1659} 1660