Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
3.49% |
15 / 430 |
|
2.94% |
1 / 34 |
CRAP | |
0.00% |
0 / 1 |
InstallController | |
3.49% |
15 / 430 |
|
2.94% |
1 / 34 |
15088.54 | |
0.00% |
0 / 1 |
validateAutoConfigureConfig | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
attachDefaultListeners | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
disabledAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
installBasicConfig | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
checkBasicConfig | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getSolrUrlFromImportConfig | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
fixbasicconfigAction | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
checkCache | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
fixcacheAction | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
checkDatabase | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
phpVersionIsNewEnough | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
checkDependencies | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
fixdependenciesAction | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
56 | |||
fixdatabaseAction | |
0.00% |
0 / 82 |
|
0.00% |
0 / 1 |
420 | |||
getPreCommands | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
getPostCommands | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
showsqlAction | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
checkILS | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
fixilsAction | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
182 | |||
testSearchService | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
checkSolr | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
fixsolrAction | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
20 | |||
checkSecurity | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
fixSecurityConfiguration | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
fixsecurityAction | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
performsecurityfixAction | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
90 | |||
checkSslCerts | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
fixsslcertsAction | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
testSslCertConfig | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
doneAction | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
homeAction | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getMinimalPhpVersion | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
getMinimalPhpVersionId | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getComposerJson | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
4.12 |
1 | <?php |
2 | |
3 | /** |
4 | * Install Controller |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010, 2022. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Controller |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org Main Site |
28 | */ |
29 | |
30 | namespace VuFind\Controller; |
31 | |
32 | use Laminas\Crypt\Password\Bcrypt; |
33 | use Laminas\Mvc\MvcEvent; |
34 | use VuFind\Config\Writer as ConfigWriter; |
35 | use VuFind\Db\Service\TagServiceInterface; |
36 | use VuFind\Db\Service\UserCardServiceInterface; |
37 | use VuFind\Db\Service\UserServiceInterface; |
38 | use VuFindSearch\Command\RetrieveCommand; |
39 | |
40 | use function count; |
41 | use function defined; |
42 | use function dirname; |
43 | use function function_exists; |
44 | use function in_array; |
45 | use function is_callable; |
46 | use function strlen; |
47 | |
48 | /** |
49 | * Class controls VuFind auto-configuration. |
50 | * |
51 | * @category VuFind |
52 | * @package Controller |
53 | * @author Demian Katz <demian.katz@villanova.edu> |
54 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
55 | * @link https://vufind.org Main Site |
56 | */ |
57 | class InstallController extends AbstractBase |
58 | { |
59 | use Feature\ConfigPathTrait; |
60 | use Feature\SecureDatabaseTrait; |
61 | |
62 | /** |
63 | * Use preDispatch event to block access when appropriate. |
64 | * |
65 | * @param MvcEvent $e Event object |
66 | * |
67 | * @return void |
68 | */ |
69 | public function validateAutoConfigureConfig(MvcEvent $e) |
70 | { |
71 | // If auto-configuration is disabled, prevent any other action from being |
72 | // accessed: |
73 | $config = $this->getConfig(); |
74 | if ( |
75 | !isset($config->System->autoConfigure) |
76 | || !$config->System->autoConfigure |
77 | ) { |
78 | $routeMatch = $e->getRouteMatch(); |
79 | $routeMatch->setParam('action', 'disabled'); |
80 | } |
81 | } |
82 | |
83 | /** |
84 | * Register the default events for this controller |
85 | * |
86 | * @return void |
87 | */ |
88 | protected function attachDefaultListeners() |
89 | { |
90 | parent::attachDefaultListeners(); |
91 | $events = $this->getEventManager(); |
92 | $events->attach( |
93 | MvcEvent::EVENT_DISPATCH, |
94 | [$this, 'validateAutoConfigureConfig'], |
95 | 1000 |
96 | ); |
97 | } |
98 | |
99 | /** |
100 | * Display disabled message. |
101 | * |
102 | * @return mixed |
103 | */ |
104 | public function disabledAction() |
105 | { |
106 | return $this->createViewModel(); |
107 | } |
108 | |
109 | /** |
110 | * Copy the basic configuration file into position and report success or |
111 | * failure. |
112 | * |
113 | * @return bool |
114 | */ |
115 | protected function installBasicConfig() |
116 | { |
117 | $config = $this->getForcedLocalConfigPath('config.ini'); |
118 | if (!file_exists($config)) { |
119 | return copy($this->getBaseConfigFilePath('config.ini'), $config); |
120 | } |
121 | return true; // report success if file already exists |
122 | } |
123 | |
124 | /** |
125 | * Check if basic configuration is taken care of. |
126 | * |
127 | * @return array |
128 | */ |
129 | protected function checkBasicConfig() |
130 | { |
131 | // Initialize status based on existence of config file... |
132 | $status = $this->installBasicConfig(); |
133 | |
134 | // See if the URL setting remains at the default (unless we already |
135 | // know we've failed): |
136 | if ($status) { |
137 | $config = $this->getConfig(); |
138 | if (stristr($config->Site->url, 'myuniversity.edu')) { |
139 | $status = false; |
140 | } |
141 | } |
142 | |
143 | return [ |
144 | 'title' => 'Basic Configuration', 'status' => $status, |
145 | 'fix' => 'fixbasicconfig', |
146 | ]; |
147 | } |
148 | |
149 | /** |
150 | * Extract the Solr base URL from the SolrMarc configuration file, |
151 | * so a custom Solr port configured in install.php can be applied to |
152 | * the initial config.ini file. |
153 | * |
154 | * Return null if no custom Solr URL can be found. |
155 | * |
156 | * @return ?string |
157 | */ |
158 | protected function getSolrUrlFromImportConfig() |
159 | { |
160 | $resolver = $this->serviceLocator->get(\VuFind\Config\PathResolver::class); |
161 | $importConfig = $resolver->getLocalConfigPath('import.properties', 'import'); |
162 | if (file_exists($importConfig)) { |
163 | $props = file_get_contents($importConfig); |
164 | preg_match('|solr.hosturl\s*=\s*(https?://\w+:\d+/\w+)|', $props, $matches); |
165 | if (!empty($matches[1])) { |
166 | return $matches[1]; |
167 | } |
168 | } |
169 | return null; |
170 | } |
171 | |
172 | /** |
173 | * Display repair instructions for basic configuration problems. |
174 | * |
175 | * @return mixed |
176 | */ |
177 | public function fixbasicconfigAction() |
178 | { |
179 | $view = $this->createViewModel(); |
180 | $config = $this->getForcedLocalConfigPath('config.ini'); |
181 | try { |
182 | if (!$this->installBasicConfig()) { |
183 | throw new \Exception('Cannot copy file into position.'); |
184 | } |
185 | $writer = new ConfigWriter($config); |
186 | $serverUrl = $this->getViewRenderer()->plugin('serverurl'); |
187 | $path = $this->url()->fromRoute('home'); |
188 | $writer->set('Site', 'url', rtrim($serverUrl($path), '/')); |
189 | if ($solrUrl = $this->getSolrUrlFromImportConfig()) { |
190 | $writer->set('Index', 'url', $solrUrl); |
191 | } |
192 | if (!$writer->save()) { |
193 | throw new \Exception('Cannot write config to disk.'); |
194 | } |
195 | } catch (\Exception $e) { |
196 | $view->configDir = dirname($config); |
197 | if ( |
198 | function_exists('posix_getpwuid') |
199 | && function_exists('posix_geteuid') |
200 | ) { |
201 | $processUser = posix_getpwuid(posix_geteuid()); |
202 | $view->runningUser = $processUser['name']; |
203 | } |
204 | } |
205 | return $view; |
206 | } |
207 | |
208 | /** |
209 | * Check if the cache directory is writable. |
210 | * |
211 | * @return array |
212 | */ |
213 | protected function checkCache() |
214 | { |
215 | $cache = $this->serviceLocator->get(\VuFind\Cache\Manager::class); |
216 | return [ |
217 | 'title' => 'Cache', |
218 | 'status' => !$cache->hasDirectoryCreationError(), |
219 | 'fix' => 'fixcache', |
220 | ]; |
221 | } |
222 | |
223 | /** |
224 | * Display repair instructions for cache problems. |
225 | * |
226 | * @return mixed |
227 | */ |
228 | public function fixcacheAction() |
229 | { |
230 | $cache = $this->serviceLocator->get(\VuFind\Cache\Manager::class); |
231 | $view = $this->createViewModel(); |
232 | $view->cacheDir = $cache->getCacheDir(); |
233 | if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) { |
234 | $processUser = posix_getpwuid(posix_geteuid()); |
235 | $view->runningUser = $processUser['name']; |
236 | } |
237 | return $view; |
238 | } |
239 | |
240 | /** |
241 | * Check if the database is accessible. |
242 | * |
243 | * @return array |
244 | */ |
245 | protected function checkDatabase() |
246 | { |
247 | try { |
248 | // Try to read the tags table just to see if we can connect to the DB: |
249 | $this->getDbService(TagServiceInterface::class)->getTagsByText('test'); |
250 | $status = true; |
251 | } catch (\Exception $e) { |
252 | $status = false; |
253 | } |
254 | return [ |
255 | 'title' => 'Database', 'status' => $status, 'fix' => 'fixdatabase', |
256 | ]; |
257 | } |
258 | |
259 | /** |
260 | * Support method for check/fix dependencies code -- do we have a new enough |
261 | * version of PHP? |
262 | * |
263 | * @return bool |
264 | */ |
265 | protected function phpVersionIsNewEnough() |
266 | { |
267 | // PHP_VERSION_ID was introduced in 5.2.7; if it's missing, we have a |
268 | // problem. |
269 | if (!defined('PHP_VERSION_ID')) { |
270 | return false; |
271 | } |
272 | |
273 | // We need at least PHP version as defined in composer.json file: |
274 | return PHP_VERSION_ID >= $this->getMinimalPhpVersionId(); |
275 | } |
276 | |
277 | /** |
278 | * Check for missing dependencies. |
279 | * |
280 | * @return array |
281 | */ |
282 | protected function checkDependencies() |
283 | { |
284 | $requiredFunctionsExist |
285 | = function_exists('mb_substr') && is_callable('imagecreatefromstring') |
286 | && function_exists('openssl_encrypt') |
287 | && class_exists('XSLTProcessor') |
288 | && defined('SODIUM_LIBRARY_VERSION'); |
289 | |
290 | return [ |
291 | 'title' => 'Dependencies', |
292 | 'status' => $requiredFunctionsExist && $this->phpVersionIsNewEnough(), |
293 | 'fix' => 'fixdependencies', |
294 | ]; |
295 | } |
296 | |
297 | /** |
298 | * Show how to fix dependency problems. |
299 | * |
300 | * @return mixed |
301 | */ |
302 | public function fixdependenciesAction() |
303 | { |
304 | $problems = 0; |
305 | |
306 | // Is our version new enough? |
307 | if (!$this->phpVersionIsNewEnough()) { |
308 | $msg = 'VuFind requires PHP version ' . $this->getMinimalPhpVersion() |
309 | . ' or newer; you are running ' . phpversion() |
310 | . '. Please upgrade.'; |
311 | $this->flashMessenger()->addMessage($msg, 'error'); |
312 | $problems++; |
313 | } |
314 | |
315 | // Is the mbstring library missing? |
316 | if (!function_exists('mb_substr')) { |
317 | $msg |
318 | = 'Your PHP installation appears to be missing the mbstring plug-in.' |
319 | . ' For better language support, it is recommended that you add' |
320 | . ' this. For details on how to do this, see ' |
321 | . 'https://vufind.org/wiki/installation ' |
322 | . 'and look at the PHP installation instructions for your platform.'; |
323 | $this->flashMessenger()->addMessage($msg, 'error'); |
324 | $problems++; |
325 | } |
326 | |
327 | // Is the GD library missing? |
328 | if (!is_callable('imagecreatefromstring')) { |
329 | $msg |
330 | = 'Your PHP installation appears to be missing the GD plug-in. ' |
331 | . 'For better graphics support, it is recommended that you add this.' |
332 | . ' For details on how to do this, see ' |
333 | . 'https://vufind.org/wiki/installation ' |
334 | . 'and look at the PHP installation instructions for your platform.'; |
335 | $this->flashMessenger()->addMessage($msg, 'error'); |
336 | $problems++; |
337 | } |
338 | |
339 | // Is the openssl library missing? |
340 | if (!function_exists('openssl_encrypt')) { |
341 | $msg |
342 | = 'Your PHP installation appears to be missing the openssl plug-in.' |
343 | . ' For better security support, it is recommended that you add' |
344 | . ' this. For details on how to do this, see ' |
345 | . 'https://vufind.org/wiki/installation ' |
346 | . 'and look at the PHP installation instructions for your platform.'; |
347 | $this->flashMessenger()->addMessage($msg, 'error'); |
348 | $problems++; |
349 | } |
350 | |
351 | // Is the XSL library missing? |
352 | if (!class_exists('XSLTProcessor')) { |
353 | $msg |
354 | = 'Your PHP installation appears to be missing the XSL plug-in.' |
355 | . ' For details on how to do this, see ' |
356 | . 'https://vufind.org/wiki/installation ' |
357 | . 'and look at the PHP installation instructions for your platform.'; |
358 | $this->flashMessenger()->addMessage($msg, 'error'); |
359 | $problems++; |
360 | } |
361 | |
362 | // Is the sodium extension missing? |
363 | if (!defined('SODIUM_LIBRARY_VERSION')) { |
364 | $msg |
365 | = 'Your PHP installation appears to be missing the sodium plug-in.' |
366 | . ' For details on how to do this, see ' |
367 | . 'https://vufind.org/wiki/installation ' |
368 | . 'and look at the PHP installation instructions for your platform.'; |
369 | $this->flashMessenger()->addMessage($msg, 'error'); |
370 | $problems++; |
371 | } |
372 | |
373 | return $this->createViewModel(['problems' => $problems]); |
374 | } |
375 | |
376 | /** |
377 | * Display repair instructions for database problems. |
378 | * |
379 | * @return mixed |
380 | */ |
381 | public function fixdatabaseAction() |
382 | { |
383 | $view = $this->createViewModel(); |
384 | $view->dbname = $this->params()->fromPost('dbname', 'vufind'); |
385 | $view->dbuser = $this->params()->fromPost('dbuser', 'vufind'); |
386 | $view->dbhost = $this->params()->fromPost('dbhost', 'localhost'); |
387 | $view->vufindhost = $this->params()->fromPost('vufindhost', 'localhost'); |
388 | $view->dbrootuser = $this->params()->fromPost('dbrootuser', 'root'); |
389 | $view->driver = $this->params()->fromPost('driver', 'mysql'); |
390 | |
391 | $skip = $this->params()->fromPost('printsql', 'nope') == 'Skip'; |
392 | |
393 | if (!preg_match('/^\w*$/', $view->dbname)) { |
394 | $this->flashMessenger() |
395 | ->addMessage('Database name must be alphanumeric.', 'error'); |
396 | } elseif (!preg_match('/^\w*$/', $view->dbuser)) { |
397 | $this->flashMessenger() |
398 | ->addMessage('Database user must be alphanumeric.', 'error'); |
399 | } elseif ($skip || $this->formWasSubmitted()) { |
400 | $newpass = $this->params()->fromPost('dbpass'); |
401 | $newpassConf = $this->params()->fromPost('dbpassconfirm'); |
402 | if ((empty($newpass) || empty($newpassConf))) { |
403 | $this->flashMessenger() |
404 | ->addMessage('Password fields must not be blank.', 'error'); |
405 | } elseif ($newpass != $newpassConf) { |
406 | $this->flashMessenger() |
407 | ->addMessage('Password fields must match.', 'error'); |
408 | } else { |
409 | // Connect to database: |
410 | $connection = $view->driver . '://' . $view->dbrootuser . ':' |
411 | . $this->params()->fromPost('dbrootpass') . '@' |
412 | . $view->dbhost; |
413 | try { |
414 | $dbName = ($view->driver == 'pgsql') |
415 | ? 'template1' : $view->driver; |
416 | $db = $this->serviceLocator |
417 | ->get(\VuFind\Db\AdapterFactory::class) |
418 | ->getAdapterFromConnectionString("{$connection}/{$dbName}"); |
419 | } catch (\Exception $e) { |
420 | $this->flashMessenger() |
421 | ->addMessage( |
422 | 'Problem initializing database adapter; ' |
423 | . 'check for missing ' . $view->driver |
424 | . ' library. Details: ' . $e->getMessage(), |
425 | 'error' |
426 | ); |
427 | return $view; |
428 | } |
429 | try { |
430 | // Get SQL together |
431 | $escapedPass = $skip |
432 | ? "'" . addslashes($newpass) . "'" |
433 | : $db->getPlatform()->quoteValue($newpass); |
434 | $preCommands = $this->getPreCommands($view, $escapedPass); |
435 | $postCommands = $this->getPostCommands($view); |
436 | $sql = file_get_contents( |
437 | APPLICATION_PATH . "/module/VuFind/sql/{$view->driver}.sql" |
438 | ); |
439 | if ($skip) { |
440 | $omnisql = ''; |
441 | foreach ($preCommands as $query) { |
442 | $omnisql .= $query . ";\n"; |
443 | } |
444 | $omnisql .= "\n" . $sql . "\n"; |
445 | foreach ($postCommands as $query) { |
446 | $omnisql .= $query . ";\n"; |
447 | } |
448 | $this->getRequest()->getQuery()->set('sql', $omnisql); |
449 | return $this->forwardTo('Install', 'showsql'); |
450 | } else { |
451 | foreach ($preCommands as $query) { |
452 | $db->query($query, $db::QUERY_MODE_EXECUTE); |
453 | } |
454 | $dbFactory = $this->serviceLocator |
455 | ->get(\VuFind\Db\AdapterFactory::class); |
456 | $db = $dbFactory->getAdapterFromConnectionString( |
457 | $connection . '/' . $view->dbname |
458 | ); |
459 | $statements = explode(';', $sql); |
460 | foreach ($statements as $current) { |
461 | // Skip empty sections: |
462 | if (strlen(trim($current)) == 0) { |
463 | continue; |
464 | } |
465 | $db->query($current, $db::QUERY_MODE_EXECUTE); |
466 | } |
467 | foreach ($postCommands as $query) { |
468 | $db->query($query, $db::QUERY_MODE_EXECUTE); |
469 | } |
470 | // If we made it this far, we can update the config file and |
471 | // forward back to the home action! |
472 | $string = "{$view->driver}://{$view->dbuser}:{$newpass}@" |
473 | . $view->dbhost . '/' . $view->dbname; |
474 | $config = $this->getForcedLocalConfigPath('config.ini'); |
475 | $writer = new ConfigWriter($config); |
476 | $writer->set('Database', 'database', $string); |
477 | if (!$writer->save()) { |
478 | return $this->forwardTo('Install', 'fixbasicconfig'); |
479 | } |
480 | } |
481 | return $this->redirect()->toRoute('install-home'); |
482 | } catch (\Exception $e) { |
483 | $this->flashMessenger()->addMessage($e->getMessage(), 'error'); |
484 | } |
485 | } |
486 | } |
487 | return $view; |
488 | } |
489 | |
490 | /** |
491 | * Get SQL commands needed to set up a particular database before |
492 | * loading the main SQL file of table definitions. |
493 | * |
494 | * @param \Laminas\View\Model $view View object containing DB settings. |
495 | * @param string $escapedPass Password to set for new DB (escaped |
496 | * appropriately for target database). |
497 | * |
498 | * @return array |
499 | */ |
500 | protected function getPreCommands($view, $escapedPass) |
501 | { |
502 | $create = 'CREATE DATABASE ' . $view->dbname; |
503 | // Special case: PostgreSQL: |
504 | if ($view->driver == 'pgsql') { |
505 | $escape = 'ALTER DATABASE ' . $view->dbname |
506 | . " SET bytea_output='escape'"; |
507 | $cuser = 'CREATE USER ' . $view->dbuser |
508 | . " WITH PASSWORD {$escapedPass}"; |
509 | $grant = 'GRANT ALL PRIVILEGES ON DATABASE ' |
510 | . "{$view->dbname} TO {$view->dbuser} "; |
511 | return [$create, $escape, $cuser, $grant]; |
512 | } |
513 | // Default: MySQL: |
514 | $user = "CREATE USER '{$view->dbuser}'@'{$view->vufindhost}' " |
515 | . "IDENTIFIED BY {$escapedPass}"; |
516 | $grant = 'GRANT SELECT,INSERT,UPDATE,DELETE ON ' |
517 | . $view->dbname |
518 | . ".* TO '{$view->dbuser}'@'{$view->vufindhost}' " |
519 | . 'WITH GRANT OPTION'; |
520 | $use = "USE {$view->dbname}"; |
521 | return [$create, $user, $grant, 'FLUSH PRIVILEGES', $use]; |
522 | } |
523 | |
524 | /** |
525 | * Get SQL commands needed to set up a particular database after |
526 | * loading the main SQL file of table definitions. |
527 | * |
528 | * @param \Laminas\View\Model $view View object containing DB settings. |
529 | * |
530 | * @return array |
531 | */ |
532 | protected function getPostCommands($view) |
533 | { |
534 | // Special case: PostgreSQL: |
535 | if ($view->driver == 'pgsql') { |
536 | $grantTables = 'GRANT ALL PRIVILEGES ON ALL TABLES IN ' |
537 | . "SCHEMA public TO {$view->dbuser} "; |
538 | $grantSequences = 'GRANT ALL PRIVILEGES ON ALL SEQUENCES' |
539 | . " IN SCHEMA public TO {$view->dbuser} "; |
540 | return [$grantTables, $grantSequences]; |
541 | } |
542 | // Default: MySQL: |
543 | return []; |
544 | } |
545 | |
546 | /** |
547 | * Display captured SQL commands for database action. |
548 | * |
549 | * @return mixed |
550 | */ |
551 | protected function showsqlAction() |
552 | { |
553 | $continue = $this->params()->fromPost('continue', 'nope'); |
554 | if ($continue == 'Next') { |
555 | return $this->redirect()->toRoute('install-home'); |
556 | } |
557 | |
558 | return $this->createViewModel( |
559 | ['sql' => $this->params()->fromQuery('sql')] |
560 | ); |
561 | } |
562 | |
563 | /** |
564 | * Check if ILS configuration is appropriate. |
565 | * |
566 | * @return array |
567 | */ |
568 | protected function checkILS() |
569 | { |
570 | $config = $this->getConfig(); |
571 | if (in_array($config->Catalog->driver, ['Sample', 'Demo'])) { |
572 | $status = false; |
573 | } else { |
574 | try { |
575 | $status = 'ils-offline' !== $this->getILS()->getOfflineMode(true); |
576 | } catch (\Exception $e) { |
577 | $status = false; |
578 | } |
579 | } |
580 | return ['title' => 'ILS', 'status' => $status, 'fix' => 'fixils']; |
581 | } |
582 | |
583 | /** |
584 | * Display repair instructions for ILS problems. |
585 | * |
586 | * @return mixed |
587 | */ |
588 | public function fixilsAction() |
589 | { |
590 | // Process incoming parameter -- user may have selected a new driver: |
591 | $newDriver = $this->params()->fromPost('driver'); |
592 | if (!empty($newDriver)) { |
593 | $configPath = $this->getForcedLocalConfigPath('config.ini'); |
594 | $writer = new ConfigWriter($configPath); |
595 | $writer->set('Catalog', 'driver', $newDriver); |
596 | if (!$writer->save()) { |
597 | return $this->forwardTo('Install', 'fixbasicconfig'); |
598 | } |
599 | // Copy configuration, if applicable: |
600 | $ilsIni = $this->getBaseConfigFilePath("{$newDriver}.ini"); |
601 | $localIlsIni = $this->getForcedLocalConfigPath("{$newDriver}.ini"); |
602 | if (file_exists($ilsIni) && !file_exists($localIlsIni)) { |
603 | if (!copy($ilsIni, $localIlsIni)) { |
604 | return $this->forwardTo('Install', 'fixbasicconfig'); |
605 | } |
606 | } |
607 | return $this->redirect()->toRoute('install-home'); |
608 | } |
609 | |
610 | // If we got this far, check whether we have an error with a real driver |
611 | // or if we need to warn the user that they have selected a fake driver: |
612 | $config = $this->getConfig(); |
613 | $view = $this->createViewModel(); |
614 | if (in_array($config->Catalog->driver, ['Sample', 'Demo'])) { |
615 | $view->demo = true; |
616 | // Get a list of available drivers: |
617 | $dir |
618 | = opendir(APPLICATION_PATH . '/module/VuFind/src/VuFind/ILS/Driver'); |
619 | $drivers = []; |
620 | $excludeList = [ |
621 | 'Sample.php', 'Demo.php', 'DriverInterface.php', 'PluginManager.php', |
622 | ]; |
623 | while ($line = readdir($dir)) { |
624 | if ( |
625 | stristr($line, '.php') && !in_array($line, $excludeList) |
626 | && !str_starts_with($line, 'Abstract') |
627 | && !str_ends_with($line, 'Factory.php') |
628 | && !str_ends_with($line, 'Trait.php') |
629 | ) { |
630 | $drivers[] = str_replace('.php', '', $line); |
631 | } |
632 | } |
633 | closedir($dir); |
634 | sort($drivers); |
635 | $view->drivers = $drivers; |
636 | } else { |
637 | $view->configPath = $this->getForcedLocalConfigPath( |
638 | "{$config->Catalog->driver}.ini" |
639 | ); |
640 | } |
641 | return $view; |
642 | } |
643 | |
644 | /** |
645 | * Support method to test the search service |
646 | * |
647 | * @return void |
648 | * @throws \Exception |
649 | */ |
650 | protected function testSearchService() |
651 | { |
652 | // Try to retrieve an arbitrary ID -- this will fail if Solr is down: |
653 | $searchService = $this->serviceLocator->get(\VuFindSearch\Service::class); |
654 | $command = new RetrieveCommand('Solr', '1'); |
655 | $searchService->invoke($command)->getResult(); |
656 | } |
657 | |
658 | /** |
659 | * Check if the Solr index is working. |
660 | * |
661 | * @return array |
662 | */ |
663 | protected function checkSolr() |
664 | { |
665 | try { |
666 | $this->testSearchService(); |
667 | $status = true; |
668 | } catch (\Exception $e) { |
669 | $status = false; |
670 | } |
671 | return ['title' => 'Solr', 'status' => $status, 'fix' => 'fixsolr']; |
672 | } |
673 | |
674 | /** |
675 | * Display repair instructions for Solr problems. |
676 | * |
677 | * @return mixed |
678 | */ |
679 | public function fixsolrAction() |
680 | { |
681 | // In Windows, localhost may fail -- see if switching to 127.0.0.1 helps: |
682 | $config = $this->getConfig(); |
683 | $configFile = $this->getForcedLocalConfigPath('config.ini'); |
684 | if (stristr($config->Index->url, 'localhost')) { |
685 | $newUrl = str_replace('localhost', '127.0.0.1', $config->Index->url); |
686 | try { |
687 | $this->testSearchService(); |
688 | |
689 | // If we got this far, the fix worked. Let's write it to disk! |
690 | $writer = new ConfigWriter($configFile); |
691 | $writer->set('Index', 'url', $newUrl); |
692 | if (!$writer->save()) { |
693 | return $this->forwardTo('Install', 'fixbasicconfig'); |
694 | } |
695 | return $this->redirect()->toRoute('install-home'); |
696 | } catch (\Exception $e) { |
697 | // Didn't work! |
698 | } |
699 | } |
700 | |
701 | // If we got this far, the automatic fix didn't work, so let's just assign |
702 | // some variables to use in offering troubleshooting advice: |
703 | $view = $this->createViewModel(); |
704 | $view->rawUrl = $config->Index->url; |
705 | $view->userUrl = str_replace( |
706 | ['localhost', '127.0.0.1'], |
707 | $this->getRequest()->getServer()->get('HTTP_HOST'), |
708 | $config->Index->url |
709 | ); |
710 | $view->core = $config->Index->default_core ?? 'biblio'; |
711 | $view->configFile = $configFile; |
712 | return $view; |
713 | } |
714 | |
715 | /** |
716 | * Check if Security configuration is set. |
717 | * |
718 | * @return array |
719 | */ |
720 | protected function checkSecurity() |
721 | { |
722 | return [ |
723 | 'title' => 'Security', |
724 | 'status' => $this->hasSecureDatabase(), |
725 | 'fix' => 'fixsecurity', |
726 | ]; |
727 | } |
728 | |
729 | /** |
730 | * Support method for fixsecurityAction(). Returns true if the configuration |
731 | * was modified, false otherwise. |
732 | * |
733 | * @param \Laminas\Config\Config $config Existing VuFind configuration |
734 | * @param ConfigWriter $writer Config writer |
735 | * |
736 | * @return bool |
737 | */ |
738 | protected function fixSecurityConfiguration($config, $writer) |
739 | { |
740 | $changed = false; |
741 | |
742 | if ( |
743 | !($config->Authentication->hash_passwords ?? false) |
744 | || !($config->Authentication->encrypt_ils_password ?? false) |
745 | ) { |
746 | $writer->set('Authentication', 'hash_passwords', true); |
747 | $writer->set('Authentication', 'encrypt_ils_password', true); |
748 | $changed = true; |
749 | } |
750 | // Only rewrite encryption key if we don't already have one: |
751 | if (empty($config->Authentication->ils_encryption_key)) { |
752 | [$algorithm, $key] = $this->getSecureAlgorithmAndKey(); |
753 | $writer->set('Authentication', 'ils_encryption_algo', $algorithm); |
754 | $writer->set('Authentication', 'ils_encryption_key', $key); |
755 | $changed = true; |
756 | } |
757 | |
758 | return $changed; |
759 | } |
760 | |
761 | /** |
762 | * Display repair instructions for Security problems. |
763 | * |
764 | * @return mixed |
765 | */ |
766 | public function fixsecurityAction() |
767 | { |
768 | // If the user doesn't want to proceed, abort now: |
769 | $userConfirmation = $this->params()->fromPost('fix-user-table', 'Unset'); |
770 | if ($userConfirmation == 'No') { |
771 | $msg = 'Security upgrade aborted.'; |
772 | $this->flashMessenger()->addMessage($msg, 'error'); |
773 | return $this->redirect()->toRoute('install-home'); |
774 | } |
775 | |
776 | // If we don't need to prompt the user, or if they confirmed, do the fix: |
777 | $userRows = $this->getDbService(UserServiceInterface::class)->getInsecureRows(); |
778 | $cardRows = $this->getDbService(UserCardServiceInterface::class)->getInsecureRows(); |
779 | if (count($userRows) + count($cardRows) == 0 || $userConfirmation == 'Yes') { |
780 | return $this->forwardTo('Install', 'performsecurityfix'); |
781 | } |
782 | |
783 | // If we got this far, we need to ask permission to proceed: |
784 | $view = $this->createViewModel(); |
785 | $view->confirmUserFix = true; |
786 | return $view; |
787 | } |
788 | |
789 | /** |
790 | * Perform fix for Security problems. |
791 | * |
792 | * @return mixed |
793 | */ |
794 | public function performsecurityfixAction() |
795 | { |
796 | // This can take a while -- don't time out! |
797 | set_time_limit(0); |
798 | |
799 | // First, set encryption/hashing to true, and set the key |
800 | $config = $this->getConfig(); |
801 | $configPath = $this->getForcedLocalConfigPath('config.ini'); |
802 | $writer = new ConfigWriter($configPath); |
803 | if ($this->fixSecurityConfiguration($config, $writer)) { |
804 | // Problem writing? Show the user an error: |
805 | if (!$writer->save()) { |
806 | return $this->forwardTo('Install', 'fixbasicconfig'); |
807 | } |
808 | |
809 | // Success? Redirect to this action in order to reload the configuration: |
810 | return $this->redirect()->toRoute('install-performsecurityfix'); |
811 | } |
812 | |
813 | // Now we want to loop through the database and update passwords (if |
814 | // necessary). |
815 | $ilsAuthenticator = $this->serviceLocator->get(\VuFind\Auth\ILSAuthenticator::class); |
816 | $userRows = $this->getDbService(UserServiceInterface::class)->getInsecureRows(); |
817 | if (count($userRows) > 0) { |
818 | $bcrypt = new Bcrypt(); |
819 | foreach ($userRows as $row) { |
820 | if ($row->password != '') { |
821 | $row->pass_hash = $bcrypt->create($row->password); |
822 | $row->password = ''; |
823 | } |
824 | if ($rawPassword = $row->getRawCatPassword()) { |
825 | $ilsAuthenticator->saveUserCatalogCredentials($row, $row->getCatUsername(), $rawPassword); |
826 | } else { |
827 | $row->save(); |
828 | } |
829 | } |
830 | $msg = count($userRows) . ' user row(s) encrypted.'; |
831 | $this->flashMessenger()->addMessage($msg, 'info'); |
832 | } |
833 | $cardService = $this->getDbService(UserCardServiceInterface::class); |
834 | $cardRows = $cardService->getInsecureRows(); |
835 | if (count($cardRows) > 0) { |
836 | foreach ($cardRows as $row) { |
837 | $row->setCatPassEnc($ilsAuthenticator->encrypt($row->getRawCatPassword())); |
838 | $row->setRawCatPassword(null); |
839 | $cardService->persistEntity($row); |
840 | } |
841 | $msg = count($cardRows) . ' user_card row(s) encrypted.'; |
842 | $this->flashMessenger()->addMessage($msg, 'info'); |
843 | } |
844 | return $this->redirect()->toRoute('install-home'); |
845 | } |
846 | |
847 | /** |
848 | * Check if SSL configuration is set properly. |
849 | * |
850 | * @return array |
851 | */ |
852 | public function checkSslCerts() |
853 | { |
854 | // Try to retrieve an SSL URL; if we're misconfigured, it will fail. |
855 | try { |
856 | $this->serviceLocator->get(\VuFindHttp\HttpService::class) |
857 | ->get('https://google.com'); |
858 | $status = true; |
859 | } catch (\VuFindHttp\Exception\RuntimeException $e) { |
860 | // Any exception means we have a problem! |
861 | $status = false; |
862 | } |
863 | |
864 | return [ |
865 | 'title' => 'SSL', 'status' => $status, 'fix' => 'fixsslcerts', |
866 | ]; |
867 | } |
868 | |
869 | /** |
870 | * Display repair instructions for SSL certificate problems. |
871 | * |
872 | * @return mixed |
873 | */ |
874 | public function fixsslcertsAction() |
875 | { |
876 | // Bail out if we've fixed the problem: |
877 | $result = $this->checkSslCerts(); |
878 | if ($result['status'] == true) { |
879 | $this->flashMessenger()->addMessage('SSL configuration fixed.', 'info'); |
880 | return $this->redirect()->toRoute('install-home'); |
881 | } |
882 | |
883 | // Find out which test to try next: |
884 | $try = $this->params()->fromQuery('try', 0); |
885 | |
886 | // Configurations to test: |
887 | $configsToTest = [ |
888 | ['sslcapath' => '/etc/ssl/certs'], |
889 | ['sslcafile' => '/etc/pki/tls/cert.pem'], |
890 | [], // reset configuration as last attempt |
891 | ]; |
892 | if (isset($configsToTest[$try])) { |
893 | return $this->testSslCertConfig($configsToTest[$try], $try); |
894 | } |
895 | |
896 | // If we got this far, we can't fix this automatically and must display |
897 | // a message. |
898 | $view = $this->createViewModel(); |
899 | return $view; |
900 | } |
901 | |
902 | /** |
903 | * Try switching to a specific SSL configuration. |
904 | * |
905 | * @param array $config Setting(s) to add to [Http] section of config.ini. |
906 | * @param int $try Which config index are we trying right now? |
907 | * |
908 | * @return \Laminas\Http\Response |
909 | */ |
910 | protected function testSslCertConfig($config, $try) |
911 | { |
912 | $file = $this->getForcedLocalConfigPath('config.ini'); |
913 | $writer = new ConfigWriter($file); |
914 | // Reset old settings |
915 | $writer->clear('Http', 'sslcapath'); |
916 | $writer->clear('Http', 'sslcafile'); |
917 | // Load new settings |
918 | foreach ($config as $setting => $value) { |
919 | $writer->set('Http', $setting, $value); |
920 | } |
921 | if (!$writer->save()) { |
922 | throw new \Exception('Cannot write config to disk.'); |
923 | } |
924 | |
925 | // Jump back to fix action so we can check if it worked (and attempt |
926 | // the next config by incrementing the $try variable, if necessary): |
927 | return $this->redirect()->toRoute( |
928 | 'install-fixsslcerts', |
929 | [], |
930 | ['query' => ['try' => $try + 1]] |
931 | ); |
932 | } |
933 | |
934 | /** |
935 | * Disable auto-configuration. |
936 | * |
937 | * @return mixed |
938 | */ |
939 | public function doneAction() |
940 | { |
941 | $config = $this->getForcedLocalConfigPath('config.ini'); |
942 | $writer = new ConfigWriter($config); |
943 | $writer->set('System', 'autoConfigure', 0); |
944 | if (!$writer->save()) { |
945 | return $this->forwardTo('Install', 'fixbasicconfig'); |
946 | } |
947 | return $this->createViewModel(['configDir' => dirname($config)]); |
948 | } |
949 | |
950 | /** |
951 | * Display summary of installation status |
952 | * |
953 | * @return mixed |
954 | */ |
955 | public function homeAction() |
956 | { |
957 | // Perform all checks (based on naming convention): |
958 | $methods = get_class_methods($this); |
959 | $checks = []; |
960 | foreach ($methods as $method) { |
961 | if (str_starts_with($method, 'check')) { |
962 | $checks[] = $this->$method(); |
963 | } |
964 | } |
965 | return $this->createViewModel(['checks' => $checks]); |
966 | } |
967 | |
968 | /** |
969 | * Get minimal PHP version required for VuFind to run. |
970 | * |
971 | * @return string |
972 | */ |
973 | protected function getMinimalPhpVersion(): string |
974 | { |
975 | $composer = $this->getComposerJson(); |
976 | if (empty($composer)) { |
977 | throw new \Exception('Cannot find composer.json'); |
978 | } |
979 | $rawVersion = $composer['require']['php'] |
980 | ?? $composer['config']['platform']['php'] |
981 | ?? ''; |
982 | $version = preg_replace('/[^0-9. ]/', '', $rawVersion); |
983 | if (empty($version) || !preg_match('/^[0-9]/', $version)) { |
984 | throw new \Exception('Cannot parse PHP version from composer.json'); |
985 | } |
986 | $versionParts = preg_split('/[. ]/', $version); |
987 | $versionParts = array_pad($versionParts, 3, '0'); |
988 | return sprintf('%d.%d.%d', ...$versionParts); |
989 | } |
990 | |
991 | /** |
992 | * Get minimal PHP version ID required for VuFind to run. |
993 | * |
994 | * @return int |
995 | */ |
996 | protected function getMinimalPhpVersionId(): int |
997 | { |
998 | $version = explode('.', $this->getMinimalPhpVersion()); |
999 | return $version[0] * 10000 + $version[1] * 100 + $version[2]; |
1000 | } |
1001 | |
1002 | /** |
1003 | * Get composer.json data as array |
1004 | * |
1005 | * @return array |
1006 | */ |
1007 | protected function getComposerJson(): array |
1008 | { |
1009 | try { |
1010 | $composerJsonFileName = APPLICATION_PATH . '/composer.json'; |
1011 | if (file_exists($composerJsonFileName)) { |
1012 | return json_decode(file_get_contents($composerJsonFileName), true); |
1013 | } |
1014 | } catch (\Throwable $exception) { |
1015 | return []; |
1016 | } |
1017 | return []; |
1018 | } |
1019 | } |