mirror of
https://github.com/shlinkio/shlink.git
synced 2024-12-31 19:26:58 -06:00
Merge pull request #224 from acelaya/feature/config-params
Feature/config params
This commit is contained in:
commit
d68dc38959
@ -3,7 +3,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
use const JSON_ERROR_NONE;
|
||||
use function array_key_exists;
|
||||
use function array_shift;
|
||||
use function getenv;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function json_last_error;
|
||||
use function json_last_error_msg;
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
|
||||
@ -40,3 +47,54 @@ function env($key, $default = null)
|
||||
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
function contains($needle, array $haystack): bool
|
||||
{
|
||||
return in_array($needle, $haystack, true);
|
||||
}
|
||||
|
||||
function json_decode(string $json, int $depth = 512, int $options = 0): array
|
||||
{
|
||||
$data = \json_decode($json, true, $depth, $options);
|
||||
if (JSON_ERROR_NONE !== json_last_error()) {
|
||||
throw new Exception\InvalidArgumentException('Error decoding JSON: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
function array_path_exists(array $path, array $array): bool
|
||||
{
|
||||
// As soon as a step is not found, the path does not exist
|
||||
$step = array_shift($path);
|
||||
if (! array_key_exists($step, $array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Once the path is empty, we have found all the parts in the path
|
||||
if (empty($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If current value is not an array, then we have not found the path
|
||||
$newArray = $array[$step];
|
||||
if (! is_array($newArray)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_path_exists($path, $newArray);
|
||||
}
|
||||
|
||||
function array_get_path(array $path, array $array)
|
||||
{
|
||||
do {
|
||||
$step = array_shift($path);
|
||||
if (! is_array($array) || ! array_key_exists($step, $array)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$array = $array[$step];
|
||||
} while (! empty($path));
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
@ -3,10 +3,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Exception;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class WrongIpException extends RuntimeException
|
||||
{
|
||||
public static function fromIpAddress($ipAddress, \Throwable $prev = null): self
|
||||
{
|
||||
return new self(\sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
||||
return new self(sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,17 @@ namespace Shlinkio\Shlink\Common\Factory;
|
||||
use Doctrine\Common\Cache;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Shlinkio\Shlink\Common;
|
||||
use Memcached;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
use function Shlinkio\Shlink\Common\contains;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
class CacheFactory implements FactoryInterface
|
||||
{
|
||||
const VALID_CACHE_ADAPTERS = [
|
||||
private const VALID_CACHE_ADAPTERS = [
|
||||
Cache\ApcuCache::class,
|
||||
Cache\ArrayCache::class,
|
||||
Cache\FilesystemCache::class,
|
||||
@ -51,14 +53,12 @@ class CacheFactory implements FactoryInterface
|
||||
{
|
||||
// Try to get the adapter from config
|
||||
$config = $container->get('config');
|
||||
if (isset($config['cache'], $config['cache']['adapter'])
|
||||
&& in_array($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS)
|
||||
) {
|
||||
if (isset($config['cache']['adapter']) && contains($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS)) {
|
||||
return $this->resolveCacheAdapter($config['cache']);
|
||||
}
|
||||
|
||||
// If the adapter has not been set in config, create one based on environment
|
||||
return Common\env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||
return env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,8 +75,8 @@ class CacheFactory implements FactoryInterface
|
||||
case Cache\PhpFileCache::class:
|
||||
return new $cacheConfig['adapter']($cacheConfig['options']['dir']);
|
||||
case Cache\MemcachedCache::class:
|
||||
$memcached = new \Memcached();
|
||||
$servers = isset($cacheConfig['options']['servers']) ? $cacheConfig['options']['servers'] : [];
|
||||
$memcached = new Memcached();
|
||||
$servers = $cacheConfig['options']['servers'] ?? [];
|
||||
|
||||
foreach ($servers as $server) {
|
||||
if (! isset($server['host'])) {
|
||||
|
@ -5,7 +5,10 @@ namespace Shlinkio\Shlink\Common\Service;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use function Shlinkio\Shlink\Common\json_decode;
|
||||
use function sprintf;
|
||||
|
||||
class IpApiLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
@ -29,10 +32,12 @@ class IpApiLocationResolver implements IpLocationResolverInterface
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
{
|
||||
try {
|
||||
$response = $this->httpClient->get(\sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||
return $this->mapFields(\json_decode((string) $response->getBody(), true));
|
||||
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||
return $this->mapFields(json_decode((string) $response->getBody()));
|
||||
} catch (GuzzleException $e) {
|
||||
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new WrongIpException('IP-API returned invalid body while locating IP address', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,11 @@ use Cake\Chronos\Chronos;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use function array_column;
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
use function key;
|
||||
use function Shlinkio\Shlink\Common\contains;
|
||||
|
||||
class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface
|
||||
{
|
||||
@ -55,19 +60,19 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
'shortCode' => 'shortCode',
|
||||
'dateCreated' => 'dateCreated',
|
||||
];
|
||||
$fieldName = \is_array($orderBy) ? \key($orderBy) : $orderBy;
|
||||
$order = \is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
|
||||
$fieldName = is_array($orderBy) ? key($orderBy) : $orderBy;
|
||||
$order = is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
|
||||
|
||||
if (\in_array($fieldName, ['visits', 'visitsCount', 'visitCount'], true)) {
|
||||
if (contains($fieldName, ['visits', 'visitsCount', 'visitCount'])) {
|
||||
$qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
|
||||
->leftJoin('s.visits', 'v')
|
||||
->groupBy('s')
|
||||
->orderBy('totalVisits', $order);
|
||||
|
||||
return \array_column($qb->getQuery()->getResult(), 0);
|
||||
return array_column($qb->getQuery()->getResult(), 0);
|
||||
}
|
||||
|
||||
if (\array_key_exists($fieldName, $fieldNameMap)) {
|
||||
if (array_key_exists($fieldName, $fieldNameMap)) {
|
||||
$qb->orderBy('s.' . $fieldNameMap[$fieldName], $order);
|
||||
}
|
||||
return $qb->getQuery()->getResult();
|
||||
|
@ -9,6 +9,9 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||
use function array_shift;
|
||||
use function explode;
|
||||
use function Shlinkio\Shlink\Common\contains;
|
||||
|
||||
class NotFoundHandler implements RequestHandlerInterface
|
||||
{
|
||||
@ -39,12 +42,12 @@ class NotFoundHandler implements RequestHandlerInterface
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$accepts = \explode(',', $request->getHeaderLine('Accept'));
|
||||
$accept = \array_shift($accepts);
|
||||
$accepts = explode(',', $request->getHeaderLine('Accept'));
|
||||
$accept = array_shift($accepts);
|
||||
$status = StatusCodeInterface::STATUS_NOT_FOUND;
|
||||
|
||||
// If the first accepted type is json, return a json response
|
||||
if (\in_array($accept, ['application/json', 'text/json', 'application/x-json'], true)) {
|
||||
if (contains($accept, ['application/json', 'text/json', 'application/x-json'])) {
|
||||
return new Response\JsonResponse([
|
||||
'error' => 'NOT_FOUND',
|
||||
'message' => 'Not found',
|
||||
|
@ -8,6 +8,7 @@ use Psr\Container\NotFoundExceptionInterface;
|
||||
use Shlinkio\Shlink\Installer\Config\ConfigCustomizerManagerInterface;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Installer\Util\AskUtilsTrait;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
@ -23,6 +24,8 @@ use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class InstallCommand extends Command
|
||||
{
|
||||
use AskUtilsTrait;
|
||||
|
||||
public const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
|
||||
|
||||
/**
|
||||
@ -143,7 +146,7 @@ class InstallCommand extends Command
|
||||
$this->io->writeln(['<info>Custom configuration properly generated!</info>', '']);
|
||||
|
||||
// If current command is not update, generate database
|
||||
if (! $this->isUpdate) {
|
||||
if (! $this->isUpdate) {
|
||||
$this->io->write('Initializing database...');
|
||||
if (! $this->runPhpCommand(
|
||||
'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
|
||||
@ -186,7 +189,10 @@ class InstallCommand extends Command
|
||||
$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?');
|
||||
$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;
|
||||
}
|
||||
@ -194,7 +200,9 @@ class InstallCommand extends Command
|
||||
// Ask the user for the older shlink path
|
||||
$keepAsking = true;
|
||||
do {
|
||||
$config->setImportedInstallationPath($this->io->ask(
|
||||
$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;
|
||||
|
@ -6,28 +6,51 @@ namespace Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function array_diff;
|
||||
use function array_keys;
|
||||
|
||||
class ApplicationConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
public const SECRET = 'SECRET';
|
||||
public const DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM';
|
||||
private const EXPECTED_KEYS = [
|
||||
self::SECRET,
|
||||
self::DISABLE_TRACK_PARAM,
|
||||
];
|
||||
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void
|
||||
{
|
||||
$io->title('APPLICATION');
|
||||
$app = $appConfig->getApp();
|
||||
$keysToAskFor = $appConfig->hasApp() ? array_diff(self::EXPECTED_KEYS, array_keys($app)) : self::EXPECTED_KEYS;
|
||||
|
||||
if ($appConfig->hasApp() && $io->confirm('Do you want to keep imported application config?')) {
|
||||
if (empty($keysToAskFor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$appConfig->setApp([
|
||||
'SECRET' => $io->ask(
|
||||
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one) '
|
||||
. '<fg=red>[DEPRECATED. TO BE REMOVED]</>'
|
||||
) ?: $this->generateRandomString(32),
|
||||
'DISABLE_TRACK_PARAM' => $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)'
|
||||
),
|
||||
]);
|
||||
$io->title('APPLICATION');
|
||||
foreach ($keysToAskFor as $key) {
|
||||
$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) '
|
||||
. '<fg=red>[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)'
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,30 @@ use Shlinkio\Shlink\Installer\Util\AskUtilsTrait;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use function array_diff;
|
||||
use function array_keys;
|
||||
use function Shlinkio\Shlink\Common\contains;
|
||||
|
||||
class DatabaseConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
use AskUtilsTrait;
|
||||
|
||||
public const DRIVER = 'DRIVER';
|
||||
public const NAME = 'NAME';
|
||||
public const USER = 'USER';
|
||||
public const PASSWORD = 'PASSWORD';
|
||||
public const HOST = 'HOST';
|
||||
public const PORT = 'PORT';
|
||||
private const DRIVER_DEPENDANT_OPTIONS = [
|
||||
self::DRIVER,
|
||||
self::NAME,
|
||||
self::USER,
|
||||
self::PASSWORD,
|
||||
self::HOST,
|
||||
self::PORT,
|
||||
];
|
||||
private const EXPECTED_KEYS = self::DRIVER_DEPENDANT_OPTIONS; // Same now, but could change in the future
|
||||
|
||||
private const DATABASE_DRIVERS = [
|
||||
'MySQL' => 'pdo_mysql',
|
||||
'PostgreSQL' => 'pdo_pgsql',
|
||||
@ -34,41 +53,79 @@ class DatabaseConfigCustomizer implements ConfigCustomizerInterface
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void
|
||||
{
|
||||
$io->title('DATABASE');
|
||||
$titlePrinted = false;
|
||||
$db = $appConfig->getDatabase();
|
||||
$doImport = $appConfig->hasDatabase();
|
||||
$keysToAskFor = $doImport ? array_diff(self::EXPECTED_KEYS, array_keys($db)) : self::EXPECTED_KEYS;
|
||||
|
||||
if ($appConfig->hasDatabase() && $io->confirm('Do you want to keep imported database config?')) {
|
||||
// If the user selected to keep DB config and is configured to use sqlite, copy DB file
|
||||
if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
// If the user selected to keep DB, try to import SQLite database
|
||||
if ($doImport) {
|
||||
$this->importSqliteDbFile($io, $appConfig);
|
||||
}
|
||||
|
||||
if (empty($keysToAskFor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select database type
|
||||
$params = [];
|
||||
$databases = \array_keys(self::DATABASE_DRIVERS);
|
||||
$dbType = $io->choice('Select database type', $databases, $databases[0]);
|
||||
$params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
|
||||
|
||||
// Ask for connection params if database is not SQLite
|
||||
if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
|
||||
$params['NAME'] = $io->ask('Database name', 'shlink');
|
||||
$params['USER'] = $this->askRequired($io, 'username', 'Database username');
|
||||
$params['PASSWORD'] = $this->askRequired($io, 'password', 'Database password');
|
||||
$params['HOST'] = $io->ask('Database host', 'localhost');
|
||||
$params['PORT'] = $io->ask('Database port', $this->getDefaultDbPort($params['DRIVER']));
|
||||
// If the driver is one of the params to ask for, ask for it first
|
||||
if (contains(self::DRIVER, $keysToAskFor)) {
|
||||
$io->title('DATABASE');
|
||||
$titlePrinted = true;
|
||||
$db[self::DRIVER] = $this->ask($io, self::DRIVER);
|
||||
$keysToAskFor = array_diff($keysToAskFor, [self::DRIVER]);
|
||||
}
|
||||
|
||||
$appConfig->setDatabase($params);
|
||||
// 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
|
||||
|
@ -5,23 +5,48 @@ namespace Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function array_diff;
|
||||
use function array_keys;
|
||||
|
||||
class LanguageConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
public const DEFAULT_LANG = 'DEFAULT';
|
||||
public const CLI_LANG = 'CLI';
|
||||
private const EXPECTED_KEYS = [
|
||||
self::DEFAULT_LANG,
|
||||
self::CLI_LANG,
|
||||
];
|
||||
|
||||
private const SUPPORTED_LANGUAGES = ['en', 'es'];
|
||||
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void
|
||||
{
|
||||
$io->title('LANGUAGE');
|
||||
$lang = $appConfig->getLanguage();
|
||||
$keysToAskFor = $appConfig->hasLanguage()
|
||||
? array_diff(self::EXPECTED_KEYS, array_keys($lang))
|
||||
: self::EXPECTED_KEYS;
|
||||
|
||||
if ($appConfig->hasLanguage() && $io->confirm('Do you want to keep imported language?')) {
|
||||
if (empty($keysToAskFor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$appConfig->setLanguage([
|
||||
'DEFAULT' => $this->chooseLanguage($io, 'Select default language for the application in general'),
|
||||
'CLI' => $this->chooseLanguage($io, 'Select default language for CLI executions'),
|
||||
]);
|
||||
$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 in general');
|
||||
case self::CLI_LANG:
|
||||
return $this->chooseLanguage($io, 'Select default language for CLI executions');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function chooseLanguage(SymfonyStyle $io, string $message): string
|
||||
|
@ -7,31 +7,61 @@ use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Installer\Util\AskUtilsTrait;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function array_diff;
|
||||
use function array_keys;
|
||||
use function str_shuffle;
|
||||
|
||||
class UrlShortenerConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
use AskUtilsTrait;
|
||||
|
||||
public const SCHEMA = 'SCHEMA';
|
||||
public const HOSTNAME = 'HOSTNAME';
|
||||
public const CHARS = 'CHARS';
|
||||
public const VALIDATE_URL = 'VALIDATE_URL';
|
||||
private const EXPECTED_KEYS = [
|
||||
self::SCHEMA,
|
||||
self::HOSTNAME,
|
||||
self::CHARS,
|
||||
self::VALIDATE_URL,
|
||||
];
|
||||
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void
|
||||
{
|
||||
$io->title('URL SHORTENER');
|
||||
$urlShortener = $appConfig->getUrlShortener();
|
||||
$doImport = $appConfig->hasUrlShortener();
|
||||
$keysToAskFor = $doImport ? array_diff(self::EXPECTED_KEYS, array_keys($urlShortener)) : self::EXPECTED_KEYS;
|
||||
|
||||
if ($appConfig->hasUrlShortener() && $io->confirm('Do you want to keep imported URL shortener config?')) {
|
||||
if (empty($keysToAskFor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask for URL shortener params
|
||||
$appConfig->setUrlShortener([
|
||||
'SCHEMA' => $io->choice(
|
||||
'Select schema for generated short URLs',
|
||||
['http', 'https'],
|
||||
'http'
|
||||
),
|
||||
'HOSTNAME' => $this->askRequired($io, 'hostname', 'Hostname for generated URLs'),
|
||||
'CHARS' => $io->ask('Character set for generated short codes (leave empty to autogenerate one)')
|
||||
?: str_shuffle(UrlShortener::DEFAULT_CHARS),
|
||||
'VALIDATE_URL' => $io->confirm('Do you want to validate long urls by 200 HTTP status code on response'),
|
||||
]);
|
||||
$io->title('URL SHORTENER');
|
||||
foreach ($keysToAskFor as $key) {
|
||||
$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:
|
||||
return $io->ask(
|
||||
'Character set for generated short codes (leave empty to autogenerate one)'
|
||||
) ?: str_shuffle(UrlShortener::DEFAULT_CHARS);
|
||||
case self::VALIDATE_URL:
|
||||
return $io->confirm('Do you want to validate long urls by 200 HTTP status code on response');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Model;
|
||||
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\ApplicationConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\DatabaseConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\LanguageConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\UrlShortenerConfigCustomizer;
|
||||
use Zend\Stdlib\ArraySerializableInterface;
|
||||
use function Shlinkio\Shlink\Common\array_get_path;
|
||||
use function Shlinkio\Shlink\Common\array_path_exists;
|
||||
|
||||
final class CustomizableAppConfig implements ArraySerializableInterface
|
||||
{
|
||||
@ -112,51 +118,52 @@ final class CustomizableAppConfig implements ArraySerializableInterface
|
||||
|
||||
public function exchangeArray(array $array): void
|
||||
{
|
||||
$this->setApp([
|
||||
'SECRET' => $array['app_options']['secret_key'] ?? null,
|
||||
]);
|
||||
$this->setApp($this->mapExistingPathsToKeys([
|
||||
ApplicationConfigCustomizer::SECRET => ['app_options', 'secret_key'],
|
||||
ApplicationConfigCustomizer::DISABLE_TRACK_PARAM => ['app_options', 'disable_track_param'],
|
||||
], $array));
|
||||
|
||||
$this->deserializeDatabase($array['entity_manager']['connection'] ?? []);
|
||||
$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'],
|
||||
], $array));
|
||||
|
||||
$this->setLanguage([
|
||||
'DEFAULT' => $array['translator']['locale'] ?? null,
|
||||
'CLI' => $array['cli']['locale'] ?? null,
|
||||
]);
|
||||
$this->setLanguage($this->mapExistingPathsToKeys([
|
||||
LanguageConfigCustomizer::DEFAULT_LANG => ['translator', 'locale'],
|
||||
LanguageConfigCustomizer::CLI_LANG => ['cli', 'locale'],
|
||||
], $array));
|
||||
|
||||
$this->setUrlShortener([
|
||||
'SCHEMA' => $array['url_shortener']['domain']['schema'] ?? null,
|
||||
'HOSTNAME' => $array['url_shortener']['domain']['hostname'] ?? null,
|
||||
'CHARS' => $array['url_shortener']['shortcode_chars'] ?? null,
|
||||
'VALIDATE_URL' => $array['url_shortener']['validate_url'] ?? true,
|
||||
]);
|
||||
$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'],
|
||||
], $array));
|
||||
}
|
||||
|
||||
private function deserializeDatabase(array $conn): void
|
||||
private function mapExistingPathsToKeys(array $map, array $config): array
|
||||
{
|
||||
if (! isset($conn['driver'])) {
|
||||
return;
|
||||
}
|
||||
$driver = $conn['driver'];
|
||||
|
||||
$params = ['DRIVER' => $driver];
|
||||
if ($driver !== 'pdo_sqlite') {
|
||||
$params['USER'] = $conn['user'] ?? null;
|
||||
$params['PASSWORD'] = $conn['password'] ?? null;
|
||||
$params['NAME'] = $conn['dbname'] ?? null;
|
||||
$params['HOST'] = $conn['host'] ?? null;
|
||||
$params['PORT'] = $conn['port'] ?? null;
|
||||
$result = [];
|
||||
foreach ($map as $key => $path) {
|
||||
if (array_path_exists($path, $config)) {
|
||||
$result[$key] = array_get_path($path, $config);
|
||||
}
|
||||
}
|
||||
|
||||
$this->setDatabase($params);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getArrayCopy(): array
|
||||
{
|
||||
$dbDriver = $this->database['DRIVER'] ?? '';
|
||||
$dbDriver = $this->database[DatabaseConfigCustomizer::DRIVER] ?? '';
|
||||
$config = [
|
||||
'app_options' => [
|
||||
'secret_key' => $this->app['SECRET'] ?? '',
|
||||
'disable_track_param' => $this->app['DISABLE_TRACK_PARAM'] ?? null,
|
||||
'secret_key' => $this->app[ApplicationConfigCustomizer::SECRET] ?? '',
|
||||
'disable_track_param' => $this->app[ApplicationConfigCustomizer::DISABLE_TRACK_PARAM] ?? null,
|
||||
],
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
@ -164,18 +171,18 @@ final class CustomizableAppConfig implements ArraySerializableInterface
|
||||
],
|
||||
],
|
||||
'translator' => [
|
||||
'locale' => $this->language['DEFAULT'] ?? 'en',
|
||||
'locale' => $this->language[LanguageConfigCustomizer::DEFAULT_LANG] ?? 'en',
|
||||
],
|
||||
'cli' => [
|
||||
'locale' => $this->language['CLI'] ?? 'en',
|
||||
'locale' => $this->language[LanguageConfigCustomizer::CLI_LANG] ?? 'en',
|
||||
],
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => $this->urlShortener['SCHEMA'] ?? 'http',
|
||||
'hostname' => $this->urlShortener['HOSTNAME'] ?? '',
|
||||
'schema' => $this->urlShortener[UrlShortenerConfigCustomizer::SCHEMA] ?? 'http',
|
||||
'hostname' => $this->urlShortener[UrlShortenerConfigCustomizer::HOSTNAME] ?? '',
|
||||
],
|
||||
'shortcode_chars' => $this->urlShortener['CHARS'] ?? '',
|
||||
'validate_url' => $this->urlShortener['VALIDATE_URL'] ?? true,
|
||||
'shortcode_chars' => $this->urlShortener[UrlShortenerConfigCustomizer::CHARS] ?? '',
|
||||
'validate_url' => $this->urlShortener[UrlShortenerConfigCustomizer::VALIDATE_URL] ?? true,
|
||||
],
|
||||
];
|
||||
|
||||
@ -183,11 +190,12 @@ final class CustomizableAppConfig implements ArraySerializableInterface
|
||||
if ($dbDriver === 'pdo_sqlite') {
|
||||
$config['entity_manager']['connection']['path'] = self::SQLITE_DB_PATH;
|
||||
} else {
|
||||
$config['entity_manager']['connection']['user'] = $this->database['USER'] ?? '';
|
||||
$config['entity_manager']['connection']['password'] = $this->database['PASSWORD'] ?? '';
|
||||
$config['entity_manager']['connection']['dbname'] = $this->database['NAME'] ?? '';
|
||||
$config['entity_manager']['connection']['host'] = $this->database['HOST'] ?? '';
|
||||
$config['entity_manager']['connection']['port'] = $this->database['PORT'] ?? '';
|
||||
$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'] = [
|
||||
|
@ -17,6 +17,8 @@ trait AskUtilsTrait
|
||||
if (empty($value)) {
|
||||
throw MissingRequiredOptionException::fromOption($optionName);
|
||||
};
|
||||
|
||||
return $value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -50,10 +50,9 @@ class ApplicationConfigCustomizerTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
public function onlyMissingOptionsAreAsked()
|
||||
{
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('the_new_secret');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('disable_param');
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
@ -62,30 +61,31 @@ class ApplicationConfigCustomizerTest extends TestCase
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'the_new_secret',
|
||||
'DISABLE_TRACK_PARAM' => 'the_new_secret',
|
||||
'SECRET' => 'foo',
|
||||
'DISABLE_TRACK_PARAM' => 'disable_param',
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
public function noQuestionsAskedIfImportedConfigContainsEverything()
|
||||
{
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('the_new_secret');
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
'DISABLE_TRACK_PARAM' => 'the_new_secret',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'foo',
|
||||
'DISABLE_TRACK_PARAM' => 'the_new_secret',
|
||||
], $config->getApp());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$ask->shouldNotHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
@ -62,64 +62,62 @@ class DatabaseConfigCustomizerTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
public function onlyMissingOptionsAreAsked()
|
||||
{
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('MySQL');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('MySQL');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('asked');
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setDatabase([
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
'NAME' => 'foo',
|
||||
'PASSWORD' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_mysql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
'NAME' => 'foo',
|
||||
'USER' => 'asked',
|
||||
'PASSWORD' => 'foo',
|
||||
'HOST' => 'asked',
|
||||
'PORT' => 'asked',
|
||||
], $config->getDatabase());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$ask->shouldHaveBeenCalledTimes(5);
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
$ask->shouldHaveBeenCalledTimes(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
public function noQuestionsAskedIfImportedConfigContainsEverything()
|
||||
{
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
$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' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
'NAME' => 'foo',
|
||||
'USER' => 'foo',
|
||||
'PASSWORD' => 'foo',
|
||||
'HOST' => 'foo',
|
||||
'PORT' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
'NAME' => 'foo',
|
||||
'USER' => 'foo',
|
||||
'PASSWORD' => 'foo',
|
||||
'HOST' => 'foo',
|
||||
'PORT' => 'foo',
|
||||
], $config->getDatabase());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
$ask->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -127,7 +125,6 @@ class DatabaseConfigCustomizerTest extends TestCase
|
||||
*/
|
||||
public function sqliteDatabaseIsImportedWhenRequested()
|
||||
{
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
$copy = $this->filesystem->copy(Argument::cetera())->willReturn(null);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
@ -140,7 +137,6 @@ class DatabaseConfigCustomizerTest extends TestCase
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_sqlite',
|
||||
], $config->getDatabase());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$copy->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ class LanguageConfigCustomizerTest extends TestCase
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
$ask = $this->io->choice(Argument::cetera())->willReturn('en');
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('en');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
@ -43,38 +43,35 @@ class LanguageConfigCustomizerTest extends TestCase
|
||||
'DEFAULT' => 'en',
|
||||
'CLI' => 'en',
|
||||
], $config->getLanguage());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$choice->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
public function onlyMissingOptionsAreAsked()
|
||||
{
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('es');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setLanguage([
|
||||
'DEFAULT' => 'en',
|
||||
'CLI' => 'en',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DEFAULT' => 'es',
|
||||
'DEFAULT' => 'en',
|
||||
'CLI' => 'es',
|
||||
], $config->getLanguage());
|
||||
$choice->shouldHaveBeenCalledTimes(2);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
public function noQuestionsAskedIfImportedConfigContainsEverything()
|
||||
{
|
||||
$ask = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('en');
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setLanguage([
|
||||
@ -88,6 +85,6 @@ class LanguageConfigCustomizerTest extends TestCase
|
||||
'DEFAULT' => 'es',
|
||||
'CLI' => 'es',
|
||||
], $config->getLanguage());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +33,8 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('something');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('something');
|
||||
$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();
|
||||
|
||||
@ -42,9 +42,9 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
||||
|
||||
$this->assertTrue($config->hasUrlShortener());
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'something',
|
||||
'HOSTNAME' => 'something',
|
||||
'CHARS' => 'something',
|
||||
'SCHEMA' => 'chosen',
|
||||
'HOSTNAME' => 'asked',
|
||||
'CHARS' => 'asked',
|
||||
'VALIDATE_URL' => true,
|
||||
], $config->getUrlShortener());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
@ -55,16 +55,44 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
public function onlyMissingOptionsAreAsked()
|
||||
{
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('foo');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('foo');
|
||||
$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' => 'bar',
|
||||
'HOSTNAME' => 'bar',
|
||||
'CHARS' => 'bar',
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'asked',
|
||||
'VALIDATE_URL' => false,
|
||||
], $config->getUrlShortener());
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,
|
||||
]);
|
||||
|
||||
@ -74,36 +102,10 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'foo',
|
||||
'VALIDATE_URL' => false,
|
||||
'VALIDATE_URL' => true,
|
||||
], $config->getUrlShortener());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setUrlShortener([
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'foo',
|
||||
'VALIDATE_URL' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'foo',
|
||||
'VALIDATE_URL' => 'foo',
|
||||
], $config->getUrlShortener());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
$ask->shouldNotHaveBeenCalled();
|
||||
$confirm->shouldNotHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
29
module/Installer/test/ConfigProviderTest.php
Normal file
29
module/Installer/test/ConfigProviderTest.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Installer;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Installer\ConfigProvider;
|
||||
|
||||
class ConfigProviderTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ConfigProvider
|
||||
*/
|
||||
protected $configProvider;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->configProvider = new ConfigProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function configIsReturned()
|
||||
{
|
||||
$config = $this->configProvider->__invoke();
|
||||
$this->assertEmpty($config);
|
||||
}
|
||||
}
|
40
module/Installer/test/CustomizableAppConfigTest.php
Normal file
40
module/Installer/test/CustomizableAppConfigTest.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Installer;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\ApplicationConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\LanguageConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
|
||||
class CustomizableAppConfigTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function exchangeArrayIgnoresAnyNonProvidedKey()
|
||||
{
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$config->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());
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function Shlinkio\Shlink\Common\contains;
|
||||
use function sprintf;
|
||||
|
||||
class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface, RequestMethodInterface
|
||||
@ -72,7 +72,7 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa
|
||||
if ($routeResult === null
|
||||
|| $routeResult->isFailure()
|
||||
|| $request->getMethod() === self::METHOD_OPTIONS
|
||||
|| in_array($routeResult->getMatchedRouteName(), $this->routesWhitelist, true)
|
||||
|| contains($routeResult->getMatchedRouteName(), $this->routesWhitelist)
|
||||
) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
@ -8,7 +8,12 @@ use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\RuntimeException;
|
||||
use function array_shift;
|
||||
use function explode;
|
||||
use function parse_str;
|
||||
use function Shlinkio\Shlink\Common\contains;
|
||||
use function Shlinkio\Shlink\Common\json_decode;
|
||||
use function trim;
|
||||
|
||||
class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface
|
||||
{
|
||||
@ -27,17 +32,17 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
|
||||
$currentParams = $request->getParsedBody();
|
||||
|
||||
// In requests that do not allow body or if the body has already been parsed, continue to next middleware
|
||||
if (! empty($currentParams) || \in_array($method, [
|
||||
if (! empty($currentParams) || contains($method, [
|
||||
self::METHOD_GET,
|
||||
self::METHOD_HEAD,
|
||||
self::METHOD_OPTIONS,
|
||||
], true)) {
|
||||
])) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
// If the accepted content is JSON, try to parse the body from JSON
|
||||
$contentType = $this->getRequestContentType($request);
|
||||
if (\in_array($contentType, ['application/json', 'text/json', 'application/x-json'], true)) {
|
||||
if (contains($contentType, ['application/json', 'text/json', 'application/x-json'])) {
|
||||
return $handler->handle($this->parseFromJson($request));
|
||||
}
|
||||
|
||||
@ -51,14 +56,13 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
|
||||
private function getRequestContentType(Request $request): string
|
||||
{
|
||||
$contentType = $request->getHeaderLine('Content-type');
|
||||
$contentTypes = \explode(';', $contentType);
|
||||
return \trim(\array_shift($contentTypes));
|
||||
$contentTypes = explode(';', $contentType);
|
||||
return trim(array_shift($contentTypes));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return Request
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private function parseFromJson(Request $request): Request
|
||||
{
|
||||
@ -67,11 +71,7 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
|
||||
return $request;
|
||||
}
|
||||
|
||||
$parsedJson = \json_decode($rawBody, true);
|
||||
if (\json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new RuntimeException(\sprintf('Error when parsing JSON request body: %s', \json_last_error_msg()));
|
||||
}
|
||||
|
||||
$parsedJson = json_decode($rawBody);
|
||||
return $request->withParsedBody($parsedJson);
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\Tag\ListTagsAction;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
use function Shlinkio\Shlink\Common\json_decode;
|
||||
|
||||
class ListTagsActionTest extends TestCase
|
||||
{
|
||||
@ -42,7 +43,7 @@ class ListTagsActionTest extends TestCase
|
||||
'tags' => [
|
||||
'data' => ['foo', 'bar'],
|
||||
],
|
||||
], \json_decode((string) $resp->getBody(), true));
|
||||
], json_decode((string) $resp->getBody()));
|
||||
$listTags->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
@ -24,10 +24,7 @@
|
||||
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">./module/Common/src</directory>
|
||||
<directory suffix=".php">./module/Core/src</directory>
|
||||
<directory suffix=".php">./module/Rest/src</directory>
|
||||
<directory suffix=".php">./module/CLI/src</directory>
|
||||
<directory suffix=".php">./module/*/src</directory>
|
||||
|
||||
<exclude>
|
||||
<directory suffix=".php">./module/Core/src/Repository</directory>
|
||||
|
Loading…
Reference in New Issue
Block a user