diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index b848b2b677d4..9cfa4219c873 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -173,7 +173,6 @@ 'VuFind\Controller\HierarchyController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\HoldsController' => 'VuFind\Controller\HoldsControllerFactory', 'VuFind\Controller\IndexController' => 'VuFind\Controller\IndexControllerFactory', - 'VuFind\Controller\InstallController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\LibGuidesController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\LibGuidesAZController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\LibraryCardsController' => 'VuFind\Controller\AbstractBaseFactory', @@ -214,8 +213,6 @@ 'holds' => 'VuFind\Controller\HoldsController', 'Index' => 'VuFind\Controller\IndexController', 'index' => 'VuFind\Controller\IndexController', - 'Install' => 'VuFind\Controller\InstallController', - 'install' => 'VuFind\Controller\InstallController', 'LibGuides' => 'VuFind\Controller\LibGuidesController', 'libguides' => 'VuFind\Controller\LibGuidesController', 'LibGuidesAZ' => 'VuFind\Controller\LibGuidesAZController', diff --git a/module/VuFind/src/VuFind/Action/Install/AbstractInstallAction.php b/module/VuFind/src/VuFind/Action/Install/AbstractInstallAction.php new file mode 100644 index 000000000000..1bec5d4b65a4 --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/AbstractInstallAction.php @@ -0,0 +1,436 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use VuFind\Action\AbstractTemplateRenderingAction; +use VuFind\Cache\Manager as CacheManager; +use VuFind\Config\ConfigManagerInterface; +use VuFind\Config\PathResolver; +use VuFind\Db\Service\PluginManager as DbServicePluginManager; +use VuFind\Db\Service\TagServiceInterface; +use VuFind\Db\Service\UserCardServiceInterface; +use VuFind\Db\Service\UserServiceInterface; +use VuFind\Http\ServerUrlHelper; +use VuFind\ILS\Connection; +use VuFind\ServiceManager\Factory\Autowire; +use VuFindHttp\HttpService; +use VuFindSearch\Command\RetrieveCommand; +use VuFindSearch\Service as SearchService; + +use function count; +use function defined; +use function function_exists; +use function is_callable; +use function sprintf; + +/** + * Abstract base class for install actions. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +abstract class AbstractInstallAction extends AbstractTemplateRenderingAction +{ + /** + * Constructor. + * + * @param CacheManager $cacheManager Cache manager + * @param Connection $ilsConnection ILS connection + * @param SearchService $searchService Search service + * @param PathResolver $pathResolver Path resolver + * @param ConfigManagerInterface $configManager Config manager + * @param ServerUrlHelper $serverUrlHelper Server URL helper + * @param HttpService $httpService HTTP service + * @param TagServiceInterface $tagService Tags database service + * @param UserServiceInterface $userService User database service + * @param UserCardServiceInterface $userCardService User card database service + * @param array $config VuFind configuration + */ + public function __construct( + protected CacheManager $cacheManager, + protected Connection $ilsConnection, + protected SearchService $searchService, + protected PathResolver $pathResolver, + protected ConfigManagerInterface $configManager, + protected ServerUrlHelper $serverUrlHelper, + protected HttpService $httpService, + #[Autowire(container: DbServicePluginManager::class)] + protected TagServiceInterface $tagService, + #[Autowire(container: DbServicePluginManager::class)] + protected UserServiceInterface $userService, + #[Autowire(container: DbServicePluginManager::class)] + protected UserCardServiceInterface $userCardService, + #[Autowire(config: 'config')] + protected array $config, + ) { + parent::__construct(); + } + + /** + * Check that everything is in order for the action to be executed. + * + * This method is executed in the very beginning of the action invocation before any permission checks etc. + * It is meant for technical checks such as route-based configuration being correctly applied. + * It may return a suitable response or throw an exception if there are issues. + * + * @param ServerRequestInterface $request Request + * @param ResponseInterface $response Response + * + * @return ?ResponseInterface + */ + protected function validateActionConfig( + ServerRequestInterface $request, + ResponseInterface $response + ): ?ResponseInterface { + // If auto-configuration is disabled, prevent any other action from being accessed: + if (!($this->config['System']['autoConfigure'] ?? false)) { + return $this->renderTemplate($request, $response, template: 'install/disabled'); + } + return null; + } + + /** + * Get path to base configuration file. + * + * @param string $configName Configuration name + * + * @return string + */ + protected function getBaseConfigFilePath(string $configName): string + { + return $this->pathResolver + ->getBaseConfigLocation($configName) + ->getPath(); + } + + /** + * Get path to local configuration file (even if it does not yet exist). + * + * @param string $configName Configuration name + * + * @return string + */ + protected function getForcedLocalConfigPath(string $configName): string + { + return $this->pathResolver + ->getForcedLocalConfigLocation($configName) + ->getPath(); + } + + /** + * Copy the basic configuration file into position and report success or failure. + * + * @return bool + */ + protected function installBasicConfig(): bool + { + $config = $this->getForcedLocalConfigPath('config'); + if (!file_exists($config)) { + // Suppress errors so we don't cause a fatal error if copy is disallowed. + return @copy($this->getBaseConfigFilePath('config'), $config); + } + return true; // report success if file already exists + } + + /** + * Fix security configuration. + * + * @param array $config Existing VuFind configuration + * + * @return array Fixed configuration + */ + protected function getFixedSecurityConfiguration(array $config): array + { + $fixedConfig = []; + + if ( + !($config['Authentication']['hash_passwords'] ?? false) + || !($config['Authentication']['encrypt_ils_password'] ?? false) + ) { + $fixedConfig['Authentication']['hash_passwords'] = true; + $fixedConfig['Authentication']['encrypt_ils_password'] = true; + } + // Only rewrite encryption key if we don't already have one: + if (empty($config['Authentication']['ils_encryption_key'])) { + [$algorithm, $key] = $this->getSecureAlgorithmAndKey(); + $fixedConfig['Authentication']['ils_encryption_algo'] = $algorithm; + $fixedConfig['Authentication']['ils_encryption_key'] = $key; + } + + return $fixedConfig; + } + + /** + * Change configuration. + * + * @param string $configName Config name + * @param array $config Config to change + * + * @return void + */ + protected function changeConfig(string $configName, array $config): void + { + $currentConfig = $this->configManager->getConfigArray($configName); + foreach ($config as $section => $sectionConfig) { + foreach ($sectionConfig as $setting => $value) { + if ($value === null) { + unset($currentConfig[$section][$setting]); + } else { + $currentConfig[$section][$setting] = $value; + } + } + } + $configLocation = $this->pathResolver->getForcedLocalConfigLocation($configName); + $baseConfigLocation = file_exists($configLocation->getPath()) + ? $configLocation + : $this->pathResolver->getBaseConfigLocation($configName); + $this->configManager->writeConfig($configLocation, $currentConfig, $baseConfigLocation); + } + + /** + * Get an array containing an ILS encryption algorithm and a randomly generated + * key. + * + * @return array + */ + protected function getSecureAlgorithmAndKey(): array + { + // Make example hash for AES + $alpha = 'abcdefghijklmnopqrstuvwxyz'; + $chars = str_repeat($alpha . strtoupper($alpha) . '0123456789,.@#%^&*', 4); + return ['aes', substr(str_shuffle($chars), 0, 32)]; + } + + /** + * Does the instance have secure database configuration and contents? + * + * @return bool + */ + protected function hasSecureDatabase(): bool + { + // Are configuration settings missing? + $status = ($this->config['Authentication']['hash_passwords'] ?? false) + && ($this->config['Authentication']['encrypt_ils_password'] ?? false); + + // If we're correctly configured, check that the data in the database is ok: + if ($status) { + try { + $userRows = $this->userService->getInsecureRows(); + $cardRows = $this->userCardService->getInsecureRows(); + $status = count($userRows) + count($cardRows) === 0; + } catch (\Exception $e) { + // Any exception means we have a problem! + $status = false; + } + } + + return $status; + } + + /** + * Support method for check/fix dependencies code -- do we have a new enough + * version of PHP? + * + * @return bool + */ + protected function phpVersionIsNewEnough(): bool + { + // PHP_VERSION_ID was introduced in 5.2.7; if it's missing, we have a problem. + if (!defined('PHP_VERSION_ID')) { + return false; + } + + // We need at least PHP version as defined in composer.json file: + return PHP_VERSION_ID >= $this->getMinimalPhpVersionId(); + } + + /** + * Get minimal PHP version required for VuFind to run. + * + * @return string + */ + protected function getMinimalPhpVersion(): string + { + $composer = $this->getComposerJson(); + if (empty($composer)) { + throw new \Exception('Cannot find composer.json'); + } + $rawVersion = $composer['require']['php'] + ?? $composer['config']['platform']['php'] + ?? ''; + $version = preg_replace('/[^0-9. ]/', '', $rawVersion); + if (empty($version) || !preg_match('/^[0-9]/', $version)) { + throw new \Exception('Cannot parse PHP version from composer.json'); + } + $versionParts = preg_split('/[. ]/', $version); + $versionParts = array_pad($versionParts, 3, '0'); + return sprintf('%d.%d.%d', ...$versionParts); + } + + /** + * Get minimal PHP version ID required for VuFind to run. + * + * @return int + */ + protected function getMinimalPhpVersionId(): int + { + $version = explode('.', $this->getMinimalPhpVersion()); + return $version[0] * 10000 + $version[1] * 100 + $version[2]; + } + + /** + * Get composer.json data as array. + * + * @return array + */ + protected function getComposerJson(): array + { + try { + $composerJsonFileName = APPLICATION_PATH . '/composer.json'; + if (file_exists($composerJsonFileName)) { + return json_decode(file_get_contents($composerJsonFileName), true); + } + } catch (\Throwable $exception) { + return []; + } + return []; + } + + /** + * Try to establish a secure connection using HTTPS. + * + * @return bool + */ + protected function testSslConnection(): bool + { + // Try to retrieve an SSL URL; if we're misconfigured, it will fail. + try { + $this->httpService->get('https://vufind.org'); + return true; + } catch (\VuFindHttp\Exception\RuntimeException $e) { + // Any exception means we have a problem! + return false; + } + } + + /** + * Support method to test the search service. + * + * @return void + * @throws \Exception + */ + protected function testSearchService(): void + { + // Try to retrieve an arbitrary ID -- this will fail if Solr is down: + $command = new RetrieveCommand('Solr', '1'); + $this->searchService->invoke($command)->getResult(); + } + + /** + * Check if the Solr index is working. + * + * @return array + */ + protected function checkMethodSolr(): array + { + try { + $this->testSearchService(); + $status = true; + } catch (\Exception $e) { + $status = false; + } + return [ + 'title' => 'Solr', + 'status' => $status, + 'fix' => 'fixsolr', + ]; + } + + /** + * Get a list of missing extensions required for proper operation. + * + * @return array + */ + protected function getMissingExtensions(): array + { + $missingExtensions = []; + // Is the mbstring library missing? + if (!function_exists('mb_substr')) { + $missingExtensions[] = 'mbstring'; + } + + // Is the GD library missing? + if (!is_callable('imagecreatefromstring')) { + $missingExtensions[] = 'GD'; + } + + // Is the openssl library missing? + if (!function_exists('openssl_encrypt')) { + $missingExtensions[] = 'openssl'; + } + + // Is the XSL library missing? + if (!class_exists('XSLTProcessor')) { + $missingExtensions[] = 'XSL'; + } + + // Is the sodium extension missing? + if (!defined('SODIUM_LIBRARY_VERSION')) { + $missingExtensions[] = 'sodium'; + } + + return $missingExtensions; + } + + /** + * Get effective user name for the current process. + * + * @return ?string + */ + protected function getProcessUserName(): ?string + { + if ( + function_exists('posix_getpwuid') + && function_exists('posix_geteuid') + && ($processUser = posix_getpwuid(posix_geteuid())) + ) { + return $processUser['name']; + } + return null; + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/DoneAction.php b/module/VuFind/src/VuFind/Action/Install/DoneAction.php new file mode 100644 index 000000000000..f97d61131db1 --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/DoneAction.php @@ -0,0 +1,78 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use VuFind\ActionHelper\ForwardHelper; + +use function dirname; + +/** + * Install done action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class DoneAction extends AbstractInstallAction +{ + /** + * Install complete -- disable auto-configuration. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + try { + $this->changeConfig( + 'config', + ['System' => ['autoConfigure' => 0]] + ); + } catch (\Exception $e) { + return $this->getHelper(ForwardHelper::class)->forwardTo($request, $response, 'Install/FixBasicConfig'); + } + return $this->renderTemplate( + $request, + $response, + ['configDir' => dirname($this->getForcedLocalConfigPath('config'))] + ); + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/FixBasicConfigAction.php b/module/VuFind/src/VuFind/Action/Install/FixBasicConfigAction.php new file mode 100644 index 000000000000..7e6a62388f30 --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/FixBasicConfigAction.php @@ -0,0 +1,108 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use VuFind\ActionHelper\RedirectHelper; + +use function dirname; + +/** + * Install "fix basic configuration" action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class FixBasicConfigAction extends AbstractInstallAction +{ + /** + * Fix basic configuration. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + $templateParams = []; + try { + if (!$this->installBasicConfig()) { + throw new \Exception('Cannot copy file into position.'); + } + // Choose secure defaults when creating initial config.ini: + $fixedConfig = $this->getFixedSecurityConfiguration($this->config); + // Set appropriate URLs: + $path = $this->routeHelper->getUrlFromRoute('home'); + $fixedConfig['Site']['url'] = rtrim($this->serverUrlHelper->getUrlForPath($path), '/'); + if ($solrUrl = $this->getSolrUrlFromImportConfig()) { + $fixedConfig['Index']['url'] = $solrUrl; + } + $this->changeConfig('config', $fixedConfig); + return $this->getHelper(RedirectHelper::class)->redirectToRoute($response, 'install-home'); + } catch (\Exception $e) { + $templateParams['configDir'] = dirname($this->getForcedLocalConfigPath('config')); + $templateParams['errorMessage'] = $e->getMessage(); + $templateParams['runningUser'] = $this->getProcessUserName(); + } + return $this->renderTemplate($request, $response, $templateParams); + } + + /** + * Extract the Solr base URL from the SolrMarc configuration file, + * so a custom Solr port configured in install.php can be applied to + * the initial config.ini file. + * + * Return null if no custom Solr URL can be found. + * + * @return ?string + */ + protected function getSolrUrlFromImportConfig(): ?string + { + $importConfig = $this->pathResolver->getLocalConfigPath('import.properties', 'import'); + if (file_exists($importConfig)) { + $props = file_get_contents($importConfig); + preg_match('|solr.hosturl\s*=\s*(https?://\w+:\d+/\w+)|', $props, $matches); + if (!empty($matches[1])) { + return $matches[1]; + } + } + return null; + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/FixCacheAction.php b/module/VuFind/src/VuFind/Action/Install/FixCacheAction.php new file mode 100644 index 000000000000..881f18c47559 --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/FixCacheAction.php @@ -0,0 +1,67 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Install "fix cache" action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class FixCacheAction extends AbstractInstallAction +{ + /** + * Display instructions for fixing cache issues. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + $templateParams = [ + 'cacheDir' => $this->cacheManager->getCacheDir(), + 'runningUser' => $this->getProcessUserName(), + ]; + return $this->renderTemplate($request, $response, $templateParams); + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/FixDatabaseAction.php b/module/VuFind/src/VuFind/Action/Install/FixDatabaseAction.php new file mode 100644 index 000000000000..34109fc5896f --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/FixDatabaseAction.php @@ -0,0 +1,194 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use VuFind\ActionHelper\FlashMessagesHelper; +use VuFind\ActionHelper\FormHelper; +use VuFind\ActionHelper\ForwardHelper; +use VuFind\ActionHelper\RedirectHelper; +use VuFind\Cache\Manager as CacheManager; +use VuFind\Config\ConfigManagerInterface; +use VuFind\Config\PathResolver; +use VuFind\Db\DbBuilder; +use VuFind\Db\Service\PluginManager as DbServicePluginManager; +use VuFind\Db\Service\TagServiceInterface; +use VuFind\Db\Service\UserCardServiceInterface; +use VuFind\Db\Service\UserServiceInterface; +use VuFind\Http\ServerUrlHelper; +use VuFind\ILS\Connection; +use VuFind\ServiceManager\Factory\Autowire; +use VuFindHttp\HttpService; +use VuFindSearch\Service as SearchService; + +/** + * Install "fix database" action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class FixDatabaseAction extends AbstractInstallAction +{ + /** + * Constructor. + * + * @param CacheManager $cacheManager Cache manager + * @param Connection $ilsConnection ILS connection + * @param SearchService $searchService Search service + * @param PathResolver $pathResolver Path resolver + * @param ConfigManagerInterface $configManager Config manager + * @param ServerUrlHelper $serverUrlHelper Server URL helper + * @param HttpService $httpService HTTP service + * @param TagServiceInterface $tagService Tags database service + * @param UserServiceInterface $userService User database service + * @param UserCardServiceInterface $userCardService User card database service + * @param array $config VuFind configuration + * @param DbBuilder $dbBuilder Database builder + */ + public function __construct( + CacheManager $cacheManager, + Connection $ilsConnection, + SearchService $searchService, + PathResolver $pathResolver, + ConfigManagerInterface $configManager, + ServerUrlHelper $serverUrlHelper, + HttpService $httpService, + #[Autowire(container: DbServicePluginManager::class)] + TagServiceInterface $tagService, + #[Autowire(container: DbServicePluginManager::class)] + UserServiceInterface $userService, + #[Autowire(container: DbServicePluginManager::class)] + UserCardServiceInterface $userCardService, + #[Autowire(config: 'config')] + array $config, + protected DbBuilder $dbBuilder, + ) { + parent::__construct( + $cacheManager, + $ilsConnection, + $searchService, + $pathResolver, + $configManager, + $serverUrlHelper, + $httpService, + $tagService, + $userService, + $userCardService, + $config + ); + } + + /** + * Display repair instructions for database problems or fix them directly. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + $dbSettings = [ + 'dbname' => $this->getPostParam('dbname', 'vufind'), + 'dbuser' => $this->getPostParam('dbuser', 'vufind'), + 'dbhost' => $this->getPostParam('dbhost', 'localhost'), + 'vufindhost' => $this->getPostParam('vufindhost', 'localhost'), + 'dbrootuser' => $this->getPostParam('dbrootuser', 'root'), + 'driver' => $this->getPostParam('driver', 'mysql'), + ]; + + $skip = $this->getPostParam('printsql') == 'Skip'; + + $flashMessagesHelper = $this->getHelper(FlashMessagesHelper::class); + if (!preg_match('/^\w*$/', $dbSettings['dbname'])) { + $flashMessagesHelper->addErrorMessage('Database name must be alphanumeric.'); + } elseif (!preg_match('/^\w*$/', $dbSettings['dbuser'])) { + $flashMessagesHelper->addErrorMessage('Database user must be alphanumeric.'); + } elseif ($skip || $this->getHelper(FormHelper::class)->formWasSubmitted($request)) { + $newpass = $this->getPostParam('dbpass'); + $newpassConf = $this->getPostParam('dbpassconfirm'); + if ((empty($newpass) || empty($newpassConf))) { + $flashMessagesHelper->addErrorMessage('Password fields must not be blank.'); + } elseif ($newpass != $newpassConf) { + $flashMessagesHelper->addErrorMessage('Password fields must match.'); + } else { + // Connect to database: + try { + $rootpass = $this->getPostParam('dbrootpass'); + $omnisql = $this->dbBuilder->build( + $dbSettings['dbname'], + $dbSettings['dbuser'], + $newpass, + $dbSettings['driver'], + $dbSettings['dbhost'], + $dbSettings['vufindhost'], + $dbSettings['dbrootuser'], + $rootpass, + $skip + ); + if ($skip) { + return $this->renderTemplate( + $request, + $response, + ['sql' => $omnisql], + 'install/showsql' + ); + } + // If we made it this far, we can update the config file and forward back to the home action! + $string = $dbSettings['driver'] . '://' . $dbSettings['dbuser'] . ':' . $newpass . '@' + . $dbSettings['dbhost'] . '/' . $dbSettings['dbname']; + try { + $this->changeConfig( + 'config', + ['Database' => ['database' => $string]] + ); + } catch (\Exception $e) { + return $this->getHelper(ForwardHelper::class) + ->forwardTo($request, $response, 'Install/FixBasicConfig'); + } + + return $this->getHelper(RedirectHelper::class)->redirectToRoute($response, 'install-home'); + } catch (\Exception $e) { + $flashMessagesHelper->addErrorMessage($e->getMessage()); + } + } + } + return $this->renderTemplate($request, $response, $dbSettings); + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/FixDependenciesAction.php b/module/VuFind/src/VuFind/Action/Install/FixDependenciesAction.php new file mode 100644 index 000000000000..a97f52284989 --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/FixDependenciesAction.php @@ -0,0 +1,79 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use VuFind\ActionHelper\FlashMessagesHelper; + +/** + * Install "fix dependencies" action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class FixDependenciesAction extends AbstractInstallAction +{ + /** + * Display instructions for fixing dependency problems. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + $problems = 0; + + // Is our version new enough? + if (!$this->phpVersionIsNewEnough()) { + $msg = 'VuFind® requires PHP version ' . $this->getMinimalPhpVersion() + . ' or newer; you are running ' . phpversion() . '. Please upgrade.'; + $this->getHelper(FlashMessagesHelper::class)->addErrorMessage($msg); + $problems++; + } + + // Are we missing extensions? + if ($missingExtensions = $this->getMissingExtensions()) { + ++$problems; + } + + return $this->renderTemplate($request, $response, compact('problems', 'missingExtensions')); + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/FixIlsAction.php b/module/VuFind/src/VuFind/Action/Install/FixIlsAction.php new file mode 100644 index 000000000000..03ed8d1b2345 --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/FixIlsAction.php @@ -0,0 +1,118 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use VuFind\ActionHelper\ForwardHelper; +use VuFind\ActionHelper\RedirectHelper; + +use function in_array; + +/** + * Install "fix ILS" action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class FixIlsAction extends AbstractInstallAction +{ + /** + * Display repair instructions for ILS problems. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + // Process incoming parameter -- user may have selected a new driver: + $newDriver = $this->getPostParam('driver'); + if (!empty($newDriver)) { + try { + $this->changeConfig( + 'config', + ['Catalog' => ['driver' => $newDriver]] + ); + } catch (\Exception $e) { + return $this->getHelper(ForwardHelper::class)->forwardTo($request, $response, 'Install/FixBasicConfig'); + } + // Copy configuration, if applicable: + $ilsIni = $this->getBaseConfigFilePath($newDriver); + $localIlsIni = $this->getForcedLocalConfigPath($newDriver); + if (file_exists($ilsIni) && !file_exists($localIlsIni)) { + if (!copy($ilsIni, $localIlsIni)) { + return $this->getHelper(ForwardHelper::class) + ->forwardTo($request, $response, 'Install/FixBasicConfig'); + } + } + return $this->getHelper(RedirectHelper::class)->redirectToRoute($response, 'install-home'); + } + + // If we got this far, check whether we have an error with a real driver or if we need to warn the user that + // they have selected a fake driver: + $templateParams = []; + $currentDriver = $this->config['Catalog']['driver'] ?? ''; + if (in_array($currentDriver, ['Sample', 'Demo'])) { + $templateParams['demo'] = true; + // Get a list of available drivers: + $dir = opendir(APPLICATION_PATH . '/module/VuFind/src/VuFind/ILS/Driver'); + $drivers = []; + $excludeList = [ + 'Sample.php', 'Demo.php', 'DriverInterface.php', 'PluginManager.php', + ]; + while ($line = readdir($dir)) { + if ( + stristr($line, '.php') && !in_array($line, $excludeList) + && !str_starts_with($line, 'Abstract') + && !str_ends_with($line, 'Factory.php') + && !str_ends_with($line, 'Trait.php') + ) { + $drivers[] = str_replace('.php', '', $line); + } + } + closedir($dir); + sort($drivers); + $templateParams['drivers'] = $drivers; + } else { + $templateParams['configPath'] = $this->getForcedLocalConfigPath($currentDriver); + } + return $this->renderTemplate($request, $response, $templateParams); + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/FixSecurityAction.php b/module/VuFind/src/VuFind/Action/Install/FixSecurityAction.php new file mode 100644 index 000000000000..b1781c8c67a4 --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/FixSecurityAction.php @@ -0,0 +1,92 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use VuFind\ActionHelper\FlashMessagesHelper; +use VuFind\ActionHelper\ForwardHelper; +use VuFind\ActionHelper\RedirectHelper; + +use function count; + +/** + * Install "fix security" action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class FixSecurityAction extends AbstractInstallAction +{ + /** + * Display repair instructions for security problems. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + $flashMessagesHelper = $this->getHelper(FlashMessagesHelper::class); + $redirectHelper = $this->getHelper(RedirectHelper::class); + // If the user doesn't want to proceed, abort now: + $userConfirmation = $this->getPostParam('fix-user-table'); + if ($userConfirmation === 'No') { + $msg = 'Security upgrade aborted.'; + $flashMessagesHelper->addErrorMessage($msg); + return $redirectHelper->redirectToRoute($response, 'install-home'); + } + + // If we don't need to prompt the user, or if they confirmed, do the fix: + try { + $userRows = $this->userService->getInsecureRows(); + $cardRows = $this->userCardService->getInsecureRows(); + } catch (\Throwable $e) { + $flashMessagesHelper + ->addErrorMessage('Cannot connect to database; please configure database before fixing security.'); + return $redirectHelper->redirectToRoute($response, 'install-home'); + } + if (count($userRows) + count($cardRows) == 0 || $userConfirmation === 'Yes') { + return $this->getHelper(ForwardHelper::class)->forwardTo($request, $response, 'Install/performsecurityfix'); + } + + // If we got this far, we need to ask permission to proceed: + return $this->renderTemplate($request, $response, ['confirmUserFix' => true]); + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/FixSolrAction.php b/module/VuFind/src/VuFind/Action/Install/FixSolrAction.php new file mode 100644 index 000000000000..f415f8a513a7 --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/FixSolrAction.php @@ -0,0 +1,98 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use VuFind\ActionHelper\ForwardHelper; +use VuFind\ActionHelper\RedirectHelper; + +/** + * Install "fix Solr" action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class FixSolrAction extends AbstractInstallAction +{ + /** + * Display repair instructions for Solr problems. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + // In Windows, localhost may fail -- see if switching to 127.0.0.1 helps: + $indexUrl = $this->config['Index']['url'] ?? ''; + if (stristr($indexUrl, 'localhost')) { + $newUrl = str_replace('localhost', '127.0.0.1', $indexUrl); + try { + $this->testSearchService(); + try { + $this->changeConfig( + 'config', + ['Index' => ['url' => $newUrl]] + ); + } catch (\Exception $e) { + return $this->getHelper(ForwardHelper::class) + ->forwardTo($request, $response, 'Install/fixbasicconfig'); + } + return $this->getHelper(RedirectHelper::class)->redirectToRoute($response, 'install-home'); + } catch (\Exception $e) { + // Didn't work! + } + } + + // If we got this far, the automatic fix didn't work, so let's just assign some variables to use in offering + // troubleshooting advice: + $templateParams = [ + 'rawUrl' => $indexUrl, + 'userUrl' => str_replace( + ['localhost', '127.0.0.1'], + $request->getServerParams()['HTTP_HOST'] ?? '', + $indexUrl + ), + 'core' => $this->config['Index']['default_core'] ?? 'biblio', + 'configFile' => $this->getForcedLocalConfigPath('config'), + ]; + return $this->renderTemplate($request, $response, $templateParams); + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/FixSslCertsAction.php b/module/VuFind/src/VuFind/Action/Install/FixSslCertsAction.php new file mode 100644 index 000000000000..41f9231274ea --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/FixSslCertsAction.php @@ -0,0 +1,117 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use VuFind\ActionHelper\FlashMessagesHelper; +use VuFind\ActionHelper\RedirectHelper; + +/** + * Install "fix SSL certificates" action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class FixSslCertsAction extends AbstractInstallAction +{ + /** + * Display repair instructions for SSL certificate problems. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + // Bail out if we've fixed the problem: + if ($result = $this->testSslConnection()) { + $this->getHelper(FlashMessagesHelper::class)->addInfoMessage('SSL configuration fixed.'); + return $this->getHelper(RedirectHelper::class)->redirectToRoute($response, 'install-home'); + } + + // Find out which test to try next: + $try = $this->getQueryParam('try', 0); + + // Configurations to test: + $configsToTest = [ + ['sslcapath' => '/etc/ssl/certs'], + ['sslcafile' => '/etc/pki/tls/cert.pem'], + [], // reset configuration as last attempt + ]; + if (isset($configsToTest[$try])) { + $this->updateSslCertConfig($configsToTest[$try], $try); + + // Jump back to fix action so we can check if it worked (and attempt the next config by incrementing the + // $try variable, if necessary): + return $this->getHelper(RedirectHelper::class)->redirectToRoute( + $response, + 'install-fixsslcerts', + queryParams: ['try' => $try + 1] + ); + } + + // If we got this far, we can't fix this automatically and must display a message. + return $this->renderTemplate($request, $response); + } + + /** + * Switch to a specific SSL configuration. + * + * @param array $config Setting(s) to add to [Http] section of config.ini. + * @param int $try Which config index are we trying right now? + * + * @return void + */ + protected function updateSslCertConfig($config, $try): void + { + // Reset old settings + $fixedConfig = [ + 'Http' => [ + 'sslcapath' => null, + 'sslcafile' => null, + ], + ]; + // Load new settings + foreach ($config as $setting => $value) { + $fixedConfig['Http'][$setting] = $value; + } + $this->changeConfig('config', $fixedConfig); + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/HomeAction.php b/module/VuFind/src/VuFind/Action/Install/HomeAction.php new file mode 100644 index 000000000000..cddd1d6a7ba1 --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/HomeAction.php @@ -0,0 +1,204 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +use function in_array; + +/** + * Install home action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class HomeAction extends AbstractInstallAction +{ + /** + * Display summary of installation status. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + // Perform all checks (based on naming convention): + $methods = get_class_methods($this); + $checks = []; + foreach ($methods as $method) { + if (str_starts_with($method, 'checkMethod')) { + $checks[] = $this->$method(); + } + } + return $this->renderTemplate($request, $response, compact('checks')); + } + + /** + * Check if basic configuration is taken care of. + * + * @return array + */ + protected function checkMethodBasicConfig(): array + { + // Initialize status based on existence of config file... + $status = $this->installBasicConfig(); + + // See if the URL setting remains at the default (unless we already + // know we've failed): + if ($status) { + if (stristr($this->config['Site']['url'], 'myuniversity.edu')) { + $status = false; + } + } + + return [ + 'title' => 'Basic Configuration', + 'status' => $status, + 'fix' => 'fixbasicconfig', + ]; + } + + /** + * Check if the cache directory is writable. + * + * @return array + */ + protected function checkMethodCache(): array + { + return [ + 'title' => 'Cache', + 'status' => !$this->cacheManager->hasDirectoryCreationError(), + 'fix' => 'fixcache', + ]; + } + + /** + * Check if the database is accessible. + * + * @return array + */ + protected function checkMethodDatabase(): array + { + try { + // Try to read the tags table just to see if we can connect to the DB: + $this->tagService->getTagsByText('test'); + $status = true; + } catch (\Exception $e) { + $status = false; + } + return [ + 'title' => 'Database', + 'status' => $status, + 'fix' => 'fixdatabase', + ]; + } + + /** + * Check for missing dependencies. + * + * @return array + */ + protected function checkMethodDependencies(): array + { + return [ + 'title' => 'Dependencies', + 'status' => $this->phpVersionIsNewEnough() && !$this->getMissingExtensions(), + 'fix' => 'fixdependencies', + ]; + } + + /** + * Check if ILS configuration is appropriate. + * + * @return array + */ + protected function checkMethodILS(): array + { + $driver = $this->config['Catalog']['driver'] ?? ''; + if (in_array($driver, ['Sample', 'Demo'])) { + $status = false; + } else { + try { + $status = 'ils-offline' !== $this->ilsConnection->getOfflineMode(true) || 'NoILS' === $driver; + } catch (\Exception $e) { + $status = false; + } + } + return [ + 'title' => 'ILS', + 'status' => $status, + 'fix' => 'fixils', + ]; + } + + /** + * Check if Security configuration is set. + * + * @return array + */ + protected function checkMethodSecurity(): array + { + try { + $secureDb = $this->hasSecureDatabase(); + } catch (\Throwable $e) { + $secureDb = false; + } + return [ + 'title' => 'Security', + 'status' => $secureDb, + 'fix' => 'fixsecurity', + ]; + } + + /** + * Check if SSL configuration is set properly. + * + * @return array + */ + public function checkMethodSslCerts(): array + { + return [ + 'title' => 'SSL', + 'status' => $this->testSslConnection(), + 'fix' => 'fixsslcerts', + ]; + } +} diff --git a/module/VuFind/src/VuFind/Action/Install/PerformSecurityFixAction.php b/module/VuFind/src/VuFind/Action/Install/PerformSecurityFixAction.php new file mode 100644 index 000000000000..ca22d1b8dd4a --- /dev/null +++ b/module/VuFind/src/VuFind/Action/Install/PerformSecurityFixAction.php @@ -0,0 +1,179 @@ +. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFind\Action\Install; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use VuFind\ActionHelper\FlashMessagesHelper; +use VuFind\ActionHelper\RedirectHelper; +use VuFind\Auth\ILSAuthenticator; +use VuFind\Cache\Manager as CacheManager; +use VuFind\Config\ConfigManagerInterface; +use VuFind\Config\PathResolver; +use VuFind\Crypt\PasswordHasher; +use VuFind\Db\Service\PluginManager as DbServicePluginManager; +use VuFind\Db\Service\TagServiceInterface; +use VuFind\Db\Service\UserCardServiceInterface; +use VuFind\Db\Service\UserServiceInterface; +use VuFind\Http\ServerUrlHelper; +use VuFind\ILS\Connection; +use VuFind\ServiceManager\Factory\Autowire; +use VuFindHttp\HttpService; +use VuFindSearch\Service as SearchService; + +use function count; + +/** + * Install "perform security fix" action. + * + * @category VuFind + * @package Action + * @author Demian Katz + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class PerformSecurityFixAction extends AbstractInstallAction +{ + /** + * Constructor. + * + * @param CacheManager $cacheManager Cache manager + * @param Connection $ilsConnection ILS connection + * @param SearchService $searchService Search service + * @param PathResolver $pathResolver Path resolver + * @param ConfigManagerInterface $configManager Config manager + * @param ServerUrlHelper $serverUrlHelper Server URL helper + * @param HttpService $httpService HTTP service + * @param TagServiceInterface $tagService Tags database service + * @param UserServiceInterface $userService User database service + * @param UserCardServiceInterface $userCardService User card database service + * @param array $config VuFind configuration + * @param PasswordHasher $passwordHasher Password hasher + * @param ILSAuthenticator $ilsAuthenticator ILS authenticator + */ + public function __construct( + CacheManager $cacheManager, + Connection $ilsConnection, + SearchService $searchService, + PathResolver $pathResolver, + ConfigManagerInterface $configManager, + ServerUrlHelper $serverUrlHelper, + HttpService $httpService, + #[Autowire(container: DbServicePluginManager::class)] + TagServiceInterface $tagService, + #[Autowire(container: DbServicePluginManager::class)] + UserServiceInterface $userService, + #[Autowire(container: DbServicePluginManager::class)] + UserCardServiceInterface $userCardService, + #[Autowire(config: 'config')] + array $config, + protected PasswordHasher $passwordHasher, + protected ILSAuthenticator $ilsAuthenticator, + ) { + parent::__construct( + $cacheManager, + $ilsConnection, + $searchService, + $pathResolver, + $configManager, + $serverUrlHelper, + $httpService, + $tagService, + $userService, + $userCardService, + $config + ); + } + + /** + * Perform security fixes. + * + * @param ServerRequestInterface $request Server request + * @param ResponseInterface $response Response + * + * @return ResponseInterface + */ + public function action( + ServerRequestInterface $request, + ResponseInterface $response, + ): ResponseInterface { + $flashMessagesHelper = $this->getHelper(FlashMessagesHelper::class); + $redirectHelper = $this->getHelper(RedirectHelper::class); + + // This can take a while -- don't time out! + set_time_limit(0); + + // First, set encryption/hashing to true, and set the key + if ($fixedConfig = $this->getFixedSecurityConfiguration($this->config)) { + try { + $this->changeConfig('config', $fixedConfig); + } catch (\Exception $e) { + // Problem writing? Show the user an error: + return $redirectHelper->redirectToRoute($response, 'install-fixbasicconfig'); + } + + // Success? Redirect to this action in order to reload the configuration: + return $redirectHelper->redirectToRoute($response, 'install-performsecurityfix'); + } + + // Now we want to loop through the database and update passwords (if + // necessary). + $userRows = $this->userService->getInsecureRows(); + if (count($userRows) > 0) { + foreach ($userRows as $row) { + if ($row->getRawPassword() != '') { + $row->setPasswordHash($this->passwordHasher->create($row->getRawPassword())); + $row->setRawPassword(''); + } + if ($rawPassword = $row->getRawCatPassword()) { + $this->ilsAuthenticator->saveUserCatalogCredentials($row, $row->getCatUsername(), $rawPassword); + } else { + $this->userService->persistEntity($row); + } + } + $msg = count($userRows) . ' user row(s) encrypted.'; + $flashMessagesHelper->addInfoMessage($msg); + } + $cardRows = $this->userCardService->getInsecureRows(); + if (count($cardRows) > 0) { + foreach ($cardRows as $row) { + $row->setCatPassEnc($this->ilsAuthenticator->encrypt($row->getRawCatPassword())); + $row->setRawCatPassword(null); + $this->userCardService->persistEntity($row); + } + $msg = count($cardRows) . ' user_card row(s) encrypted.'; + $flashMessagesHelper->addInfoMessage($msg); + } + return $redirectHelper->redirectToRoute($response, 'install-home'); + } +} diff --git a/module/VuFind/src/VuFind/Action/PluginManager.php b/module/VuFind/src/VuFind/Action/PluginManager.php index aa23b71394c6..bbfc32ba37f2 100644 --- a/module/VuFind/src/VuFind/Action/PluginManager.php +++ b/module/VuFind/src/VuFind/Action/PluginManager.php @@ -117,6 +117,16 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager 'externalauth/ezproxylogin' => ExternalAuth\EzproxyLoginAction::class, + 'install/fixbasicconfig' => Install\FixBasicConfigAction::class, + 'install/fixcache' => Install\FixCacheAction::class, + 'install/fixdatabase' => Install\FixDatabaseAction::class, + 'install/fixdependencies' => Install\FixDependenciesAction::class, + 'install/fixils' => Install\FixIlsAction::class, + 'install/fixsolr' => Install\FixSolrAction::class, + 'install/fixsecurity' => Install\FixSecurityAction::class, + 'install/performsecurityfix' => Install\PerformSecurityFixAction::class, + 'install/fixsslcerts' => Install\FixSslCertsAction::class, + 'missingrecord/home' => MissingRecord\HomeAction::class, 'myresearch/cataloglogin' => MyResearch\CatalogLoginAction::class, diff --git a/module/VuFind/src/VuFind/Controller/InstallController.php b/module/VuFind/src/VuFind/Controller/InstallController.php deleted file mode 100644 index 7e3fb0abe8d4..000000000000 --- a/module/VuFind/src/VuFind/Controller/InstallController.php +++ /dev/null @@ -1,937 +0,0 @@ -. - * - * @category VuFind - * @package Controller - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Controller; - -use Laminas\Mvc\MvcEvent; -use VuFind\Crypt\PasswordHasher; -use VuFind\Db\DbBuilder; -use VuFind\Db\Service\TagServiceInterface; -use VuFind\Db\Service\UserCardServiceInterface; -use VuFind\Db\Service\UserServiceInterface; -use VuFindSearch\Command\RetrieveCommand; - -use function count; -use function defined; -use function dirname; -use function function_exists; -use function in_array; -use function is_callable; -use function sprintf; - -/** - * Class controls VuFind auto-configuration. - * - * @category VuFind - * @package Controller - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class InstallController extends AbstractBase -{ - use Feature\ConfigPathTrait; - use Feature\SecureDatabaseTrait; - - /** - * Use preDispatch event to block access when appropriate. - * - * @param MvcEvent $e Event object - * - * @return void - */ - public function validateAutoConfigureConfig(MvcEvent $e) - { - // If auto-configuration is disabled, prevent any other action from being - // accessed: - $config = $this->getConfigArray(); - if (!($config['System']['autoConfigure'] ?? false)) { - $routeMatch = $e->getRouteMatch(); - $routeMatch->setParam('action', 'disabled'); - } - } - - /** - * Register the default events for this controller. - * - * @return void - */ - protected function attachDefaultListeners() - { - parent::attachDefaultListeners(); - $events = $this->getEventManager(); - $events->attach( - MvcEvent::EVENT_DISPATCH, - [$this, 'validateAutoConfigureConfig'], - 1000 - ); - } - - /** - * Display disabled message. - * - * @return mixed - */ - public function disabledAction() - { - return $this->createViewModel(); - } - - /** - * Copy the basic configuration file into position and report success or - * failure. - * - * @return bool - */ - protected function installBasicConfig() - { - $config = $this->getForcedLocalConfigPath('config'); - if (!file_exists($config)) { - // Suppress errors so we don't cause a fatal error if copy is disallowed. - return @copy($this->getBaseConfigFilePath('config'), $config); - } - return true; // report success if file already exists - } - - /** - * Check if basic configuration is taken care of. - * - * @return array - */ - protected function checkBasicConfig() - { - // Initialize status based on existence of config file... - $status = $this->installBasicConfig(); - - // See if the URL setting remains at the default (unless we already - // know we've failed): - if ($status) { - $config = $this->getConfigArray(); - if (stristr($config['Site']['url'], 'myuniversity.edu')) { - $status = false; - } - } - - return [ - 'title' => 'Basic Configuration', 'status' => $status, - 'fix' => 'fixbasicconfig', - ]; - } - - /** - * Extract the Solr base URL from the SolrMarc configuration file, - * so a custom Solr port configured in install.php can be applied to - * the initial config.ini file. - * - * Return null if no custom Solr URL can be found. - * - * @return ?string - */ - protected function getSolrUrlFromImportConfig() - { - $resolver = $this->getService(\VuFind\Config\PathResolver::class); - $importConfig = $resolver->getLocalConfigPath('import.properties', 'import'); - if (file_exists($importConfig)) { - $props = file_get_contents($importConfig); - preg_match('|solr.hosturl\s*=\s*(https?://\w+:\d+/\w+)|', $props, $matches); - if (!empty($matches[1])) { - return $matches[1]; - } - } - return null; - } - - /** - * Display repair instructions for basic configuration problems. - * - * @return mixed - */ - public function fixbasicconfigAction() - { - $view = $this->createViewModel(); - $config = $this->getConfigArray(); - try { - if (!$this->installBasicConfig()) { - throw new \Exception('Cannot copy file into position.'); - } - // Choose secure defaults when creating initial config.ini: - $fixedConfig = $this->getFixedSecurityConfiguration($config); - // Set appropriate URLs: - $serverUrl = $this->getViewRenderer()->plugin('serverurl'); - $path = $this->url()->fromRoute('home'); - $fixedConfig['Site']['url'] = rtrim($serverUrl($path), '/'); - if ($solrUrl = $this->getSolrUrlFromImportConfig()) { - $fixedConfig['Index']['url'] = $solrUrl; - } - $this->changeConfig('config', $fixedConfig); - return $this->redirect()->toRoute('install-home'); - } catch (\Exception $e) { - $view->configDir = dirname($this->getForcedLocalConfigPath('config')); - $view->errorMessage = $e->getMessage(); - if ( - function_exists('posix_getpwuid') - && function_exists('posix_geteuid') - ) { - $processUser = posix_getpwuid(posix_geteuid()); - $view->runningUser = $processUser['name']; - } - } - return $view; - } - - /** - * Check if the cache directory is writable. - * - * @return array - */ - protected function checkCache() - { - $cache = $this->getService(\VuFind\Cache\Manager::class); - return [ - 'title' => 'Cache', - 'status' => !$cache->hasDirectoryCreationError(), - 'fix' => 'fixcache', - ]; - } - - /** - * Display repair instructions for cache problems. - * - * @return mixed - */ - public function fixcacheAction() - { - $cache = $this->getService(\VuFind\Cache\Manager::class); - $view = $this->createViewModel(); - $view->cacheDir = $cache->getCacheDir(); - if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) { - $processUser = posix_getpwuid(posix_geteuid()); - $view->runningUser = $processUser['name']; - } - return $view; - } - - /** - * Check if the database is accessible. - * - * @return array - */ - protected function checkDatabase() - { - try { - // Try to read the tags table just to see if we can connect to the DB: - $this->getDbService(TagServiceInterface::class)->getTagsByText('test'); - $status = true; - } catch (\Exception $e) { - $status = false; - } - return [ - 'title' => 'Database', 'status' => $status, 'fix' => 'fixdatabase', - ]; - } - - /** - * Support method for check/fix dependencies code -- do we have a new enough - * version of PHP? - * - * @return bool - */ - protected function phpVersionIsNewEnough() - { - // PHP_VERSION_ID was introduced in 5.2.7; if it's missing, we have a - // problem. - if (!defined('PHP_VERSION_ID')) { - return false; - } - - // We need at least PHP version as defined in composer.json file: - return PHP_VERSION_ID >= $this->getMinimalPhpVersionId(); - } - - /** - * Check for missing dependencies. - * - * @return array - */ - protected function checkDependencies() - { - $requiredFunctionsExist - = function_exists('mb_substr') && is_callable('imagecreatefromstring') - && function_exists('openssl_encrypt') - && class_exists('XSLTProcessor') - && defined('SODIUM_LIBRARY_VERSION'); - - return [ - 'title' => 'Dependencies', - 'status' => $requiredFunctionsExist && $this->phpVersionIsNewEnough(), - 'fix' => 'fixdependencies', - ]; - } - - /** - * Show how to fix dependency problems. - * - * @return mixed - */ - public function fixdependenciesAction() - { - $problems = 0; - - // Is our version new enough? - if (!$this->phpVersionIsNewEnough()) { - $msg = 'VuFind requires PHP version ' . $this->getMinimalPhpVersion() - . ' or newer; you are running ' . phpversion() - . '. Please upgrade.'; - $this->getFlashMessenger()->addErrorMessage($msg); - $problems++; - } - - // Is the mbstring library missing? - if (!function_exists('mb_substr')) { - $msg - = 'Your PHP installation appears to be missing the mbstring plug-in.' - . ' For better language support, it is recommended that you add' - . ' this. For details on how to do this, see ' - . 'https://vufind.org/wiki/installation ' - . 'and look at the PHP installation instructions for your platform.'; - $this->getFlashMessenger()->addErrorMessage($msg); - $problems++; - } - - // Is the GD library missing? - if (!is_callable('imagecreatefromstring')) { - $msg - = 'Your PHP installation appears to be missing the GD plug-in. ' - . 'For better graphics support, it is recommended that you add this.' - . ' For details on how to do this, see ' - . 'https://vufind.org/wiki/installation ' - . 'and look at the PHP installation instructions for your platform.'; - $this->getFlashMessenger()->addErrorMessage($msg); - $problems++; - } - - // Is the openssl library missing? - if (!function_exists('openssl_encrypt')) { - $msg - = 'Your PHP installation appears to be missing the openssl plug-in.' - . ' For better security support, it is recommended that you add' - . ' this. For details on how to do this, see ' - . 'https://vufind.org/wiki/installation ' - . 'and look at the PHP installation instructions for your platform.'; - $this->getFlashMessenger()->addErrorMessage($msg); - $problems++; - } - - // Is the XSL library missing? - if (!class_exists('XSLTProcessor')) { - $msg - = 'Your PHP installation appears to be missing the XSL plug-in.' - . ' For details on how to do this, see ' - . 'https://vufind.org/wiki/installation ' - . 'and look at the PHP installation instructions for your platform.'; - $this->getFlashMessenger()->addErrorMessage($msg); - $problems++; - } - - // Is the sodium extension missing? - if (!defined('SODIUM_LIBRARY_VERSION')) { - $msg - = 'Your PHP installation appears to be missing the sodium plug-in.' - . ' For details on how to do this, see ' - . 'https://vufind.org/wiki/installation ' - . 'and look at the PHP installation instructions for your platform.'; - $this->getFlashMessenger()->addErrorMessage($msg); - $problems++; - } - - return $this->createViewModel(['problems' => $problems]); - } - - /** - * Display repair instructions for database problems. - * - * @return mixed - */ - public function fixdatabaseAction() - { - $dbSettings = [ - 'dbname' => $this->params()->fromPost('dbname', 'vufind'), - 'dbuser' => $this->params()->fromPost('dbuser', 'vufind'), - 'dbhost' => $this->params()->fromPost('dbhost', 'localhost'), - 'vufindhost' => $this->params()->fromPost('vufindhost', 'localhost'), - 'dbrootuser' => $this->params()->fromPost('dbrootuser', 'root'), - 'driver' => $this->params()->fromPost('driver', 'mysql'), - ]; - $view = $this->createViewModel($dbSettings); - - $skip = $this->params()->fromPost('printsql', 'nope') == 'Skip'; - - if (!preg_match('/^\w*$/', $view->dbname)) { - $this->getFlashMessenger() - ->addErrorMessage('Database name must be alphanumeric.'); - } elseif (!preg_match('/^\w*$/', $view->dbuser)) { - $this->getFlashMessenger() - ->addErrorMessage('Database user must be alphanumeric.'); - } elseif ($skip || $this->formWasSubmitted()) { - $newpass = $this->params()->fromPost('dbpass'); - $newpassConf = $this->params()->fromPost('dbpassconfirm'); - if ((empty($newpass) || empty($newpassConf))) { - $this->getFlashMessenger() - ->addErrorMessage('Password fields must not be blank.'); - } elseif ($newpass != $newpassConf) { - $this->getFlashMessenger() - ->addErrorMessage('Password fields must match.'); - } else { - // Connect to database: - try { - $builder = $this->serviceLocator->get(DbBuilder::class); - $rootpass = $this->params()->fromPost('dbrootpass'); - $omnisql = $builder->build( - $dbSettings['dbname'], - $dbSettings['dbuser'], - $newpass, - $dbSettings['driver'], - $dbSettings['dbhost'], - $dbSettings['vufindhost'], - $dbSettings['dbrootuser'], - $rootpass, - $skip - ); - if ($skip) { - $this->getRequest()->getQuery()->set('sql', $omnisql); - return $this->forwardTo('Install', 'showsql'); - } else { - // If we made it this far, we can update the config file and - // forward back to the home action! - $string = "{$view->driver}://{$view->dbuser}:{$newpass}@" - . $view->dbhost . '/' . $view->dbname; - try { - $this->changeConfig( - 'config', - ['Database' => ['database' => $string]] - ); - } catch (\Exception $e) { - return $this->forwardTo('Install', 'fixbasicconfig'); - } - } - return $this->redirect()->toRoute('install-home'); - } catch (\Exception $e) { - $this->getFlashMessenger()->addErrorMessage($e->getMessage()); - } - } - } - return $view; - } - - /** - * Display captured SQL commands for database action. - * - * @return mixed - */ - protected function showsqlAction() - { - $continue = $this->params()->fromPost('continue', 'nope'); - if ($continue == 'Next') { - return $this->redirect()->toRoute('install-home'); - } - - return $this->createViewModel( - ['sql' => $this->params()->fromQuery('sql')] - ); - } - - /** - * Check if ILS configuration is appropriate. - * - * @return array - */ - protected function checkILS() - { - $config = $this->getConfigArray(); - if (in_array($config['Catalog']['driver'], ['Sample', 'Demo'])) { - $status = false; - } else { - try { - $status = 'ils-offline' !== $this->getILS()->getOfflineMode(true) - || ('NoILS' === $config['Catalog']['driver']); - } catch (\Exception $e) { - $status = false; - } - } - return ['title' => 'ILS', 'status' => $status, 'fix' => 'fixils']; - } - - /** - * Display repair instructions for ILS problems. - * - * @return mixed - */ - public function fixilsAction() - { - // Process incoming parameter -- user may have selected a new driver: - $newDriver = $this->params()->fromPost('driver'); - if (!empty($newDriver)) { - try { - $this->changeConfig( - 'config', - ['Catalog' => ['driver' => $newDriver]] - ); - } catch (\Exception $e) { - return $this->forwardTo('Install', 'fixbasicconfig'); - } - // Copy configuration, if applicable: - $ilsIni = $this->getBaseConfigFilePath($newDriver); - $localIlsIni = $this->getForcedLocalConfigPath($newDriver); - if (file_exists($ilsIni) && !file_exists($localIlsIni)) { - if (!copy($ilsIni, $localIlsIni)) { - return $this->forwardTo('Install', 'fixbasicconfig'); - } - } - return $this->redirect()->toRoute('install-home'); - } - - // If we got this far, check whether we have an error with a real driver - // or if we need to warn the user that they have selected a fake driver: - $config = $this->getConfigArray(); - $view = $this->createViewModel(); - if (in_array($config['Catalog']['driver'], ['Sample', 'Demo'])) { - $view->demo = true; - // Get a list of available drivers: - $dir - = opendir(APPLICATION_PATH . '/module/VuFind/src/VuFind/ILS/Driver'); - $drivers = []; - $excludeList = [ - 'Sample.php', 'Demo.php', 'DriverInterface.php', 'PluginManager.php', - ]; - while ($line = readdir($dir)) { - if ( - stristr($line, '.php') && !in_array($line, $excludeList) - && !str_starts_with($line, 'Abstract') - && !str_ends_with($line, 'Factory.php') - && !str_ends_with($line, 'Trait.php') - ) { - $drivers[] = str_replace('.php', '', $line); - } - } - closedir($dir); - sort($drivers); - $view->drivers = $drivers; - } else { - $view->configPath = $this->getForcedLocalConfigPath($config['Catalog']['driver']); - } - return $view; - } - - /** - * Support method to test the search service. - * - * @return void - * @throws \Exception - */ - protected function testSearchService() - { - // Try to retrieve an arbitrary ID -- this will fail if Solr is down: - $searchService = $this->getService(\VuFindSearch\Service::class); - $command = new RetrieveCommand('Solr', '1'); - $searchService->invoke($command)->getResult(); - } - - /** - * Check if the Solr index is working. - * - * @return array - */ - protected function checkSolr() - { - try { - $this->testSearchService(); - $status = true; - } catch (\Exception $e) { - $status = false; - } - return ['title' => 'Solr', 'status' => $status, 'fix' => 'fixsolr']; - } - - /** - * Display repair instructions for Solr problems. - * - * @return mixed - */ - public function fixsolrAction() - { - // In Windows, localhost may fail -- see if switching to 127.0.0.1 helps: - $config = $this->getConfigArray(); - if (stristr($config['Index']['url'], 'localhost')) { - $newUrl = str_replace('localhost', '127.0.0.1', $config['Index']['url']); - try { - $this->testSearchService(); - try { - $this->changeConfig( - 'config', - ['Index' => ['url' => $newUrl]] - ); - } catch (\Exception $e) { - return $this->forwardTo('Install', 'fixbasicconfig'); - } - return $this->redirect()->toRoute('install-home'); - } catch (\Exception $e) { - // Didn't work! - } - } - - // If we got this far, the automatic fix didn't work, so let's just assign - // some variables to use in offering troubleshooting advice: - $view = $this->createViewModel(); - $view->rawUrl = $config['Index']['url']; - $view->userUrl = str_replace( - ['localhost', '127.0.0.1'], - $this->getRequest()->getServer()->get('HTTP_HOST'), - $config['Index']['url'] - ); - $view->core = $config['Index']['default_core'] ?? 'biblio'; - $view->configFile = $this->getForcedLocalConfigPath('config'); - return $view; - } - - /** - * Check if Security configuration is set. - * - * @return array - */ - protected function checkSecurity() - { - try { - $secureDb = $this->hasSecureDatabase(); - } catch (\Throwable $e) { - $secureDb = false; - } - return [ - 'title' => 'Security', - 'status' => $secureDb, - 'fix' => 'fixsecurity', - ]; - } - - /** - * Support method for fixsecurityAction(). Returns true if the configuration - * was modified, false otherwise. - * - * @param array $config Existing VuFind configuration - * - * @return array - */ - protected function getFixedSecurityConfiguration(array $config): array - { - $fixedConfig = []; - - if ( - !($config['Authentication']['hash_passwords'] ?? false) - || !($config['Authentication']['encrypt_ils_password'] ?? false) - ) { - $fixedConfig['Authentication']['hash_passwords'] = true; - $fixedConfig['Authentication']['encrypt_ils_password'] = true; - } - // Only rewrite encryption key if we don't already have one: - if (empty($config['Authentication']['ils_encryption_key'])) { - [$algorithm, $key] = $this->getSecureAlgorithmAndKey(); - $fixedConfig['Authentication']['ils_encryption_algo'] = $algorithm; - $fixedConfig['Authentication']['ils_encryption_key'] = $key; - } - - return $fixedConfig; - } - - /** - * Display repair instructions for Security problems. - * - * @return mixed - */ - public function fixsecurityAction() - { - // If the user doesn't want to proceed, abort now: - $userConfirmation = $this->params()->fromPost('fix-user-table', 'Unset'); - if ($userConfirmation == 'No') { - $msg = 'Security upgrade aborted.'; - $this->getFlashMessenger()->addErrorMessage($msg); - return $this->redirect()->toRoute('install-home'); - } - - // If we don't need to prompt the user, or if they confirmed, do the fix: - try { - $userRows = $this->getDbService(UserServiceInterface::class)->getInsecureRows(); - $cardRows = $this->getDbService(UserCardServiceInterface::class)->getInsecureRows(); - } catch (\Throwable $e) { - $this->getFlashMessenger() - ->addErrorMessage('Cannot connect to database; please configure database before fixing security.'); - return $this->redirect()->toRoute('install-home'); - } - if (count($userRows) + count($cardRows) == 0 || $userConfirmation == 'Yes') { - return $this->forwardTo('Install', 'performsecurityfix'); - } - - // If we got this far, we need to ask permission to proceed: - $view = $this->createViewModel(); - $view->confirmUserFix = true; - return $view; - } - - /** - * Perform fix for Security problems. - * - * @return mixed - */ - public function performsecurityfixAction() - { - // This can take a while -- don't time out! - set_time_limit(0); - - // First, set encryption/hashing to true, and set the key - $config = $this->getConfigArray(); - if ($fixedConfig = $this->getFixedSecurityConfiguration($config)) { - try { - $this->changeConfig('config', $fixedConfig); - } catch (\Exception $e) { - // Problem writing? Show the user an error: - return $this->forwardTo('Install', 'fixbasicconfig'); - } - - // Success? Redirect to this action in order to reload the configuration: - return $this->redirect()->toRoute('install-performsecurityfix'); - } - - // Now we want to loop through the database and update passwords (if - // necessary). - $ilsAuthenticator = $this->getService(\VuFind\Auth\ILSAuthenticator::class); - $userService = $this->getDbService(UserServiceInterface::class); - $userRows = $userService->getInsecureRows(); - if (count($userRows) > 0) { - $hasher = $this->getService(PasswordHasher::class); - foreach ($userRows as $row) { - if ($row->getRawPassword() != '') { - $row->setPasswordHash($hasher->create($row->getRawPassword())); - $row->setRawPassword(''); - } - if ($rawPassword = $row->getRawCatPassword()) { - $ilsAuthenticator->saveUserCatalogCredentials($row, $row->getCatUsername(), $rawPassword); - } else { - $userService->persistEntity($row); - } - } - $msg = count($userRows) . ' user row(s) encrypted.'; - $this->getFlashMessenger()->addInfoMessage($msg); - } - $cardService = $this->getDbService(UserCardServiceInterface::class); - $cardRows = $cardService->getInsecureRows(); - if (count($cardRows) > 0) { - foreach ($cardRows as $row) { - $row->setCatPassEnc($ilsAuthenticator->encrypt($row->getRawCatPassword())); - $row->setRawCatPassword(null); - $cardService->persistEntity($row); - } - $msg = count($cardRows) . ' user_card row(s) encrypted.'; - $this->getFlashMessenger()->addInfoMessage($msg); - } - return $this->redirect()->toRoute('install-home'); - } - - /** - * Check if SSL configuration is set properly. - * - * @return array - */ - public function checkSslCerts() - { - // Try to retrieve an SSL URL; if we're misconfigured, it will fail. - try { - $this->getService(\VuFindHttp\HttpService::class) - ->get('https://google.com'); - $status = true; - } catch (\VuFindHttp\Exception\RuntimeException $e) { - // Any exception means we have a problem! - $status = false; - } - - return [ - 'title' => 'SSL', 'status' => $status, 'fix' => 'fixsslcerts', - ]; - } - - /** - * Display repair instructions for SSL certificate problems. - * - * @return mixed - */ - public function fixsslcertsAction() - { - // Bail out if we've fixed the problem: - $result = $this->checkSslCerts(); - if ($result['status'] == true) { - $this->getFlashMessenger()->addInfoMessage('SSL configuration fixed.'); - return $this->redirect()->toRoute('install-home'); - } - - // Find out which test to try next: - $try = $this->params()->fromQuery('try', 0); - - // Configurations to test: - $configsToTest = [ - ['sslcapath' => '/etc/ssl/certs'], - ['sslcafile' => '/etc/pki/tls/cert.pem'], - [], // reset configuration as last attempt - ]; - if (isset($configsToTest[$try])) { - return $this->testSslCertConfig($configsToTest[$try], $try); - } - - // If we got this far, we can't fix this automatically and must display - // a message. - $view = $this->createViewModel(); - return $view; - } - - /** - * Try switching to a specific SSL configuration. - * - * @param array $config Setting(s) to add to [Http] section of config.ini. - * @param int $try Which config index are we trying right now? - * - * @return \Laminas\Http\Response - */ - protected function testSslCertConfig($config, $try) - { - // Reset old settings - $fixedConfig = [ - 'Http' => [ - 'sslcapath' => null, - 'sslcafile' => null, - ], - ]; - // Load new settings - foreach ($config as $setting => $value) { - $fixedConfig['Http'][$setting] = $value; - } - $this->changeConfig('config', $fixedConfig); - - // Jump back to fix action so we can check if it worked (and attempt - // the next config by incrementing the $try variable, if necessary): - return $this->redirect()->toRoute( - 'install-fixsslcerts', - [], - ['query' => ['try' => $try + 1]] - ); - } - - /** - * Disable auto-configuration. - * - * @return mixed - */ - public function doneAction() - { - try { - $this->changeConfig( - 'config', - ['System' => ['autoConfigure' => 0]] - ); - } catch (\Exception $e) { - return $this->forwardTo('Install', 'fixbasicconfig'); - } - return $this->createViewModel(['configDir' => dirname($this->getForcedLocalConfigPath('config'))]); - } - - /** - * Display summary of installation status. - * - * @return mixed - */ - public function homeAction() - { - // Perform all checks (based on naming convention): - $methods = get_class_methods($this); - $checks = []; - foreach ($methods as $method) { - if (str_starts_with($method, 'check')) { - $checks[] = $this->$method(); - } - } - return $this->createViewModel(['checks' => $checks]); - } - - /** - * Get minimal PHP version required for VuFind to run. - * - * @return string - */ - protected function getMinimalPhpVersion(): string - { - $composer = $this->getComposerJson(); - if (empty($composer)) { - throw new \Exception('Cannot find composer.json'); - } - $rawVersion = $composer['require']['php'] - ?? $composer['config']['platform']['php'] - ?? ''; - $version = preg_replace('/[^0-9. ]/', '', $rawVersion); - if (empty($version) || !preg_match('/^[0-9]/', $version)) { - throw new \Exception('Cannot parse PHP version from composer.json'); - } - $versionParts = preg_split('/[. ]/', $version); - $versionParts = array_pad($versionParts, 3, '0'); - return sprintf('%d.%d.%d', ...$versionParts); - } - - /** - * Get minimal PHP version ID required for VuFind to run. - * - * @return int - */ - protected function getMinimalPhpVersionId(): int - { - $version = explode('.', $this->getMinimalPhpVersion()); - return $version[0] * 10000 + $version[1] * 100 + $version[2]; - } - - /** - * Get composer.json data as array. - * - * @return array - */ - protected function getComposerJson(): array - { - try { - $composerJsonFileName = APPLICATION_PATH . '/composer.json'; - if (file_exists($composerJsonFileName)) { - return json_decode(file_get_contents($composerJsonFileName), true); - } - } catch (\Throwable $exception) { - return []; - } - return []; - } -} diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/InstallTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/InstallTest.php new file mode 100644 index 000000000000..4b811f5e40ed --- /dev/null +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/InstallTest.php @@ -0,0 +1,79 @@ +. + * + * @category VuFind + * @package Tests + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ + +namespace VuFindTest\Mink; + +use Generator; +use PHPUnit\Framework\Attributes\DataProvider; + +/** + * Installer test class. + * + * @category VuFind + * @package Tests + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class InstallTest extends \VuFindTest\Integration\MinkTestCase +{ + /** + * Data provider for testAutoConfigureSetting(). + * + * @return Generator + */ + public static function autoconfigureProvider(): Generator + { + yield 'enabled' => [true]; + yield 'disabled' => [false]; + } + + /** + * Test that the installer respects the autoConfigure setting. + * + * @param bool $autoConfigure Should we enable or disable the autoConfigure setting? + * + * @return void + */ + #[DataProvider('autoconfigureProvider')] + public function testAutoConfigureSetting(bool $autoConfigure): void + { + $this->changeConfigs(['config' => ['System' => compact('autoConfigure')]]); + $session = $this->getMinkSession(); + $session->visit($this->getVuFindUrl() . '/Install'); + $page = $session->getPage(); + $this->assertSame('Auto Configure', $this->findCssAndGetText($page, '.vc-page-title')); + $getExpected = fn ($ac) => $ac ? 'Basic Configuration' : 'Auto configuration is disabled.'; + $expected = $getExpected($autoConfigure); + $notExpected = $getExpected(!$autoConfigure); + $pageContent = (string)$page->getContent(); + $this->assertStringContainsString($expected, $pageContent); + $this->assertStringNotContainsString($notExpected, $pageContent); + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/InstallControllerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Action/Install/AbstractInstallActionTest.php similarity index 71% rename from module/VuFind/tests/unit-tests/src/VuFindTest/Controller/InstallControllerTest.php rename to module/VuFind/tests/unit-tests/src/VuFindTest/Action/Install/AbstractInstallActionTest.php index 57dc950390d5..af95cafd5d57 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/InstallControllerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Action/Install/AbstractInstallActionTest.php @@ -1,11 +1,12 @@ + * @author Ere Maijala * @license https://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:testing:unit_tests Wiki */ declare(strict_types=1); -namespace VuFindTest\Controller; +namespace VuFindTest\Action\Install; -use VuFind\Controller\InstallController; +use PHPUnit\Framework\MockObject\MockObject; +use VuFind\Action\Install\AbstractInstallAction; +use VuFind\Action\Install\HomeAction; +use VuFindTest\Feature\AutowireTrait; +use VuFindTest\Feature\ReflectionTrait; /** - * Class InstallControllerTest. + * Class AbstractInstallActionTest. * * @category VuFind * @package Tests * @author Josef Moravec + * @author Ere Maijala * @license https://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:testing:unit_tests Wiki */ -class InstallControllerTest extends \PHPUnit\Framework\TestCase +class AbstractInstallActionTest extends \PHPUnit\Framework\TestCase { + use AutowireTrait; + use ReflectionTrait; + /** * Test getMinimalPhpVersion with actual composer.json file. * * @return void */ - public function testGetMinimalPhpVersionWithActualData() + public function testGetMinimalPhpVersionWithActualData(): void { - $controller = new InstallController( - new \VuFindTest\Container\MockContainer($this) - ); - $method = $this->getMinimalPhpVersionMethod(); + // Test the method in the abstract base class by instantiating a concrete class extending it: + $action = $this->getAutowiredObject(HomeAction::class); $this->assertEquals( '8.2.0', - $method->invokeArgs($controller, []) + $this->callMethod($action, 'getMinimalPhpVersion') ); } @@ -66,13 +74,12 @@ public function testGetMinimalPhpVersionWithActualData() * * @return void */ - public function testGetMinimalPhpVersionWithMissingFile() + public function testGetMinimalPhpVersionWithMissingFile(): void { - $controller = $this->mockControllerWithComposerJson([]); - $method = $this->getMinimalPhpVersionMethod(); + $action = $this->getMockActionWithComposerJson([]); $this->expectException(\Exception::class); $this->expectExceptionMessage('Cannot find composer.json'); - $method->invokeArgs($controller, []); + $this->callMethod($action, 'getMinimalPhpVersion'); } /** @@ -80,13 +87,12 @@ public function testGetMinimalPhpVersionWithMissingFile() * * @return void */ - public function testGetMinimalPhpVersionWithMissingPhpVersion() + public function testGetMinimalPhpVersionWithMissingPhpVersion(): void { - $controller = $this->mockControllerWithComposerJson(['name' => 'vufind/vufind']); - $method = $this->getMinimalPhpVersionMethod(); + $action = $this->getMockActionWithComposerJson(['name' => 'vufind/vufind']); $this->expectException(\Exception::class); $this->expectExceptionMessage('Cannot parse PHP version from composer.json'); - $method->invokeArgs($controller, []); + $this->callMethod($action, 'getMinimalPhpVersion'); } /** @@ -188,13 +194,12 @@ public static function getMinimalPhpVersionProvider(): \Iterator * @return void */ #[\PHPUnit\Framework\Attributes\DataProvider('getMinimalPhpVersionProvider')] - public function testGetMinimalPhpVersion($json, $expected) + public function testGetMinimalPhpVersion($json, $expected): void { - $controller = $this->mockControllerWithComposerJson($json); - $method = $this->getMinimalPhpVersionMethod(); + $action = $this->getMockActionWithComposerJson($json); $this->assertEquals( $expected, - $method->invokeArgs($controller, []) + $this->callMethod($action, 'getMinimalPhpVersion') ); } @@ -203,32 +208,20 @@ public function testGetMinimalPhpVersion($json, $expected) * * @param array $json JSON data * - * @return InstallController + * @return MockObject&AbstractInstallAction */ - protected function mockControllerWithComposerJson( + protected function getMockActionWithComposerJson( array $json - ): InstallController { - $controller = $this->getMockBuilder(InstallController::class) + ): AbstractInstallAction { + // Test the abstract base class by instantiating a concrete class extending it: + $action = $this->getMockBuilder(HomeAction::class) ->disableOriginalConstructor() ->onlyMethods(['getComposerJson']) ->getMock(); - $controller->expects($this->once())->method('getComposerJson') + $action->expects($this->once())->method('getComposerJson') ->willReturn($json); - return $controller; - } - - /** - * Return method InstallController::getMinimalPhpVersion. - * - * @return \ReflectionMethod - */ - protected function getMinimalPhpVersionMethod(): \ReflectionMethod - { - return new \ReflectionMethod( - InstallController::class, - 'getMinimalPhpVersion' - ); + return $action; } } diff --git a/themes/bootstrap5/templates/install/fixbasicconfig.phtml b/themes/bootstrap5/templates/install/fixbasicconfig.phtml index 80dc509dd974..2491c4e504ed 100644 --- a/themes/bootstrap5/templates/install/fixbasicconfig.phtml +++ b/themes/bootstrap5/templates/install/fixbasicconfig.phtml @@ -8,7 +8,7 @@ configDir)): ?>

VuFind® cannot write to escapeHtml($this->configDir)?>.

- errorMessage): ?>

Error: errorMessage?>

+ errorMessage): ?>

Error message: errorMessage?>

Please make sure that write permissions are available on this directory.

diff --git a/themes/bootstrap5/templates/install/fixdependencies.phtml b/themes/bootstrap5/templates/install/fixdependencies.phtml index 38b50be75132..05f4ffe12b30 100644 --- a/themes/bootstrap5/templates/install/fixdependencies.phtml +++ b/themes/bootstrap5/templates/install/fixdependencies.phtml @@ -7,4 +7,20 @@ flashmessages()?> -problems == 0): ?>

transEsc('No dependency problems found') ?>.

+missingExtensions): ?> +
+

+ transEsc('Your PHP installation appears to be missing the following extensions that need to be installed:')?> + missingExtensions)) ?> +

+

+ translate('For details on how to do this, see https://vufind.org/wiki/installation and look at the PHP installation instructions for your platform.')?> +

+
+ + +problems == 0): ?> +

+ transEsc('No dependency problems found') ?>. +

+