Elgg  Version master
ElggInstaller.php
Go to the documentation of this file.
1 <?php
2 
4 use Elgg\Config;
5 use Elgg\Database;
16 
42 
43  public const MARIADB_MINIMAL_VERSION = '10.6';
44  public const MYSQL_MINIMAL_VERSION = '8.0';
45  public const PHP_MINIMAL_VERSION = '8.1.0';
46 
47  protected array $steps = [
48  'welcome',
49  'requirements',
50  'database',
51  'settings',
52  'admin',
53  'complete',
54  ];
55 
56  protected array $has_completed = [
57  'config' => false,
58  'database' => false,
59  'settings' => false,
60  'admin' => false,
61  ];
62 
63  protected bool $is_action = false;
64 
68  protected $app;
69 
75  public function run(): \Elgg\Http\ResponseBuilder {
76  $app = $this->getApp();
77 
78  $this->is_action = $app->internal_services->request->getMethod() === 'POST';
79 
80  $step = $this->getCurrentStep();
81 
82  $this->determineInstallStatus();
83 
85  if ($response) {
86  return $response;
87  }
88 
89  // check if this is an install being resumed
90  $response = $this->resumeInstall($step);
91  if ($response) {
92  return $response;
93  }
94 
95  $this->finishBootstrapping($step);
96 
97  $params = $app->internal_services->request->request->all();
98 
99  $method = 'run' . ucwords($step);
100 
101  return $this->$method($params);
102  }
103 
110  protected function getApp(): Application {
111  if ($this->app) {
112  return $this->app;
113  }
114 
115  try {
116  $config = new Config();
117  $config->installer_running = true;
118  $config->dbencoding = 'utf8mb4';
119  $config->boot_cache_ttl = 0;
120  $config->system_cache_enabled = false;
121  $config->simplecache_enabled = false;
122  $config->debug = \Psr\Log\LogLevel::WARNING;
123  $config->cacheroot = Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/caches/';
124  $config->assetroot = Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/assets/';
125 
126  $app = Application::factory([
127  'config' => $config,
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->internal_services->boot->getCache()->disable();
142  $app->internal_services->pluginsCache->disable();
143  $app->internal_services->sessionCache->disable();
144  $app->internal_services->dataCache->disable();
145  $app->internal_services->autoloadManager->getCache()->disable();
146 
147  $current_step = $this->getCurrentStep();
148  $index_admin = array_search('admin', $this->getSteps());
149  $index_complete = array_search('complete', $this->getSteps());
150  $index_step = array_search($current_step, $this->getSteps());
151 
152  // For the admin creation action and the complete step we use the Elgg core session handling.
153  // Otherwise, use default php session handling
154  $use_elgg_session = ($index_step == $index_admin) || ($index_step == $index_complete);
155  if (!$use_elgg_session) {
156  $session = \ElggSession::fromFiles($app->internal_services->config);
157  $session->setName('Elgg_install');
158  $app->internal_services->set('session', $session);
159  }
160 
161  $app->internal_services->views->setViewtype('installation');
162  $app->internal_services->views->registerViewtypeFallback('installation');
163  $app->internal_services->views->registerViewsFromPath(Paths::elgg());
164  $app->internal_services->translator->registerTranslations(Paths::elgg() . 'install/languages/', true);
165 
166  return $this->app;
167  } catch (ConfigurationException $ex) {
168  throw new InstallationException($ex->getMessage());
169  }
170  }
171 
187  public function batchInstall(array $params, bool $create_htaccess = false): void {
188  $app = $this->getApp();
189 
190  $defaults = [
191  'dbhost' => 'localhost',
192  'dbport' => '3306',
193  'dbprefix' => 'elgg_',
194  'language' => 'en',
195  'siteaccess' => ACCESS_PUBLIC,
196  ];
197  $params = array_merge($defaults, $params);
198 
199  $required_params = [
200  'dbuser',
201  'dbpassword',
202  'dbname',
203  'sitename',
204  'wwwroot',
205  'dataroot',
206  'displayname',
207  'email',
208  'username',
209  'password',
210  ];
211  foreach ($required_params as $key) {
212  if (empty($params[$key])) {
213  throw new InstallationException(elgg_echo('install:error:requiredfield', [$key]));
214  }
215  }
216 
217  // password is passed in once
218  $params['password1'] = $params['password'];
219  $params['password2'] = $params['password'];
220 
221  if ($create_htaccess) {
222  $rewrite_tester = new RewriteTester();
223  if (!$rewrite_tester->createHtaccess($params['wwwroot'])) {
224  throw new InstallationException(elgg_echo('install:error:htaccess'));
225  }
226  }
227 
228  if (!\Elgg\Http\Urls::isValidMultiByteUrl($params['wwwroot'])) {
229  throw new InstallationException(elgg_echo('install:error:wwwroot', [$params['wwwroot']]));
230  }
231 
232  // sanitize dataroot path
233  $params['dataroot'] = Paths::sanitize($params['dataroot']);
234 
235  $this->determineInstallStatus();
236 
237  if (!$this->has_completed['config']) {
238  if (!$this->createSettingsFile($params)) {
239  throw new InstallationException(elgg_echo('install:error:settings'));
240  }
241  }
242 
243  $this->loadSettingsFile();
244 
245  // Make sure settings file matches parameters
246  $config = $app->internal_services->config;
247  $config_keys = [
248  // param key => config key
249  'dbhost' => 'dbhost',
250  'dbport' => 'dbport',
251  'dbuser' => 'dbuser',
252  'dbpassword' => 'dbpass',
253  'dbname' => 'dbname',
254  'dataroot' => 'dataroot',
255  'dbprefix' => 'dbprefix',
256  ];
257  foreach ($config_keys as $params_key => $config_key) {
258  if ($params[$params_key] !== $config->$config_key) {
259  throw new InstallationException(elgg_echo('install:error:settings_mismatch', [$config_key, $params[$params_key], $config->$config_key]));
260  }
261  }
262 
263  if (!$this->connectToDatabase()) {
264  throw new InstallationException(elgg_echo('install:error:databasesettings'));
265  }
266 
267  if (!$this->has_completed['database']) {
268  if (!$this->installDatabase()) {
269  throw new InstallationException(elgg_echo('install:error:cannotloadtables'));
270  }
271  }
272 
273  // load remaining core libraries
274  $this->finishBootstrapping('settings');
275 
276  if (!$this->saveSiteSettings($params)) {
277  throw new InstallationException(elgg_echo('install:error:savesitesettings'));
278  }
279 
280  if (!$this->createAdminAccount($params)) {
281  throw new InstallationException(elgg_echo('install:admin:cannot_create'));
282  }
283  }
284 
293  protected function render(string $step, array $vars = []): \Elgg\Http\OkResponse {
294  $vars['next_step'] = $this->getNextStep($step);
295 
296  $title = elgg_echo("install:{$step}");
297  $body = elgg_view("install/pages/{$step}", $vars);
298 
300  $title,
301  $body,
302  'default',
303  [
304  'step' => $step,
305  'steps' => $this->getSteps(),
306  ]
307  );
308 
309  return new \Elgg\Http\OkResponse($output);
310  }
311 
321  protected function runWelcome(): \Elgg\Http\OkResponse {
322  return $this->render('welcome');
323  }
324 
334  protected function runRequirements(array $vars = []): \Elgg\Http\OkResponse {
335 
336  $report = [];
337 
338  // check PHP parameters and libraries
339  $this->checkPHP($report);
340 
341  // check URL rewriting
342  $this->checkRewriteRules($report);
343 
344  // check for existence of settings file
345  if ($this->checkSettingsFile($report) !== true) {
346  // no file, so check permissions on engine directory
348  }
349 
350  // check the database later
351  $report['database'] = [
352  [
353  'severity' => 'notice',
354  'message' => elgg_echo('install:check:database'),
355  ],
356  ];
357 
358  return $this->render('requirements', [
359  'report' => $report,
360  'num_failures' => $this->countNumConditions($report, 'error'),
361  'num_warnings' => $this->countNumConditions($report, 'warning'),
362  ]);
363  }
364 
374  protected function runDatabase(array $submissionVars = []): \Elgg\Http\ResponseBuilder {
375 
376  $app = $this->getApp();
377 
378  $formVars = [
379  'dbuser' => [
380  'type' => 'text',
381  'value' => '',
382  'required' => true,
383  ],
384  'dbpassword' => [
385  'type' => 'password',
386  'value' => '',
387  'required' => false,
388  ],
389  'dbname' => [
390  'type' => 'text',
391  'value' => '',
392  'required' => true,
393  ],
394  'dbhost' => [
395  'type' => 'text',
396  'value' => 'localhost',
397  'required' => true,
398  ],
399  'dbport' => [
400  'type' => 'number',
401  'value' => 3306,
402  'required' => true,
403  'min' => 0,
404  'max' => 65535,
405  ],
406  'dbprefix' => [
407  'type' => 'text',
408  'value' => 'elgg_',
409  'required' => false,
410  ],
411  'dataroot' => [
412  'type' => 'text',
413  'value' => '',
414  'required' => true,
415  ],
416  'wwwroot' => [
417  'type' => 'url',
418  'value' => $app->internal_services->config->wwwroot,
419  'required' => true,
420  ],
421  'timezone' => [
422  'type' => 'dropdown',
423  'value' => 'UTC',
424  'options' => \DateTimeZone::listIdentifiers(),
425  'required' => true
426  ]
427  ];
428 
429  if ($this->checkSettingsFile()) {
430  // user manually created settings file so we fake out action test
431  $this->is_action = true;
432  }
433 
434  if ($this->is_action) {
435  $getResponse = function () use ($app, $submissionVars, $formVars) {
436  // only create settings file if it doesn't exist
437  if (!$this->checkSettingsFile()) {
438  if (!$this->validateDatabaseVars($submissionVars, $formVars)) {
439  // error so we break out of action and serve same page
440  return;
441  }
442 
443  if (!$this->createSettingsFile($submissionVars)) {
444  return;
445  }
446  }
447 
448  // check db version and connect
449  if (!$this->connectToDatabase()) {
450  return;
451  }
452 
453  if (!$this->installDatabase()) {
454  return;
455  }
456 
457  $app->internal_services->system_messages->addSuccessMessage(elgg_echo('install:success:database'));
458 
459  return $this->continueToNextStep('database');
460  };
461 
462  $response = $getResponse();
463  if ($response) {
464  return $response;
465  }
466  }
467 
468  $formVars = $this->makeFormSticky($formVars, $submissionVars);
469 
470  $params = ['variables' => $formVars,];
471 
472  if ($this->checkSettingsFile()) {
473  // settings file exists and we're here so failed to create database
474  $params['failure'] = true;
475  }
476 
477  return $this->render('database', $params);
478  }
479 
489  protected function runSettings(array $submissionVars = []): \Elgg\Http\ResponseBuilder {
490 
491  $app = $this->getApp();
492 
493  $formVars = [
494  'sitename' => [
495  'type' => 'text',
496  'value' => 'My New Community',
497  'required' => true,
498  ],
499  'siteemail' => [
500  'type' => 'email',
501  'value' => '',
502  'required' => false,
503  ],
504  'siteaccess' => [
505  'type' => 'access',
506  'value' => ACCESS_PUBLIC,
507  'required' => true,
508  ],
509  ];
510 
511  if ($this->is_action) {
512  $getResponse = function () use ($app, $submissionVars, $formVars) {
513 
514  if (!$this->validateSettingsVars($submissionVars, $formVars)) {
515  return;
516  }
517 
518  if (!$this->saveSiteSettings($submissionVars)) {
519  return;
520  }
521 
522  $app->internal_services->system_messages->addSuccessMessage(elgg_echo('install:success:settings'));
523 
524  return $this->continueToNextStep('settings');
525  };
526 
527  $response = $getResponse();
528  if ($response) {
529  return $response;
530  }
531  }
532 
533  $formVars = $this->makeFormSticky($formVars, $submissionVars);
534 
535  return $this->render('settings', ['variables' => $formVars]);
536  }
537 
547  protected function runAdmin(array $submissionVars = []): \Elgg\Http\ResponseBuilder {
548  $app = $this->getApp();
549 
550  $formVars = [
551  'displayname' => [
552  'type' => 'text',
553  'value' => '',
554  'required' => true,
555  ],
556  'email' => [
557  'type' => 'email',
558  'value' => '',
559  'required' => true,
560  ],
561  'username' => [
562  'type' => 'text',
563  'value' => '',
564  'required' => true,
565  ],
566  'password1' => [
567  'type' => 'password',
568  'value' => '',
569  'required' => true,
570  'pattern' => '.{6,}',
571  ],
572  'password2' => [
573  'type' => 'password',
574  'value' => '',
575  'required' => true,
576  ],
577  ];
578 
579  if ($this->is_action) {
580  $getResponse = function () use ($app, $submissionVars, $formVars) {
581  if (!$this->validateAdminVars($submissionVars, $formVars)) {
582  return;
583  }
584 
585  if (!$this->createAdminAccount($submissionVars, true)) {
586  return;
587  }
588 
589  $app->internal_services->system_messages->addSuccessMessage(elgg_echo('install:success:admin'));
590 
591  return $this->continueToNextStep('admin');
592  };
593 
594  $response = $getResponse();
595  if ($response) {
596  return $response;
597  }
598  }
599 
600  // Bit of a hack to get the password help to show right number of characters
601  // We burn the value into the stored translation.
602 
603  $lang = $app->internal_services->translator->getCurrentLanguage();
604  $translations = $app->internal_services->translator->getLoadedTranslations();
605 
606  $app->internal_services->translator->addTranslation($lang, [
607  'install:admin:help:password1' => sprintf(
608  $translations[$lang]['install:admin:help:password1'],
609  $app->internal_services->config->min_password_length
610  ),
611  ]);
612 
613  $formVars = $this->makeFormSticky($formVars, $submissionVars);
614 
615  return $this->render('admin', ['variables' => $formVars]);
616  }
617 
623  protected function runComplete(): \Elgg\Http\ResponseBuilder {
624 
625  // nudge to check out settings
626  $link = elgg_view_url(elgg_normalize_url('admin/site_settings'), elgg_echo('install:complete:admin_notice:link_text'));
627  $notice = elgg_format_element('p', [], elgg_echo('install:complete:admin_notice', [$link]));
628 
629  $custom_index_link = elgg_view_url(elgg_normalize_url('admin/plugin_settings/custom_index'), elgg_echo('admin:plugin_settings'));
630  $notice .= elgg_format_element('p', [], elgg_echo('install:complete:admin_notice:custom_index', [$custom_index_link]));
631 
632  elgg_add_admin_notice('fresh_install', $notice);
633 
634  $result = $this->render('complete');
635 
636  elgg_delete_directory(Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/');
637 
638  return $result;
639  }
640 
650  protected function getSteps(): array {
651  return $this->steps;
652  }
653 
659  protected function getCurrentStep(): string {
660  $step = get_input('step', 'welcome');
661 
662  return in_array($step, $this->getSteps()) ? $step : 'welcome';
663  }
664 
672  protected function continueToNextStep(string $currentStep): \Elgg\Http\RedirectResponse {
673  $this->is_action = false;
674 
675  return new \Elgg\Http\RedirectResponse($this->getNextStepUrl($currentStep));
676  }
677 
685  protected function getNextStep(string $currentStep): string {
686  $index = 1 + array_search($currentStep, $this->steps);
687 
688  return $this->steps[$index] ?? '';
689  }
690 
698  protected function getNextStepUrl(string $currentStep): string {
699  $app = $this->getApp();
700  $nextStep = $this->getNextStep($currentStep);
701 
702  return $app->internal_services->config->wwwroot . "install.php?step={$nextStep}";
703  }
704 
711  protected function determineInstallStatus(): void {
712  $app = $this->getApp();
713 
714  $path = Config::resolvePath();
715  if (!is_file($path) || !is_readable($path)) {
716  return;
717  }
718 
719  $this->loadSettingsFile();
720 
721  $this->has_completed['config'] = true;
722 
723  // must be able to connect to database to jump install steps
724  $dbSettingsPass = $this->checkDatabaseSettings(
725  $app->internal_services->config->dbuser,
726  $app->internal_services->config->dbpass,
727  $app->internal_services->config->dbname,
728  $app->internal_services->config->dbhost,
729  $app->internal_services->config->dbport
730  );
731 
732  if (!$dbSettingsPass) {
733  return;
734  }
735 
736  $db = $app->internal_services->db;
737 
738  try {
739  // check that the config table has been created
740  $result = $db->getConnection('read')->executeQuery('SHOW TABLES');
741  if (empty($result)) {
742  return;
743  }
744 
745  foreach ($result->fetchAllAssociative() as $table) {
746  if (in_array("{$db->prefix}config", $table)) {
747  $this->has_completed['database'] = true;
748  }
749  }
750 
751  if ($this->has_completed['database'] === false) {
752  return;
753  }
754 
755  // check that the config table has entries
756  $qb = \Elgg\Database\Select::fromTable(\Elgg\Database\ConfigTable::TABLE_NAME);
757  $qb->select('COUNT(*) AS total');
758 
759  $result = $db->getDataRow($qb);
760  if (!empty($result) && $result->total > 0) {
761  $this->has_completed['settings'] = true;
762  } else {
763  return;
764  }
765 
766  // check that the users entity table has an entry
767  $qb = \Elgg\Database\Select::fromTable(\Elgg\Database\EntityTable::TABLE_NAME, \Elgg\Database\EntityTable::DEFAULT_JOIN_ALIAS);
768  $qb->select('COUNT(*) AS total')
769  ->where($qb->compare('type', '=', 'user', ELGG_VALUE_STRING));
770 
771  $result = $db->getDataRow($qb);
772  if (!empty($result) && $result->total > 0) {
773  $this->has_completed['admin'] = true;
774  } else {
775  return;
776  }
777  } catch (DatabaseException $ex) {
778  throw new InstallationException('Elgg can not connect to the database: ' . $ex->getMessage(), $ex->getCode(), $ex);
779  }
780  }
781 
790  protected function checkInstallCompletion(string $step): ?\Elgg\Http\RedirectResponse {
791  if ($step === 'complete') {
792  return null;
793  }
794 
795  if (!in_array(false, $this->has_completed)) {
796  // install complete but someone is trying to view an install page
797  return new \Elgg\Http\RedirectResponse('/');
798  }
799 
800  return null;
801  }
802 
811  protected function resumeInstall(string $step): ?\Elgg\Http\RedirectResponse {
812  // only do a resume from the first step
813  if ($step !== 'welcome') {
814  return null;
815  }
816 
817  if ($this->has_completed['database'] === false) {
818  return null;
819  }
820 
821  if ($this->has_completed['settings'] === false) {
822  return new \Elgg\Http\RedirectResponse('install.php?step=settings');
823  }
824 
825  if ($this->has_completed['admin'] === false) {
826  return new \Elgg\Http\RedirectResponse('install.php?step=admin');
827  }
828 
829  // everything appears to be set up
830  return new \Elgg\Http\RedirectResponse('install.php?step=complete');
831  }
832 
845  protected function finishBootstrapping(string $step): void {
846 
847  $app = $this->getApp();
848 
849  $index_db = array_search('database', $this->getSteps());
850  $index_step = array_search($step, $this->getSteps());
851 
852  if ($index_step > $index_db) {
853  // once the database has been created, load rest of engine
854 
855  // dummy site needed to boot
856  $app->internal_services->config->site = new \ElggSite();
857 
858  $app->bootCore();
859  }
860  }
861 
868  protected function loadSettingsFile(): void {
869  try {
870  $app = $this->getApp();
871 
872  $config = Config::factory();
873  $app->internal_services->set('config', $config);
874 
875  // in case the DB instance is already captured in services, we re-inject its settings.
876  $app->internal_services->db->resetConnections(DbConfig::fromElggConfig($config));
877  } catch (\Exception $e) {
878  throw new InstallationException(elgg_echo('InstallationException:CannotLoadSettings'), 0, $e);
879  }
880  }
881 
894  protected function makeFormSticky(array $formVars = [], array $submissionVars = []): array {
895  foreach ($submissionVars as $field => $value) {
896  $formVars[$field]['value'] = $value;
897  }
898 
899  return $formVars;
900  }
901 
902  /* Requirement checks support methods */
903 
911  protected function isInstallDirWritable(array &$report): bool {
912  if (!is_writable(Paths::projectConfig())) {
913  $report['settings'] = [
914  [
915  'severity' => 'error',
916  'message' => elgg_echo('install:check:installdir', [Paths::PATH_TO_CONFIG]),
917  ],
918  ];
919 
920  return false;
921  }
922 
923  return true;
924  }
925 
933  protected function checkSettingsFile(array &$report = []): bool {
934  if (!is_file(Config::resolvePath())) {
935  return false;
936  }
937 
938  if (!is_readable(Config::resolvePath())) {
939  $report['settings'] = [
940  [
941  'severity' => 'error',
942  'message' => elgg_echo('install:check:readsettings'),
943  ],
944  ];
945  }
946 
947  return true;
948  }
949 
957  protected function checkPHP(array &$report): void {
958  $phpReport = [];
959 
960  if (version_compare(PHP_VERSION, self::PHP_MINIMAL_VERSION, '<')) {
961  $phpReport[] = [
962  'severity' => 'error',
963  'message' => elgg_echo('install:check:php:version', [self::PHP_MINIMAL_VERSION, PHP_VERSION]),
964  ];
965  }
966 
967  $this->checkPhpExtensions($phpReport);
968 
969  $this->checkPhpDirectives($phpReport);
970 
971  if (count($phpReport) == 0) {
972  $phpReport[] = [
973  'severity' => 'success',
974  'message' => elgg_echo('install:check:php:success'),
975  ];
976  }
977 
978  $report['php'] = $phpReport;
979  }
980 
988  protected function checkPhpExtensions(array &$phpReport): void {
989  $extensions = get_loaded_extensions();
991  'pdo_mysql',
992  'json',
993  'xml',
994  'gd',
995  'intl',
996  ];
997  foreach ($requiredExtensions as $extension) {
998  if (!in_array($extension, $extensions)) {
999  $phpReport[] = [
1000  'severity' => 'error',
1001  'message' => elgg_echo('install:check:php:extension', [$extension]),
1002  ];
1003  }
1004  }
1005 
1007  'mbstring',
1008  ];
1009  foreach ($recommendedExtensions as $extension) {
1010  if (!in_array($extension, $extensions)) {
1011  $phpReport[] = [
1012  'severity' => 'warning',
1013  'message' => elgg_echo('install:check:php:extension:recommend', [$extension]),
1014  ];
1015  }
1016  }
1017  }
1018 
1026  protected function checkPhpDirectives(array &$phpReport): void {
1027  if (ini_get('open_basedir')) {
1028  $phpReport[] = [
1029  'severity' => 'warning',
1030  'message' => elgg_echo('install:check:php:open_basedir'),
1031  ];
1032  }
1033 
1034  if (ini_get('safe_mode')) {
1035  $phpReport[] = [
1036  'severity' => 'warning',
1037  'message' => elgg_echo('install:check:php:safe_mode'),
1038  ];
1039  }
1040 
1041  if (ini_get('arg_separator.output') !== '&') {
1042  $separator = htmlspecialchars(ini_get('arg_separator.output'));
1043  $phpReport[] = [
1044  'severity' => 'error',
1045  'message' => elgg_echo('install:check:php:arg_separator', [$separator]),
1046  ];
1047  }
1048 
1049  if (ini_get('register_globals')) {
1050  $phpReport[] = [
1051  'severity' => 'error',
1052  'message' => elgg_echo('install:check:php:register_globals'),
1053  ];
1054  }
1055 
1056  if (ini_get('session.auto_start')) {
1057  $phpReport[] = [
1058  'severity' => 'error',
1059  'message' => elgg_echo('install:check:php:session.auto_start'),
1060  ];
1061  }
1062  }
1063 
1071  protected function checkRewriteRules(array &$report): void {
1072  $tester = new RewriteTester();
1073 
1074  $url = $this->getApp()->internal_services->config->wwwroot;
1075  $url .= Request::REWRITE_TEST_TOKEN . '?' . http_build_query([Request::REWRITE_TEST_TOKEN => '1']);
1076 
1077  $report['rewrite'] = [$tester->run($url)];
1078  }
1079 
1088  protected function countNumConditions(array $report, string $condition): int {
1089  $count = 0;
1090  foreach ($report as $checks) {
1091  foreach ($checks as $check) {
1092  if ($check['severity'] === $condition) {
1093  $count++;
1094  }
1095  }
1096  }
1097 
1098  return $count;
1099  }
1100 
1113  protected function validateDatabaseVars(array $submissionVars, array $formVars): bool {
1114 
1115  $app = $this->getApp();
1116 
1117  foreach ($formVars as $field => $info) {
1118  if ($info['required'] === true && !$submissionVars[$field]) {
1119  $name = elgg_echo("install:database:label:{$field}");
1120  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1121 
1122  return false;
1123  }
1124  }
1125 
1126  if (!empty($submissionVars['wwwroot']) && !\Elgg\Http\Urls::isValidMultiByteUrl($submissionVars['wwwroot'])) {
1127  $save_value = $this->sanitizeInputValue($submissionVars['wwwroot']);
1128  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:wwwroot', [$save_value]));
1129 
1130  return false;
1131  }
1132 
1133  // check that data root is absolute path
1134  if (stripos(PHP_OS, 'win') === 0) {
1135  if (strpos($submissionVars['dataroot'], ':') !== 1) {
1136  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1137  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:relative_path', [$save_value]));
1138 
1139  return false;
1140  }
1141  } else {
1142  if (!str_starts_with($submissionVars['dataroot'], '/')) {
1143  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1144  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:relative_path', [$save_value]));
1145 
1146  return false;
1147  }
1148  }
1149 
1150  // check that data root exists
1151  if (!is_dir($submissionVars['dataroot'])) {
1152  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1153  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:datadirectoryexists', [$save_value]));
1154 
1155  return false;
1156  }
1157 
1158  // check that data root is writable
1159  if (!is_writable($submissionVars['dataroot'])) {
1160  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1161  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:writedatadirectory', [$save_value]));
1162 
1163  return false;
1164  }
1165 
1166  // check that data root is not subdirectory of Elgg root
1167  if (stripos($submissionVars['dataroot'], Paths::project()) === 0) {
1168  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1169  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:locationdatadirectory', [$save_value]));
1170 
1171  return false;
1172  }
1173 
1174  // according to postgres documentation: SQL identifiers and key words must
1175  // begin with a letter (a-z, but also letters with diacritical marks and
1176  // non-Latin letters) or an underscore (_). Subsequent characters in an
1177  // identifier or key word can be letters, underscores, digits (0-9), or dollar signs ($).
1178  // Refs #4994
1179  if (!empty($submissionVars['dbprefix']) && !preg_match('/^[a-zA-Z_][\w]*$/', $submissionVars['dbprefix'])) {
1180  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:database_prefix'));
1181 
1182  return false;
1183  }
1184 
1185  return $this->checkDatabaseSettings(
1186  $submissionVars['dbuser'],
1187  $submissionVars['dbpassword'],
1188  $submissionVars['dbname'],
1189  $submissionVars['dbhost'],
1190  $submissionVars['dbport']
1191  );
1192  }
1193 
1205  protected function checkDatabaseSettings(string $user, string $password, string $dbname, string $host, int $port = null): bool {
1206  $app = $this->getApp();
1207 
1208  $config = new DbConfig((object) [
1209  'dbhost' => $host,
1210  'dbport' => $port,
1211  'dbuser' => $user,
1212  'dbpass' => $password,
1213  'dbname' => $dbname,
1214  'dbencoding' => 'utf8mb4',
1215  ]);
1216  $db = new Database($config, $app->internal_services->queryCache, $app->internal_services->config);
1217 
1218  try {
1219  $db->getConnection('read')->executeQuery('SELECT 1');
1220  } catch (DatabaseException $e) {
1221  if (str_starts_with($e->getMessage(), "Elgg couldn't connect")) {
1222  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:databasesettings'));
1223  } else {
1224  $save_value = $this->sanitizeInputValue($dbname);
1225  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:nodatabase', [$save_value]));
1226  }
1227 
1228  return false;
1229  }
1230 
1231  // check MySQL version
1232  $version = $db->getServerVersion();
1233  $min_version = $db->isMariaDB() ? self::MARIADB_MINIMAL_VERSION : self::MYSQL_MINIMAL_VERSION;
1234 
1235  if (version_compare($version, $min_version, '<')) {
1236  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:database_version', [$min_version, $version]));
1237 
1238  return false;
1239  }
1240 
1241  return true;
1242  }
1243 
1251  protected function createSettingsFile(array $params): bool {
1252  $app = $this->getApp();
1253 
1254  $template = file_get_contents(Paths::elgg() . 'elgg-config/settings.example.php');
1255  if (!$template) {
1256  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:readsettingsphp'));
1257 
1258  return false;
1259  }
1260 
1261  foreach ($params as $k => $v) {
1262  // do some sanitization
1263  switch ($k) {
1264  case 'dataroot':
1265  $v = Paths::sanitize($v);
1266  break;
1267  case 'dbpassword':
1268  $v = addslashes($v);
1269  break;
1270  }
1271 
1272  $template = str_replace('{{' . $k . '}}', $v, $template);
1273  }
1274 
1275  $result = file_put_contents(Config::resolvePath(), $template);
1276  if ($result === false) {
1277  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:writesettingphp'));
1278 
1279  return false;
1280  }
1281 
1282  $config = (object) [
1283  'dbhost' => elgg_extract('dbhost', $params, 'localhost'),
1284  'dbport' => elgg_extract('dbport', $params, 3306),
1285  'dbuser' => elgg_extract('dbuser', $params),
1286  'dbpass' => elgg_extract('dbpassword', $params),
1287  'dbname' => elgg_extract('dbname', $params),
1288  'dbencoding' => elgg_extract('dbencoding', $params, 'utf8mb4'),
1289  'dbprefix' => elgg_extract('dbprefix', $params, 'elgg_'),
1290  ];
1291 
1292  $dbConfig = new DbConfig($config);
1293  $this->getApp()->internal_services->set('dbConfig', $dbConfig);
1294  $this->getApp()->internal_services->db->resetConnections($dbConfig);
1295 
1296  return true;
1297  }
1298 
1304  protected function connectToDatabase(): bool {
1305  try {
1306  $app = $this->getApp();
1307  $app->internal_services->db->setupConnections();
1308  } catch (DatabaseException $e) {
1309  $app->internal_services->system_messages->addErrorMessage($e->getMessage());
1310 
1311  return false;
1312  }
1313 
1314  return true;
1315  }
1316 
1322  protected function installDatabase(): bool {
1323  try {
1324  return $this->getApp()->migrate();
1325  } catch (\Exception $e) {
1326  return false;
1327  }
1328  }
1329 
1342  protected function createDataDirectory(array &$submissionVars, array $formVars): bool {
1343  // did the user have option of Elgg creating the data directory
1344  if ($formVars['dataroot']['type'] !== 'combo') {
1345  return true;
1346  }
1347 
1348  // did the user select the option
1349  if ($submissionVars['dataroot'] !== 'dataroot-checkbox') {
1350  return true;
1351  }
1352 
1353  $dir = \Elgg\Project\Paths::sanitize($submissionVars['path']) . 'data';
1354  if (file_exists($dir) || mkdir($dir, 0755)) {
1355  $submissionVars['dataroot'] = $dir;
1356  if (!file_exists("{$dir}/.htaccess")) {
1357  $htaccess = "Order Deny,Allow\nDeny from All\n";
1358  if (!file_put_contents("$dir/.htaccess", $htaccess)) {
1359  return false;
1360  }
1361  }
1362 
1363  return true;
1364  }
1365 
1366  return false;
1367  }
1368 
1377  protected function validateSettingsVars(array $submissionVars, array $formVars): bool {
1378  $app = $this->getApp();
1379 
1380  foreach ($formVars as $field => $info) {
1381  $submissionVars[$field] = trim($submissionVars[$field]);
1382  if ($info['required'] === true && $submissionVars[$field] === '') {
1383  $name = elgg_echo("install:settings:label:{$field}");
1384  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1385 
1386  return false;
1387  }
1388  }
1389 
1390  // check that email address is email address
1391  if ($submissionVars['siteemail'] && !elgg_is_valid_email((string) $submissionVars['siteemail'])) {
1392  $save_value = $this->sanitizeInputValue($submissionVars['siteemail']);
1393  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:emailaddress', [$save_value]));
1394 
1395  return false;
1396  }
1397 
1398  return true;
1399  }
1400 
1408  protected function saveSiteSettings(array $submissionVars): bool {
1409  $app = $this->getApp();
1410 
1412 
1413  if (!$site->guid) {
1414  $site = new \ElggSite();
1415  $site->name = strip_tags($submissionVars['sitename']);
1416  $site->access_id = ACCESS_PUBLIC;
1417  $site->email = $submissionVars['siteemail'];
1418  $site->save();
1419  }
1420 
1421  if ($site->guid !== 1) {
1422  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:createsite'));
1423 
1424  return false;
1425  }
1426 
1427  $app->internal_services->config->site = $site;
1428 
1429  $sets = [
1430  'installed' => time(),
1431  'simplecache_enabled' => 1,
1432  'system_cache_enabled' => 1,
1433  'simplecache_minify_js' => true,
1434  'simplecache_minify_css' => true,
1435  'lastcache' => time(),
1436  'language' => 'en',
1437  'default_access' => $submissionVars['siteaccess'],
1438  'allow_registration' => false,
1439  'require_admin_validation' => false,
1440  'walled_garden' => false,
1441  'allow_user_default_access' => '',
1442  'default_limit' => 10,
1443  ];
1444 
1445  foreach ($sets as $key => $value) {
1447  }
1448 
1449  try {
1450  _elgg_services()->plugins->generateEntities();
1451 
1452  $app->internal_services->reset('plugins');
1453 
1454  if (elgg_extract('activate_plugins', $submissionVars, true)) {
1455  $plugins = $app->internal_services->plugins->find('all');
1456 
1457  foreach ($plugins as $plugin) {
1458  $plugin_config = $plugin->getStaticConfig('plugin', []);
1459  if (!elgg_extract('activate_on_install', $plugin_config, false)) {
1460  continue;
1461  }
1462 
1463  try {
1464  $plugin->activate();
1465  } catch (PluginException $e) {
1466  // do nothing
1467  }
1468  }
1469  }
1470 
1471  // Wo don't need to run upgrades on new installations
1472  $app->internal_services->events->unregisterHandler('create:after', 'object', \Elgg\Upgrade\CreateAdminNoticeHandler::class);
1473  $upgrades = $app->internal_services->upgradeLocator->locate();
1474  foreach ($upgrades as $upgrade) {
1475  $upgrade->setCompleted();
1476  }
1477  } catch (\Exception $e) {
1478  $app->internal_services->logger->log(\Psr\Log\LogLevel::ERROR, $e);
1479  }
1480 
1481  return true;
1482  }
1483 
1492  protected function validateAdminVars(array $submissionVars, array $formVars): bool {
1493 
1494  $app = $this->getApp();
1495 
1496  foreach ($formVars as $field => $info) {
1497  if ($info['required'] === true && !$submissionVars[$field]) {
1498  $name = elgg_echo("install:admin:label:{$field}");
1499  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1500 
1501  return false;
1502  }
1503  }
1504 
1505  if ($submissionVars['password1'] !== $submissionVars['password2']) {
1506  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:mismatch'));
1507 
1508  return false;
1509  }
1510 
1511  if (trim($submissionVars['password1']) === '') {
1512  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:empty'));
1513 
1514  return false;
1515  }
1516 
1517  $minLength = $app->internal_services->configTable->get('min_password_length');
1518  if (strlen($submissionVars['password1']) < $minLength) {
1519  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:tooshort'));
1520 
1521  return false;
1522  }
1523 
1524  // check that email address is email address
1525  if ($submissionVars['email'] && !elgg_is_valid_email((string) $submissionVars['email'])) {
1526  $save_value = $this->sanitizeInputValue($submissionVars['email']);
1527  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:emailaddress', [$save_value]));
1528 
1529  return false;
1530  }
1531 
1532  return true;
1533  }
1534 
1543  protected function createAdminAccount(array $submissionVars, bool $login = false): bool {
1544  $app = $this->getApp();
1545 
1546  try {
1548  'username' => $submissionVars['username'],
1549  'password' => $submissionVars['password1'],
1550  'name' => $submissionVars['displayname'],
1551  'email' => $submissionVars['email'],
1552  ]);
1553  } catch (RegistrationException $e) {
1554  $app->internal_services->system_messages->addErrorMessage($e->getMessage());
1555 
1556  return false;
1557  }
1558 
1559  elgg_call(ELGG_IGNORE_ACCESS, function() use ($app, $user) {
1560  if (!$user->makeAdmin()) {
1561  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:adminaccess'));
1562  }
1563  });
1564 
1565  // add validation data to satisfy user validation plugins
1566  $user->validated = true;
1567  $user->validated_method = 'admin_user';
1568 
1569  if (!$login) {
1570  return true;
1571  }
1572 
1573  try {
1574  elgg_login($user);
1575  } catch (LoginException $ex) {
1576  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:adminlogin'));
1577 
1578  return false;
1579  }
1580 
1581  return true;
1582  }
1583 
1591  protected function sanitizeInputValue($input_value) {
1592  if (is_array($input_value)) {
1593  return array_map([$this, __FUNCTION__], $input_value);
1594  }
1595 
1596  if (!is_string($input_value)) {
1597  return $input_value;
1598  }
1599 
1600  return htmlspecialchars($input_value);
1601  }
1602 }
elgg_call(int $flags, Closure $closure)
Calls a callable autowiring the arguments using public DI services and applying logic based on flags...
Definition: elgglib.php:304
elgg
Definition: install.js:27
$current_step
Install sidebar.
Definition: sidebar.php:9
batchInstall(array $params, bool $create_htaccess=false)
A batch install of Elgg.
checkPhpDirectives(array &$phpReport)
Check PHP parameters.
$plugin
foreach(array_keys($combine_languages) as $language) $translations
A generic parent class for Configuration exceptions.
Bundled plugins(the contents of the"/mod"directory) are available only under the GPLv2 license.The remainder of the project is available under either MIT or GPLv2.Both licenses can be found below.More info and that you know you can do these things To protect your we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights These restrictions translate to certain responsibilities for you if you distribute copies of the or if you modify it For if you distribute copies of such a whether gratis or for a you must give the recipients all the rights that you have You must make sure that receive or can get the source code And you must show them these terms so they know their rights We protect your rights with two steps
Definition: LICENSE.txt:1
$requiredExtensions
$params
Saves global plugin settings.
Definition: save.php:13
Database configuration service.
Definition: DbConfig.php:13
runDatabase(array $submissionVars=[])
Database set up controller.
Elgg registration action.
$defaults
Generic entity header upload helper.
Definition: header.php:6
runSettings(array $submissionVars=[])
Site settings controller.
createSettingsFile(array $params)
Writes the settings file to the engine directory.
Generic parent class for login exceptions.
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
$response
Definition: content.php:10
$title
Definition: generic.php:50
$version
elgg_register_user(array $params=[])
Registers a user.
Definition: users.php:162
c Accompany it with the information you received as to the offer to distribute corresponding source complete source code means all the source code for all modules it plus any associated interface definition plus the scripts used to control compilation and installation of the executable as a special the source code distributed need not include anything that is normally and so on of the operating system on which the executable unless that component itself accompanies the executable If distribution of executable or object code is made by offering access to copy from a designated then offering equivalent access to copy the source code from the same place counts as distribution of the source even though third parties are not compelled to copy the source along with the object code You may not or distribute the Program except as expressly provided under this License Any attempt otherwise to sublicense or distribute the Program is void
Definition: LICENSE.txt:215
elgg_echo(string $message_key, array $args=[], string $language= '')
Elgg language module Functions to manage language and translations.
Definition: languages.php:17
installDatabase()
Create the database tables.
run()
Dispatches a request to one of the step controllers.
static fromFiles(Config $config)
Create a session stored in files.
Could not register a new user for whatever reason.
getSteps()
Step management.
getApp()
Build the application needed by the installer.
$site
Definition: icons.php:5
Test if URL rewriting is working.
sanitizeInputValue($input_value)
Sanitize input to help prevent XSS.
Elgg Installer.
if(!$item instanceof ElggEntity) $link
Definition: container.php:16
const MARIADB_MINIMAL_VERSION
runAdmin(array $submissionVars=[])
Admin account controller.
getNextStep(string $currentStep)
Get the next step as a string.
$value
Definition: generic.php:51
get_input(string $variable, $default=null, bool $filter_result=true)
Parameter input functions.
Definition: input.php:20
checkDatabaseSettings(string $user, string $password, string $dbname, string $host, int $port=null)
Confirm the settings for the database.
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:254
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
string project
Definition: conf.py:49
Updates the basic settings for the primary site object.
$upgrades
Lists pending upgrades.
Definition: upgrades.php:11
$path
Definition: details.php:70
saveSiteSettings(array $submissionVars)
Initialize the site including site entity, plugins, and configuration.
$tester
getCurrentStep()
Returns current step.
const ELGG_IGNORE_ACCESS
elgg_call() flags
Definition: constants.php:130
$separator
Definition: tags.php:56
elgg_view(string $view, array $vars=[], string $viewtype= '')
Return a parsed view.
Definition: views.php:156
if(empty($guid)) $upgrade
Definition: upgrade.php:11
checkPHP(array &$report)
Check version of PHP, extensions, and variables.
if(!$user||!$user->canEdit()) $password
const MYSQL_MINIMAL_VERSION
makeFormSticky(array $formVars=[], array $submissionVars=[])
Action handling methods.
createAdminAccount(array $submissionVars, bool $login=false)
Create a user account for the admin.
elgg_view_page(string $title, string|array $body, string $page_shell= 'default', array $vars=[])
Assembles and outputs a full page.
Definition: views.php:235
$lang
Definition: html.php:13
runWelcome()
Step controllers.
foreach($recommendedExtensions as $extension) if(empty(ini_get('session.gc_probability'))||empty(ini_get('session.gc_divisor'))) $db
runComplete()
Controller for last step.
checkPhpExtensions(array &$phpReport)
Check the server&#39;s PHP extensions.
Thrown when there is a major problem with the installation.
elgg_save_config(string $name, $value)
Save a configuration setting.
A generic parent class for database exceptions.
runRequirements(array $vars=[])
Requirements controller.
$user
Definition: ban.php:7
continueToNextStep(string $currentStep)
Forwards the browser to the next step.
$count
Definition: ban.php:24
array $has_completed
isInstallDirWritable(array &$report)
Indicates whether the webserver can add settings.php on its own or not.
resumeInstall(string $step)
Check if this is a case of a install being resumed and figure out where to continue from...
$body
Definition: useradd.php:55
$table
Definition: user.php:37
validateSettingsVars(array $submissionVars, array $formVars)
Validate the site settings form variables.
elgg_get_site_entity()
Get the current site entity.
Definition: entities.php:101
validateDatabaseVars(array $submissionVars, array $formVars)
Database support methods.
$extensions
checkRewriteRules(array &$report)
Confirm that the rewrite rules are firing.
if(elgg_extract('required', $vars)) if(elgg_extract('disabled', $vars)) $field
Definition: field.php:38
countNumConditions(array $report, string $condition)
Count the number of failures in the requirements report.
determineInstallStatus()
Updates $this->has_completed according to the current installation.
if($container instanceof ElggGroup &&$container->guid!=elgg_get_page_owner_guid()) $key
Definition: summary.php:44
getNextStepUrl(string $currentStep)
Get the URL of the next step.
Load, boot, and implement a front controller for an Elgg application.
Definition: Application.php:47
elgg_delete_directory(string $directory, bool $leave_base_directory=false)
Delete a directory and all its contents.
Definition: filestore.php:51
$step
Definition: time.php:40
$report
if(isset($_COOKIE['elggperm'])) $session
Definition: login_as.php:29
elgg_is_valid_email(string $address)
Validates an email address.
Definition: input.php:89
$vars
Definition: theme.php:5
elgg_view_url(string $href, string $text=null, array $options=[])
Helper function for outputting urls.
Definition: views.php:1427
const ELGG_VALUE_STRING
Definition: constants.php:112
loadSettingsFile()
Load settings.
checkSettingsFile(array &$report=[])
Check that the settings file exists.
createDataDirectory(array &$submissionVars, array $formVars)
Site settings support methods.
foreach($requiredExtensions as $extension) $recommendedExtensions
elgg_login(\ElggUser $user, bool $persistent=false)
Log in a user.
Definition: sessions.php:81
$min_version
foreach($plugin_guids as $guid) if(empty($deactivated_plugins)) $url
Definition: deactivate.php:39
checkInstallCompletion(string $step)
Security check to ensure the installer cannot be run after installation has finished.
static sanitize($path, $append_slash=true)
Sanitize file paths ensuring that they begin and end with slashes etc.
Definition: Paths.php:76
elgg_add_admin_notice(string $id, string $message)
Write a persistent message to the admin view.
Definition: admin.php:51
_elgg_services()
Get the global service provider.
Definition: elgglib.php:351
const ACCESS_PUBLIC
Definition: constants.php:12
connectToDatabase()
Bootstrap database connection before entire engine is available.
elgg_format_element(string $tag_name, array $attributes=[], string $text= '', array $options=[])
Format an HTML element.
Definition: output.php:145
$index
Definition: gallery.php:40
elgg_normalize_url(string $url)
Definition: output.php:163
validateAdminVars(array $submissionVars, array $formVars)
Validate account form variables.
Login as the specified user.
$output
Definition: download.php:9
$qb
Definition: queue.php:12
render(string $step, array $vars=[])
Renders the data passed by a controller.
array bool $is_action
const PHP_MINIMAL_VERSION
$extension
Definition: default.php:25
finishBootstrapping(string $step)
Bootstrapping.