1 | <?php |
||
2 | |||
3 | use Elgg\Database; |
||
4 | use Elgg\Filesystem\Directory; |
||
5 | use Elgg\Application; |
||
6 | use Elgg\Config; |
||
7 | use Elgg\Database\DbConfig; |
||
8 | use Elgg\Project\Paths; |
||
9 | use Elgg\Di\ServiceProvider; |
||
10 | use Elgg\Http\Request; |
||
11 | |||
12 | /** |
||
13 | * Elgg Installer. |
||
14 | * Controller for installing Elgg. Supports both web-based on CLI installation. |
||
15 | * |
||
16 | * This controller steps the user through the install process. The method for |
||
17 | * each step handles both the GET and POST requests. There is no XSS/CSRF protection |
||
18 | * on the POST processing since the installer is only run once by the administrator. |
||
19 | * |
||
20 | * The installation process can be resumed by hitting the first page. The installer |
||
21 | * will try to figure out where to pick up again. |
||
22 | * |
||
23 | * All the logic for the installation process is in this class, but it depends on |
||
24 | * the core libraries. To do this, we selectively load a subset of the core libraries |
||
25 | * for the first few steps and then load the entire engine once the database and |
||
26 | * site settings are configured. In addition, this controller does its own session |
||
27 | * handling until the database is setup. |
||
28 | * |
||
29 | * There is an aborted attempt in the code at creating the data directory for |
||
30 | * users as a subdirectory of Elgg's root. The idea was to protect this directory |
||
31 | * through a .htaccess file. The problem is that a malicious user can upload a |
||
32 | * .htaccess of his own that overrides the protection for his user directory. The |
||
33 | * best solution is server level configuration that turns off AllowOverride for the |
||
34 | * data directory. See ticket #3453 for discussion on this. |
||
35 | */ |
||
36 | class ElggInstaller { |
||
37 | |||
38 | private $steps = [ |
||
39 | 'welcome', |
||
40 | 'requirements', |
||
41 | 'database', |
||
42 | 'settings', |
||
43 | 'admin', |
||
44 | 'complete', |
||
45 | ]; |
||
46 | |||
47 | private $has_completed = [ |
||
48 | 'config' => false, |
||
49 | 'database' => false, |
||
50 | 'settings' => false, |
||
51 | 'admin' => false, |
||
52 | ]; |
||
53 | |||
54 | private $is_action = false; |
||
55 | |||
56 | private $autoLogin = true; |
||
57 | |||
58 | /** |
||
59 | * @var Application |
||
60 | */ |
||
61 | private $app; |
||
62 | |||
63 | /** |
||
64 | * Dispatches a request to one of the step controllers |
||
65 | * |
||
66 | * @return \Elgg\Http\ResponseBuilder |
||
67 | * @throws InstallationException |
||
68 | */ |
||
69 | 8 | public function run() { |
|
70 | 8 | $app = $this->getApp(); |
|
71 | |||
72 | 8 | $this->is_action = $app->_services->request->getMethod() === 'POST'; |
|
73 | |||
74 | 8 | $step = get_input('step', 'welcome'); |
|
75 | |||
76 | 8 | if (!in_array($step, $this->getSteps())) { |
|
77 | $step = 'welcome'; |
||
78 | } |
||
79 | |||
80 | 8 | $this->determineInstallStatus(); |
|
81 | |||
82 | 8 | $response = $this->checkInstallCompletion($step); |
|
83 | 8 | if ($response) { |
|
84 | return $response; |
||
85 | } |
||
86 | |||
87 | // check if this is an install being resumed |
||
88 | 8 | $response = $this->resumeInstall($step); |
|
89 | 8 | if ($response) { |
|
90 | return $response; |
||
91 | } |
||
92 | |||
93 | 8 | $this->finishBootstrapping($step); |
|
94 | |||
95 | 8 | $params = $app->_services->request->request->all(); |
|
96 | |||
97 | 8 | $method = "run" . ucwords($step); |
|
98 | 8 | return $this->$method($params); |
|
99 | } |
||
100 | |||
101 | /** |
||
102 | * Build the application needed by the installer |
||
103 | * |
||
104 | * @return Application |
||
105 | * @throws InstallationException |
||
106 | */ |
||
107 | protected function getApp() { |
||
108 | if ($this->app) { |
||
109 | return $this->app; |
||
110 | } |
||
111 | |||
112 | try { |
||
113 | $config = new Config(); |
||
114 | $config->elgg_config_locks = false; |
||
115 | $config->installer_running = true; |
||
116 | $config->dbencoding = 'utf8mb4'; |
||
117 | |||
118 | $services = new ServiceProvider($config); |
||
119 | |||
120 | $app = Application::factory([ |
||
121 | 'service_provider' => $services, |
||
122 | 'handle_exceptions' => false, |
||
123 | 'handle_shutdown' => false, |
||
124 | ]); |
||
125 | |||
126 | // Don't set global $CONFIG, because loading the settings file may require it to write to |
||
127 | // it, and it can have array sets (e.g. cookie config) that fail when using a proxy for |
||
128 | // the config service. |
||
129 | //$app->setGlobalConfig(); |
||
130 | |||
131 | Application::setInstance($app); |
||
132 | $app->loadCore(); |
||
133 | $this->app = $app; |
||
134 | |||
135 | $app->_services->setValue('session', \ElggSession::getMock()); |
||
136 | $app->_services->views->setViewtype('installation'); |
||
137 | $app->_services->views->registerViewtypeFallback('installation'); |
||
138 | $app->_services->views->registerPluginViews(Paths::elgg()); |
||
139 | $app->_services->translator->registerTranslations(Paths::elgg() . "install/languages/", true); |
||
140 | |||
141 | return $this->app; |
||
142 | } catch (ConfigurationException $ex) { |
||
143 | throw new InstallationException($ex->getMessage()); |
||
144 | } |
||
145 | } |
||
146 | |||
147 | /** |
||
148 | * Set the auto login flag |
||
149 | * |
||
150 | * @param bool $flag Auto login |
||
151 | * |
||
152 | * @return void |
||
153 | */ |
||
154 | public function setAutoLogin($flag) { |
||
155 | $this->autoLogin = (bool) $flag; |
||
156 | } |
||
157 | |||
158 | /** |
||
159 | * A batch install of Elgg |
||
160 | * |
||
161 | * All required parameters must be passed in as an associative array. See |
||
162 | * $requiredParams for a list of them. This creates the necessary files, |
||
163 | * loads the database, configures the site settings, and creates the admin |
||
164 | * account. If it fails, an exception is thrown. It does not check any of |
||
165 | * the requirements as the multiple step web installer does. |
||
166 | * |
||
167 | * @param array $params Array of key value pairs |
||
168 | * @param bool $create_htaccess Should .htaccess be created |
||
169 | * |
||
170 | * @return void |
||
171 | * @throws InstallationException |
||
172 | */ |
||
173 | 1 | public function batchInstall(array $params, $create_htaccess = false) { |
|
174 | 1 | $app = $this->getApp(); |
|
175 | |||
176 | $defaults = [ |
||
177 | 1 | 'dbhost' => 'localhost', |
|
178 | 'dbprefix' => 'elgg_', |
||
179 | 'language' => 'en', |
||
180 | 'siteaccess' => ACCESS_PUBLIC, |
||
181 | ]; |
||
182 | 1 | $params = array_merge($defaults, $params); |
|
183 | |||
184 | $required_params = [ |
||
185 | 1 | 'dbuser', |
|
186 | 'dbpassword', |
||
187 | 'dbname', |
||
188 | 'sitename', |
||
189 | 'wwwroot', |
||
190 | 'dataroot', |
||
191 | 'displayname', |
||
192 | 'email', |
||
193 | 'username', |
||
194 | 'password', |
||
195 | ]; |
||
196 | 1 | foreach ($required_params as $key) { |
|
197 | 1 | if (empty($params[$key])) { |
|
198 | $msg = elgg_echo('install:error:requiredfield', [$key]); |
||
199 | 1 | throw new InstallationException($msg); |
|
200 | } |
||
201 | } |
||
202 | |||
203 | // password is passed in once |
||
204 | 1 | $params['password1'] = $params['password2'] = $params['password']; |
|
205 | |||
206 | 1 | if ($create_htaccess) { |
|
207 | $rewrite_tester = new ElggRewriteTester(); |
||
208 | if (!$rewrite_tester->createHtaccess($params['wwwroot'])) { |
||
209 | throw new InstallationException(elgg_echo('install:error:htaccess')); |
||
210 | } |
||
211 | } |
||
212 | |||
213 | 1 | if (!empty($params['wwwroot']) && !_elgg_sane_validate_url($params['wwwroot'])) { |
|
214 | throw new InstallationException(elgg_echo('install:error:wwwroot', [$params['wwwroot']])); |
||
215 | } |
||
216 | |||
217 | 1 | $this->determineInstallStatus(); |
|
218 | |||
219 | 1 | if (!$this->has_completed['config']) { |
|
220 | 1 | if (!$this->createSettingsFile($params)) { |
|
221 | throw new InstallationException(elgg_echo('install:error:settings')); |
||
222 | } |
||
223 | } |
||
224 | |||
225 | 1 | $this->loadSettingsFile(); |
|
226 | |||
227 | // Make sure settings file matches parameters |
||
228 | 1 | $config = $app->_services->config; |
|
229 | $config_keys = [ |
||
230 | // param key => config key |
||
231 | 1 | 'dbhost' => 'dbhost', |
|
232 | 'dbuser' => 'dbuser', |
||
233 | 'dbpassword' => 'dbpass', |
||
234 | 'dbname' => 'dbname', |
||
235 | 'dataroot' => 'dataroot', |
||
236 | 'dbprefix' => 'dbprefix', |
||
237 | ]; |
||
238 | 1 | foreach ($config_keys as $params_key => $config_key) { |
|
239 | 1 | if ($params[$params_key] !== $config->$config_key) { |
|
240 | 1 | throw new InstallationException(elgg_echo('install:error:settings_mismatch', [$config_key])); |
|
241 | } |
||
242 | } |
||
243 | |||
244 | 1 | if (!$this->connectToDatabase()) { |
|
245 | throw new InstallationException(elgg_echo('install:error:databasesettings')); |
||
246 | } |
||
247 | |||
248 | 1 | if (!$this->has_completed['database']) { |
|
249 | 1 | if (!$this->installDatabase()) { |
|
250 | throw new InstallationException(elgg_echo('install:error:cannotloadtables')); |
||
251 | } |
||
252 | } |
||
253 | |||
254 | // load remaining core libraries |
||
255 | 1 | $this->finishBootstrapping('settings'); |
|
256 | |||
257 | 1 | if (!$this->saveSiteSettings($params)) { |
|
258 | throw new InstallationException(elgg_echo('install:error:savesitesettings')); |
||
259 | } |
||
260 | |||
261 | 1 | if (!$this->createAdminAccount($params)) { |
|
262 | throw new InstallationException(elgg_echo('install:admin:cannot_create')); |
||
263 | } |
||
264 | 1 | } |
|
265 | |||
266 | /** |
||
267 | * Renders the data passed by a controller |
||
268 | * |
||
269 | * @param string $step The current step |
||
270 | * @param array $vars Array of vars to pass to the view |
||
271 | * |
||
272 | * @return \Elgg\Http\OkResponse |
||
273 | */ |
||
274 | 5 | protected function render($step, $vars = []) { |
|
275 | 5 | $vars['next_step'] = $this->getNextStep($step); |
|
276 | |||
277 | 5 | $title = elgg_echo("install:$step"); |
|
278 | 5 | $body = elgg_view("install/pages/$step", $vars); |
|
279 | |||
280 | 5 | $output = elgg_view_page( |
|
281 | 5 | $title, |
|
282 | 5 | $body, |
|
283 | 5 | 'default', |
|
284 | [ |
||
285 | 5 | 'step' => $step, |
|
286 | 5 | 'steps' => $this->getSteps(), |
|
287 | ] |
||
288 | ); |
||
289 | |||
290 | 5 | return new \Elgg\Http\OkResponse($output); |
|
291 | } |
||
292 | |||
293 | /** |
||
294 | * Step controllers |
||
295 | */ |
||
296 | |||
297 | /** |
||
298 | * Welcome controller |
||
299 | * |
||
300 | * @param array $vars Not used |
||
301 | * |
||
302 | * @return \Elgg\Http\ResponseBuilder |
||
303 | */ |
||
304 | 1 | protected function runWelcome($vars) { |
|
305 | 1 | return $this->render('welcome'); |
|
306 | } |
||
307 | |||
308 | /** |
||
309 | * Requirements controller |
||
310 | * |
||
311 | * Checks version of php, libraries, permissions, and rewrite rules |
||
312 | * |
||
313 | * @param array $vars Vars |
||
314 | * |
||
315 | * @return \Elgg\Http\ResponseBuilder |
||
316 | * @throws InstallationException |
||
317 | */ |
||
318 | 1 | protected function runRequirements($vars) { |
|
319 | |||
320 | 1 | $report = []; |
|
321 | |||
322 | // check PHP parameters and libraries |
||
323 | 1 | $this->checkPHP($report); |
|
324 | |||
325 | // check URL rewriting |
||
326 | 1 | $this->checkRewriteRules($report); |
|
327 | |||
328 | // check for existence of settings file |
||
329 | 1 | if ($this->checkSettingsFile($report) != true) { |
|
330 | // no file, so check permissions on engine directory |
||
331 | 1 | $this->isInstallDirWritable($report); |
|
332 | } |
||
333 | |||
334 | // check the database later |
||
335 | 1 | $report['database'] = [ |
|
336 | [ |
||
337 | 1 | 'severity' => 'info', |
|
338 | 1 | 'message' => elgg_echo('install:check:database') |
|
339 | ] |
||
340 | ]; |
||
341 | |||
342 | // any failures? |
||
343 | 1 | $numFailures = $this->countNumConditions($report, 'failure'); |
|
344 | |||
345 | // any warnings |
||
346 | 1 | $numWarnings = $this->countNumConditions($report, 'warning'); |
|
347 | |||
348 | |||
349 | $params = [ |
||
350 | 1 | 'report' => $report, |
|
351 | 1 | 'num_failures' => $numFailures, |
|
352 | 1 | 'num_warnings' => $numWarnings, |
|
353 | ]; |
||
354 | |||
355 | 1 | return $this->render('requirements', $params); |
|
356 | } |
||
357 | |||
358 | /** |
||
359 | * Database set up controller |
||
360 | * |
||
361 | * Creates the settings.php file and creates the database tables |
||
362 | * |
||
363 | * @param array $submissionVars Submitted form variables |
||
364 | * |
||
365 | * @return \Elgg\Http\ResponseBuilder |
||
366 | * @throws ConfigurationException |
||
367 | */ |
||
368 | 2 | protected function runDatabase($submissionVars) { |
|
369 | |||
370 | 2 | $app = $this->getApp(); |
|
371 | |||
372 | $formVars = [ |
||
373 | 2 | 'dbuser' => [ |
|
374 | 'type' => 'text', |
||
375 | 'value' => '', |
||
376 | 'required' => true, |
||
377 | ], |
||
378 | 'dbpassword' => [ |
||
379 | 'type' => 'password', |
||
380 | 'value' => '', |
||
381 | 'required' => false, |
||
382 | ], |
||
383 | 'dbname' => [ |
||
384 | 'type' => 'text', |
||
385 | 'value' => '', |
||
386 | 'required' => true, |
||
387 | ], |
||
388 | 'dbhost' => [ |
||
389 | 'type' => 'text', |
||
390 | 'value' => 'localhost', |
||
391 | 'required' => true, |
||
392 | ], |
||
393 | 'dbprefix' => [ |
||
394 | 'type' => 'text', |
||
395 | 'value' => 'elgg_', |
||
396 | 'required' => true, |
||
397 | ], |
||
398 | 'dataroot' => [ |
||
399 | 'type' => 'text', |
||
400 | 'value' => '', |
||
401 | 'required' => true, |
||
402 | ], |
||
403 | 'wwwroot' => [ |
||
404 | 2 | 'type' => 'url', |
|
405 | 2 | 'value' => $app->_services->config->wwwroot, |
|
406 | 'required' => true, |
||
407 | ], |
||
408 | 'timezone' => [ |
||
409 | 2 | 'type' => 'dropdown', |
|
410 | 2 | 'value' => 'UTC', |
|
411 | 2 | 'options' => \DateTimeZone::listIdentifiers(), |
|
412 | 'required' => true |
||
413 | ] |
||
414 | ]; |
||
415 | |||
416 | 2 | if ($this->checkSettingsFile()) { |
|
417 | // user manually created settings file so we fake out action test |
||
418 | $this->is_action = true; |
||
419 | } |
||
420 | |||
421 | 2 | if ($this->is_action) { |
|
422 | 1 | $getResponse = function () use ($submissionVars, $formVars) { |
|
423 | // only create settings file if it doesn't exist |
||
424 | 1 | if (!$this->checkSettingsFile()) { |
|
425 | 1 | if (!$this->validateDatabaseVars($submissionVars, $formVars)) { |
|
426 | // error so we break out of action and serve same page |
||
427 | return; |
||
428 | } |
||
429 | |||
430 | 1 | if (!$this->createSettingsFile($submissionVars)) { |
|
431 | return; |
||
432 | } |
||
433 | } |
||
434 | |||
435 | // check db version and connect |
||
436 | 1 | if (!$this->connectToDatabase()) { |
|
437 | return; |
||
438 | } |
||
439 | |||
440 | 1 | if (!$this->installDatabase()) { |
|
441 | return; |
||
442 | } |
||
443 | |||
444 | 1 | system_message(elgg_echo('install:success:database')); |
|
445 | |||
446 | 1 | return $this->continueToNextStep('database'); |
|
447 | 1 | }; |
|
448 | |||
449 | 1 | $response = $getResponse(); |
|
450 | 1 | if ($response) { |
|
451 | 1 | return $response; |
|
452 | } |
||
453 | } |
||
454 | |||
455 | 1 | $formVars = $this->makeFormSticky($formVars, $submissionVars); |
|
456 | |||
457 | 1 | $params = ['variables' => $formVars,]; |
|
458 | |||
459 | 1 | if ($this->checkSettingsFile()) { |
|
460 | // settings file exists and we're here so failed to create database |
||
461 | $params['failure'] = true; |
||
462 | } |
||
463 | |||
464 | 1 | return $this->render('database', $params); |
|
465 | } |
||
466 | |||
467 | /** |
||
468 | * Site settings controller |
||
469 | * |
||
470 | * Sets the site name, URL, data directory, etc. |
||
471 | * |
||
472 | * @param array $submissionVars Submitted vars |
||
473 | * |
||
474 | * @return \Elgg\Http\ResponseBuilder |
||
475 | */ |
||
476 | 2 | protected function runSettings($submissionVars) { |
|
477 | $formVars = [ |
||
478 | 2 | 'sitename' => [ |
|
479 | 'type' => 'text', |
||
480 | 'value' => 'My New Community', |
||
481 | 'required' => true, |
||
482 | ], |
||
483 | 'siteemail' => [ |
||
484 | 'type' => 'email', |
||
485 | 'value' => '', |
||
486 | 'required' => false, |
||
487 | ], |
||
488 | 'siteaccess' => [ |
||
489 | 'type' => 'access', |
||
490 | 'value' => ACCESS_PUBLIC, |
||
491 | 'required' => true, |
||
492 | ], |
||
493 | ]; |
||
494 | |||
495 | 2 | if ($this->is_action) { |
|
496 | 1 | $getResponse = function () use ($submissionVars, $formVars) { |
|
497 | |||
498 | 1 | if (!$this->validateSettingsVars($submissionVars, $formVars)) { |
|
499 | return; |
||
500 | } |
||
501 | |||
502 | 1 | if (!$this->saveSiteSettings($submissionVars)) { |
|
503 | return; |
||
504 | } |
||
505 | |||
506 | 1 | system_message(elgg_echo('install:success:settings')); |
|
507 | |||
508 | 1 | return $this->continueToNextStep('settings'); |
|
509 | 1 | }; |
|
510 | |||
511 | 1 | $response = $getResponse(); |
|
512 | 1 | if ($response) { |
|
513 | 1 | return $response; |
|
514 | } |
||
515 | } |
||
516 | |||
517 | 1 | $formVars = $this->makeFormSticky($formVars, $submissionVars); |
|
518 | |||
519 | 1 | return $this->render('settings', ['variables' => $formVars]); |
|
520 | } |
||
521 | |||
522 | /** |
||
523 | * Admin account controller |
||
524 | * |
||
525 | * Creates an admin user account |
||
526 | * |
||
527 | * @param array $submissionVars Submitted vars |
||
528 | * |
||
529 | * @return \Elgg\Http\ResponseBuilder |
||
530 | * @throws InstallationException |
||
531 | */ |
||
532 | 2 | protected function runAdmin($submissionVars) { |
|
533 | $formVars = [ |
||
534 | 2 | 'displayname' => [ |
|
535 | 'type' => 'text', |
||
536 | 'value' => '', |
||
537 | 'required' => true, |
||
538 | ], |
||
539 | 'email' => [ |
||
540 | 'type' => 'email', |
||
541 | 'value' => '', |
||
542 | 'required' => true, |
||
543 | ], |
||
544 | 'username' => [ |
||
545 | 'type' => 'text', |
||
546 | 'value' => '', |
||
547 | 'required' => true, |
||
548 | ], |
||
549 | 'password1' => [ |
||
550 | 'type' => 'password', |
||
551 | 'value' => '', |
||
552 | 'required' => true, |
||
553 | 'pattern' => '.{6,}', |
||
554 | ], |
||
555 | 'password2' => [ |
||
556 | 'type' => 'password', |
||
557 | 'value' => '', |
||
558 | 'required' => true, |
||
559 | ], |
||
560 | ]; |
||
561 | |||
562 | 2 | if ($this->is_action) { |
|
563 | 1 | $getResponse = function () use ($submissionVars, $formVars) { |
|
564 | 1 | if (!$this->validateAdminVars($submissionVars, $formVars)) { |
|
565 | return; |
||
566 | } |
||
567 | |||
568 | 1 | if (!$this->createAdminAccount($submissionVars, $this->autoLogin)) { |
|
569 | return; |
||
570 | } |
||
571 | |||
572 | 1 | system_message(elgg_echo('install:success:admin')); |
|
573 | |||
574 | 1 | return $this->continueToNextStep('admin'); |
|
575 | 1 | }; |
|
576 | |||
577 | 1 | $response = $getResponse(); |
|
578 | 1 | if ($response) { |
|
579 | 1 | return $response; |
|
580 | } |
||
581 | } |
||
582 | |||
583 | // Bit of a hack to get the password help to show right number of characters |
||
584 | // We burn the value into the stored translation. |
||
585 | 1 | $app = $this->getApp(); |
|
586 | 1 | $lang = $app->_services->translator->getCurrentLanguage(); |
|
587 | 1 | $translations = $app->_services->translator->getLoadedTranslations(); |
|
588 | 1 | $app->_services->translator->addTranslation($lang, [ |
|
589 | 1 | 'install:admin:help:password1' => sprintf( |
|
590 | 1 | $translations[$lang]['install:admin:help:password1'], |
|
591 | 1 | $app->_services->config->min_password_length |
|
592 | ), |
||
593 | ]); |
||
594 | |||
595 | 1 | $formVars = $this->makeFormSticky($formVars, $submissionVars); |
|
596 | |||
597 | 1 | return $this->render('admin', ['variables' => $formVars]); |
|
598 | } |
||
599 | |||
600 | /** |
||
601 | * Controller for last step |
||
602 | * |
||
603 | * @return \Elgg\Http\ResponseBuilder |
||
604 | */ |
||
605 | protected function runComplete() { |
||
606 | |||
607 | // nudge to check out settings |
||
608 | $link = elgg_format_element([ |
||
609 | '#tag_name' => 'a', |
||
610 | '#text' => elgg_echo('install:complete:admin_notice:link_text'), |
||
611 | 'href' => elgg_normalize_url('admin/settings/basic'), |
||
612 | ]); |
||
613 | $notice = elgg_echo('install:complete:admin_notice', [$link]); |
||
614 | elgg_add_admin_notice('fresh_install', $notice); |
||
615 | |||
616 | return $this->render('complete'); |
||
617 | } |
||
618 | |||
619 | /** |
||
620 | * Step management |
||
621 | */ |
||
622 | |||
623 | /** |
||
624 | * Get an array of steps |
||
625 | * |
||
626 | * @return array |
||
627 | */ |
||
628 | 9 | protected function getSteps() { |
|
629 | 9 | return $this->steps; |
|
630 | } |
||
631 | |||
632 | /** |
||
633 | * Forwards the browser to the next step |
||
634 | * |
||
635 | * @param string $currentStep Current installation step |
||
636 | * |
||
637 | * @return \Elgg\Http\RedirectResponse |
||
638 | * @throws InstallationException |
||
639 | */ |
||
640 | 3 | protected function continueToNextStep($currentStep) { |
|
641 | 3 | $this->is_action = false; |
|
642 | |||
643 | 3 | return new \Elgg\Http\RedirectResponse($this->getNextStepUrl($currentStep)); |
|
644 | } |
||
645 | |||
646 | /** |
||
647 | * Get the next step as a string |
||
648 | * |
||
649 | * @param string $currentStep Current installation step |
||
650 | * |
||
651 | * @return string |
||
652 | */ |
||
653 | 8 | protected function getNextStep($currentStep) { |
|
654 | 8 | $index = 1 + array_search($currentStep, $this->steps); |
|
655 | 8 | if (isset($this->steps[$index])) { |
|
656 | 8 | return $this->steps[$index]; |
|
657 | } else { |
||
658 | return null; |
||
659 | } |
||
660 | } |
||
661 | |||
662 | /** |
||
663 | * Get the URL of the next step |
||
664 | * |
||
665 | * @param string $currentStep Current installation step |
||
666 | * |
||
667 | * @return string |
||
668 | * @throws InstallationException |
||
669 | */ |
||
670 | 3 | protected function getNextStepUrl($currentStep) { |
|
671 | 3 | $app = $this->getApp(); |
|
672 | 3 | $nextStep = $this->getNextStep($currentStep); |
|
673 | |||
674 | 3 | return $app->_services->config->wwwroot . "install.php?step=$nextStep"; |
|
675 | } |
||
676 | |||
677 | /** |
||
678 | * Updates $this->has_completed according to the current installation |
||
679 | * |
||
680 | * @return void |
||
681 | * @throws InstallationException |
||
682 | */ |
||
683 | 9 | protected function determineInstallStatus() { |
|
684 | 9 | $app = $this->getApp(); |
|
685 | |||
686 | 9 | $path = Config::resolvePath(); |
|
687 | 9 | if (!is_file($path) || !is_readable($path)) { |
|
688 | 5 | return; |
|
689 | } |
||
690 | |||
691 | 4 | $this->loadSettingsFile(); |
|
692 | |||
693 | 4 | $this->has_completed['config'] = true; |
|
694 | |||
695 | // must be able to connect to database to jump install steps |
||
696 | 4 | $dbSettingsPass = $this->checkDatabaseSettings( |
|
697 | 4 | $app->_services->config->dbuser, |
|
698 | 4 | $app->_services->config->dbpass, |
|
699 | 4 | $app->_services->config->dbname, |
|
700 | 4 | $app->_services->config->dbhost |
|
701 | ); |
||
702 | |||
703 | 4 | if (!$dbSettingsPass) { |
|
704 | return; |
||
705 | } |
||
706 | |||
707 | 4 | $db = $app->_services->db; |
|
708 | |||
709 | try { |
||
710 | // check that the config table has been created |
||
711 | 4 | $result = $db->getData("SHOW TABLES"); |
|
712 | 4 | if (!$result) { |
|
0 ignored issues
–
show
|
|||
713 | return; |
||
714 | } |
||
715 | 4 | foreach ($result as $table) { |
|
716 | 4 | $table = (array) $table; |
|
717 | 4 | if (in_array("{$db->prefix}config", $table)) { |
|
718 | 4 | $this->has_completed['database'] = true; |
|
719 | } |
||
720 | } |
||
721 | 4 | if ($this->has_completed['database'] == false) { |
|
722 | 4 | return; |
|
723 | } |
||
724 | |||
725 | // check that the config table has entries |
||
726 | $qb = \Elgg\Database\Select::fromTable('config'); |
||
727 | $qb->select('COUNT(*) AS total'); |
||
728 | |||
729 | $result = $db->getData($qb); |
||
730 | if ($result && $result[0]->total > 0) { |
||
731 | $this->has_completed['settings'] = true; |
||
732 | } else { |
||
733 | return; |
||
734 | } |
||
735 | |||
736 | // check that the users entity table has an entry |
||
737 | $qb = \Elgg\Database\Select::fromTable('entities'); |
||
738 | $qb->select('COUNT(*) AS total') |
||
739 | ->where($qb->compare('type', '=', 'user', ELGG_VALUE_STRING)); |
||
740 | |||
741 | $result = $db->getData($qb); |
||
742 | if ($result && $result[0]->total > 0) { |
||
743 | $this->has_completed['admin'] = true; |
||
744 | } else { |
||
745 | return; |
||
746 | } |
||
747 | } catch (DatabaseException $ex) { |
||
748 | throw new InstallationException('Elgg can not connect to the database: ' . $ex->getMessage()); |
||
749 | } |
||
750 | |||
751 | return; |
||
752 | } |
||
753 | |||
754 | /** |
||
755 | * Security check to ensure the installer cannot be run after installation |
||
756 | * has finished. If this is detected, the viewer is sent to the front page. |
||
757 | * |
||
758 | * @param string $step Installation step to check against |
||
759 | * |
||
760 | * @return \Elgg\Http\RedirectResponse|null |
||
761 | */ |
||
762 | 8 | protected function checkInstallCompletion($step) { |
|
763 | 8 | if ($step != 'complete') { |
|
764 | 8 | if (!in_array(false, $this->has_completed)) { |
|
765 | // install complete but someone is trying to view an install page |
||
766 | return new \Elgg\Http\RedirectResponse('/'); |
||
767 | } |
||
768 | } |
||
769 | 8 | } |
|
770 | |||
771 | /** |
||
772 | * Check if this is a case of a install being resumed and figure |
||
773 | * out where to continue from. Returns the best guess on the step. |
||
774 | * |
||
775 | * @param string $step Installation step to resume from |
||
776 | * |
||
777 | * @return \Elgg\Http\RedirectResponse|null |
||
778 | */ |
||
779 | 8 | protected function resumeInstall($step) { |
|
780 | // only do a resume from the first step |
||
781 | 8 | if ($step !== 'welcome') { |
|
782 | 7 | return null; |
|
783 | } |
||
784 | |||
785 | 1 | if ($this->has_completed['database'] == false) { |
|
786 | 1 | return null; |
|
787 | } |
||
788 | |||
789 | if ($this->has_completed['settings'] == false) { |
||
790 | return new \Elgg\Http\RedirectResponse("install.php?step=settings"); |
||
791 | } |
||
792 | |||
793 | if ($this->has_completed['admin'] == false) { |
||
794 | return new \Elgg\Http\RedirectResponse("install.php?step=admin"); |
||
795 | } |
||
796 | |||
797 | // everything appears to be set up |
||
798 | return new \Elgg\Http\RedirectResponse("install.php?step=complete"); |
||
799 | } |
||
800 | |||
801 | /** |
||
802 | * Bootstrapping |
||
803 | */ |
||
804 | |||
805 | /** |
||
806 | * Load remaining engine libraries and complete bootstrapping |
||
807 | * |
||
808 | * @param string $step Which step to boot strap for. Required because |
||
809 | * boot strapping is different until the DB is populated. |
||
810 | * |
||
811 | * @return void |
||
812 | * @throws InstallationException |
||
813 | */ |
||
814 | 9 | protected function finishBootstrapping($step) { |
|
815 | |||
816 | 9 | $app = $this->getApp(); |
|
817 | |||
818 | 9 | $index_db = array_search('database', $this->getSteps()); |
|
819 | 9 | $index_settings = array_search('settings', $this->getSteps()); |
|
820 | 9 | $index_admin = array_search('admin', $this->getSteps()); |
|
821 | 9 | $index_complete = array_search('complete', $this->getSteps()); |
|
822 | 9 | $index_step = array_search($step, $this->getSteps()); |
|
823 | |||
824 | // To log in the user, we need to use the Elgg core session handling. |
||
825 | // Otherwise, use default php session handling |
||
826 | 9 | $use_elgg_session = ($index_step == $index_admin && $this->is_action) || ($index_step == $index_complete); |
|
827 | 9 | if (!$use_elgg_session) { |
|
828 | 8 | $this->createSessionFromFile(); |
|
829 | } |
||
830 | |||
831 | 9 | if ($index_step > $index_db) { |
|
832 | // once the database has been created, load rest of engine |
||
833 | |||
834 | // dummy site needed to boot |
||
835 | 5 | $app->_services->config->site = new ElggSite(); |
|
836 | |||
837 | 5 | $app->bootCore(); |
|
838 | } |
||
839 | 9 | } |
|
840 | |||
841 | /** |
||
842 | * Load settings |
||
843 | * |
||
844 | * @return void |
||
845 | * @throws InstallationException |
||
846 | */ |
||
847 | 5 | protected function loadSettingsFile() { |
|
848 | try { |
||
849 | 5 | $app = $this->getApp(); |
|
850 | |||
851 | 5 | $config = Config::fromFile(Config::resolvePath()); |
|
852 | 5 | $app->_services->setValue('config', $config); |
|
853 | |||
854 | // in case the DB instance is already captured in services, we re-inject its settings. |
||
855 | 5 | $app->_services->db->resetConnections(DbConfig::fromElggConfig($config)); |
|
856 | } catch (\Exception $e) { |
||
857 | $msg = elgg_echo('InstallationException:CannotLoadSettings'); |
||
858 | throw new InstallationException($msg, 0, $e); |
||
859 | } |
||
860 | 5 | } |
|
861 | |||
862 | /** |
||
863 | * Action handling methods |
||
864 | */ |
||
865 | |||
866 | /** |
||
867 | * If form is reshown, remember previously submitted variables |
||
868 | * |
||
869 | * @param array $formVars Vars int he form |
||
870 | * @param array $submissionVars Submitted vars |
||
871 | * |
||
872 | * @return array |
||
873 | */ |
||
874 | 3 | protected function makeFormSticky($formVars, $submissionVars) { |
|
875 | 3 | foreach ($submissionVars as $field => $value) { |
|
876 | $formVars[$field]['value'] = $value; |
||
877 | } |
||
878 | |||
879 | 3 | return $formVars; |
|
880 | } |
||
881 | |||
882 | /* Requirement checks support methods */ |
||
883 | |||
884 | /** |
||
885 | * Indicates whether the webserver can add settings.php on its own or not. |
||
886 | * |
||
887 | * @param array $report The requirements report object |
||
888 | * |
||
889 | * @return bool |
||
890 | */ |
||
891 | 1 | protected function isInstallDirWritable(&$report) { |
|
892 | 1 | if (!is_writable(Paths::projectConfig())) { |
|
893 | $msg = elgg_echo('install:check:installdir', [Paths::PATH_TO_CONFIG]); |
||
894 | $report['settings'] = [ |
||
895 | [ |
||
896 | 'severity' => 'failure', |
||
897 | 'message' => $msg, |
||
898 | ] |
||
899 | ]; |
||
900 | |||
901 | return false; |
||
902 | } |
||
903 | |||
904 | 1 | return true; |
|
905 | } |
||
906 | |||
907 | /** |
||
908 | * Check that the settings file exists |
||
909 | * |
||
910 | * @param array $report The requirements report array |
||
911 | * |
||
912 | * @return bool |
||
913 | */ |
||
914 | 3 | protected function checkSettingsFile(&$report = []) { |
|
915 | 3 | if (!is_file(Config::resolvePath())) { |
|
916 | 3 | return false; |
|
917 | } |
||
918 | |||
919 | if (!is_readable(Config::resolvePath())) { |
||
920 | $report['settings'] = [ |
||
921 | [ |
||
922 | 'severity' => 'failure', |
||
923 | 'message' => elgg_echo('install:check:readsettings'), |
||
924 | ] |
||
925 | ]; |
||
926 | } |
||
927 | |||
928 | return true; |
||
929 | } |
||
930 | |||
931 | /** |
||
932 | * Check version of PHP, extensions, and variables |
||
933 | * |
||
934 | * @param array $report The requirements report array |
||
935 | * |
||
936 | * @return void |
||
937 | */ |
||
938 | 1 | protected function checkPHP(&$report) { |
|
939 | 1 | $phpReport = []; |
|
940 | |||
941 | 1 | $min_php_version = '7.0.0'; |
|
942 | 1 | if (version_compare(PHP_VERSION, $min_php_version, '<')) { |
|
943 | $phpReport[] = [ |
||
944 | 'severity' => 'failure', |
||
945 | 'message' => elgg_echo('install:check:php:version', [$min_php_version, PHP_VERSION]) |
||
946 | ]; |
||
947 | } |
||
948 | |||
949 | 1 | $this->checkPhpExtensions($phpReport); |
|
950 | |||
951 | 1 | $this->checkPhpDirectives($phpReport); |
|
952 | |||
953 | 1 | if (count($phpReport) == 0) { |
|
954 | 1 | $phpReport[] = [ |
|
955 | 1 | 'severity' => 'pass', |
|
956 | 1 | 'message' => elgg_echo('install:check:php:success') |
|
957 | ]; |
||
958 | } |
||
959 | |||
960 | 1 | $report['php'] = $phpReport; |
|
961 | 1 | } |
|
962 | |||
963 | /** |
||
964 | * Check the server's PHP extensions |
||
965 | * |
||
966 | * @param array $phpReport The PHP requirements report array |
||
967 | * |
||
968 | * @return void |
||
969 | */ |
||
970 | 1 | protected function checkPhpExtensions(&$phpReport) { |
|
971 | 1 | $extensions = get_loaded_extensions(); |
|
972 | $requiredExtensions = [ |
||
973 | 1 | 'pdo_mysql', |
|
974 | 'json', |
||
975 | 'xml', |
||
976 | 'gd', |
||
977 | ]; |
||
978 | 1 | foreach ($requiredExtensions as $extension) { |
|
979 | 1 | if (!in_array($extension, $extensions)) { |
|
980 | $phpReport[] = [ |
||
981 | 'severity' => 'failure', |
||
982 | 1 | 'message' => elgg_echo('install:check:php:extension', [$extension]) |
|
983 | ]; |
||
984 | } |
||
985 | } |
||
986 | |||
987 | $recommendedExtensions = [ |
||
988 | 1 | 'mbstring', |
|
989 | ]; |
||
990 | 1 | foreach ($recommendedExtensions as $extension) { |
|
991 | 1 | if (!in_array($extension, $extensions)) { |
|
992 | $phpReport[] = [ |
||
993 | 'severity' => 'warning', |
||
994 | 1 | 'message' => elgg_echo('install:check:php:extension:recommend', [$extension]) |
|
995 | ]; |
||
996 | } |
||
997 | } |
||
998 | 1 | } |
|
999 | |||
1000 | /** |
||
1001 | * Check PHP parameters |
||
1002 | * |
||
1003 | * @param array $phpReport The PHP requirements report array |
||
1004 | * |
||
1005 | * @return void |
||
1006 | */ |
||
1007 | 1 | protected function checkPhpDirectives(&$phpReport) { |
|
1008 | 1 | if (ini_get('open_basedir')) { |
|
1009 | $phpReport[] = [ |
||
1010 | 'severity' => 'warning', |
||
1011 | 'message' => elgg_echo("install:check:php:open_basedir") |
||
1012 | ]; |
||
1013 | } |
||
1014 | |||
1015 | 1 | if (ini_get('safe_mode')) { |
|
1016 | $phpReport[] = [ |
||
1017 | 'severity' => 'warning', |
||
1018 | 'message' => elgg_echo("install:check:php:safe_mode") |
||
1019 | ]; |
||
1020 | } |
||
1021 | |||
1022 | 1 | if (ini_get('arg_separator.output') !== '&') { |
|
1023 | $separator = htmlspecialchars(ini_get('arg_separator.output')); |
||
1024 | $msg = elgg_echo("install:check:php:arg_separator", [$separator]); |
||
1025 | $phpReport[] = [ |
||
1026 | 'severity' => 'failure', |
||
1027 | 'message' => $msg, |
||
1028 | ]; |
||
1029 | } |
||
1030 | |||
1031 | 1 | if (ini_get('register_globals')) { |
|
1032 | $phpReport[] = [ |
||
1033 | 'severity' => 'failure', |
||
1034 | 'message' => elgg_echo("install:check:php:register_globals") |
||
1035 | ]; |
||
1036 | } |
||
1037 | |||
1038 | 1 | if (ini_get('session.auto_start')) { |
|
1039 | $phpReport[] = [ |
||
1040 | 'severity' => 'failure', |
||
1041 | 'message' => elgg_echo("install:check:php:session.auto_start") |
||
1042 | ]; |
||
1043 | } |
||
1044 | 1 | } |
|
1045 | |||
1046 | /** |
||
1047 | * Confirm that the rewrite rules are firing |
||
1048 | * |
||
1049 | * @param array $report The requirements report array |
||
1050 | * |
||
1051 | * @return void |
||
1052 | * @throws InstallationException |
||
1053 | */ |
||
1054 | protected function checkRewriteRules(&$report) { |
||
1055 | $app = $this->getApp(); |
||
1056 | |||
1057 | $tester = new ElggRewriteTester(); |
||
1058 | $url = $app->_services->config->wwwroot; |
||
1059 | $url .= Request::REWRITE_TEST_TOKEN . '?' . http_build_query([ |
||
1060 | Request::REWRITE_TEST_TOKEN => '1', |
||
1061 | ]); |
||
1062 | $report['rewrite'] = [$tester->run($url, Paths::project())]; |
||
1063 | } |
||
1064 | |||
1065 | /** |
||
1066 | * Count the number of failures in the requirements report |
||
1067 | * |
||
1068 | * @param array $report The requirements report array |
||
1069 | * @param string $condition 'failure' or 'warning' |
||
1070 | * |
||
1071 | * @return int |
||
1072 | */ |
||
1073 | 1 | protected function countNumConditions($report, $condition) { |
|
1074 | 1 | $count = 0; |
|
1075 | 1 | foreach ($report as $category => $checks) { |
|
1076 | 1 | foreach ($checks as $check) { |
|
1077 | 1 | if ($check['severity'] === $condition) { |
|
1078 | 1 | $count++; |
|
1079 | } |
||
1080 | } |
||
1081 | } |
||
1082 | |||
1083 | 1 | return $count; |
|
1084 | } |
||
1085 | |||
1086 | |||
1087 | /** |
||
1088 | * Database support methods |
||
1089 | */ |
||
1090 | |||
1091 | /** |
||
1092 | * Validate the variables for the database step |
||
1093 | * |
||
1094 | * @param array $submissionVars Submitted vars |
||
1095 | * @param array $formVars Vars in the form |
||
1096 | * |
||
1097 | * @return bool |
||
1098 | * @throws InstallationException |
||
1099 | */ |
||
1100 | 1 | protected function validateDatabaseVars($submissionVars, $formVars) { |
|
1101 | |||
1102 | 1 | $app = $this->getApp(); |
|
1103 | |||
1104 | 1 | foreach ($formVars as $field => $info) { |
|
1105 | 1 | if ($info['required'] == true && !$submissionVars[$field]) { |
|
1106 | $name = elgg_echo("install:database:label:$field"); |
||
1107 | register_error(elgg_echo('install:error:requiredfield', [$name])); |
||
1108 | |||
1109 | 1 | return false; |
|
1110 | } |
||
1111 | } |
||
1112 | |||
1113 | 1 | if (!empty($submissionVars['wwwroot']) && !_elgg_sane_validate_url($submissionVars['wwwroot'])) { |
|
1114 | register_error(elgg_echo('install:error:wwwroot', [$submissionVars['wwwroot']])); |
||
1115 | |||
1116 | return false; |
||
1117 | } |
||
1118 | |||
1119 | // check that data root is absolute path |
||
1120 | 1 | if (stripos(PHP_OS, 'win') === 0) { |
|
1121 | if (strpos($submissionVars['dataroot'], ':') !== 1) { |
||
1122 | $msg = elgg_echo('install:error:relative_path', [$submissionVars['dataroot']]); |
||
1123 | register_error($msg); |
||
1124 | |||
1125 | return false; |
||
1126 | } |
||
1127 | } else { |
||
1128 | 1 | if (strpos($submissionVars['dataroot'], '/') !== 0) { |
|
1129 | $msg = elgg_echo('install:error:relative_path', [$submissionVars['dataroot']]); |
||
1130 | register_error($msg); |
||
1131 | |||
1132 | return false; |
||
1133 | } |
||
1134 | } |
||
1135 | |||
1136 | // check that data root exists |
||
1137 | 1 | if (!is_dir($submissionVars['dataroot'])) { |
|
1138 | $msg = elgg_echo('install:error:datadirectoryexists', [$submissionVars['dataroot']]); |
||
1139 | register_error($msg); |
||
1140 | |||
1141 | return false; |
||
1142 | } |
||
1143 | |||
1144 | // check that data root is writable |
||
1145 | 1 | if (!is_writable($submissionVars['dataroot'])) { |
|
1146 | $msg = elgg_echo('install:error:writedatadirectory', [$submissionVars['dataroot']]); |
||
1147 | register_error($msg); |
||
1148 | |||
1149 | return false; |
||
1150 | } |
||
1151 | |||
1152 | 1 | if (!$app->_services->config->data_dir_override) { |
|
1153 | // check that data root is not subdirectory of Elgg root |
||
1154 | 1 | if (stripos($submissionVars['dataroot'], $app->_services->config->path) === 0) { |
|
1155 | $msg = elgg_echo('install:error:locationdatadirectory', [$submissionVars['dataroot']]); |
||
1156 | register_error($msg); |
||
1157 | |||
1158 | return false; |
||
1159 | } |
||
1160 | } |
||
1161 | |||
1162 | // according to postgres documentation: SQL identifiers and key words must |
||
1163 | // begin with a letter (a-z, but also letters with diacritical marks and |
||
1164 | // non-Latin letters) or an underscore (_). Subsequent characters in an |
||
1165 | // identifier or key word can be letters, underscores, digits (0-9), or dollar signs ($). |
||
1166 | // Refs #4994 |
||
1167 | 1 | if (!preg_match("/^[a-zA-Z_][\w]*$/", $submissionVars['dbprefix'])) { |
|
1168 | register_error(elgg_echo('install:error:database_prefix')); |
||
1169 | |||
1170 | return false; |
||
1171 | } |
||
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.