Defined new configs for not found redirects

This commit is contained in:
Alejandro Celaya
2019-11-02 11:30:09 +01:00
parent 6293d57fde
commit b59f4e2805
9 changed files with 115 additions and 25 deletions

View File

@@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
Generated short codes have 5 characters, and shlink makes sure they keep unique, while making it backwards-compatible. Generated short codes have 5 characters, and shlink makes sure they keep unique, while making it backwards-compatible.
* [#497](https://github.com/shlinkio/shlink/issues/497) Officially added support for MariaDB.
#### Changed #### Changed
* [#458](https://github.com/shlinkio/shlink/issues/458) Updated coding styles to use [shlinkio/php-coding-standard](https://github.com/shlinkio/php-coding-standard) v2.0.0. * [#458](https://github.com/shlinkio/shlink/issues/458) Updated coding styles to use [shlinkio/php-coding-standard](https://github.com/shlinkio/php-coding-standard) v2.0.0.

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
return [
'not_found_redirects' => [
'invalid_short_url' => null, // Formerly url_shortener.not_found_short_url.redirect_to
'404' => null,
'base_url' => null,
],
];

View File

@@ -12,10 +12,6 @@ return [
'hostname' => env('SHORTENED_URL_HOSTNAME'), 'hostname' => env('SHORTENED_URL_HOSTNAME'),
], ],
'validate_url' => true, 'validate_url' => true,
'not_found_short_url' => [
'enable_redirection' => false,
'redirect_to' => null,
],
], ],
]; ];

View File

@@ -31,4 +31,5 @@ return (new ConfigAggregator\ConfigAggregator([
], 'data/cache/app_config.php', [ ], 'data/cache/app_config.php', [
Core\Config\SimplifiedConfigParser::class, Core\Config\SimplifiedConfigParser::class,
Core\Config\BasePathPrefixer::class, Core\Config\BasePathPrefixer::class,
Core\Config\DeprecatedConfigParser::class,
]))->getMergedConfig(); ]))->getMergedConfig();

View File

