diff --git a/bin/install b/bin/install index 13c0a3e8..e3502509 100755 --- a/bin/install +++ b/bin/install @@ -2,11 +2,7 @@ build(Application::class)->run(); +$run = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php'; +$run(false); diff --git a/bin/update b/bin/update index 8e2e02b5..3be9bf17 100755 --- a/bin/update +++ b/bin/update @@ -2,11 +2,7 @@ build(Application::class, ['isUpdate' => true])->run(); +$run = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php'; +$run(true); diff --git a/composer.json b/composer.json index 3865d0da..aa3c36fe 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "lstrojny/functional-php": "^1.8", "mikehaertl/phpwkhtmltopdf": "^2.2", "monolog/monolog": "^1.21", + "shlinkio/shlink-installer": "^1.0", "symfony/console": "^4.2", "symfony/filesystem": "^4.2", "symfony/lock": "^4.2", @@ -36,7 +37,7 @@ "theorchard/monolog-cascade": "^0.4", "zendframework/zend-config": "^3.0", "zendframework/zend-config-aggregator": "^1.0", - "zendframework/zend-diactoros": "^2.0 <2.0.2", + "zendframework/zend-diactoros": "^2.1.1", "zendframework/zend-expressive": "^3.0", "zendframework/zend-expressive-fastroute": "^3.0", "zendframework/zend-expressive-helpers": "^5.0", @@ -68,8 +69,7 @@ "Shlinkio\\Shlink\\CLI\\": "module/CLI/src", "Shlinkio\\Shlink\\Rest\\": "module/Rest/src", "Shlinkio\\Shlink\\Core\\": "module/Core/src", - "Shlinkio\\Shlink\\Common\\": "module/Common/src", - "Shlinkio\\Shlink\\Installer\\": "module/Installer/src" + "Shlinkio\\Shlink\\Common\\": "module/Common/src" }, "files": [ "module/Common/functions/functions.php" @@ -87,8 +87,7 @@ "ShlinkioTest\\Shlink\\Common\\": [ "module/Common/test", "module/Common/test-db" - ], - "ShlinkioTest\\Shlink\\Installer\\": "module/Installer/test" + ] } }, "scripts": { diff --git a/config/config.php b/config/config.php index 9b28cf82..7821afd2 100644 --- a/config/config.php +++ b/config/config.php @@ -18,7 +18,6 @@ return (new ConfigAggregator\ConfigAggregator([ Common\ConfigProvider::class, Core\ConfigProvider::class, CLI\ConfigProvider::class, - Installer\ConfigProvider::class, Rest\ConfigProvider::class, new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'), diff --git a/config/install-container.php b/config/install-container.php deleted file mode 100644 index f26b7598..00000000 --- a/config/install-container.php +++ /dev/null @@ -1,48 +0,0 @@ - [ - 'factories' => [ - Application::class => InstallApplicationFactory::class, - Filesystem::class => InvokableFactory::class, - ], - 'services' => [ - 'random-chars-generator' => function () { - return str_shuffle(UrlShortenerOptions::DEFAULT_CHARS); - }, - ], - ], - - 'config_customizer_plugins' => [ - 'factories' => [ - Plugin\DatabaseConfigCustomizer::class => ConfigAbstractFactory::class, - Plugin\UrlShortenerConfigCustomizer::class => ConfigAbstractFactory::class, - Plugin\LanguageConfigCustomizer::class => InvokableFactory::class, - Plugin\ApplicationConfigCustomizer::class => InvokableFactory::class, - ], - ], - - ConfigAbstractFactory::class => [ - Plugin\DatabaseConfigCustomizer::class => [Filesystem::class], - Plugin\UrlShortenerConfigCustomizer::class => ['random-chars-generator'], - ], -]; - -$container = new ServiceManager($config['dependencies']); -$container->setService('config', $config); - -return $container; diff --git a/module/Installer/config/module.config.php b/module/Installer/config/module.config.php deleted file mode 100644 index 353ace17..00000000 --- a/module/Installer/config/module.config.php +++ /dev/null @@ -1,4 +0,0 @@ -configWriter = $configWriter; - $this->isUpdate = $isUpdate; - $this->filesystem = $filesystem; - $this->configCustomizers = $configCustomizers; - $this->phpFinder = $phpFinder ?: new PhpExecutableFinder(); - } - - protected function configure(): void - { - $this - ->setName('shlink:install') - ->setDescription('Installs or updates Shlink'); - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - * @return void - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - protected function execute(InputInterface $input, OutputInterface $output): void - { - $this->io = new SymfonyStyle($input, $output); - - $this->io->writeln([ - 'Welcome to Shlink!!', - 'This tool will guide you through the installation process.', - ]); - - // Check if a cached config file exists and drop it if so - if ($this->filesystem->exists('data/cache/app_config.php')) { - $this->io->write('Deleting old cached config...'); - try { - $this->filesystem->remove('data/cache/app_config.php'); - $this->io->writeln(' Success'); - } catch (IOException $e) { - $this->io->error( - 'Failed! You will have to manually delete the data/cache/app_config.php file to' - . ' get new config applied.' - ); - if ($this->io->isVerbose()) { - $this->getApplication()->renderException($e, $output); - } - return; - } - } - - // If running update command, ask the user to import previous config - $config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig(); - - // Ask for custom config params - foreach ([ - Plugin\DatabaseConfigCustomizer::class, - Plugin\UrlShortenerConfigCustomizer::class, - Plugin\LanguageConfigCustomizer::class, - Plugin\ApplicationConfigCustomizer::class, - ] as $pluginName) { - /** @var Plugin\ConfigCustomizerInterface $configCustomizer */ - $configCustomizer = $this->configCustomizers->get($pluginName); - $configCustomizer->process($this->io, $config); - } - - // Generate config params files - $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false); - $this->io->writeln(['Custom configuration properly generated!', '']); - - // If current command is not update, generate database - if (! $this->isUpdate) { - $this->io->write('Initializing database...'); - if (! $this->execPhp( - ['vendor/doctrine/orm/bin/doctrine.php', 'orm:schema-tool:create'], - 'Error generating database.', - $output - )) { - return; - } - } - - // Run database migrations - $this->io->write('Updating database...'); - if (! $this->execPhp( - ['vendor/doctrine/migrations/bin/doctrine-migrations.php', 'migrations:migrate'], - 'Error updating database.', - $output - )) { - return; - } - - // Generate proxies - $this->io->write('Generating proxies...'); - if (! $this->execPhp( - ['vendor/doctrine/orm/bin/doctrine.php', 'orm:generate-proxies'], - 'Error generating proxies.', - $output - )) { - return; - } - - // Download GeoLite2 db file - $this->io->write('Downloading GeoLite2 db...'); - if (! $this->execPhp(['bin/cli', 'visit:update-db'], 'Error downloading GeoLite2 db.', $output)) { - return; - } - - $this->io->success('Installation complete!'); - } - - /** - * @return CustomizableAppConfig - * @throws RuntimeException - */ - private function importConfig(): CustomizableAppConfig - { - $config = new CustomizableAppConfig(); - - // Ask the user if he/she wants to import an older configuration - $importConfig = $this->io->confirm( - 'Do you want to import configuration from previous installation? (You will still be asked for any new ' - . 'config option that did not exist in previous shlink versions)' - ); - if (! $importConfig) { - return $config; - } - - // Ask the user for the older shlink path - $keepAsking = true; - do { - $config->setImportedInstallationPath($this->askRequired( - $this->io, - 'previous installation path', - 'Previous shlink installation path from which to import config' - )); - $configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH; - $configExists = $this->filesystem->exists($configFile); - - if (! $configExists) { - $keepAsking = $this->io->confirm( - 'Provided path does not seem to be a valid shlink root path. Do you want to try another path?' - ); - } - } while (! $configExists && $keepAsking); - - // If after some retries the user has chosen not to test another path, return - if (! $configExists) { - return $config; - } - - // Read the config file - $config->exchangeArray(include $configFile); - return $config; - } - - private function execPhp(array $command, string $errorMessage, OutputInterface $output): bool - { - if ($this->processHelper === null) { - $this->processHelper = $this->getHelper('process'); - } - - if ($this->phpBinary === null) { - $this->phpBinary = $this->phpFinder->find(false) ?: 'php'; - } - - array_unshift($command, $this->phpBinary); - $this->io->write( - ' [Running "' . implode(' ', $command) . '"] ', - false, - OutputInterface::VERBOSITY_VERBOSE - ); - $process = $this->processHelper->run($output, $command); - if ($process->isSuccessful()) { - $this->io->writeln(' Success!'); - return true; - } - - if (! $this->io->isVerbose()) { - $this->io->error($errorMessage . ' Run this command with -vvv to see specific error info.'); - } - - return false; - } -} diff --git a/module/Installer/src/Config/ConfigCustomizerManager.php b/module/Installer/src/Config/ConfigCustomizerManager.php deleted file mode 100644 index 7f44103d..00000000 --- a/module/Installer/src/Config/ConfigCustomizerManager.php +++ /dev/null @@ -1,12 +0,0 @@ -getApp(); - $keysToAskFor = $appConfig->hasApp() ? array_diff(self::EXPECTED_KEYS, array_keys($app)) : self::EXPECTED_KEYS; - - if (empty($keysToAskFor)) { - return; - } - - $io->title('APPLICATION'); - foreach ($keysToAskFor as $key) { - // Skip visits threshold when the user decided not to check visits on deletions - if ($key === self::VISITS_THRESHOLD && ! $app[self::CHECK_VISITS_THRESHOLD]) { - continue; - } - - $app[$key] = $this->ask($io, $key); - } - $appConfig->setApp($app); - } - - private function ask(SymfonyStyle $io, string $key) - { - switch ($key) { - case self::SECRET: - return $io->ask( - 'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one) ' - . '[DEPRECATED. TO BE REMOVED]' - ) ?: $this->generateRandomString(32); - case self::DISABLE_TRACK_PARAM: - return $io->ask( - 'Provide a parameter name that you will be able to use to disable tracking on specific request to ' - . 'short URLs (leave empty and this feature won\'t be enabled)' - ); - case self::CHECK_VISITS_THRESHOLD: - return $io->confirm( - 'Do you want to enable a safety check which will not allow short URLs to be deleted when they ' - . 'have more than a specific amount of visits?' - ); - case self::VISITS_THRESHOLD: - return $io->ask( - 'What is the amount of visits from which the system will not allow short URLs to be deleted?', - 15, - [$this, 'validateVisitsThreshold'] - ); - } - - return ''; - } - - public function validateVisitsThreshold($value): int - { - if (! is_numeric($value) || $value < 1) { - throw new InvalidConfigOptionException( - sprintf('Provided value "%s" is invalid. Expected a number greater than 1', $value) - ); - } - - return (int) $value; - } -} diff --git a/module/Installer/src/Config/Plugin/ConfigCustomizerInterface.php b/module/Installer/src/Config/Plugin/ConfigCustomizerInterface.php deleted file mode 100644 index 908f6f90..00000000 --- a/module/Installer/src/Config/Plugin/ConfigCustomizerInterface.php +++ /dev/null @@ -1,12 +0,0 @@ - 'pdo_mysql', - 'PostgreSQL' => 'pdo_pgsql', - 'SQLite' => 'pdo_sqlite', - ]; - - /** @var Filesystem */ - private $filesystem; - - public function __construct(Filesystem $filesystem) - { - $this->filesystem = $filesystem; - } - - /** - * @throws IOException - */ - public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void - { - $titlePrinted = false; - $db = $appConfig->getDatabase(); - $doImport = $appConfig->hasDatabase(); - $keysToAskFor = $doImport ? array_diff(self::EXPECTED_KEYS, array_keys($db)) : self::EXPECTED_KEYS; - - // If the user selected to keep DB, try to import SQLite database - if ($doImport) { - $this->importSqliteDbFile($io, $appConfig); - } - - if (empty($keysToAskFor)) { - return; - } - - // If the driver is one of the params to ask for, ask for it first - if (contains($keysToAskFor, self::DRIVER)) { - $io->title('DATABASE'); - $titlePrinted = true; - $db[self::DRIVER] = $this->ask($io, self::DRIVER); - $keysToAskFor = array_diff($keysToAskFor, [self::DRIVER]); - } - - // If driver is SQLite, do not ask any driver-dependant option - if ($db[self::DRIVER] === self::DATABASE_DRIVERS['SQLite']) { - $keysToAskFor = array_diff($keysToAskFor, self::DRIVER_DEPENDANT_OPTIONS); - } - - if (! $titlePrinted && ! empty($keysToAskFor)) { - $io->title('DATABASE'); - } - foreach ($keysToAskFor as $key) { - $db[$key] = $this->ask($io, $key, $db); - } - $appConfig->setDatabase($db); - } - - private function importSqliteDbFile(SymfonyStyle $io, CustomizableAppConfig $appConfig): void - { - if ($appConfig->getDatabase()[self::DRIVER] !== self::DATABASE_DRIVERS['SQLite']) { - return; - } - - try { - $this->filesystem->copy( - $appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH, - CustomizableAppConfig::SQLITE_DB_PATH - ); - } catch (IOException $e) { - $io->error('It wasn\'t possible to import the SQLite database'); - throw $e; - } - } - - private function ask(SymfonyStyle $io, string $key, array $params = []) - { - switch ($key) { - case self::DRIVER: - $databases = array_keys(self::DATABASE_DRIVERS); - $dbType = $io->choice('Select database type', $databases, $databases[0]); - return self::DATABASE_DRIVERS[$dbType]; - case self::NAME: - return $io->ask('Database name', 'shlink'); - case self::USER: - return $this->askRequired($io, 'username', 'Database username'); - case self::PASSWORD: - return $this->askRequired($io, 'password', 'Database password'); - case self::HOST: - return $io->ask('Database host', 'localhost'); - case self::PORT: - return $io->ask('Database port', $this->getDefaultDbPort($params[self::DRIVER])); - } - - return ''; - } - - private function getDefaultDbPort(string $driver): string - { - return $driver === 'pdo_mysql' ? '3306' : '5432'; - } -} diff --git a/module/Installer/src/Config/Plugin/LanguageConfigCustomizer.php b/module/Installer/src/Config/Plugin/LanguageConfigCustomizer.php deleted file mode 100644 index fa157dfb..00000000 --- a/module/Installer/src/Config/Plugin/LanguageConfigCustomizer.php +++ /dev/null @@ -1,52 +0,0 @@ -getLanguage(); - $keysToAskFor = $appConfig->hasLanguage() - ? array_diff(self::EXPECTED_KEYS, array_keys($lang)) - : self::EXPECTED_KEYS; - - if (empty($keysToAskFor)) { - return; - } - - $io->title('LANGUAGE'); - foreach ($keysToAskFor as $key) { - $lang[$key] = $this->ask($io, $key); - } - $appConfig->setLanguage($lang); - } - - private function ask(SymfonyStyle $io, string $key) - { - switch ($key) { - case self::DEFAULT_LANG: - return $this->chooseLanguage($io, 'Select default language for the application error pages'); - } - - return ''; - } - - private function chooseLanguage(SymfonyStyle $io, string $message): string - { - return $io->choice($message, self::SUPPORTED_LANGUAGES, self::SUPPORTED_LANGUAGES[0]); - } -} diff --git a/module/Installer/src/Config/Plugin/UrlShortenerConfigCustomizer.php b/module/Installer/src/Config/Plugin/UrlShortenerConfigCustomizer.php deleted file mode 100644 index fa718509..00000000 --- a/module/Installer/src/Config/Plugin/UrlShortenerConfigCustomizer.php +++ /dev/null @@ -1,99 +0,0 @@ -randomCharsGenerator = $randomCharsGenerator; - } - - public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void - { - $urlShortener = $appConfig->getUrlShortener(); - $doImport = $appConfig->hasUrlShortener(); - $keysToAskFor = $doImport ? array_diff(self::EXPECTED_KEYS, array_keys($urlShortener)) : self::EXPECTED_KEYS; - - if (empty($keysToAskFor)) { - return; - } - - // Print title if there are keys other than "chars" - $onlyKeyIsChars = count($keysToAskFor) === 1 && contains($keysToAskFor, self::CHARS); - if (! $onlyKeyIsChars) { - $io->title('URL SHORTENER'); - } - foreach ($keysToAskFor as $key) { - // Skip not found redirect URL when the user decided not to redirect - if ($key === self::NOT_FOUND_REDIRECT_TO && ! $urlShortener[self::ENABLE_NOT_FOUND_REDIRECTION]) { - continue; - } - - $urlShortener[$key] = $this->ask($io, $key); - } - $appConfig->setUrlShortener($urlShortener); - } - - private function ask(SymfonyStyle $io, string $key) - { - switch ($key) { - case self::SCHEMA: - return $io->choice( - 'Select schema for generated short URLs', - ['http', 'https'], - 'http' - ); - case self::HOSTNAME: - return $this->askRequired($io, 'hostname', 'Hostname for generated URLs'); - case self::CHARS: - // This won't actually ask anything, just generate the chars. Asking for this was confusing for users - return ($this->randomCharsGenerator)(); - case self::VALIDATE_URL: - return $io->confirm('Do you want to validate long urls by 200 HTTP status code on response'); - case self::ENABLE_NOT_FOUND_REDIRECTION: - return $io->confirm( - 'Do you want to enable a redirection to a custom URL when a user hits an invalid short URL? ' . - '(If not enabled, the user will see a default "404 not found" page)', - false - ); - case self::NOT_FOUND_REDIRECT_TO: - return $this->askRequired( - $io, - 'redirect URL', - 'Custom URL to redirect to when a user hits an invalid short URL' - ); - } - - return ''; - } -} diff --git a/module/Installer/src/ConfigProvider.php b/module/Installer/src/ConfigProvider.php deleted file mode 100644 index 6be4cf16..00000000 --- a/module/Installer/src/ConfigProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -get(Filesystem::class), - new ConfigCustomizerManager($container, $container->get('config')['config_customizer_plugins']), - $isUpdate - ); - $app->add($command); - $app->setDefaultCommand($command->getName(), true); - - return $app; - } -} diff --git a/module/Installer/src/Model/CustomizableAppConfig.php b/module/Installer/src/Model/CustomizableAppConfig.php deleted file mode 100644 index c7c6cdd6..00000000 --- a/module/Installer/src/Model/CustomizableAppConfig.php +++ /dev/null @@ -1,218 +0,0 @@ -database; - } - - public function setDatabase(array $database): self - { - $this->database = $database; - return $this; - } - - public function hasDatabase(): bool - { - return ! empty($this->database); - } - - public function getUrlShortener(): array - { - return $this->urlShortener; - } - - public function setUrlShortener(array $urlShortener): self - { - $this->urlShortener = $urlShortener; - return $this; - } - - public function hasUrlShortener(): bool - { - return ! empty($this->urlShortener); - } - - public function getLanguage(): array - { - return $this->language; - } - - public function setLanguage(array $language): self - { - $this->language = $language; - return $this; - } - - public function hasLanguage(): bool - { - return ! empty($this->language); - } - - public function getApp(): array - { - return $this->app; - } - - public function setApp(array $app): self - { - $this->app = $app; - return $this; - } - - public function hasApp(): bool - { - return ! empty($this->app); - } - - public function getImportedInstallationPath(): ?string - { - return $this->importedInstallationPath; - } - - public function setImportedInstallationPath(string $importedInstallationPath): self - { - $this->importedInstallationPath = $importedInstallationPath; - return $this; - } - - public function hasImportedInstallationPath(): bool - { - return $this->importedInstallationPath !== null; - } - - public function exchangeArray(array $array): void - { - $pathCollection = new PathCollection($array); - - $this->setApp($this->mapExistingPathsToKeys([ - ApplicationConfigCustomizer::SECRET => ['app_options', 'secret_key'], - ApplicationConfigCustomizer::DISABLE_TRACK_PARAM => ['app_options', 'disable_track_param'], - ApplicationConfigCustomizer::CHECK_VISITS_THRESHOLD => ['delete_short_urls', 'check_visits_threshold'], - ApplicationConfigCustomizer::VISITS_THRESHOLD => ['delete_short_urls', 'visits_threshold'], - ], $pathCollection)); - - $this->setDatabase($this->mapExistingPathsToKeys([ - DatabaseConfigCustomizer::DRIVER => ['entity_manager', 'connection', 'driver'], - DatabaseConfigCustomizer::USER => ['entity_manager', 'connection', 'user'], - DatabaseConfigCustomizer::PASSWORD => ['entity_manager', 'connection', 'password'], - DatabaseConfigCustomizer::NAME => ['entity_manager', 'connection', 'dbname'], - DatabaseConfigCustomizer::HOST => ['entity_manager', 'connection', 'host'], - DatabaseConfigCustomizer::PORT => ['entity_manager', 'connection', 'port'], - ], $pathCollection)); - - $this->setLanguage($this->mapExistingPathsToKeys([ - LanguageConfigCustomizer::DEFAULT_LANG => ['translator', 'locale'], - ], $pathCollection)); - - $this->setUrlShortener($this->mapExistingPathsToKeys([ - UrlShortenerConfigCustomizer::SCHEMA => ['url_shortener', 'domain', 'schema'], - UrlShortenerConfigCustomizer::HOSTNAME => ['url_shortener', 'domain', 'hostname'], - UrlShortenerConfigCustomizer::CHARS => ['url_shortener', 'shortcode_chars'], - UrlShortenerConfigCustomizer::VALIDATE_URL => ['url_shortener', 'validate_url'], - UrlShortenerConfigCustomizer::ENABLE_NOT_FOUND_REDIRECTION => [ - 'url_shortener', - 'not_found_short_url', - 'enable_redirection', - ], - UrlShortenerConfigCustomizer::NOT_FOUND_REDIRECT_TO => [ - 'url_shortener', - 'not_found_short_url', - 'redirect_to', - ], - ], $pathCollection)); - } - - private function mapExistingPathsToKeys(array $map, PathCollection $pathCollection): array - { - $result = []; - foreach ($map as $key => $path) { - if ($pathCollection->pathExists($path)) { - $result[$key] = $pathCollection->getValueInPath($path); - } - } - - return $result; - } - - public function getArrayCopy(): array - { - $dbDriver = $this->database[DatabaseConfigCustomizer::DRIVER] ?? ''; - $config = [ - 'app_options' => [ - 'secret_key' => $this->app[ApplicationConfigCustomizer::SECRET] ?? '', - 'disable_track_param' => $this->app[ApplicationConfigCustomizer::DISABLE_TRACK_PARAM] ?? null, - ], - 'delete_short_urls' => [ - 'check_visits_threshold' => $this->app[ApplicationConfigCustomizer::CHECK_VISITS_THRESHOLD] ?? true, - 'visits_threshold' => $this->app[ApplicationConfigCustomizer::VISITS_THRESHOLD] ?? 15, - ], - 'entity_manager' => [ - 'connection' => [ - 'driver' => $dbDriver, - ], - ], - 'translator' => [ - 'locale' => $this->language[LanguageConfigCustomizer::DEFAULT_LANG] ?? 'en', - ], - 'url_shortener' => [ - 'domain' => [ - 'schema' => $this->urlShortener[UrlShortenerConfigCustomizer::SCHEMA] ?? 'http', - 'hostname' => $this->urlShortener[UrlShortenerConfigCustomizer::HOSTNAME] ?? '', - ], - 'shortcode_chars' => $this->urlShortener[UrlShortenerConfigCustomizer::CHARS] ?? '', - 'validate_url' => $this->urlShortener[UrlShortenerConfigCustomizer::VALIDATE_URL] ?? true, - 'not_found_short_url' => [ - 'enable_redirection' => - $this->urlShortener[UrlShortenerConfigCustomizer::ENABLE_NOT_FOUND_REDIRECTION] ?? false, - 'redirect_to' => $this->urlShortener[UrlShortenerConfigCustomizer::NOT_FOUND_REDIRECT_TO] ?? null, - ], - ], - ]; - - // Build dynamic database config based on selected driver - if ($dbDriver === 'pdo_sqlite') { - $config['entity_manager']['connection']['path'] = self::SQLITE_DB_PATH; - } else { - $config['entity_manager']['connection']['user'] = $this->database[DatabaseConfigCustomizer::USER] ?? ''; - $config['entity_manager']['connection']['password'] = - $this->database[DatabaseConfigCustomizer::PASSWORD] ?? ''; - $config['entity_manager']['connection']['dbname'] = $this->database[DatabaseConfigCustomizer::NAME] ?? ''; - $config['entity_manager']['connection']['host'] = $this->database[DatabaseConfigCustomizer::HOST] ?? ''; - $config['entity_manager']['connection']['port'] = $this->database[DatabaseConfigCustomizer::PORT] ?? ''; - - if ($dbDriver === 'pdo_mysql') { - $config['entity_manager']['connection']['driverOptions'] = [ - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', - ]; - } - } - - return $config; - } -} diff --git a/module/Installer/src/Util/AskUtilsTrait.php b/module/Installer/src/Util/AskUtilsTrait.php deleted file mode 100644 index a301393f..00000000 --- a/module/Installer/src/Util/AskUtilsTrait.php +++ /dev/null @@ -1,24 +0,0 @@ -ask($question, null, function ($value) use ($optionName) { - if (empty($value)) { - throw MissingRequiredOptionException::fromOption($optionName); - }; - - return $value; - }); - } -} diff --git a/module/Installer/test-resources/config/params/generated_config.php b/module/Installer/test-resources/config/params/generated_config.php deleted file mode 100644 index 881ab67d..00000000 --- a/module/Installer/test-resources/config/params/generated_config.php +++ /dev/null @@ -1,2 +0,0 @@ -prophesize(Process::class); - $processMock->isSuccessful()->willReturn(true); - $processHelper = $this->prophesize(ProcessHelper::class); - $processHelper->getName()->willReturn('process'); - $processHelper->setHelperSet(Argument::any())->willReturn(null); - $processHelper->run(Argument::cetera())->willReturn($processMock->reveal()); - - $this->filesystem = $this->prophesize(Filesystem::class); - $this->filesystem->exists(Argument::cetera())->willReturn(false); - - $this->configWriter = $this->prophesize(WriterInterface::class); - - $configCustomizer = $this->prophesize(ConfigCustomizerInterface::class); - $configCustomizers = $this->prophesize(ConfigCustomizerManagerInterface::class); - $configCustomizers->get(Argument::cetera())->willReturn($configCustomizer->reveal()); - - $finder = $this->prophesize(PhpExecutableFinder::class); - $finder->find(false)->willReturn('php'); - - $app = new Application(); - $helperSet = $app->getHelperSet(); - $helperSet->set($processHelper->reveal()); - $app->setHelperSet($helperSet); - $this->command = new InstallCommand( - $this->configWriter->reveal(), - $this->filesystem->reveal(), - $configCustomizers->reveal(), - false, - $finder->reveal() - ); - $app->add($this->command); - - $this->commandTester = new CommandTester($this->command); - } - - /** - * @test - */ - public function generatedConfigIsProperlyPersisted() - { - $this->configWriter->toFile(Argument::any(), Argument::type('array'), false)->shouldBeCalledOnce(); - $this->commandTester->execute([]); - } - - /** - * @test - */ - public function cachedConfigIsDeletedIfExists() - { - /** @var MethodProphecy $appConfigExists */ - $appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn(true); - /** @var MethodProphecy $appConfigRemove */ - $appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willReturn(null); - - $this->commandTester->execute([]); - - $appConfigExists->shouldHaveBeenCalledOnce(); - $appConfigRemove->shouldHaveBeenCalledOnce(); - } - - /** - * @test - */ - public function exceptionWhileDeletingCachedConfigCancelsProcess() - { - /** @var MethodProphecy $appConfigExists */ - $appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn(true); - /** @var MethodProphecy $appConfigRemove */ - $appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willThrow(IOException::class); - /** @var MethodProphecy $configToFile */ - $configToFile = $this->configWriter->toFile(Argument::cetera())->willReturn(true); - - $this->commandTester->execute([]); - - $appConfigExists->shouldHaveBeenCalledOnce(); - $appConfigRemove->shouldHaveBeenCalledOnce(); - $configToFile->shouldNotHaveBeenCalled(); - } - - /** - * @test - */ - public function whenCommandIsUpdatePreviousConfigCanBeImported() - { - $ref = new ReflectionObject($this->command); - $prop = $ref->getProperty('isUpdate'); - $prop->setAccessible(true); - $prop->setValue($this->command, true); - - /** @var MethodProphecy $importedConfigExists */ - $importedConfigExists = $this->filesystem->exists( - __DIR__ . '/../../test-resources/' . InstallCommand::GENERATED_CONFIG_PATH - )->willReturn(true); - - $this->commandTester->setInputs([ - '', - '/foo/bar/wrong_previous_shlink', - '', - __DIR__ . '/../../test-resources', - ]); - $this->commandTester->execute([]); - - $importedConfigExists->shouldHaveBeenCalled(); - } -} diff --git a/module/Installer/test/Config/Plugin/ApplicationConfigCustomizerTest.php b/module/Installer/test/Config/Plugin/ApplicationConfigCustomizerTest.php deleted file mode 100644 index 221f4935..00000000 --- a/module/Installer/test/Config/Plugin/ApplicationConfigCustomizerTest.php +++ /dev/null @@ -1,171 +0,0 @@ -io = $this->prophesize(SymfonyStyle::class); - $this->io->title(Argument::any())->willReturn(null); - - $this->plugin = new ApplicationConfigCustomizer(); - } - - /** - * @test - */ - public function configIsRequestedToTheUser() - { - $ask = $this->io->ask(Argument::cetera())->willReturn('asked'); - $confirm = $this->io->confirm(Argument::cetera())->willReturn(false); - - $config = new CustomizableAppConfig(); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertTrue($config->hasApp()); - $this->assertEquals([ - 'SECRET' => 'asked', - 'DISABLE_TRACK_PARAM' => 'asked', - 'CHECK_VISITS_THRESHOLD' => false, - ], $config->getApp()); - $ask->shouldHaveBeenCalledTimes(2); - $confirm->shouldHaveBeenCalledOnce(); - } - - /** - * @test - */ - public function visitsThresholdIsRequestedIfCheckIsEnabled() - { - $ask = $this->io->ask(Argument::cetera())->will(function (array $args) { - $message = array_shift($args); - return strpos($message, 'What is the amount of visits') === 0 ? 20 : 'asked'; - }); - $confirm = $this->io->confirm(Argument::cetera())->willReturn(true); - - $config = new CustomizableAppConfig(); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertTrue($config->hasApp()); - $this->assertEquals([ - 'SECRET' => 'asked', - 'DISABLE_TRACK_PARAM' => 'asked', - 'CHECK_VISITS_THRESHOLD' => true, - 'VISITS_THRESHOLD' => 20, - ], $config->getApp()); - $ask->shouldHaveBeenCalledTimes(3); - $confirm->shouldHaveBeenCalledOnce(); - } - - /** - * @test - */ - public function onlyMissingOptionsAreAsked() - { - $ask = $this->io->ask(Argument::cetera())->willReturn('disable_param'); - $config = new CustomizableAppConfig(); - $config->setApp([ - 'SECRET' => 'foo', - 'CHECK_VISITS_THRESHOLD' => true, - 'VISITS_THRESHOLD' => 20, - ]); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertEquals([ - 'SECRET' => 'foo', - 'DISABLE_TRACK_PARAM' => 'disable_param', - 'CHECK_VISITS_THRESHOLD' => true, - 'VISITS_THRESHOLD' => 20, - ], $config->getApp()); - $ask->shouldHaveBeenCalledOnce(); - } - - /** - * @test - */ - public function noQuestionsAskedIfImportedConfigContainsEverything() - { - $ask = $this->io->ask(Argument::cetera())->willReturn('the_new_secret'); - - $config = new CustomizableAppConfig(); - $config->setApp([ - 'SECRET' => 'foo', - 'DISABLE_TRACK_PARAM' => 'the_new_secret', - 'CHECK_VISITS_THRESHOLD' => true, - 'VISITS_THRESHOLD' => 20, - ]); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertEquals([ - 'SECRET' => 'foo', - 'DISABLE_TRACK_PARAM' => 'the_new_secret', - 'CHECK_VISITS_THRESHOLD' => true, - 'VISITS_THRESHOLD' => 20, - ], $config->getApp()); - $ask->shouldNotHaveBeenCalled(); - } - - /** - * @test - * @dataProvider provideInvalidValues - * @param mixed $value - */ - public function validateVisitsThresholdThrowsExceptionWhenProvidedValueIsInvalid($value) - { - $this->expectException(InvalidConfigOptionException::class); - $this->plugin->validateVisitsThreshold($value); - } - - public function provideInvalidValues(): array - { - return [ - 'string' => ['foo'], - 'empty string' => [''], - 'negative number' => [-5], - 'negative number as string' => ['-5'], - 'zero' => [0], - 'zero as string' => ['0'], - ]; - } - - /** - * @test - * @dataProvider provideValidValues - * @param mixed $value - */ - public function validateVisitsThresholdCastsToIntWhenProvidedValueIsValid($value, int $expected) - { - $this->assertEquals($expected, $this->plugin->validateVisitsThreshold($value)); - } - - public function provideValidValues(): array - { - return [ - 'positive as string' => ['20', 20], - 'positive as integer' => [5, 5], - 'one as string' => ['1', 1], - 'one as integer' => [1, 1], - ]; - } -} diff --git a/module/Installer/test/Config/Plugin/DatabaseConfigCustomizerTest.php b/module/Installer/test/Config/Plugin/DatabaseConfigCustomizerTest.php deleted file mode 100644 index 104d01b1..00000000 --- a/module/Installer/test/Config/Plugin/DatabaseConfigCustomizerTest.php +++ /dev/null @@ -1,136 +0,0 @@ -io = $this->prophesize(SymfonyStyle::class); - $this->io->title(Argument::any())->willReturn(null); - $this->filesystem = $this->prophesize(Filesystem::class); - - $this->plugin = new DatabaseConfigCustomizer($this->filesystem->reveal()); - } - - /** - * @test - */ - public function configIsRequestedToTheUser() - { - $choice = $this->io->choice(Argument::cetera())->willReturn('MySQL'); - $ask = $this->io->ask(Argument::cetera())->willReturn('param'); - $config = new CustomizableAppConfig(); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertTrue($config->hasDatabase()); - $this->assertEquals([ - 'DRIVER' => 'pdo_mysql', - 'NAME' => 'param', - 'USER' => 'param', - 'PASSWORD' => 'param', - 'HOST' => 'param', - 'PORT' => 'param', - ], $config->getDatabase()); - $choice->shouldHaveBeenCalledOnce(); - $ask->shouldHaveBeenCalledTimes(5); - } - - /** - * @test - */ - public function onlyMissingOptionsAreAsked() - { - $choice = $this->io->choice(Argument::cetera())->willReturn('MySQL'); - $ask = $this->io->ask(Argument::cetera())->willReturn('asked'); - - $config = new CustomizableAppConfig(); - $config->setDatabase([ - 'DRIVER' => 'pdo_pgsql', - 'NAME' => 'foo', - 'PASSWORD' => 'foo', - ]); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertEquals([ - 'DRIVER' => 'pdo_pgsql', - 'NAME' => 'foo', - 'USER' => 'asked', - 'PASSWORD' => 'foo', - 'HOST' => 'asked', - 'PORT' => 'asked', - ], $config->getDatabase()); - $choice->shouldNotHaveBeenCalled(); - $ask->shouldHaveBeenCalledTimes(3); - } - - /** - * @test - */ - public function noQuestionsAskedIfImportedConfigContainsEverything() - { - $choice = $this->io->choice(Argument::cetera())->willReturn('MySQL'); - $ask = $this->io->ask(Argument::cetera())->willReturn('asked'); - - $config = new CustomizableAppConfig(); - $config->setDatabase([ - 'DRIVER' => 'pdo_pgsql', - 'NAME' => 'foo', - 'USER' => 'foo', - 'PASSWORD' => 'foo', - 'HOST' => 'foo', - 'PORT' => 'foo', - ]); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertEquals([ - 'DRIVER' => 'pdo_pgsql', - 'NAME' => 'foo', - 'USER' => 'foo', - 'PASSWORD' => 'foo', - 'HOST' => 'foo', - 'PORT' => 'foo', - ], $config->getDatabase()); - $choice->shouldNotHaveBeenCalled(); - $ask->shouldNotHaveBeenCalled(); - } - - /** - * @test - */ - public function sqliteDatabaseIsImportedWhenRequested() - { - $copy = $this->filesystem->copy(Argument::cetera())->willReturn(null); - - $config = new CustomizableAppConfig(); - $config->setDatabase([ - 'DRIVER' => 'pdo_sqlite', - ]); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertEquals([ - 'DRIVER' => 'pdo_sqlite', - ], $config->getDatabase()); - $copy->shouldHaveBeenCalledOnce(); - } -} diff --git a/module/Installer/test/Config/Plugin/LanguageConfigCustomizerTest.php b/module/Installer/test/Config/Plugin/LanguageConfigCustomizerTest.php deleted file mode 100644 index e9035d1c..00000000 --- a/module/Installer/test/Config/Plugin/LanguageConfigCustomizerTest.php +++ /dev/null @@ -1,79 +0,0 @@ -io = $this->prophesize(SymfonyStyle::class); - $this->io->title(Argument::any())->willReturn(null); - $this->plugin = new LanguageConfigCustomizer(); - } - - /** - * @test - */ - public function configIsRequestedToTheUser() - { - $choice = $this->io->choice(Argument::cetera())->willReturn('en'); - $config = new CustomizableAppConfig(); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertTrue($config->hasLanguage()); - $this->assertEquals([ - 'DEFAULT' => 'en', - ], $config->getLanguage()); - $choice->shouldHaveBeenCalledOnce(); - } - - /** - * @test - */ - public function onlyMissingOptionsAreAsked() - { - $choice = $this->io->choice(Argument::cetera())->willReturn('es'); - $config = new CustomizableAppConfig(); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertEquals([ - 'DEFAULT' => 'es', - ], $config->getLanguage()); - $choice->shouldHaveBeenCalledOnce(); - } - - /** - * @test - */ - public function noQuestionsAskedIfImportedConfigContainsEverything() - { - $choice = $this->io->choice(Argument::cetera())->willReturn('en'); - - $config = new CustomizableAppConfig(); - $config->setLanguage([ - 'DEFAULT' => 'es', - ]); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertEquals([ - 'DEFAULT' => 'es', - ], $config->getLanguage()); - $choice->shouldNotHaveBeenCalled(); - } -} diff --git a/module/Installer/test/Config/Plugin/UrlShortenerConfigCustomizerTest.php b/module/Installer/test/Config/Plugin/UrlShortenerConfigCustomizerTest.php deleted file mode 100644 index 2a5079a6..00000000 --- a/module/Installer/test/Config/Plugin/UrlShortenerConfigCustomizerTest.php +++ /dev/null @@ -1,148 +0,0 @@ -io = $this->prophesize(SymfonyStyle::class); - $this->io->title(Argument::any())->willReturn(null); - $this->plugin = new UrlShortenerConfigCustomizer(function () { - return 'the_chars'; - }); - } - - /** - * @test - */ - public function configIsRequestedToTheUser() - { - $choice = $this->io->choice(Argument::cetera())->willReturn('chosen'); - $ask = $this->io->ask(Argument::cetera())->willReturn('asked'); - $confirm = $this->io->confirm(Argument::cetera())->willReturn(true); - $config = new CustomizableAppConfig(); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertTrue($config->hasUrlShortener()); - $this->assertEquals([ - 'SCHEMA' => 'chosen', - 'HOSTNAME' => 'asked', - 'CHARS' => 'the_chars', - 'VALIDATE_URL' => true, - 'ENABLE_NOT_FOUND_REDIRECTION' => true, - 'NOT_FOUND_REDIRECT_TO' => 'asked', - ], $config->getUrlShortener()); - $ask->shouldHaveBeenCalledTimes(2); - $choice->shouldHaveBeenCalledOnce(); - $confirm->shouldHaveBeenCalledTimes(2); - } - - /** - * @test - */ - public function onlyMissingOptionsAreAsked() - { - $choice = $this->io->choice(Argument::cetera())->willReturn('chosen'); - $ask = $this->io->ask(Argument::cetera())->willReturn('asked'); - $confirm = $this->io->confirm(Argument::cetera())->willReturn(false); - $config = new CustomizableAppConfig(); - $config->setUrlShortener([ - 'SCHEMA' => 'foo', - 'ENABLE_NOT_FOUND_REDIRECTION' => true, - 'NOT_FOUND_REDIRECT_TO' => 'foo', - ]); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertEquals([ - 'SCHEMA' => 'foo', - 'HOSTNAME' => 'asked', - 'CHARS' => 'the_chars', - 'VALIDATE_URL' => false, - 'ENABLE_NOT_FOUND_REDIRECTION' => true, - 'NOT_FOUND_REDIRECT_TO' => 'foo', - ], $config->getUrlShortener()); - $choice->shouldNotHaveBeenCalled(); - $ask->shouldHaveBeenCalledOnce(); - $confirm->shouldHaveBeenCalledOnce(); - } - - /** - * @test - */ - public function noQuestionsAskedIfImportedConfigContainsEverything() - { - $choice = $this->io->choice(Argument::cetera())->willReturn('chosen'); - $ask = $this->io->ask(Argument::cetera())->willReturn('asked'); - $confirm = $this->io->confirm(Argument::cetera())->willReturn(false); - - $config = new CustomizableAppConfig(); - $config->setUrlShortener([ - 'SCHEMA' => 'foo', - 'HOSTNAME' => 'foo', - 'CHARS' => 'foo', - 'VALIDATE_URL' => true, - 'ENABLE_NOT_FOUND_REDIRECTION' => true, - 'NOT_FOUND_REDIRECT_TO' => 'foo', - ]); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertEquals([ - 'SCHEMA' => 'foo', - 'HOSTNAME' => 'foo', - 'CHARS' => 'foo', - 'VALIDATE_URL' => true, - 'ENABLE_NOT_FOUND_REDIRECTION' => true, - 'NOT_FOUND_REDIRECT_TO' => 'foo', - ], $config->getUrlShortener()); - $choice->shouldNotHaveBeenCalled(); - $ask->shouldNotHaveBeenCalled(); - $confirm->shouldNotHaveBeenCalled(); - } - - /** - * @test - */ - public function redirectUrlOptionIsNotAskedIfAnswerToPreviousQuestionIsNo() - { - $ask = $this->io->ask(Argument::cetera())->willReturn('asked'); - $confirm = $this->io->confirm(Argument::cetera())->willReturn(false); - - $config = new CustomizableAppConfig(); - $config->setUrlShortener([ - 'SCHEMA' => 'foo', - 'HOSTNAME' => 'foo', - 'CHARS' => 'foo', - 'VALIDATE_URL' => true, - ]); - - $this->plugin->process($this->io->reveal(), $config); - - $this->assertTrue($config->hasUrlShortener()); - $this->assertEquals([ - 'SCHEMA' => 'foo', - 'HOSTNAME' => 'foo', - 'CHARS' => 'foo', - 'VALIDATE_URL' => true, - 'ENABLE_NOT_FOUND_REDIRECTION' => false, - ], $config->getUrlShortener()); - $ask->shouldNotHaveBeenCalled(); - $confirm->shouldHaveBeenCalledOnce(); - } -} diff --git a/module/Installer/test/ConfigProviderTest.php b/module/Installer/test/ConfigProviderTest.php deleted file mode 100644 index db5999b2..00000000 --- a/module/Installer/test/ConfigProviderTest.php +++ /dev/null @@ -1,27 +0,0 @@ -configProvider = new ConfigProvider(); - } - - /** - * @test - */ - public function configIsReturned() - { - $config = $this->configProvider->__invoke(); - $this->assertEmpty($config); - } -} diff --git a/module/Installer/test/CustomizableAppConfigTest.php b/module/Installer/test/CustomizableAppConfigTest.php deleted file mode 100644 index c630d719..00000000 --- a/module/Installer/test/CustomizableAppConfigTest.php +++ /dev/null @@ -1,40 +0,0 @@ -exchangeArray([ - 'app_options' => [ - 'disable_track_param' => null, - ], - 'translator' => [ - 'locale' => 'es', - ], - ]); - - $this->assertFalse($config->hasDatabase()); - $this->assertFalse($config->hasUrlShortener()); - $this->assertTrue($config->hasApp()); - $this->assertTrue($config->hasLanguage()); - $this->assertEquals([ - ApplicationConfigCustomizer::DISABLE_TRACK_PARAM => null, - ], $config->getApp()); - $this->assertEquals([ - LanguageConfigCustomizer::DEFAULT_LANG => 'es', - ], $config->getLanguage()); - } -} diff --git a/module/Installer/test/Exception/MissingRequiredOptionExceptionTest.php b/module/Installer/test/Exception/MissingRequiredOptionExceptionTest.php deleted file mode 100644 index ea5b9113..00000000 --- a/module/Installer/test/Exception/MissingRequiredOptionExceptionTest.php +++ /dev/null @@ -1,19 +0,0 @@ -assertEquals('The "foo" is required and can\'t be empty', $e->getMessage()); - } -} diff --git a/module/Installer/test/Factory/InstallApplicationFactoryTest.php b/module/Installer/test/Factory/InstallApplicationFactoryTest.php deleted file mode 100644 index 326dd251..00000000 --- a/module/Installer/test/Factory/InstallApplicationFactoryTest.php +++ /dev/null @@ -1,34 +0,0 @@ -factory = new InstallApplicationFactory(); - } - - /** - * @test - */ - public function serviceIsCreated(): void - { - $instance = ($this->factory)(new ServiceManager(['services' => [ - Filesystem::class => $this->prophesize(Filesystem::class)->reveal(), - 'config' => ['config_customizer_plugins' => []], - ]]), ''); - - $this->assertInstanceOf(Application::class, $instance); - } -}