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 = sys_get_temp_dir() . 'elgginstaller/caches';
124  $config->assetroot = 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->bootCache->disable();
142  $app->internal_services->pluginsCache->disable();
143  $app->internal_services->accessCache->disable();
144  $app->internal_services->metadataCache->disable();
145  $app->internal_services->serverCache->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  if ($params['dataroot'] !== $config->dataroot) {
248  throw new InstallationException(elgg_echo('install:error:settings_mismatch', ['dataroot', $params['dataroot'], $config->dataroot]));
249  }
250 
251  $db_config = $app->internal_services->dbConfig->getConnectionConfig();
252  $db_config_keys = [
253  // param key => db config key
254  'dbhost' => 'host',
255  'dbport' => 'port',
256  'dbuser' => 'user',
257  'dbpassword' => 'password',
258  'dbname' => 'database',
259  'dbprefix' => 'prefix',
260  ];
261  foreach ($db_config_keys as $params_key => $db_config_key) {
262  if ($params[$params_key] !== (string) $db_config[$db_config_key]) {
263  throw new InstallationException(elgg_echo('install:error:settings_mismatch', [$db_config_key, $params[$params_key], $db_config[$db_config_key]]));
264  }
265  }
266 
267  if (!$this->connectToDatabase()) {
268  throw new InstallationException(elgg_echo('install:error:databasesettings'));
269  }
270 
271  if (!$this->has_completed['database']) {
272  if (!$this->installDatabase()) {
273  throw new InstallationException(elgg_echo('install:error:cannotloadtables'));
274  }
275  }
276 
277  // load remaining core libraries
278  $this->finishBootstrapping('settings');
279 
280  if (!$this->saveSiteSettings($params)) {
281  throw new InstallationException(elgg_echo('install:error:savesitesettings'));
282  }
283 
284  if (!$this->createAdminAccount($params)) {
285  throw new InstallationException(elgg_echo('install:admin:cannot_create'));
286  }
287  }
288 
297  protected function render(string $step, array $vars = []): \Elgg\Http\OkResponse {
298  $vars['next_step'] = $this->getNextStep($step);
299 
300  $title = elgg_echo("install:{$step}");
301  $body = elgg_view("install/pages/{$step}", $vars);
302 
304  $title,
305  $body,
306  'default',
307  [
308  'step' => $step,
309  'steps' => $this->getSteps(),
310  ]
311  );
312 
313  return new \Elgg\Http\OkResponse($output);
314  }
315 
325  protected function runWelcome(): \Elgg\Http\OkResponse {
326  return $this->render('welcome');
327  }
328 
338  protected function runRequirements(array $vars = []): \Elgg\Http\OkResponse {
339 
340  $report = [];
341 
342  // check PHP parameters and libraries
343  $this->checkPHP($report);
344 
345  // check URL rewriting
346  $this->checkRewriteRules($report);
347 
348  // check for existence of settings file
349  if ($this->checkSettingsFile($report) !== true) {
350  // no file, so check permissions on engine directory
352  }
353 
354  // check the database later
355  $report['database'] = [
356  [
357  'severity' => 'notice',
358  'message' => elgg_echo('install:check:database'),
359  ],
360  ];
361 
362  return $this->render('requirements', [
363  'report' => $report,
364  'num_failures' => $this->countNumConditions($report, 'error'),
365  'num_warnings' => $this->countNumConditions($report, 'warning'),
366  ]);
367  }
368 
378  protected function runDatabase(array $submissionVars = []): \Elgg\Http\ResponseBuilder {
379 
380  $app = $this->getApp();
381 
382  $formVars = [
383  'dbuser' => [
384  'type' => 'text',
385  'value' => '',
386  'required' => true,
387  ],
388  'dbpassword' => [
389  'type' => 'password',
390  'value' => '',
391  'required' => false,
392  ],
393  'dbname' => [
394  'type' => 'text',
395  'value' => '',
396  'required' => true,
397  ],
398  'dbhost' => [
399  'type' => 'text',
400  'value' => 'localhost',
401  'required' => true,
402  ],
403  'dbport' => [
404  'type' => 'number',
405  'value' => 3306,
406  'required' => true,
407  'min' => 0,
408  'max' => 65535,
409  ],
410  'dbprefix' => [
411  'type' => 'text',
412  'value' => 'elgg_',
413  'required' => false,
414  ],
415  'dataroot' => [
416  'type' => 'text',
417  'value' => '',
418  'required' => true,
419  ],
420  'wwwroot' => [
421  'type' => 'url',
422  'value' => $app->internal_services->config->wwwroot,
423  'required' => true,
424  ],
425  'timezone' => [
426  'type' => 'dropdown',
427  'value' => 'UTC',
428  'options' => \DateTimeZone::listIdentifiers(),
429  'required' => true
430  ]
431  ];
432 
433  if ($this->checkSettingsFile()) {
434  // user manually created settings file so we fake out action test
435  $this->is_action = true;
436  }
437 
438  if ($this->is_action) {
439  $getResponse = function () use ($app, $submissionVars, $formVars) {
440  // only create settings file if it doesn't exist
441  if (!$this->checkSettingsFile()) {
442  if (!$this->validateDatabaseVars($submissionVars, $formVars)) {
443  // error so we break out of action and serve same page
444  return;
445  }
446 
447  if (!$this->createSettingsFile($submissionVars)) {
448  return;
449  }
450  }
451 
452  // check db version and connect
453  if (!$this->connectToDatabase()) {
454  return;
455  }
456 
457  if (!$this->installDatabase()) {
458  return;
459  }
460 
461  $app->internal_services->system_messages->addSuccessMessage(elgg_echo('install:success:database'));
462 
463  return $this->continueToNextStep('database');
464  };
465 
466  $response = $getResponse();
467  if ($response) {
468  return $response;
469  }
470  }
471 
472  $formVars = $this->makeFormSticky($formVars, $submissionVars);
473 
474  $params = ['variables' => $formVars,];
475 
476  if ($this->checkSettingsFile()) {
477  // settings file exists and we're here so failed to create database
478  $params['failure'] = true;
479  }
480 
481  return $this->render('database', $params);
482  }
483 
493  protected function runSettings(array $submissionVars = []): \Elgg\Http\ResponseBuilder {
494 
495  $app = $this->getApp();
496 
497  $formVars = [
498  'sitename' => [
499  'type' => 'text',
500  'value' => 'My New Community',
501  'required' => true,
502  ],
503  'siteemail' => [
504  'type' => 'email',
505  'value' => '',
506  'required' => false,
507  ],
508  'siteaccess' => [
509  'type' => 'access',
510  'value' => ACCESS_PUBLIC,
511  'required' => true,
512  ],
513  ];
514 
515  if ($this->is_action) {
516  $getResponse = function () use ($app, $submissionVars, $formVars) {
517 
518  if (!$this->validateSettingsVars($submissionVars, $formVars)) {
519  return;
520  }
521 
522  if (!$this->saveSiteSettings($submissionVars)) {
523  return;
524  }
525 
526  $app->internal_services->system_messages->addSuccessMessage(elgg_echo('install:success:settings'));
527 
528  return $this->continueToNextStep('settings');
529  };
530 
531  $response = $getResponse();
532  if ($response) {
533  return $response;
534  }
535  }
536 
537  $formVars = $this->makeFormSticky($formVars, $submissionVars);
538 
539  return $this->render('settings', ['variables' => $formVars]);
540  }
541 
551  protected function runAdmin(array $submissionVars = []): \Elgg\Http\ResponseBuilder {
552  $app = $this->getApp();
553 
554  $formVars = [
555  'displayname' => [
556  'type' => 'text',
557  'value' => '',
558  'required' => true,
559  ],
560  'email' => [
561  'type' => 'email',
562  'value' => '',
563  'required' => true,
564  ],
565  'username' => [
566  'type' => 'text',
567  'value' => '',
568  'required' => true,
569  ],
570  'password1' => [
571  'type' => 'password',
572  'value' => '',
573  'required' => true,
574  'pattern' => '.{6,}',
575  ],
576  'password2' => [
577  'type' => 'password',
578  'value' => '',
579  'required' => true,
580  ],
581  ];
582 
583  if ($this->is_action) {
584  $getResponse = function () use ($app, $submissionVars, $formVars) {
585  if (!$this->validateAdminVars($submissionVars, $formVars)) {
586  return;
587  }
588 
589  if (!$this->createAdminAccount($submissionVars, true)) {
590  return;
591  }
592 
593  $app->internal_services->system_messages->addSuccessMessage(elgg_echo('install:success:admin'));
594 
595  return $this->continueToNextStep('admin');
596  };
597 
598  $response = $getResponse();
599  if ($response) {
600  return $response;
601  }
602  }
603 
604  // Bit of a hack to get the password help to show right number of characters
605  // We burn the value into the stored translation.
606 
607  $lang = $app->internal_services->translator->getCurrentLanguage();
608  $translations = $app->internal_services->translator->getLoadedTranslations();
609 
610  $app->internal_services->translator->addTranslation($lang, [
611  'install:admin:help:password1' => sprintf(
612  $translations[$lang]['install:admin:help:password1'],
613  $app->internal_services->config->min_password_length
614  ),
615  ]);
616 
617  $formVars = $this->makeFormSticky($formVars, $submissionVars);
618 
619  return $this->render('admin', ['variables' => $formVars]);
620  }
621 
627  protected function runComplete(): \Elgg\Http\ResponseBuilder {
628 
629  // nudge to check out settings
630  $link = elgg_view_url(elgg_normalize_url('admin/site_settings'), elgg_echo('install:complete:admin_notice:link_text'));
631  $notice = elgg_format_element('p', [], elgg_echo('install:complete:admin_notice', [$link]));
632 
633  $custom_index_link = elgg_view_url(elgg_normalize_url('admin/plugin_settings/custom_index'), elgg_echo('admin:plugin_settings'));
634  $notice .= elgg_format_element('p', [], elgg_echo('install:complete:admin_notice:custom_index', [$custom_index_link]));
635 
636  elgg_add_admin_notice('fresh_install', $notice);
637 
638  $result = $this->render('complete');
639 
640  elgg_delete_directory(Paths::sanitize(sys_get_temp_dir()) . 'elgginstaller/');
641 
642  return $result;
643  }
644 
654  protected function getSteps(): array {
655  return $this->steps;
656  }
657 
663  protected function getCurrentStep(): string {
664  $step = get_input('step', 'welcome');
665 
666  return in_array($step, $this->getSteps()) ? $step : 'welcome';
667  }
668 
676  protected function continueToNextStep(string $currentStep): \Elgg\Http\RedirectResponse {
677  $this->is_action = false;
678 
679  return new \Elgg\Http\RedirectResponse($this->getNextStepUrl($currentStep));
680  }
681 
689  protected function getNextStep(string $currentStep): string {
690  $index = 1 + array_search($currentStep, $this->steps);
691 
692  return $this->steps[$index] ?? '';
693  }
694 
702  protected function getNextStepUrl(string $currentStep): string {
703  $app = $this->getApp();
704  $nextStep = $this->getNextStep($currentStep);
705 
706  return $app->internal_services->config->wwwroot . "install.php?step={$nextStep}";
707  }
708 
715  protected function determineInstallStatus(): void {
716  $app = $this->getApp();
717 
718  $path = Config::resolvePath();
719  if (!is_file($path) || !is_readable($path)) {
720  return;
721  }
722 
723  $this->loadSettingsFile();
724 
725  $this->has_completed['config'] = true;
726 
727  // must be able to connect to database to jump install steps
728  $dbSettingsPass = $this->checkDatabaseSettings($app->internal_services->dbConfig);
729 
730  if (!$dbSettingsPass) {
731  return;
732  }
733 
734  $db = $app->internal_services->db;
735 
736  try {
737  // check that the config table has been created
738  $result = $db->getConnection('read')->executeQuery('SHOW TABLES');
739  if (empty($result)) {
740  return;
741  }
742 
743  foreach ($result->fetchAllAssociative() as $table) {
744  if (in_array("{$db->prefix}config", $table)) {
745  $this->has_completed['database'] = true;
746  }
747  }
748 
749  if ($this->has_completed['database'] === false) {
750  return;
751  }
752 
753  // check that the config table has entries
754  $qb = \Elgg\Database\Select::fromTable(\Elgg\Database\ConfigTable::TABLE_NAME);
755  $qb->select('COUNT(*) AS total');
756 
757  $result = $db->getDataRow($qb);
758  if (!empty($result) && $result->total > 0) {
759  $this->has_completed['settings'] = true;
760  } else {
761  return;
762  }
763 
764  // check that the users entity table has an entry
765  $qb = \Elgg\Database\Select::fromTable(\Elgg\Database\EntityTable::TABLE_NAME, \Elgg\Database\EntityTable::DEFAULT_JOIN_ALIAS);
766  $qb->select('COUNT(*) AS total')
767  ->where($qb->compare('type', '=', 'user', ELGG_VALUE_STRING));
768 
769  $result = $db->getDataRow($qb);
770  if (!empty($result) && $result->total > 0) {
771  $this->has_completed['admin'] = true;
772  } else {
773  return;
774  }
775  } catch (DatabaseException $ex) {
776  throw new InstallationException('Elgg can not connect to the database: ' . $ex->getMessage(), $ex->getCode(), $ex);
777  }
778  }
779 
788  protected function checkInstallCompletion(string $step): ?\Elgg\Http\RedirectResponse {
789  if ($step === 'complete') {
790  return null;
791  }
792 
793  if (!in_array(false, $this->has_completed)) {
794  // install complete but someone is trying to view an install page
795  return new \Elgg\Http\RedirectResponse('/');
796  }
797 
798  return null;
799  }
800 
809  protected function resumeInstall(string $step): ?\Elgg\Http\RedirectResponse {
810  // only do a resume from the first step
811  if ($step !== 'welcome') {
812  return null;
813  }
814 
815  if ($this->has_completed['database'] === false) {
816  return null;
817  }
818 
819  if ($this->has_completed['settings'] === false) {
820  return new \Elgg\Http\RedirectResponse('install.php?step=settings');
821  }
822 
823  if ($this->has_completed['admin'] === false) {
824  return new \Elgg\Http\RedirectResponse('install.php?step=admin');
825  }
826 
827  // everything appears to be set up
828  return new \Elgg\Http\RedirectResponse('install.php?step=complete');
829  }
830 
843  protected function finishBootstrapping(string $step): void {
844 
845  $app = $this->getApp();
846 
847  $index_db = array_search('database', $this->getSteps());
848  $index_step = array_search($step, $this->getSteps());
849 
850  if ($index_step > $index_db) {
851  // once the database has been created, load rest of engine
852 
853  // dummy site needed to boot
854  $app->internal_services->config->site = new \ElggSite();
855 
856  $app->bootCore();
857  }
858  }
859 
866  protected function loadSettingsFile(): void {
867  try {
868  $app = $this->getApp();
869 
870  $config = Config::factory();
871  $app->internal_services->set('config', $app->internal_services->initConfig($config));
872 
873  // in case the DB instance is already captured in services, we re-inject its settings.
874  $app->internal_services->db->resetConnections($app->internal_services->dbConfig);
875  } catch (\Exception $e) {
876  throw new InstallationException(elgg_echo('InstallationException:CannotLoadSettings'), 0, $e);
877  }
878  }
879 
892  protected function makeFormSticky(array $formVars = [], array $submissionVars = []): array {
893  foreach ($submissionVars as $field => $value) {
894  $formVars[$field]['value'] = $value;
895  }
896 
897  return $formVars;
898  }
899 
900  /* Requirement checks support methods */
901 
909  protected function isInstallDirWritable(array &$report): bool {
910  if (!is_writable(Paths::projectConfig())) {
911  $report['settings'] = [
912  [
913  'severity' => 'error',
914  'message' => elgg_echo('install:check:installdir', [Paths::PATH_TO_CONFIG]),
915  ],
916  ];
917 
918  return false;
919  }
920 
921  return true;
922  }
923 
931  protected function checkSettingsFile(array &$report = []): bool {
932  if (!is_file(Config::resolvePath())) {
933  return false;
934  }
935 
936  if (!is_readable(Config::resolvePath())) {
937  $report['settings'] = [
938  [
939  'severity' => 'error',
940  'message' => elgg_echo('install:check:readsettings'),
941  ],
942  ];
943  }
944 
945  return true;
946  }
947 
955  protected function checkPHP(array &$report): void {
956  $phpReport = [];
957 
958  if (version_compare(PHP_VERSION, self::PHP_MINIMAL_VERSION, '<')) {
959  $phpReport[] = [
960  'severity' => 'error',
961  'message' => elgg_echo('install:check:php:version', [self::PHP_MINIMAL_VERSION, PHP_VERSION]),
962  ];
963  }
964 
965  $this->checkPhpExtensions($phpReport);
966 
967  $this->checkPhpDirectives($phpReport);
968 
969  if (count($phpReport) == 0) {
970  $phpReport[] = [
971  'severity' => 'success',
972  'message' => elgg_echo('install:check:php:success'),
973  ];
974  }
975 
976  $report['php'] = $phpReport;
977  }
978 
986  protected function checkPhpExtensions(array &$phpReport): void {
987  $extensions = get_loaded_extensions();
989  'pdo_mysql',
990  'json',
991  'xml',
992  'gd',
993  'intl',
994  ];
995  foreach ($requiredExtensions as $extension) {
996  if (!in_array($extension, $extensions)) {
997  $phpReport[] = [
998  'severity' => 'error',
999  'message' => elgg_echo('install:check:php:extension', [$extension]),
1000  ];
1001  }
1002  }
1003 
1005  'mbstring',
1006  ];
1007  foreach ($recommendedExtensions as $extension) {
1008  if (!in_array($extension, $extensions)) {
1009  $phpReport[] = [
1010  'severity' => 'warning',
1011  'message' => elgg_echo('install:check:php:extension:recommend', [$extension]),
1012  ];
1013  }
1014  }
1015  }
1016 
1024  protected function checkPhpDirectives(array &$phpReport): void {
1025  if (ini_get('open_basedir')) {
1026  $phpReport[] = [
1027  'severity' => 'warning',
1028  'message' => elgg_echo('install:check:php:open_basedir'),
1029  ];
1030  }
1031 
1032  if (ini_get('safe_mode')) {
1033  $phpReport[] = [
1034  'severity' => 'warning',
1035  'message' => elgg_echo('install:check:php:safe_mode'),
1036  ];
1037  }
1038 
1039  if (ini_get('arg_separator.output') !== '&') {
1040  $separator = htmlspecialchars(ini_get('arg_separator.output'));
1041  $phpReport[] = [
1042  'severity' => 'error',
1043  'message' => elgg_echo('install:check:php:arg_separator', [$separator]),
1044  ];
1045  }
1046 
1047  if (ini_get('register_globals')) {
1048  $phpReport[] = [
1049  'severity' => 'error',
1050  'message' => elgg_echo('install:check:php:register_globals'),
1051  ];
1052  }
1053 
1054  if (ini_get('session.auto_start')) {
1055  $phpReport[] = [
1056  'severity' => 'error',
1057  'message' => elgg_echo('install:check:php:session.auto_start'),
1058  ];
1059  }
1060  }
1061 
1069  protected function checkRewriteRules(array &$report): void {
1070  $tester = new RewriteTester();
1071 
1072  $url = $this->getApp()->internal_services->config->wwwroot;
1073  $url .= Request::REWRITE_TEST_TOKEN . '?' . http_build_query([Request::REWRITE_TEST_TOKEN => '1']);
1074 
1075  $report['rewrite'] = [$tester->run($url)];
1076  }
1077 
1086  protected function countNumConditions(array $report, string $condition): int {
1087  $count = 0;
1088  foreach ($report as $checks) {
1089  foreach ($checks as $check) {
1090  if ($check['severity'] === $condition) {
1091  $count++;
1092  }
1093  }
1094  }
1095 
1096  return $count;
1097  }
1098 
1111  protected function validateDatabaseVars(array $submissionVars, array $formVars): bool {
1112 
1113  $app = $this->getApp();
1114 
1115  foreach ($formVars as $field => $info) {
1116  if ($info['required'] === true && !$submissionVars[$field]) {
1117  $name = elgg_echo("install:database:label:{$field}");
1118  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1119 
1120  return false;
1121  }
1122  }
1123 
1124  if (!empty($submissionVars['wwwroot']) && !\Elgg\Http\Urls::isValidMultiByteUrl($submissionVars['wwwroot'])) {
1125  $save_value = $this->sanitizeInputValue($submissionVars['wwwroot']);
1126  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:wwwroot', [$save_value]));
1127 
1128  return false;
1129  }
1130 
1131  // check that data root is absolute path
1132  if (stripos(PHP_OS, 'win') === 0) {
1133  if (strpos($submissionVars['dataroot'], ':') !== 1) {
1134  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1135  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:relative_path', [$save_value]));
1136 
1137  return false;
1138  }
1139  } else {
1140  if (!str_starts_with($submissionVars['dataroot'], '/')) {
1141  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1142  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:relative_path', [$save_value]));
1143 
1144  return false;
1145  }
1146  }
1147 
1148  // check that data root exists
1149  if (!is_dir($submissionVars['dataroot'])) {
1150  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1151  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:datadirectoryexists', [$save_value]));
1152 
1153  return false;
1154  }
1155 
1156  // check that data root is writable
1157  if (!is_writable($submissionVars['dataroot'])) {
1158  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1159  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:writedatadirectory', [$save_value]));
1160 
1161  return false;
1162  }
1163 
1164  // check that data root is not subdirectory of Elgg root
1165  if (stripos($submissionVars['dataroot'], Paths::project()) === 0) {
1166  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1167  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:locationdatadirectory', [$save_value]));
1168 
1169  return false;
1170  }
1171 
1172  // according to postgres documentation: SQL identifiers and key words must
1173  // begin with a letter (a-z, but also letters with diacritical marks and
1174  // non-Latin letters) or an underscore (_). Subsequent characters in an
1175  // identifier or key word can be letters, underscores, digits (0-9), or dollar signs ($).
1176  // Refs #4994
1177  if (!empty($submissionVars['dbprefix']) && !preg_match('/^[a-zA-Z_][\w]*$/', $submissionVars['dbprefix'])) {
1178  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:database_prefix'));
1179 
1180  return false;
1181  }
1182 
1183  $config = new DbConfig((object) [
1184  'dbhost' => $submissionVars['dbhost'],
1185  'dbport' => $submissionVars['dbport'],
1186  'dbuser' => $submissionVars['dbuser'],
1187  'dbpass' => $submissionVars['dbpassword'],
1188  'dbname' => $submissionVars['dbname'],
1189  'dbencoding' => 'utf8mb4',
1190  ]);
1191 
1192  return $this->checkDatabaseSettings($config);
1193  }
1194 
1202  protected function checkDatabaseSettings(DbConfig $config): bool {
1203  $app = $this->getApp();
1204 
1205  $db = new Database($config, $app->internal_services->queryCache, $app->internal_services->config);
1206 
1207  try {
1208  $db->getConnection('read')->executeQuery('SELECT 1');
1209  } catch (DatabaseException $e) {
1210  if (str_starts_with($e->getMessage(), "Elgg couldn't connect")) {
1211  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:databasesettings'));
1212  } else {
1213  $database = (string) elgg_extract('database', $config->getConnectionConfig());
1214  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:nodatabase', [$database]));
1215  }
1216 
1217  return false;
1218  }
1219 
1220  // check MySQL version
1221  $version = $db->getServerVersion();
1222  $min_version = $db->isMariaDB() ? self::MARIADB_MINIMAL_VERSION : self::MYSQL_MINIMAL_VERSION;
1223 
1224  if (version_compare($version, $min_version, '<')) {
1225  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:database_version', [$min_version, $version]));
1226 
1227  return false;
1228  }
1229 
1230  return true;
1231  }
1232 
1240  protected function createSettingsFile(array $params): bool {
1241  $app = $this->getApp();
1242 
1243  $template = file_get_contents(Paths::elgg() . 'elgg-config/settings.example.php');
1244  if (!$template) {
1245  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:readsettingsphp'));
1246 
1247  return false;
1248  }
1249 
1250  foreach ($params as $k => $v) {
1251  // do some sanitization
1252  switch ($k) {
1253  case 'dataroot':
1254  $v = Paths::sanitize($v);
1255  break;
1256  case 'dbpassword':
1257  $v = addslashes($v);
1258  break;
1259  }
1260 
1261  $template = str_replace('{{' . $k . '}}', $v, $template);
1262  }
1263 
1264  $result = file_put_contents(Config::resolvePath(), $template);
1265  if ($result === false) {
1266  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:writesettingphp'));
1267 
1268  return false;
1269  }
1270 
1271  $config = (object) [
1272  'dbhost' => elgg_extract('dbhost', $params, 'localhost'),
1273  'dbport' => elgg_extract('dbport', $params, 3306),
1274  'dbuser' => elgg_extract('dbuser', $params),
1275  'dbpass' => elgg_extract('dbpassword', $params),
1276  'dbname' => elgg_extract('dbname', $params),
1277  'dbencoding' => elgg_extract('dbencoding', $params, 'utf8mb4'),
1278  'dbprefix' => elgg_extract('dbprefix', $params, 'elgg_'),
1279  ];
1280 
1281  $dbConfig = new DbConfig($config);
1282  $this->getApp()->internal_services->set('dbConfig', $dbConfig);
1283  $this->getApp()->internal_services->db->resetConnections($dbConfig);
1284 
1285  return true;
1286  }
1287 
1293  protected function connectToDatabase(): bool {
1294  try {
1295  $app = $this->getApp();
1296  $app->internal_services->db->setupConnections();
1297  } catch (DatabaseException $e) {
1298  $app->internal_services->system_messages->addErrorMessage($e->getMessage());
1299 
1300  return false;
1301  }
1302 
1303  return true;
1304  }
1305 
1311  protected function installDatabase(): bool {
1312  try {
1313  return $this->getApp()->migrate();
1314  } catch (\Exception $e) {
1315  return false;
1316  }
1317  }
1318 
1331  protected function createDataDirectory(array &$submissionVars, array $formVars): bool {
1332  // did the user have option of Elgg creating the data directory
1333  if ($formVars['dataroot']['type'] !== 'combo') {
1334  return true;
1335  }
1336 
1337  // did the user select the option
1338  if ($submissionVars['dataroot'] !== 'dataroot-checkbox') {
1339  return true;
1340  }
1341 
1342  $dir = \Elgg\Project\Paths::sanitize($submissionVars['path']) . 'data';
1343  if (file_exists($dir) || mkdir($dir, 0755)) {
1344  $submissionVars['dataroot'] = $dir;
1345  if (!file_exists("{$dir}/.htaccess")) {
1346  $htaccess = "Order Deny,Allow\nDeny from All\n";
1347  if (!file_put_contents("$dir/.htaccess", $htaccess)) {
1348  return false;
1349  }
1350  }
1351 
1352  return true;
1353  }
1354 
1355  return false;
1356  }
1357 
1366  protected function validateSettingsVars(array $submissionVars, array $formVars): bool {
1367  $app = $this->getApp();
1368 
1369  foreach ($formVars as $field => $info) {
1370  $submissionVars[$field] = trim($submissionVars[$field]);
1371  if ($info['required'] === true && $submissionVars[$field] === '') {
1372  $name = elgg_echo("install:settings:label:{$field}");
1373  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1374 
1375  return false;
1376  }
1377  }
1378 
1379  // check that email address is email address
1380  if ($submissionVars['siteemail'] && !elgg_is_valid_email((string) $submissionVars['siteemail'])) {
1381  $save_value = $this->sanitizeInputValue($submissionVars['siteemail']);
1382  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:emailaddress', [$save_value]));
1383 
1384  return false;
1385  }
1386 
1387  return true;
1388  }
1389 
1397  protected function saveSiteSettings(array $submissionVars): bool {
1398  $app = $this->getApp();
1399 
1401 
1402  if (!$site->guid) {
1403  $site = new \ElggSite();
1404  $site->name = strip_tags($submissionVars['sitename']);
1405  $site->access_id = ACCESS_PUBLIC;
1406  $site->email = $submissionVars['siteemail'];
1407  $site->save();
1408  }
1409 
1410  if ($site->guid !== 1) {
1411  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:createsite'));
1412 
1413  return false;
1414  }
1415 
1416  $app->internal_services->config->site = $site;
1417 
1418  $sets = [
1419  'installed' => time(),
1420  'simplecache_enabled' => 1,
1421  'system_cache_enabled' => 1,
1422  'simplecache_minify_js' => true,
1423  'simplecache_minify_css' => true,
1424  'lastcache' => time(),
1425  'language' => 'en',
1426  'default_access' => $submissionVars['siteaccess'],
1427  'allow_registration' => false,
1428  'require_admin_validation' => false,
1429  'walled_garden' => false,
1430  'allow_user_default_access' => '',
1431  'default_limit' => 10,
1432  ];
1433 
1434  foreach ($sets as $key => $value) {
1436  }
1437 
1438  try {
1439  _elgg_services()->plugins->generateEntities();
1440 
1441  $app->internal_services->reset('plugins');
1442 
1443  if (elgg_extract('activate_plugins', $submissionVars, true)) {
1444  $plugins = $app->internal_services->plugins->find('all');
1445 
1446  foreach ($plugins as $plugin) {
1447  $plugin_config = $plugin->getStaticConfig('plugin', []);
1448  if (!elgg_extract('activate_on_install', $plugin_config, false)) {
1449  continue;
1450  }
1451 
1452  try {
1453  $plugin->activate();
1454  } catch (PluginException $e) {
1455  // do nothing
1456  }
1457  }
1458  }
1459 
1460  // Wo don't need to run upgrades on new installations
1461  $app->internal_services->events->unregisterHandler('create:after', 'object', \Elgg\Upgrade\CreateAdminNoticeHandler::class);
1462  $upgrades = $app->internal_services->upgradeLocator->locate();
1463  foreach ($upgrades as $upgrade) {
1464  $upgrade->setCompleted();
1465  }
1466  } catch (\Exception $e) {
1467  $app->internal_services->logger->log(\Psr\Log\LogLevel::ERROR, $e);
1468  }
1469 
1470  return true;
1471  }
1472 
1481  protected function validateAdminVars(array $submissionVars, array $formVars): bool {
1482 
1483  $app = $this->getApp();
1484 
1485  foreach ($formVars as $field => $info) {
1486  if ($info['required'] === true && !$submissionVars[$field]) {
1487  $name = elgg_echo("install:admin:label:{$field}");
1488  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1489 
1490  return false;
1491  }
1492  }
1493 
1494  if ($submissionVars['password1'] !== $submissionVars['password2']) {
1495  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:mismatch'));
1496 
1497  return false;
1498  }
1499 
1500  if (trim($submissionVars['password1']) === '') {
1501  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:empty'));
1502 
1503  return false;
1504  }
1505 
1506  $minLength = $app->internal_services->configTable->get('min_password_length');
1507  if (strlen($submissionVars['password1']) < $minLength) {
1508  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:tooshort'));
1509 
1510  return false;
1511  }
1512 
1513  // check that email address is email address
1514  if ($submissionVars['email'] && !elgg_is_valid_email((string) $submissionVars['email'])) {
1515  $save_value = $this->sanitizeInputValue($submissionVars['email']);
1516  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:emailaddress', [$save_value]));
1517 
1518  return false;
1519  }
1520 
1521  return true;
1522  }
1523 
1532  protected function createAdminAccount(array $submissionVars, bool $login = false): bool {
1533  $app = $this->getApp();
1534 
1535  try {
1537  'username' => $submissionVars['username'],
1538  'password' => $submissionVars['password1'],
1539  'name' => $submissionVars['displayname'],
1540  'email' => $submissionVars['email'],
1541  ]);
1542  } catch (RegistrationException $e) {
1543  $app->internal_services->system_messages->addErrorMessage($e->getMessage());
1544 
1545  return false;
1546  }
1547 
1548  elgg_call(ELGG_IGNORE_ACCESS, function() use ($app, $user) {
1549  if (!$user->makeAdmin()) {
1550  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:adminaccess'));
1551  }
1552  });
1553 
1554  // add validation data to satisfy user validation plugins
1555  $user->validated = true;
1556  $user->validated_method = 'admin_user';
1557 
1558  if (!$login) {
1559  return true;
1560  }
1561 
1562  try {
1563  elgg_login($user);
1564  } catch (LoginException $ex) {
1565  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:adminlogin'));
1566 
1567  return false;
1568  }
1569 
1570  return true;
1571  }
1572 
1580  protected function sanitizeInputValue($input_value) {
1581  if (is_array($input_value)) {
1582  return array_map([$this, __FUNCTION__], $input_value);
1583  }
1584 
1585  if (!is_string($input_value)) {
1586  return $input_value;
1587  }
1588 
1589  return htmlspecialchars($input_value);
1590  }
1591 }
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:154
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
if(elgg_extract('input_type', $vars)) if(elgg_extract('required', $vars)) if(elgg_extract('disabled', $vars)) $field
Definition: field.php:42
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
getConnectionConfig(string $type=self::READ_WRITE)
Get the connection configuration.
Definition: DbConfig.php:131
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:121
$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.
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.
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
checkDatabaseSettings(DbConfig $config)
Confirm the settings for the database.
$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.