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)) { |
|||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
$path of type Elgg\Config is incompatible with the type string expected by parameter $filename of is_readable() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
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()); |
|||
1 ignored issue
–
show
|
|||||
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 | |||||