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) { |
|
0 ignored issues
–
show
|
|||
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) { |
|
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 | } |
||
1172 | |||
1173 | 1 | return $this->checkDatabaseSettings( |
|
1174 | 1 | $submissionVars['dbuser'], |
|
1175 | 1 | $submissionVars['dbpassword'], |
|
1176 | 1 | $submissionVars['dbname'], |
|
1177 | 1 | $submissionVars['dbhost'] |
|
1178 | ); |
||
1179 | } |
||
1180 | |||
1181 | /** |
When comparing two booleans, it is generally considered safer to use the strict comparison operator.