Elgg  Version 5.1
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.3';
44  public const MYSQL_MINIMAL_VERSION = '5.7';
45  public const PHP_MINIMAL_VERSION = '8.0.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->plugins->getCache()->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->registerPluginViews(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('config');
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('entities', 'e');
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  ];
996  foreach ($requiredExtensions as $extension) {
997  if (!in_array($extension, $extensions)) {
998  $phpReport[] = [
999  'severity' => 'error',
1000  'message' => elgg_echo('install:check:php:extension', [$extension]),
1001  ];
1002  }
1003  }
1004 
1006  'mbstring',
1007  ];
1008  foreach ($recommendedExtensions as $extension) {
1009  if (!in_array($extension, $extensions)) {
1010  $phpReport[] = [
1011  'severity' => 'warning',
1012  'message' => elgg_echo('install:check:php:extension:recommend', [$extension]),
1013  ];
1014  }
1015  }
1016  }
1017 
1025  protected function checkPhpDirectives(array &$phpReport): void {
1026  if (ini_get('open_basedir')) {
1027  $phpReport[] = [
1028  'severity' => 'warning',
1029  'message' => elgg_echo('install:check:php:open_basedir'),
1030  ];
1031  }
1032 
1033  if (ini_get('safe_mode')) {
1034  $phpReport[] = [
1035  'severity' => 'warning',
1036  'message' => elgg_echo('install:check:php:safe_mode'),
1037  ];
1038  }
1039 
1040  if (ini_get('arg_separator.output') !== '&') {
1041  $separator = htmlspecialchars(ini_get('arg_separator.output'));
1042  $phpReport[] = [
1043  'severity' => 'error',
1044  'message' => elgg_echo('install:check:php:arg_separator', [$separator]),
1045  ];
1046  }
1047 
1048  if (ini_get('register_globals')) {
1049  $phpReport[] = [
1050  'severity' => 'error',
1051  'message' => elgg_echo('install:check:php:register_globals'),
1052  ];
1053  }
1054 
1055  if (ini_get('session.auto_start')) {
1056  $phpReport[] = [
1057  'severity' => 'error',
1058  'message' => elgg_echo('install:check:php:session.auto_start'),
1059  ];
1060  }
1061  }
1062 
1070  protected function checkRewriteRules(array &$report): void {
1071  $tester = new RewriteTester();
1072 
1073  $url = $this->getApp()->internal_services->config->wwwroot;
1074  $url .= Request::REWRITE_TEST_TOKEN . '?' . http_build_query([Request::REWRITE_TEST_TOKEN => '1']);
1075 
1076  $report['rewrite'] = [$tester->run($url)];
1077  }
1078 
1087  protected function countNumConditions(array $report, string $condition): int {
1088  $count = 0;
1089  foreach ($report as $checks) {
1090  foreach ($checks as $check) {
1091  if ($check['severity'] === $condition) {
1092  $count++;
1093  }
1094  }
1095  }
1096 
1097  return $count;
1098  }
1099 
1112  protected function validateDatabaseVars(array $submissionVars, array $formVars): bool {
1113 
1114  $app = $this->getApp();
1115 
1116  foreach ($formVars as $field => $info) {
1117  if ($info['required'] === true && !$submissionVars[$field]) {
1118  $name = elgg_echo("install:database:label:{$field}");
1119  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1120 
1121  return false;
1122  }
1123  }
1124 
1125  if (!empty($submissionVars['wwwroot']) && !\Elgg\Http\Urls::isValidMultiByteUrl($submissionVars['wwwroot'])) {
1126  $save_value = $this->sanitizeInputValue($submissionVars['wwwroot']);
1127  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:wwwroot', [$save_value]));
1128 
1129  return false;
1130  }
1131 
1132  // check that data root is absolute path
1133  if (stripos(PHP_OS, 'win') === 0) {
1134  if (strpos($submissionVars['dataroot'], ':') !== 1) {
1135  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1136  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:relative_path', [$save_value]));
1137 
1138  return false;
1139  }
1140  } else {
1141  if (!str_starts_with($submissionVars['dataroot'], '/')) {
1142  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1143  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:relative_path', [$save_value]));
1144 
1145  return false;
1146  }
1147  }
1148 
1149  // check that data root exists
1150  if (!is_dir($submissionVars['dataroot'])) {
1151  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1152  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:datadirectoryexists', [$save_value]));
1153 
1154  return false;
1155  }
1156 
1157  // check that data root is writable
1158  if (!is_writable($submissionVars['dataroot'])) {
1159  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1160  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:writedatadirectory', [$save_value]));
1161 
1162  return false;
1163  }
1164 
1165  // check that data root is not subdirectory of Elgg root
1166  if (stripos($submissionVars['dataroot'], Paths::project()) === 0) {
1167  $save_value = $this->sanitizeInputValue($submissionVars['dataroot']);
1168  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:locationdatadirectory', [$save_value]));
1169 
1170  return false;
1171  }
1172 
1173  // according to postgres documentation: SQL identifiers and key words must
1174  // begin with a letter (a-z, but also letters with diacritical marks and
1175  // non-Latin letters) or an underscore (_). Subsequent characters in an
1176  // identifier or key word can be letters, underscores, digits (0-9), or dollar signs ($).
1177  // Refs #4994
1178  if (!empty($submissionVars['dbprefix']) && !preg_match('/^[a-zA-Z_][\w]*$/', $submissionVars['dbprefix'])) {
1179  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:database_prefix'));
1180 
1181  return false;
1182  }
1183 
1184  return $this->checkDatabaseSettings(
1185  $submissionVars['dbuser'],
1186  $submissionVars['dbpassword'],
1187  $submissionVars['dbname'],
1188  $submissionVars['dbhost'],
1189  $submissionVars['dbport']
1190  );
1191  }
1192 
1204  protected function checkDatabaseSettings(string $user, string $password, string $dbname, string $host, int $port = null): bool {
1205  $app = $this->getApp();
1206 
1207  $config = new DbConfig((object) [
1208  'dbhost' => $host,
1209  'dbport' => $port,
1210  'dbuser' => $user,
1211  'dbpass' => $password,
1212  'dbname' => $dbname,
1213  'dbencoding' => 'utf8mb4',
1214  ]);
1215  $db = new Database($config, $app->internal_services->queryCache, $app->internal_services->config);
1216 
1217  try {
1218  $db->getConnection('read')->executeQuery('SELECT 1');
1219  } catch (DatabaseException $e) {
1220  if (str_starts_with($e->getMessage(), "Elgg couldn't connect")) {
1221  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:databasesettings'));
1222  } else {
1223  $save_value = $this->sanitizeInputValue($dbname);
1224  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:nodatabase', [$save_value]));
1225  }
1226 
1227  return false;
1228  }
1229 
1230  // check MySQL version
1231  $version = $db->getServerVersion();
1232  $min_version = $db->isMariaDB() ? self::MARIADB_MINIMAL_VERSION : self::MYSQL_MINIMAL_VERSION;
1233 
1234  if (version_compare($version, $min_version, '<')) {
1235  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:database_version', [$min_version, $version]));
1236 
1237  return false;
1238  }
1239 
1240  return true;
1241  }
1242 
1250  protected function createSettingsFile(array $params): bool {
1251  $app = $this->getApp();
1252 
1253  $template = Application::elggDir()->getContents('elgg-config/settings.example.php');
1254  if (!$template) {
1255  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:readsettingsphp'));
1256 
1257  return false;
1258  }
1259 
1260  foreach ($params as $k => $v) {
1261  // do some sanitization
1262  switch ($k) {
1263  case 'dataroot':
1264  $v = Paths::sanitize($v);
1265  break;
1266  case 'dbpassword':
1267  $v = addslashes($v);
1268  break;
1269  }
1270 
1271  $template = str_replace('{{' . $k . '}}', $v, $template);
1272  }
1273 
1274  $result = file_put_contents(Config::resolvePath(), $template);
1275  if ($result === false) {
1276  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:writesettingphp'));
1277 
1278  return false;
1279  }
1280 
1281  $config = (object) [
1282  'dbhost' => elgg_extract('dbhost', $params, 'localhost'),
1283  'dbport' => elgg_extract('dbport', $params, 3306),
1284  'dbuser' => elgg_extract('dbuser', $params),
1285  'dbpass' => elgg_extract('dbpassword', $params),
1286  'dbname' => elgg_extract('dbname', $params),
1287  'dbencoding' => elgg_extract('dbencoding', $params, 'utf8mb4'),
1288  'dbprefix' => elgg_extract('dbprefix', $params, 'elgg_'),
1289  ];
1290 
1291  $dbConfig = new DbConfig($config);
1292  $this->getApp()->internal_services->set('dbConfig', $dbConfig);
1293  $this->getApp()->internal_services->db->resetConnections($dbConfig);
1294 
1295  return true;
1296  }
1297 
1303  protected function connectToDatabase(): bool {
1304  try {
1305  $app = $this->getApp();
1306  $app->internal_services->db->setupConnections();
1307  } catch (DatabaseException $e) {
1308  $app->internal_services->system_messages->addErrorMessage($e->getMessage());
1309 
1310  return false;
1311  }
1312 
1313  return true;
1314  }
1315 
1321  protected function installDatabase(): bool {
1322  try {
1323  return $this->getApp()->migrate();
1324  } catch (\Exception $e) {
1325  return false;
1326  }
1327  }
1328 
1341  protected function createDataDirectory(array &$submissionVars, array $formVars): bool {
1342  // did the user have option of Elgg creating the data directory
1343  if ($formVars['dataroot']['type'] !== 'combo') {
1344  return true;
1345  }
1346 
1347  // did the user select the option
1348  if ($submissionVars['dataroot'] !== 'dataroot-checkbox') {
1349  return true;
1350  }
1351 
1352  $dir = \Elgg\Project\Paths::sanitize($submissionVars['path']) . 'data';
1353  if (file_exists($dir) || mkdir($dir, 0755)) {
1354  $submissionVars['dataroot'] = $dir;
1355  if (!file_exists("{$dir}/.htaccess")) {
1356  $htaccess = "Order Deny,Allow\nDeny from All\n";
1357  if (!file_put_contents("$dir/.htaccess", $htaccess)) {
1358  return false;
1359  }
1360  }
1361 
1362  return true;
1363  }
1364 
1365  return false;
1366  }
1367 
1376  protected function validateSettingsVars(array $submissionVars, array $formVars): bool {
1377  $app = $this->getApp();
1378 
1379  foreach ($formVars as $field => $info) {
1380  $submissionVars[$field] = trim($submissionVars[$field]);
1381  if ($info['required'] === true && $submissionVars[$field] === '') {
1382  $name = elgg_echo("install:settings:label:{$field}");
1383  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1384 
1385  return false;
1386  }
1387  }
1388 
1389  // check that email address is email address
1390  if ($submissionVars['siteemail'] && !elgg_is_valid_email((string) $submissionVars['siteemail'])) {
1391  $save_value = $this->sanitizeInputValue($submissionVars['siteemail']);
1392  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:emailaddress', [$save_value]));
1393 
1394  return false;
1395  }
1396 
1397  return true;
1398  }
1399 
1407  protected function saveSiteSettings(array $submissionVars): bool {
1408  $app = $this->getApp();
1409 
1411 
1412  if (!$site->guid) {
1413  $site = new \ElggSite();
1414  $site->name = strip_tags($submissionVars['sitename']);
1415  $site->access_id = ACCESS_PUBLIC;
1416  $site->email = $submissionVars['siteemail'];
1417  $site->save();
1418  }
1419 
1420  if ($site->guid !== 1) {
1421  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:createsite'));
1422 
1423  return false;
1424  }
1425 
1426  $app->internal_services->config->site = $site;
1427 
1428  $sets = [
1429  'installed' => time(),
1430  'simplecache_enabled' => 1,
1431  'system_cache_enabled' => 1,
1432  'simplecache_minify_js' => true,
1433  'simplecache_minify_css' => true,
1434  'lastcache' => time(),
1435  'language' => 'en',
1436  'default_access' => $submissionVars['siteaccess'],
1437  'allow_registration' => false,
1438  'require_admin_validation' => false,
1439  'walled_garden' => false,
1440  'allow_user_default_access' => '',
1441  'default_limit' => 10,
1442  ];
1443 
1444  foreach ($sets as $key => $value) {
1446  }
1447 
1448  try {
1449  _elgg_services()->plugins->generateEntities();
1450 
1451  $app->internal_services->reset('plugins');
1452 
1453  if (elgg_extract('activate_plugins', $submissionVars, true)) {
1454  $plugins = $app->internal_services->plugins->find('any');
1455 
1456  foreach ($plugins as $plugin) {
1457  $plugin_config = $plugin->getStaticConfig('plugin', []);
1458  if (!elgg_extract('activate_on_install', $plugin_config, false)) {
1459  continue;
1460  }
1461 
1462  try {
1463  $plugin->activate();
1464  } catch (PluginException $e) {
1465  // do nothing
1466  }
1467  }
1468  }
1469 
1470  // Wo don't need to run upgrades on new installations
1471  $app->internal_services->events->unregisterHandler('create:after', 'object', \Elgg\Upgrade\CreateAdminNoticeHandler::class);
1472  $upgrades = $app->internal_services->upgradeLocator->locate();
1473  foreach ($upgrades as $upgrade) {
1474  $upgrade->setCompleted();
1475  }
1476  } catch (\Exception $e) {
1477  $app->internal_services->logger->log(\Psr\Log\LogLevel::ERROR, $e);
1478  }
1479 
1480  return true;
1481  }
1482 
1491  protected function validateAdminVars(array $submissionVars, array $formVars): bool {
1492 
1493  $app = $this->getApp();
1494 
1495  foreach ($formVars as $field => $info) {
1496  if ($info['required'] === true && !$submissionVars[$field]) {
1497  $name = elgg_echo("install:admin:label:{$field}");
1498  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:requiredfield', [$name]));
1499 
1500  return false;
1501  }
1502  }
1503 
1504  if ($submissionVars['password1'] !== $submissionVars['password2']) {
1505  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:mismatch'));
1506 
1507  return false;
1508  }
1509 
1510  if (trim($submissionVars['password1']) === '') {
1511  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:empty'));
1512 
1513  return false;
1514  }
1515 
1516  $minLength = $app->internal_services->configTable->get('min_password_length');
1517  if (strlen($submissionVars['password1']) < $minLength) {
1518  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:admin:password:tooshort'));
1519 
1520  return false;
1521  }
1522 
1523  // check that email address is email address
1524  if ($submissionVars['email'] && !elgg_is_valid_email((string) $submissionVars['email'])) {
1525  $save_value = $this->sanitizeInputValue($submissionVars['email']);
1526  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:emailaddress', [$save_value]));
1527 
1528  return false;
1529  }
1530 
1531  return true;
1532  }
1533 
1542  protected function createAdminAccount(array $submissionVars, bool $login = false): bool {
1543  $app = $this->getApp();
1544 
1545  try {
1547  'username' => $submissionVars['username'],
1548  'password' => $submissionVars['password1'],
1549  'name' => $submissionVars['displayname'],
1550  'email' => $submissionVars['email'],
1551  ]);
1552  } catch (RegistrationException $e) {
1553  $app->internal_services->system_messages->addErrorMessage($e->getMessage());
1554 
1555  return false;
1556  }
1557 
1558  elgg_call(ELGG_IGNORE_ACCESS, function() use ($app, $user) {
1559  if (!$user->makeAdmin()) {
1560  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:adminaccess'));
1561  }
1562  });
1563 
1564  // add validation data to satisfy user validation plugins
1565  $user->validated = true;
1566  $user->validated_method = 'admin_user';
1567 
1568  if (!$login) {
1569  return true;
1570  }
1571 
1572  try {
1573  elgg_login($user);
1574  } catch (LoginException $ex) {
1575  $app->internal_services->system_messages->addErrorMessage(elgg_echo('install:error:adminlogin'));
1576 
1577  return false;
1578  }
1579 
1580  return true;
1581  }
1582 
1590  protected function sanitizeInputValue($input_value) {
1591  if (is_array($input_value)) {
1592  return array_map([$this, __FUNCTION__], $input_value);
1593  }
1594 
1595  if (!is_string($input_value)) {
1596  return $input_value;
1597  }
1598 
1599  return htmlspecialchars($input_value);
1600  }
1601 }
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:299
$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.
$plugins
Definition: categories.php:3
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:177
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:256
$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:98
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:48
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:1481
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:346
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:11
render(string $step, array $vars=[])
Renders the data passed by a controller.
var elgg
Definition: elgglib.js:4
array bool $is_action
const PHP_MINIMAL_VERSION
$extension
Definition: default.php:25
finishBootstrapping(string $step)
Bootstrapping.