diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f73a443..07cae7fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). -## [Unreleased] +## [4.2.2] - 2024-10-14 ### Added * *Nothing* @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ### Fixed * [#2213](https://github.com/shlinkio/shlink/issues/2213) Fix spaces being replaced with underscores in query parameter names, when forwarded from short URL to long URL. * [#2217](https://github.com/shlinkio/shlink/issues/2217) Fix docker image tag suffix being leaked to the version set inside Shlink, producing invalid SemVer version patterns. +* [#2212](https://github.com/shlinkio/shlink/issues/2212) Fix env vars read in docker entry point not properly falling back to their `_FILE` suffixed counterpart. ## [4.2.1] - 2024-10-04 diff --git a/composer.json b/composer.json index 5f7078df..11109b64 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "ramsey/uuid": "^4.7", "shlinkio/doctrine-specification": "^2.1.1", "shlinkio/shlink-common": "^6.3", - "shlinkio/shlink-config": "dev-main#76a96ee as 3.1", + "shlinkio/shlink-config": "^3.2", "shlinkio/shlink-event-dispatcher": "^4.1", "shlinkio/shlink-importer": "^5.3.2", "shlinkio/shlink-installer": "^9.2", diff --git a/config/autoload/cache.global.php b/config/autoload/cache.global.php index 94a9a183..6ae37de7 100644 --- a/config/autoload/cache.global.php +++ b/config/autoload/cache.global.php @@ -6,7 +6,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars; return (static function (): array { $redisServers = EnvVars::REDIS_SERVERS->loadFromEnv(); - $redis = ['pub_sub_enabled' => $redisServers !== null && EnvVars::REDIS_PUB_SUB_ENABLED->loadFromEnv(false)]; + $redis = ['pub_sub_enabled' => $redisServers !== null && EnvVars::REDIS_PUB_SUB_ENABLED->loadFromEnv()]; $cacheRedisBlock = $redisServers === null ? [] : [ 'redis' => [ 'servers' => $redisServers, @@ -16,7 +16,7 @@ return (static function (): array { return [ 'cache' => [ - 'namespace' => EnvVars::CACHE_NAMESPACE->loadFromEnv('Shlink'), + 'namespace' => EnvVars::CACHE_NAMESPACE->loadFromEnv(), ...$cacheRedisBlock, ], 'redis' => $redis, diff --git a/config/autoload/entity-manager.global.php b/config/autoload/entity-manager.global.php index c3aa6632..4338a8b6 100644 --- a/config/autoload/entity-manager.global.php +++ b/config/autoload/entity-manager.global.php @@ -23,11 +23,6 @@ return (static function (): array { $value = $envVar->loadFromEnv(); return $value === null ? null : (string) $value; }; - $resolveDefaultPort = static fn () => match ($driver) { - 'postgres' => '5432', - 'mssql' => '1433', - default => '3306', - }; $resolveCharset = static fn () => match ($driver) { // This does not determine charsets or collations in tables or columns, but the charset used in the data // flowing in the connection, so it has to match what has been set in the database. @@ -43,11 +38,11 @@ return (static function (): array { ], default => [ 'driver' => $resolveDriver(), - 'dbname' => EnvVars::DB_NAME->loadFromEnv('shlink'), + 'dbname' => EnvVars::DB_NAME->loadFromEnv(), 'user' => $readCredentialAsString(EnvVars::DB_USER), 'password' => $readCredentialAsString(EnvVars::DB_PASSWORD), - 'host' => EnvVars::DB_HOST->loadFromEnv(EnvVars::DB_UNIX_SOCKET->loadFromEnv()), - 'port' => EnvVars::DB_PORT->loadFromEnv($resolveDefaultPort()), + 'host' => EnvVars::DB_HOST->loadFromEnv(), + 'port' => EnvVars::DB_PORT->loadFromEnv(), 'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null, 'charset' => $resolveCharset(), 'driverOptions' => $driver !== 'mssql' ? [] : [ diff --git a/config/autoload/matomo.global.php b/config/autoload/matomo.global.php index 120ad289..d7369ea7 100644 --- a/config/autoload/matomo.global.php +++ b/config/autoload/matomo.global.php @@ -7,7 +7,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars; return [ 'matomo' => [ - 'enabled' => (bool) EnvVars::MATOMO_ENABLED->loadFromEnv(false), + 'enabled' => (bool) EnvVars::MATOMO_ENABLED->loadFromEnv(), 'base_url' => EnvVars::MATOMO_BASE_URL->loadFromEnv(), 'site_id' => EnvVars::MATOMO_SITE_ID->loadFromEnv(), 'api_token' => EnvVars::MATOMO_API_TOKEN->loadFromEnv(), diff --git a/config/autoload/mercure.global.php b/config/autoload/mercure.global.php index 67143919..c50e1f8e 100644 --- a/config/autoload/mercure.global.php +++ b/config/autoload/mercure.global.php @@ -15,7 +15,7 @@ return (static function (): array { 'mercure' => [ 'public_hub_url' => $publicUrl, - 'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL->loadFromEnv($publicUrl), + 'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL->loadFromEnv(), 'jwt_secret' => EnvVars::MERCURE_JWT_SECRET->loadFromEnv(), 'jwt_issuer' => 'Shlink', ], diff --git a/config/autoload/qr-codes.global.php b/config/autoload/qr-codes.global.php index 919beffa..23caadf2 100644 --- a/config/autoload/qr-codes.global.php +++ b/config/autoload/qr-codes.global.php @@ -4,32 +4,17 @@ declare(strict_types=1); use Shlinkio\Shlink\Core\Config\EnvVars; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_BG_COLOR; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_COLOR; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_MARGIN; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_ROUND_BLOCK_SIZE; -use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE; - return [ 'qr_codes' => [ - 'size' => (int) EnvVars::DEFAULT_QR_CODE_SIZE->loadFromEnv(DEFAULT_QR_CODE_SIZE), - 'margin' => (int) EnvVars::DEFAULT_QR_CODE_MARGIN->loadFromEnv(DEFAULT_QR_CODE_MARGIN), - 'format' => EnvVars::DEFAULT_QR_CODE_FORMAT->loadFromEnv(DEFAULT_QR_CODE_FORMAT), - 'error_correction' => EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION->loadFromEnv( - DEFAULT_QR_CODE_ERROR_CORRECTION, - ), - 'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv( - DEFAULT_QR_CODE_ROUND_BLOCK_SIZE, - ), - 'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv( - DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS, - ), - 'color' => EnvVars::DEFAULT_QR_CODE_COLOR->loadFromEnv(DEFAULT_QR_CODE_COLOR), - 'bg_color' => EnvVars::DEFAULT_QR_CODE_BG_COLOR->loadFromEnv(DEFAULT_QR_CODE_BG_COLOR), + 'size' => (int) EnvVars::DEFAULT_QR_CODE_SIZE->loadFromEnv(), + 'margin' => (int) EnvVars::DEFAULT_QR_CODE_MARGIN->loadFromEnv(), + 'format' => EnvVars::DEFAULT_QR_CODE_FORMAT->loadFromEnv(), + 'error_correction' => EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION->loadFromEnv(), + 'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv(), + 'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(), + 'color' => EnvVars::DEFAULT_QR_CODE_COLOR->loadFromEnv(), + 'bg_color' => EnvVars::DEFAULT_QR_CODE_BG_COLOR->loadFromEnv(), 'logo_url' => EnvVars::DEFAULT_QR_CODE_LOGO_URL->loadFromEnv(), ], diff --git a/config/autoload/rabbit.global.php b/config/autoload/rabbit.global.php index fd8cda68..86e2dd6a 100644 --- a/config/autoload/rabbit.global.php +++ b/config/autoload/rabbit.global.php @@ -7,13 +7,13 @@ use Shlinkio\Shlink\Core\Config\EnvVars; return [ 'rabbitmq' => [ - 'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(false), + 'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(), 'host' => EnvVars::RABBITMQ_HOST->loadFromEnv(), - 'use_ssl' => (bool) EnvVars::RABBITMQ_USE_SSL->loadFromEnv(false), - 'port' => (int) EnvVars::RABBITMQ_PORT->loadFromEnv('5672'), + 'use_ssl' => (bool) EnvVars::RABBITMQ_USE_SSL->loadFromEnv(), + 'port' => (int) EnvVars::RABBITMQ_PORT->loadFromEnv(), 'user' => EnvVars::RABBITMQ_USER->loadFromEnv(), 'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(), - 'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv('/'), + 'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv(), ], ]; diff --git a/config/autoload/redirects.global.php b/config/autoload/redirects.global.php index 26a3c032..dbf026db 100644 --- a/config/autoload/redirects.global.php +++ b/config/autoload/redirects.global.php @@ -4,9 +4,6 @@ declare(strict_types=1); use Shlinkio\Shlink\Core\Config\EnvVars; -use const Shlinkio\Shlink\DEFAULT_REDIRECT_CACHE_LIFETIME; -use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE; - return [ 'not_found_redirects' => [ @@ -16,10 +13,8 @@ return [ ], 'redirects' => [ - 'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE->loadFromEnv(DEFAULT_REDIRECT_STATUS_CODE->value), - 'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME->loadFromEnv( - DEFAULT_REDIRECT_CACHE_LIFETIME, - ), + 'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE->loadFromEnv(), + 'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME->loadFromEnv(), ], ]; diff --git a/config/autoload/robots.global.php b/config/autoload/robots.global.php index 8954dc53..35235d72 100644 --- a/config/autoload/robots.global.php +++ b/config/autoload/robots.global.php @@ -7,7 +7,7 @@ namespace Shlinkio\Shlink\Core; return [ 'robots' => [ - 'allow-all-short-urls' => (bool) Config\EnvVars::ROBOTS_ALLOW_ALL_SHORT_URLS->loadFromEnv(false), + 'allow-all-short-urls' => (bool) Config\EnvVars::ROBOTS_ALLOW_ALL_SHORT_URLS->loadFromEnv(), 'user-agents' => splitByComma(Config\EnvVars::ROBOTS_USER_AGENTS->loadFromEnv()), ], diff --git a/config/autoload/router.global.php b/config/autoload/router.global.php index d13bf7d4..db389f3a 100644 --- a/config/autoload/router.global.php +++ b/config/autoload/router.global.php @@ -8,7 +8,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars; return [ 'router' => [ - 'base_path' => EnvVars::BASE_PATH->loadFromEnv(''), + 'base_path' => EnvVars::BASE_PATH->loadFromEnv(), 'fastroute' => [ // Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console diff --git a/config/autoload/routes.config.php b/config/autoload/routes.config.php index 6d072228..f7c6ce07 100644 --- a/config/autoload/routes.config.php +++ b/config/autoload/routes.config.php @@ -21,7 +21,7 @@ return (static function (): array { $overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class; // TODO This should be based on config, not the env var - $shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false) ? '[/]' : ''; + $shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv() ? '[/]' : ''; return [ diff --git a/config/autoload/tracking.global.php b/config/autoload/tracking.global.php index 267bb76d..713a209e 100644 --- a/config/autoload/tracking.global.php +++ b/config/autoload/tracking.global.php @@ -11,25 +11,25 @@ return [ 'tracking' => [ // Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations // This applies only if IP address tracking is enabled - 'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true), + 'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(), // Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence - 'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true), + 'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(), // A query param that, if provided, will disable tracking of one particular visit. Always takes precedence 'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(), // If true, visits will not be tracked at all - 'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false), + 'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(), // If true, visits will be tracked, but neither the IP address, nor the location will be resolved - 'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false), + 'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(), // If true, the referrer will not be tracked - 'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false), + 'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(), // If true, the user agent will not be tracked - 'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false), + 'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(), // A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default 'disable_tracking_from' => splitByComma(EnvVars::DISABLE_TRACKING_FROM->loadFromEnv()), diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 43bd5a74..98b07ea2 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -5,29 +5,28 @@ declare(strict_types=1); use Shlinkio\Shlink\Core\Config\EnvVars; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode; -use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH; use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH; return (static function (): array { $shortCodesLength = max( - (int) EnvVars::DEFAULT_SHORT_CODES_LENGTH->loadFromEnv(DEFAULT_SHORT_CODES_LENGTH), + (int) EnvVars::DEFAULT_SHORT_CODES_LENGTH->loadFromEnv(), MIN_SHORT_CODES_LENGTH, ); - $modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value); + $modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(); $mode = ShortUrlMode::tryFrom($modeFromEnv) ?? ShortUrlMode::STRICT; return [ 'url_shortener' => [ 'domain' => [ // TODO Refactor this structure to url_shortener.schema and url_shortener.default_domain - 'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED->loadFromEnv(true)) ? 'https' : 'http', - 'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(''), + 'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED->loadFromEnv()) ? 'https' : 'http', + 'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(), ], 'default_short_codes_length' => $shortCodesLength, - 'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(true), - 'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false), - 'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false), - 'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false), + 'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(), + 'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(), + 'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(), + 'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(), 'mode' => $mode, ], diff --git a/config/container.php b/config/container.php index 13d64353..3a1e3355 100644 --- a/config/container.php +++ b/config/container.php @@ -19,8 +19,8 @@ require 'vendor/autoload.php'; loadEnvVarsFromConfig('config/params/generated_config.php', enumValues(EnvVars::class)); // This is one of the first files loaded. Configure the timezone and memory limit here -ini_set('memory_limit', EnvVars::MEMORY_LIMIT->loadFromEnv('512M')); -date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get())); +ini_set('memory_limit', EnvVars::MEMORY_LIMIT->loadFromEnv()); +date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv()); // This class alias tricks the ConfigAbstractFactory to return Lock\Factory instances even with a different service name // It needs to be placed here as individual config files will not be loaded once config is cached diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index faa506a9..e2cce68f 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -8,14 +8,19 @@ mkdir -p data/cache data/locks data/log data/proxies flags="--no-interaction --clear-db-cache" +# Read env vars through Shlink command, so that it applies the `_FILE` env var fallback logic +geolite_license_key=$(bin/cli env-var:read GEOLITE_LICENSE_KEY) +skip_initial_geolite_download=$(bin/cli env-var:read SKIP_INITIAL_GEOLITE_DOWNLOAD) +initial_api_key=$(bin/cli env-var:read INITIAL_API_KEY) + # Skip downloading GeoLite2 db file if the license key env var was not defined or skipping was explicitly set -if [ -z "${GEOLITE_LICENSE_KEY}" ] || [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" = "true" ]; then +if [ -z "${geolite_license_key}" ] || [ "${skip_initial_geolite_download}" = "true" ]; then flags="${flags} --skip-download-geolite" fi # If INITIAL_API_KEY was provided, create an initial API key -if [ -n "${INITIAL_API_KEY}" ]; then - flags="${flags} --initial-api-key=${INITIAL_API_KEY}" +if [ -n "${initial_api_key}" ]; then + flags="${flags} --initial-api-key=${initial_api_key}" fi php vendor/bin/shlink-installer init ${flags} diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index e60bb2e1..8283a7b6 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -45,6 +45,8 @@ return [ Command\RedirectRule\ManageRedirectRulesCommand::class, Command\Integration\MatomoSendVisitsCommand::NAME => Command\Integration\MatomoSendVisitsCommand::class, + + Command\Config\ReadEnvVarCommand::NAME => Command\Config\ReadEnvVarCommand::class, ], ], diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 3853fd1d..0ee1a331 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -75,6 +75,8 @@ return [ Command\RedirectRule\ManageRedirectRulesCommand::class => ConfigAbstractFactory::class, Command\Integration\MatomoSendVisitsCommand::class => ConfigAbstractFactory::class, + + Command\Config\ReadEnvVarCommand::class => InvokableFactory::class, ], ], diff --git a/module/CLI/src/Command/Config/ReadEnvVarCommand.php b/module/CLI/src/Command/Config/ReadEnvVarCommand.php new file mode 100644 index 00000000..1f436eeb --- /dev/null +++ b/module/CLI/src/Command/Config/ReadEnvVarCommand.php @@ -0,0 +1,68 @@ +loadEnvVar = $loadEnvVar ?? static fn (string $envVar) => EnvVars::from($envVar)->loadFromEnv(); + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setName(self::NAME) + ->setHidden() + ->setDescription('Display current value for an env var') + ->addArgument('envVar', InputArgument::REQUIRED, 'The env var to read'); + } + + protected function interact(InputInterface $input, OutputInterface $output): void + { + $io = new SymfonyStyle($input, $output); + $envVar = $input->getArgument('envVar'); + $validEnvVars = enumValues(EnvVars::class); + + if ($envVar === null) { + $envVar = $io->choice('Select the env var to read', $validEnvVars); + } + + if (! contains($envVar, $validEnvVars)) { + throw new InvalidArgumentException(sprintf('%s is not a valid Shlink environment variable', $envVar)); + } + + $input->setArgument('envVar', $envVar); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $envVar = $input->getArgument('envVar'); + $output->writeln(formatEnvVarValue(($this->loadEnvVar)($envVar))); + + return ExitCode::EXIT_SUCCESS; + } +} diff --git a/module/CLI/src/Command/Db/AbstractDatabaseCommand.php b/module/CLI/src/Command/Db/AbstractDatabaseCommand.php index f803c50c..a85cb999 100644 --- a/module/CLI/src/Command/Db/AbstractDatabaseCommand.php +++ b/module/CLI/src/Command/Db/AbstractDatabaseCommand.php @@ -17,7 +17,7 @@ abstract class AbstractDatabaseCommand extends AbstractLockedCommand public function __construct( LockFactory $locker, - private ProcessRunnerInterface $processRunner, + private readonly ProcessRunnerInterface $processRunner, PhpExecutableFinder $phpFinder, ) { parent::__construct($locker); diff --git a/module/CLI/test/Command/Config/ReadEnvVarCommandTest.php b/module/CLI/test/Command/Config/ReadEnvVarCommandTest.php new file mode 100644 index 00000000..c377cf86 --- /dev/null +++ b/module/CLI/test/Command/Config/ReadEnvVarCommandTest.php @@ -0,0 +1,54 @@ +commandTester = CliTestUtils::testerForCommand(new ReadEnvVarCommand(fn () => $this->envVarValue)); + } + + #[Test] + public function errorIsThrownIfProvidedEnvVarIsInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('foo is not a valid Shlink environment variable'); + + $this->commandTester->execute(['envVar' => 'foo']); + } + + #[Test] + public function valueIsPrintedIfProvidedEnvVarIsValid(): void + { + $this->commandTester->execute(['envVar' => EnvVars::BASE_PATH->value]); + $output = $this->commandTester->getDisplay(); + + self::assertStringNotContainsString('Select the env var to read', $output); + self::assertStringContainsString($this->envVarValue, $output); + } + + #[Test] + public function envVarNameIsRequestedIfArgumentIsMissing(): void + { + $this->commandTester->setInputs([EnvVars::BASE_PATH->value]); + $this->commandTester->execute([]); + $output = $this->commandTester->getDisplay(); + + self::assertStringContainsString('Select the env var to read', $output); + self::assertStringContainsString($this->envVarValue, $output); + } +} diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index 01436967..f478048b 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -4,12 +4,27 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Config; +use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode; + +use function date_default_timezone_get; use function file_get_contents; use function is_file; use function Shlinkio\Shlink\Config\env; use function Shlinkio\Shlink\Config\parseEnvVar; use function sprintf; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_BG_COLOR; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_COLOR; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_MARGIN; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_ROUND_BLOCK_SIZE; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE; +use const Shlinkio\Shlink\DEFAULT_REDIRECT_CACHE_LIFETIME; +use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE; +use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH; + enum EnvVars: string { case DELETE_SHORT_URL_THRESHOLD = 'DELETE_SHORT_URL_THRESHOLD'; @@ -74,10 +89,68 @@ enum EnvVars: string case ROBOTS_USER_AGENTS = 'ROBOTS_USER_AGENTS'; case TIMEZONE = 'TIMEZONE'; case MEMORY_LIMIT = 'MEMORY_LIMIT'; + case INITIAL_API_KEY = 'INITIAL_API_KEY'; + case SKIP_INITIAL_GEOLITE_DOWNLOAD = 'SKIP_INITIAL_GEOLITE_DOWNLOAD'; - public function loadFromEnv(mixed $default = null): mixed + public function loadFromEnv(): mixed { - return env($this->value) ?? $this->loadFromFileEnv() ?? $default; + return env($this->value) ?? $this->loadFromFileEnv() ?? $this->defaultValue(); + } + + private function defaultValue(): string|int|bool|null + { + return match ($this) { + self::MEMORY_LIMIT => '512M', + self::TIMEZONE => date_default_timezone_get(), + + self::DEFAULT_SHORT_CODES_LENGTH => DEFAULT_SHORT_CODES_LENGTH, + self::SHORT_URL_MODE => ShortUrlMode::STRICT->value, + self::IS_HTTPS_ENABLED, self::AUTO_RESOLVE_TITLES => true, + self::REDIRECT_APPEND_EXTRA_PATH, + self::MULTI_SEGMENT_SLUGS_ENABLED, + self::SHORT_URL_TRAILING_SLASH => false, + self::DEFAULT_DOMAIN, self::BASE_PATH => '', + self::CACHE_NAMESPACE => 'Shlink', + + self::REDIS_PUB_SUB_ENABLED, + self::MATOMO_ENABLED, + self::ROBOTS_ALLOW_ALL_SHORT_URLS => false, + + self::DB_NAME => 'shlink', + self::DB_HOST => self::DB_UNIX_SOCKET->loadFromEnv(), + self::DB_DRIVER => 'sqlite', + self::DB_PORT => match (self::DB_DRIVER->loadFromEnv()) { + 'postgres' => '5432', + 'mssql' => '1433', + default => '3306', + }, + + self::MERCURE_INTERNAL_HUB_URL => self::MERCURE_PUBLIC_HUB_URL->loadFromEnv(), + + self::DEFAULT_QR_CODE_SIZE, => DEFAULT_QR_CODE_SIZE, + self::DEFAULT_QR_CODE_MARGIN, => DEFAULT_QR_CODE_MARGIN, + self::DEFAULT_QR_CODE_FORMAT, => DEFAULT_QR_CODE_FORMAT, + self::DEFAULT_QR_CODE_ERROR_CORRECTION, => DEFAULT_QR_CODE_ERROR_CORRECTION, + self::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE, => DEFAULT_QR_CODE_ROUND_BLOCK_SIZE, + self::QR_CODE_FOR_DISABLED_SHORT_URLS, => DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS, + self::DEFAULT_QR_CODE_COLOR, => DEFAULT_QR_CODE_COLOR, + self::DEFAULT_QR_CODE_BG_COLOR, => DEFAULT_QR_CODE_BG_COLOR, + + self::RABBITMQ_ENABLED, self::RABBITMQ_USE_SSL => false, + self::RABBITMQ_PORT => 5672, + self::RABBITMQ_VHOST => '/', + + self::REDIRECT_STATUS_CODE => DEFAULT_REDIRECT_STATUS_CODE->value, + self::REDIRECT_CACHE_LIFETIME => DEFAULT_REDIRECT_CACHE_LIFETIME, + + self::ANONYMIZE_REMOTE_ADDR, self::TRACK_ORPHAN_VISITS => true, + self::DISABLE_TRACKING, + self::DISABLE_IP_TRACKING, + self::DISABLE_REFERRER_TRACKING, + self::DISABLE_UA_TRACKING => false, + + default => null, + }; } /** diff --git a/module/Core/test/Config/EnvVarsTest.php b/module/Core/test/Config/EnvVarsTest.php index dd83393b..c2d8f8e7 100644 --- a/module/Core/test/Config/EnvVarsTest.php +++ b/module/Core/test/Config/EnvVarsTest.php @@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\Config; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Config\EnvVars; @@ -24,9 +25,9 @@ class EnvVarsTest extends TestCase protected function tearDown(): void { - putenv(EnvVars::BASE_PATH->value . '='); - putenv(EnvVars::DB_NAME->value . '='); - putenv(EnvVars::DB_PASSWORD->value . '_FILE='); + putenv(EnvVars::BASE_PATH->value); + putenv(EnvVars::DB_NAME->value); + putenv(EnvVars::DB_PASSWORD->value . '_FILE'); } #[Test, DataProvider('provideExistingEnvVars')] @@ -37,26 +38,23 @@ class EnvVarsTest extends TestCase public static function provideExistingEnvVars(): iterable { - yield 'DB_NAME' => [EnvVars::DB_NAME, true]; - yield 'BASE_PATH' => [EnvVars::BASE_PATH, true]; - yield 'DB_DRIVER' => [EnvVars::DB_DRIVER, false]; + yield 'DB_NAME (is set)' => [EnvVars::DB_NAME, true]; + yield 'BASE_PATH (is set)' => [EnvVars::BASE_PATH, true]; + yield 'DB_DRIVER (has default)' => [EnvVars::DB_DRIVER, true]; yield 'DEFAULT_REGULAR_404_REDIRECT' => [EnvVars::DEFAULT_REGULAR_404_REDIRECT, false]; } #[Test, DataProvider('provideEnvVarsValues')] - public function expectedValueIsLoadedFromEnv(EnvVars $envVar, mixed $expected, mixed $default): void + public function expectedValueIsLoadedFromEnv(EnvVars $envVar, mixed $expected): void { - self::assertEquals($expected, $envVar->loadFromEnv($default)); + self::assertEquals($expected, $envVar->loadFromEnv()); } public static function provideEnvVarsValues(): iterable { - yield 'DB_NAME without default' => [EnvVars::DB_NAME, 'shlink', null]; - yield 'DB_NAME with default' => [EnvVars::DB_NAME, 'shlink', 'foobar']; - yield 'BASE_PATH without default' => [EnvVars::BASE_PATH, 'the_base_path', null]; - yield 'BASE_PATH with default' => [EnvVars::BASE_PATH, 'the_base_path', 'foobar']; - yield 'DB_DRIVER without default' => [EnvVars::DB_DRIVER, null, null]; - yield 'DB_DRIVER with default' => [EnvVars::DB_DRIVER, 'foobar', 'foobar']; + yield 'DB_NAME (is set)' => [EnvVars::DB_NAME, 'shlink']; + yield 'BASE_PATH (is set)' => [EnvVars::BASE_PATH, 'the_base_path']; + yield 'DB_DRIVER (has default)' => [EnvVars::DB_DRIVER, 'sqlite']; } #[Test] @@ -64,4 +62,20 @@ class EnvVarsTest extends TestCase { self::assertEquals('this_is_the_password', EnvVars::DB_PASSWORD->loadFromEnv()); } + + #[Test] + #[TestWith(['mysql', '3306'])] + #[TestWith(['maria', '3306'])] + #[TestWith(['postgres', '5432'])] + #[TestWith(['mssql', '1433'])] + public function defaultPortIsResolvedBasedOnDbDriver(string $dbDriver, string $expectedPort): void + { + putenv(EnvVars::DB_DRIVER->value . '=' . $dbDriver); + + try { + self::assertEquals($expectedPort, EnvVars::DB_PORT->loadFromEnv()); + } finally { + putenv(EnvVars::DB_DRIVER->value); + } + } }