@@ -101,7 +101,9 @@ This is the complete list of supported env vars:
* `DISABLE_TRACK_PARAM`: The name of a query param that can be used to visit short URLs avoiding the visit to be tracked. This feature won't be available if not value is provided. * `DISABLE_TRACK_PARAM`: The name of a query param that can be used to visit short URLs avoiding the visit to be tracked. This feature won't be available if not value is provided.
* `DELETE_SHORT_URL_THRESHOLD`: The amount of visits on short URLs which will not allow them to be deleted. Defaults to `15`. * `DELETE_SHORT_URL_THRESHOLD`: The amount of visits on short URLs which will not allow them to be deleted. Defaults to `15`.
* `VALIDATE_URLS`: Boolean which tells if shlink should validate a status 20x (after following redirects) is returned when trying to shorten a URL. Defaults to `true`. * `VALIDATE_URLS`: Boolean which tells if shlink should validate a status 20x (after following redirects) is returned when trying to shorten a URL. Defaults to `true`.
* `NOT_FOUND_REDIRECT_TO`: If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page. * `INVALID_SHORT_URL_REDIRECT_TO`: If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `404_REDIRECT_TO`: If a URL is provided here, when a user tries to access a URL not matching any one supported by the router, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `BASE_URL_REDIRECT_TO`: If a URL is provided here, when a user tries to access Shlink's base URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `BASE_PATH`: The base path from which you plan to serve shlink, in case you don't want to serve it from the root of the domain. Defaults to `''`. * `BASE_PATH`: The base path from which you plan to serve shlink, in case you don't want to serve it from the root of the domain. Defaults to `''`.
* `REDIS_SERVERS`: A comma-separated list of redis servers where Shlink locks are stored (locks are used to prevent some operations to be run more than once in parallel). * `REDIS_SERVERS`: A comma-separated list of redis servers where Shlink locks are stored (locks are used to prevent some operations to be run more than once in parallel).
@@ -111,6 +113,7 @@ This is the complete list of supported env vars:
In the future, these redis servers could be used for other caching operations performed by shlink. In the future, these redis servers could be used for other caching operations performed by shlink.
* `NOT_FOUND_REDIRECT_TO`: **Deprecated since v1.20 in favor of `INVALID_SHORT_URL_REDIRECT_TO`** If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `SHORTCODE_CHARS`: **Ignored when using Shlink 1.20 or newer**. A charset to use when building short codes. Only needed when using more than one shlink instance ([Multi instance considerations](#multi-instance-considerations)). * `SHORTCODE_CHARS`: **Ignored when using Shlink 1.20 or newer**. A charset to use when building short codes. Only needed when using more than one shlink instance ([Multi instance considerations](#multi-instance-considerations)).
An example using all env vars could look like this: An example using all env vars could look like this:
@@ -151,7 +154,9 @@ The whole configuration should have this format, but it can be split into multip
"short_domain_schema": "https", "short_domain_schema": "https",
"short_domain_host": "doma.in", "short_domain_host": "doma.in",
"validate_url": false, "validate_url": false,
"not_found_redirect_to": "https://my-landing-page.com", "invalid_short_url_redirect_to": "https://my-landing-page.com",
"404_redirect_to": "https://my-landing-page.com",
"base_url_redirect_to": "https://my-landing-page.com",
"redis_servers": [ "redis_servers": [
"tcp://172.20.0.1:6379", "tcp://172.20.0.1:6379",
"tcp://172.20.0.2:6379" "tcp://172.20.0.2:6379"
@@ -163,11 +168,13 @@ The whole configuration should have this format, but it can be split into multip
"password": "123abc", "password": "123abc",
"host": "something.rds.amazonaws.com", "host": "something.rds.amazonaws.com",
"port": "3306" "port": "3306"
} },
"not_found_redirect_to": "https://my-landing-page.com"
} }
``` ```
> This is internally parsed to how shlink expects the config. If you are using a version previous to 1.17.0, this parser is not present and you need to provide a config structure like the one [documented previously](https://github.com/shlinkio/shlink-docker-image/tree/v1.16.3#provide-config-via-volumes). > This is internally parsed to how shlink expects the config. If you are using a version previous to 1.17.0, this parser is not present and you need to provide a config structure like the one [documented previously](https://github.com/shlinkio/shlink-docker-image/tree/v1.16.3#provide-config-via-volumes).
> The `not_found_redirect_to` option has been deprecated when `404_redirect_to` and `base_url_redirect_to` have been introduced. Use `invalid_short_url_redirect_to` instead (however, it will still work for backwards compatibility).
Once created just run shlink with the volume: Once created just run shlink with the volume:

View File

@@ -77,7 +77,7 @@ $helper = new class {
} }
$driverOptions = ! contains(['maria', 'mysql'], $driver) ? [] : [ $driverOptions = ! contains(['maria', 'mysql'], $driver) ? [] : [
// PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', // 1002 -> PDO::MYSQL_ATTR_INIT_COMMAND
1002 => 'SET NAMES utf8', 1002 => 'SET NAMES utf8',
]; ];
return [ return [
@@ -91,13 +91,12 @@ $helper = new class {
]; ];
} }
public function getNotFoundConfig(): array public function getNotFoundRedirectsConfig(): array
{ {
$notFoundRedirectTo = env('NOT_FOUND_REDIRECT_TO');
return [ return [
'enable_redirection' => $notFoundRedirectTo !== null, 'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO', env('NOT_FOUND_REDIRECT_TO')),
'redirect_to' => $notFoundRedirectTo, '404' => env('404_REDIRECT_TO'),
'base_url' => env('BASE_URL_REDIRECT_TO'),
]; ];
} }
}; };
@@ -126,9 +125,10 @@ return [
'hostname' => env('SHORT_DOMAIN_HOST', ''), 'hostname' => env('SHORT_DOMAIN_HOST', ''),
], ],
'validate_url' => (bool) env('VALIDATE_URLS', true), 'validate_url' => (bool) env('VALIDATE_URLS', true),
'not_found_short_url' => $helper->getNotFoundConfig(),
], ],
'not_found_redirects' => $helper->getNotFoundRedirectsConfig(),
'logger' => [ 'logger' => [
'handlers' => [ 'handlers' => [
'shlink_rotating_handler' => [ 'shlink_rotating_handler' => [

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Config;
use function Functional\compose;
class DeprecatedConfigParser
{
public function __invoke(array $config): array
{
return compose([$this, 'parseNotFoundRedirect'])($config);
}
public function parseNotFoundRedirect(array $config): array
{
// If the new config value is already set, keep it
if (isset($config['not_found_redirects']['invalid_short_url'])) {
return $config;
}
$oldRedirectEnabled = $config['url_shortener']['not_found_short_url']['enable_redirection'] ?? false;
$oldRedirectValue = $config['url_shortener']['not_found_short_url']['redirect_to'] ?? null;
$config['not_found_redirects']['invalid_short_url'] = $oldRedirectEnabled ? $oldRedirectValue : null;
return $config;
}
}

View File

@@ -7,10 +7,13 @@ namespace Shlinkio\Shlink\Core\Config;
use Shlinkio\Shlink\Installer\Util\PathCollection; use Shlinkio\Shlink\Installer\Util\PathCollection;
use Zend\Stdlib\ArrayUtils; use Zend\Stdlib\ArrayUtils;
use function array_flip;
use function array_intersect_key; use function array_intersect_key;
use function array_key_exists; use function array_key_exists;
use function array_keys;
use function Functional\contains; use function Functional\contains;
use function Functional\reduce_left; use function Functional\reduce_left;
use function uksort;
class SimplifiedConfigParser class SimplifiedConfigParser
{ {
@@ -19,17 +22,16 @@ class SimplifiedConfigParser
'short_domain_schema' => ['url_shortener', 'domain', 'schema'], 'short_domain_schema' => ['url_shortener', 'domain', 'schema'],
'short_domain_host' => ['url_shortener', 'domain', 'hostname'], 'short_domain_host' => ['url_shortener', 'domain', 'hostname'],
'validate_url' => ['url_shortener', 'validate_url'], 'validate_url' => ['url_shortener', 'validate_url'],
'not_found_redirect_to' => ['url_shortener', 'not_found_short_url', 'redirect_to'], 'not_found_redirect_to' => ['not_found_redirects', 'invalid_short_url'], // Deprecated
'invalid_short_url_redirect_to' => ['not_found_redirects', 'invalid_short_url'],
'404_redirect_to' => ['not_found_redirects', '404'],
'base_url_redirect_to' => ['not_found_redirects', 'base_path'],
'db_config' => ['entity_manager', 'connection'], 'db_config' => ['entity_manager', 'connection'],
'delete_short_url_threshold' => ['delete_short_urls', 'visits_threshold'], 'delete_short_url_threshold' => ['delete_short_urls', 'visits_threshold'],
'redis_servers' => ['redis', 'servers'], 'redis_servers' => ['redis', 'servers'],
'base_path' => ['router', 'base_path'], 'base_path' => ['router', 'base_path'],
]; ];
private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [ private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [
'not_found_redirect_to' => [
'path' => ['url_shortener', 'not_found_short_url', 'enable_redirection'],
'value' => true,
],
'delete_short_url_threshold' => [ 'delete_short_url_threshold' => [
'path' => ['delete_short_urls', 'check_visits_threshold'], 'path' => ['delete_short_urls', 'check_visits_threshold'],
'value' => true, 'value' => true,
@@ -43,9 +45,9 @@ class SimplifiedConfigParser
public function __invoke(array $config): array public function __invoke(array $config): array
{ {
$existingKeys = array_intersect_key($config, self::SIMPLIFIED_CONFIG_MAPPING); $configForExistingKeys = $this->getConfigForKeysInMappingOrderedByMapping($config);
return reduce_left($existingKeys, function ($value, string $key, $c, PathCollection $collection) { return reduce_left($configForExistingKeys, function ($value, string $key, $c, PathCollection $collection) {
$path = self::SIMPLIFIED_CONFIG_MAPPING[$key]; $path = self::SIMPLIFIED_CONFIG_MAPPING[$key];
if (contains(self::SIMPLIFIED_MERGEABLE_CONFIG, $key)) { if (contains(self::SIMPLIFIED_MERGEABLE_CONFIG, $key)) {
$value = ArrayUtils::merge($collection->getValueInPath($path), $value); $value = ArrayUtils::merge($collection->getValueInPath($path), $value);
@@ -60,4 +62,20 @@ class SimplifiedConfigParser
return $collection; return $collection;
}, new PathCollection($config))->toArray(); }, new PathCollection($config))->toArray();
} }
private function getConfigForKeysInMappingOrderedByMapping(array $config): array
{
// Ignore any config which is not defined in the mapping
$configForExistingKeys = array_intersect_key($config, self::SIMPLIFIED_CONFIG_MAPPING);
// Order the config by their key, based on the order it was defined in the mapping.
// This mainly allows deprecating keys and defining new ones that will replace the older and always take
// preference, while the old one keeps working for backwards compatibility if the new one is not provided.
$simplifiedConfigOrder = array_flip(array_keys(self::SIMPLIFIED_CONFIG_MAPPING));
uksort($configForExistingKeys, function (string $a, string $b) use ($simplifiedConfigOrder): int {
return $simplifiedConfigOrder[$a] - $simplifiedConfigOrder[$b];
});
return $configForExistingKeys;
}
} }

View File

@@ -75,10 +75,6 @@ class SimplifiedConfigParserTest extends TestCase
'hostname' => 'doma.in', 'hostname' => 'doma.in',
], ],
'validate_url' => false, 'validate_url' => false,
'not_found_short_url' => [
'redirect_to' => 'foobar.com',
'enable_redirection' => true,
],
], ],
'delete_short_urls' => [ 'delete_short_urls' => [
@@ -102,10 +98,38 @@ class SimplifiedConfigParserTest extends TestCase
'router' => [ 'router' => [
'base_path' => '/foo/bar', 'base_path' => '/foo/bar',
], ],
'not_found_redirects' => [
'invalid_short_url' => 'foobar.com',
],
]; ];
$result = ($this->postProcessor)(array_merge($config, $simplified)); $result = ($this->postProcessor)(array_merge($config, $simplified));
$this->assertEquals(array_merge($expected, $simplified), $result); $this->assertEquals(array_merge($expected, $simplified), $result);
} }
/**
* @test
* @dataProvider provideConfigWithDeprecates
*/
public function properlyMapsDeprecatedConfigs(array $config, string $expected): void
{
$result = ($this->postProcessor)($config);
$this->assertEquals($expected, $result['not_found_redirects']['invalid_short_url']);
}
public function provideConfigWithDeprecates(): iterable
{
yield 'only deprecated config' => [['not_found_redirect_to' => 'old_value'], 'old_value'];
yield 'only new config' => [['invalid_short_url_redirect_to' => 'new_value'], 'new_value'];
yield 'both configs, new first' => [
['invalid_short_url_redirect_to' => 'new_value', 'not_found_redirect_to' => 'old_value'],
'new_value',
];
yield 'both configs, deprecated first' => [
['not_found_redirect_to' => 'old_value', 'invalid_short_url_redirect_to' => 'new_value'],
'new_value',
];
}
} }