mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
Merge pull request #526 from acelaya-forks/feature/enhanced-not-found-redirect
Feature/enhanced not found redirect
This commit is contained in:
commit
bf24660ddb
12
CHANGELOG.md
12
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).
|
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
## [Unreleased]
|
## 1.20.0 - 2019-11-02
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
|
||||||
@ -14,6 +14,16 @@ 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.
|
||||||
|
|
||||||
|
* [#418](https://github.com/shlinkio/shlink/issues/418) and [#419](https://github.com/shlinkio/shlink/issues/419) Added support to redirect any 404 error to a custom URL.
|
||||||
|
|
||||||
|
It was already possible to configure this but only for invalid short URLs. Shlink now also support configuring redirects for the base URL and any other kind of "not found" error.
|
||||||
|
|
||||||
|
The three URLs can be different, and it is already possible to pass them to the docker image via configuration or env vars.
|
||||||
|
|
||||||
|
The installer also asks for these two new configuration options.
|
||||||
|
|
||||||
|
* [#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.
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
"pugx/shortid-php": "^0.5",
|
"pugx/shortid-php": "^0.5",
|
||||||
"shlinkio/shlink-common": "^2.2.1",
|
"shlinkio/shlink-common": "^2.2.1",
|
||||||
"shlinkio/shlink-event-dispatcher": "^1.0",
|
"shlinkio/shlink-event-dispatcher": "^1.0",
|
||||||
"shlinkio/shlink-installer": "^2.1",
|
"shlinkio/shlink-installer": "^3.0",
|
||||||
"shlinkio/shlink-ip-geolocation": "^1.1",
|
"shlinkio/shlink-ip-geolocation": "^1.1",
|
||||||
"symfony/console": "^4.3",
|
"symfony/console": "^4.3",
|
||||||
"symfony/filesystem": "^4.3",
|
"symfony/filesystem": "^4.3",
|
||||||
|
@ -12,8 +12,6 @@ return [
|
|||||||
Plugin\UrlShortenerConfigCustomizer::HOSTNAME,
|
Plugin\UrlShortenerConfigCustomizer::HOSTNAME,
|
||||||
Plugin\UrlShortenerConfigCustomizer::CHARS,
|
Plugin\UrlShortenerConfigCustomizer::CHARS,
|
||||||
Plugin\UrlShortenerConfigCustomizer::VALIDATE_URL,
|
Plugin\UrlShortenerConfigCustomizer::VALIDATE_URL,
|
||||||
Plugin\UrlShortenerConfigCustomizer::ENABLE_NOT_FOUND_REDIRECTION,
|
|
||||||
Plugin\UrlShortenerConfigCustomizer::NOT_FOUND_REDIRECT_TO,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
Plugin\ApplicationConfigCustomizer::class => [
|
Plugin\ApplicationConfigCustomizer::class => [
|
||||||
@ -32,6 +30,12 @@ return [
|
|||||||
Plugin\DatabaseConfigCustomizer::HOST,
|
Plugin\DatabaseConfigCustomizer::HOST,
|
||||||
Plugin\DatabaseConfigCustomizer::PORT,
|
Plugin\DatabaseConfigCustomizer::PORT,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
Plugin\RedirectsConfigCustomizer::class => [
|
||||||
|
Plugin\RedirectsConfigCustomizer::INVALID_SHORT_URL_REDIRECT_TO,
|
||||||
|
Plugin\RedirectsConfigCustomizer::REGULAR_404_REDIRECT_TO,
|
||||||
|
Plugin\RedirectsConfigCustomizer::BASE_URL_REDIRECT_TO,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'installation_commands' => [
|
'installation_commands' => [
|
||||||
|
13
config/autoload/redirects.global.php
Normal file
13
config/autoload/redirects.global.php
Normal 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
|
||||||
|
'regular_404' => null,
|
||||||
|
'base_url' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
@ -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,
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -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();
|
||||||
|
@ -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.
|
||||||
|
* `REGULAR_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:
|
||||||
@ -130,7 +133,9 @@ docker run \
|
|||||||
-e DISABLE_TRACK_PARAM="no-track" \
|
-e DISABLE_TRACK_PARAM="no-track" \
|
||||||
-e DELETE_SHORT_URL_THRESHOLD=30 \
|
-e DELETE_SHORT_URL_THRESHOLD=30 \
|
||||||
-e VALIDATE_URLS=false \
|
-e VALIDATE_URLS=false \
|
||||||
-e "NOT_FOUND_REDIRECT_TO=https://www.google.com" \
|
-e "INVALID_SHORT_URL_REDIRECT_TO=https://my-landing-page.com" \
|
||||||
|
-e "REGULAR_404_REDIRECT_TO=https://my-landing-page.com" \
|
||||||
|
-e "BASE_URL_REDIRECT_TO=https://my-landing-page.com" \
|
||||||
-e "REDIS_SERVERS=tcp://172.20.0.1:6379,tcp://172.20.0.2:6379" \
|
-e "REDIS_SERVERS=tcp://172.20.0.1:6379,tcp://172.20.0.2:6379" \
|
||||||
-e "BASE_PATH=/my-campaign" \
|
-e "BASE_PATH=/my-campaign" \
|
||||||
shlinkio/shlink
|
shlinkio/shlink
|
||||||
@ -151,7 +156,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",
|
||||||
|
"regular_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 +170,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 `regular_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:
|
||||||
|
|
||||||
|
@ -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,
|
'regular_404' => env('REGULAR_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' => [
|
||||||
|
@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core;
|
|||||||
|
|
||||||
use Doctrine\Common\Cache\Cache;
|
use Doctrine\Common\Cache\Cache;
|
||||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
||||||
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
|
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
|
||||||
use Zend\Expressive\Router\RouterInterface;
|
use Zend\Expressive\Router\RouterInterface;
|
||||||
@ -20,7 +21,7 @@ return [
|
|||||||
|
|
||||||
Options\AppOptions::class => ConfigAbstractFactory::class,
|
Options\AppOptions::class => ConfigAbstractFactory::class,
|
||||||
Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class,
|
Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class,
|
||||||
Options\NotFoundShortUrlOptions::class => ConfigAbstractFactory::class,
|
Options\NotFoundRedirectOptions::class => ConfigAbstractFactory::class,
|
||||||
Options\UrlShortenerOptions::class => ConfigAbstractFactory::class,
|
Options\UrlShortenerOptions::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Service\UrlShortener::class => ConfigAbstractFactory::class,
|
Service\UrlShortener::class => ConfigAbstractFactory::class,
|
||||||
@ -40,11 +41,15 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
NotFoundHandler::class => [TemplateRendererInterface::class],
|
NotFoundHandler::class => [
|
||||||
|
TemplateRendererInterface::class,
|
||||||
|
NotFoundRedirectOptions::class,
|
||||||
|
'config.router.base_path',
|
||||||
|
],
|
||||||
|
|
||||||
Options\AppOptions::class => ['config.app_options'],
|
Options\AppOptions::class => ['config.app_options'],
|
||||||
Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'],
|
Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'],
|
||||||
Options\NotFoundShortUrlOptions::class => ['config.url_shortener.not_found_short_url'],
|
Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'],
|
||||||
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
||||||
|
|
||||||
Service\UrlShortener::class => ['httpClient', 'em', Options\UrlShortenerOptions::class],
|
Service\UrlShortener::class => ['httpClient', 'em', Options\UrlShortenerOptions::class],
|
||||||
@ -58,7 +63,6 @@ return [
|
|||||||
Service\UrlShortener::class,
|
Service\UrlShortener::class,
|
||||||
Service\VisitsTracker::class,
|
Service\VisitsTracker::class,
|
||||||
Options\AppOptions::class,
|
Options\AppOptions::class,
|
||||||
Options\NotFoundShortUrlOptions::class,
|
|
||||||
'Logger_Shlink',
|
'Logger_Shlink',
|
||||||
],
|
],
|
||||||
Action\PixelAction::class => [
|
Action\PixelAction::class => [
|
||||||
|
@ -11,7 +11,7 @@ return [
|
|||||||
|
|
||||||
'routes' => [
|
'routes' => [
|
||||||
[
|
[
|
||||||
'name' => 'long-url-redirect',
|
'name' => Action\RedirectAction::class,
|
||||||
'path' => '/{shortCode}',
|
'path' => '/{shortCode}',
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
IpAddress::class,
|
IpAddress::class,
|
||||||
@ -20,7 +20,7 @@ return [
|
|||||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
'allowed_methods' => [RequestMethod::METHOD_GET],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'pixel-tracking',
|
'name' => Action\PixelAction::class,
|
||||||
'path' => '/{shortCode}/track',
|
'path' => '/{shortCode}/track',
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
IpAddress::class,
|
IpAddress::class,
|
||||||
@ -29,7 +29,7 @@ return [
|
|||||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
'allowed_methods' => [RequestMethod::METHOD_GET],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'short-url-qr-code',
|
'name' => Action\QrCodeAction::class,
|
||||||
'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]',
|
'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]',
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
Middleware\QrCodeCacheMiddleware::class,
|
Middleware\QrCodeCacheMiddleware::class,
|
||||||
|
@ -11,7 +11,6 @@ use Psr\Http\Server\RequestHandlerInterface;
|
|||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Psr\Log\NullLogger;
|
use Psr\Log\NullLogger;
|
||||||
use Shlinkio\Shlink\Common\Response\ResponseUtilsTrait;
|
use Shlinkio\Shlink\Common\Response\ResponseUtilsTrait;
|
||||||
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
|
|
||||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||||
@ -22,7 +21,6 @@ use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGeneratorInterface;
|
|||||||
class PreviewAction implements MiddlewareInterface
|
class PreviewAction implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
use ResponseUtilsTrait;
|
use ResponseUtilsTrait;
|
||||||
use ErrorResponseBuilderTrait;
|
|
||||||
|
|
||||||
/** @var PreviewGeneratorInterface */
|
/** @var PreviewGeneratorInterface */
|
||||||
private $previewGenerator;
|
private $previewGenerator;
|
||||||
@ -60,7 +58,7 @@ class PreviewAction implements MiddlewareInterface
|
|||||||
return $this->generateImageResponse($imagePath);
|
return $this->generateImageResponse($imagePath);
|
||||||
} catch (InvalidShortCodeException | EntityDoesNotExistException | PreviewGenerationException $e) {
|
} catch (InvalidShortCodeException | EntityDoesNotExistException | PreviewGenerationException $e) {
|
||||||
$this->logger->warning('An error occurred while generating preview image. {e}', ['e' => $e]);
|
$this->logger->warning('An error occurred while generating preview image. {e}', ['e' => $e]);
|
||||||
return $this->buildErrorResponse($request, $handler);
|
return $handler->handle($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ use Psr\Http\Server\RequestHandlerInterface;
|
|||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Psr\Log\NullLogger;
|
use Psr\Log\NullLogger;
|
||||||
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
||||||
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
|
|
||||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||||
@ -21,8 +20,6 @@ use Zend\Expressive\Router\RouterInterface;
|
|||||||
|
|
||||||
class QrCodeAction implements MiddlewareInterface
|
class QrCodeAction implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
use ErrorResponseBuilderTrait;
|
|
||||||
|
|
||||||
private const DEFAULT_SIZE = 300;
|
private const DEFAULT_SIZE = 300;
|
||||||
private const MIN_SIZE = 50;
|
private const MIN_SIZE = 50;
|
||||||
private const MAX_SIZE = 1000;
|
private const MAX_SIZE = 1000;
|
||||||
@ -65,10 +62,10 @@ class QrCodeAction implements MiddlewareInterface
|
|||||||
$this->urlShortener->shortCodeToUrl($shortCode, $domain);
|
$this->urlShortener->shortCodeToUrl($shortCode, $domain);
|
||||||
} catch (InvalidShortCodeException | EntityDoesNotExistException $e) {
|
} catch (InvalidShortCodeException | EntityDoesNotExistException $e) {
|
||||||
$this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]);
|
$this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]);
|
||||||
return $this->buildErrorResponse($request, $handler);
|
return $handler->handle($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = $this->router->generateUri('long-url-redirect', ['shortCode' => $shortCode]);
|
$path = $this->router->generateUri(RedirectAction::class, ['shortCode' => $shortCode]);
|
||||||
$size = $this->getSizeParam($request);
|
$size = $this->getSizeParam($request);
|
||||||
|
|
||||||
$qrCode = new QrCode((string) $request->getUri()->withPath($path)->withQuery(''));
|
$qrCode = new QrCode((string) $request->getUri()->withPath($path)->withQuery(''));
|
||||||
|
@ -7,31 +7,10 @@ namespace Shlinkio\Shlink\Core\Action;
|
|||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
|
|
||||||
use Shlinkio\Shlink\Core\Options;
|
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
|
||||||
use Zend\Diactoros\Response\RedirectResponse;
|
use Zend\Diactoros\Response\RedirectResponse;
|
||||||
|
|
||||||
class RedirectAction extends AbstractTrackingAction
|
class RedirectAction extends AbstractTrackingAction
|
||||||
{
|
{
|
||||||
use ErrorResponseBuilderTrait;
|
|
||||||
|
|
||||||
/** @var Options\NotFoundShortUrlOptions */
|
|
||||||
private $notFoundOptions;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
UrlShortenerInterface $urlShortener,
|
|
||||||
VisitsTrackerInterface $visitTracker,
|
|
||||||
Options\AppOptions $appOptions,
|
|
||||||
Options\NotFoundShortUrlOptions $notFoundOptions,
|
|
||||||
?LoggerInterface $logger = null
|
|
||||||
) {
|
|
||||||
parent::__construct($urlShortener, $visitTracker, $appOptions, $logger);
|
|
||||||
$this->notFoundOptions = $notFoundOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function createSuccessResp(string $longUrl): Response
|
protected function createSuccessResp(string $longUrl): Response
|
||||||
{
|
{
|
||||||
// Return a redirect response to the long URL.
|
// Return a redirect response to the long URL.
|
||||||
@ -39,14 +18,8 @@ class RedirectAction extends AbstractTrackingAction
|
|||||||
return new RedirectResponse($longUrl);
|
return new RedirectResponse($longUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createErrorResp(
|
protected function createErrorResp(ServerRequestInterface $request, RequestHandlerInterface $handler): Response
|
||||||
ServerRequestInterface $request,
|
{
|
||||||
RequestHandlerInterface $handler
|
return $handler->handle($request);
|
||||||
): Response {
|
|
||||||
if ($this->notFoundOptions->isRedirectionEnabled()) {
|
|
||||||
return new RedirectResponse($this->notFoundOptions->getRedirectTo());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->buildErrorResponse($request, $handler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Action\Util;
|
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
|
||||||
|
|
||||||
trait ErrorResponseBuilderTrait
|
|
||||||
{
|
|
||||||
private function buildErrorResponse(
|
|
||||||
ServerRequestInterface $request,
|
|
||||||
RequestHandlerInterface $handler
|
|
||||||
): ResponseInterface {
|
|
||||||
$request = $request->withAttribute(NotFoundHandler::NOT_FOUND_TEMPLATE, 'ShlinkCore::invalid-short-code');
|
|
||||||
return $handler->handle($request);
|
|
||||||
}
|
|
||||||
}
|
|
33
module/Core/src/Config/DeprecatedConfigParser.php
Normal file
33
module/Core/src/Config/DeprecatedConfigParser.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?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;
|
||||||
|
if (! $oldRedirectEnabled) {
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldRedirectValue = $config['url_shortener']['not_found_short_url']['redirect_to'] ?? null;
|
||||||
|
$config['not_found_redirects']['invalid_short_url'] = $oldRedirectValue;
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
}
|
@ -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'],
|
||||||
|
'regular_404_redirect_to' => ['not_found_redirects', 'regular_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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
65
module/Core/src/Options/NotFoundRedirectOptions.php
Normal file
65
module/Core/src/Options/NotFoundRedirectOptions.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\Options;
|
||||||
|
|
||||||
|
use Zend\Stdlib\AbstractOptions;
|
||||||
|
|
||||||
|
class NotFoundRedirectOptions extends AbstractOptions
|
||||||
|
{
|
||||||
|
/** @var string|null */
|
||||||
|
private $invalidShortUrl;
|
||||||
|
/** @var string|null */
|
||||||
|
private $regular404;
|
||||||
|
/** @var string|null */
|
||||||
|
private $baseUrl;
|
||||||
|
|
||||||
|
public function getInvalidShortUrlRedirect(): ?string
|
||||||
|
{
|
||||||
|
return $this->invalidShortUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasInvalidShortUrlRedirect(): bool
|
||||||
|
{
|
||||||
|
return $this->invalidShortUrl !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setInvalidShortUrl(?string $invalidShortUrl): self
|
||||||
|
{
|
||||||
|
$this->invalidShortUrl = $invalidShortUrl;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRegular404Redirect(): ?string
|
||||||
|
{
|
||||||
|
return $this->regular404;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasRegular404Redirect(): bool
|
||||||
|
{
|
||||||
|
return $this->regular404 !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setRegular404(?string $regular404): self
|
||||||
|
{
|
||||||
|
$this->regular404 = $regular404;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBaseUrlRedirect(): ?string
|
||||||
|
{
|
||||||
|
return $this->baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasBaseUrlRedirect(): bool
|
||||||
|
{
|
||||||
|
return $this->baseUrl !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setBaseUrl(?string $baseUrl): self
|
||||||
|
{
|
||||||
|
$this->baseUrl = $baseUrl;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Options;
|
|
||||||
|
|
||||||
use Zend\Stdlib\AbstractOptions;
|
|
||||||
|
|
||||||
class NotFoundShortUrlOptions extends AbstractOptions
|
|
||||||
{
|
|
||||||
/** @var bool */
|
|
||||||
private $enableRedirection = false;
|
|
||||||
/** @var string|null */
|
|
||||||
private $redirectTo;
|
|
||||||
|
|
||||||
public function isRedirectionEnabled(): bool
|
|
||||||
{
|
|
||||||
return $this->enableRedirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function setEnableRedirection(bool $enableRedirection = true): self
|
|
||||||
{
|
|
||||||
$this->enableRedirection = $enableRedirection;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRedirectTo(): string
|
|
||||||
{
|
|
||||||
return $this->redirectTo ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function setRedirectTo(?string $redirectTo): self
|
|
||||||
{
|
|
||||||
$this->redirectTo = $redirectTo;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,29 +5,42 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core\Response;
|
namespace Shlinkio\Shlink\Core\Response;
|
||||||
|
|
||||||
use Fig\Http\Message\StatusCodeInterface;
|
use Fig\Http\Message\StatusCodeInterface;
|
||||||
|
use InvalidArgumentException;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||||
|
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||||
use Zend\Diactoros\Response;
|
use Zend\Diactoros\Response;
|
||||||
|
use Zend\Expressive\Router\RouteResult;
|
||||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||||
|
|
||||||
use function array_shift;
|
use function array_shift;
|
||||||
use function explode;
|
use function explode;
|
||||||
use function Functional\contains;
|
use function Functional\contains;
|
||||||
|
use function rtrim;
|
||||||
|
|
||||||
class NotFoundHandler implements RequestHandlerInterface
|
class NotFoundHandler implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
public const NOT_FOUND_TEMPLATE = 'notFoundTemplate';
|
public const NOT_FOUND_TEMPLATE = 'ShlinkCore::error/404';
|
||||||
|
public const INVALID_SHORT_CODE_TEMPLATE = 'ShlinkCore::invalid-short-code';
|
||||||
|
|
||||||
/** @var TemplateRendererInterface */
|
/** @var TemplateRendererInterface */
|
||||||
private $renderer;
|
private $renderer;
|
||||||
|
/** @var NotFoundRedirectOptions */
|
||||||
|
private $redirectOptions;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $defaultTemplate;
|
private $shlinkBasePath;
|
||||||
|
|
||||||
public function __construct(TemplateRendererInterface $renderer, string $defaultTemplate = 'ShlinkCore::error/404')
|
public function __construct(
|
||||||
{
|
TemplateRendererInterface $renderer,
|
||||||
|
NotFoundRedirectOptions $redirectOptions,
|
||||||
|
string $shlinkBasePath
|
||||||
|
) {
|
||||||
$this->renderer = $renderer;
|
$this->renderer = $renderer;
|
||||||
$this->defaultTemplate = $defaultTemplate;
|
$this->redirectOptions = $redirectOptions;
|
||||||
|
$this->shlinkBasePath = $shlinkBasePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,10 +49,17 @@ class NotFoundHandler implements RequestHandlerInterface
|
|||||||
* @param ServerRequestInterface $request
|
* @param ServerRequestInterface $request
|
||||||
*
|
*
|
||||||
* @return ResponseInterface
|
* @return ResponseInterface
|
||||||
* @throws \InvalidArgumentException
|
* @throws InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||||
{
|
{
|
||||||
|
/** @var RouteResult $routeResult */
|
||||||
|
$routeResult = $request->getAttribute(RouteResult::class, RouteResult::fromRouteFailure(null));
|
||||||
|
$redirectResponse = $this->createRedirectResponse($routeResult, $request->getUri());
|
||||||
|
if ($redirectResponse !== null) {
|
||||||
|
return $redirectResponse;
|
||||||
|
}
|
||||||
|
|
||||||
$accepts = explode(',', $request->getHeaderLine('Accept'));
|
$accepts = explode(',', $request->getHeaderLine('Accept'));
|
||||||
$accept = array_shift($accepts);
|
$accept = array_shift($accepts);
|
||||||
$status = StatusCodeInterface::STATUS_NOT_FOUND;
|
$status = StatusCodeInterface::STATUS_NOT_FOUND;
|
||||||
@ -52,7 +72,30 @@ class NotFoundHandler implements RequestHandlerInterface
|
|||||||
], $status);
|
], $status);
|
||||||
}
|
}
|
||||||
|
|
||||||
$notFoundTemplate = $request->getAttribute(self::NOT_FOUND_TEMPLATE, $this->defaultTemplate);
|
$template = $routeResult->isFailure() ? self::NOT_FOUND_TEMPLATE : self::INVALID_SHORT_CODE_TEMPLATE;
|
||||||
return new Response\HtmlResponse($this->renderer->render($notFoundTemplate), $status);
|
return new Response\HtmlResponse($this->renderer->render($template), $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createRedirectResponse(RouteResult $routeResult, UriInterface $uri): ?ResponseInterface
|
||||||
|
{
|
||||||
|
$isBaseUrl = rtrim($uri->getPath(), '/') === $this->shlinkBasePath;
|
||||||
|
|
||||||
|
if ($isBaseUrl && $this->redirectOptions->hasBaseUrlRedirect()) {
|
||||||
|
return new Response\RedirectResponse($this->redirectOptions->getBaseUrlRedirect());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isBaseUrl && $routeResult->isFailure() && $this->redirectOptions->hasRegular404Redirect()) {
|
||||||
|
return new Response\RedirectResponse($this->redirectOptions->getRegular404Redirect());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$routeResult->isSuccess() &&
|
||||||
|
$routeResult->getMatchedRouteName() === RedirectAction::class &&
|
||||||
|
$this->redirectOptions->hasInvalidShortUrlRedirect()
|
||||||
|
) {
|
||||||
|
return new Response\RedirectResponse($this->redirectOptions->getInvalidShortUrlRedirect());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,20 +25,16 @@ class RedirectActionTest extends TestCase
|
|||||||
private $urlShortener;
|
private $urlShortener;
|
||||||
/** @var ObjectProphecy */
|
/** @var ObjectProphecy */
|
||||||
private $visitTracker;
|
private $visitTracker;
|
||||||
/** @var Options\NotFoundShortUrlOptions */
|
|
||||||
private $notFoundOptions;
|
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||||
$this->visitTracker = $this->prophesize(VisitsTracker::class);
|
$this->visitTracker = $this->prophesize(VisitsTracker::class);
|
||||||
$this->notFoundOptions = new Options\NotFoundShortUrlOptions();
|
|
||||||
|
|
||||||
$this->action = new RedirectAction(
|
$this->action = new RedirectAction(
|
||||||
$this->urlShortener->reveal(),
|
$this->urlShortener->reveal(),
|
||||||
$this->visitTracker->reveal(),
|
$this->visitTracker->reveal(),
|
||||||
new Options\AppOptions(['disableTrackParam' => 'foobar']),
|
new Options\AppOptions(['disableTrackParam' => 'foobar'])
|
||||||
$this->notFoundOptions
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,29 +74,6 @@ class RedirectActionTest extends TestCase
|
|||||||
$handle->shouldHaveBeenCalledOnce();
|
$handle->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function redirectToCustomUrlIsReturnedIfConfiguredSoAndShortUrlIsNotFound(): void
|
|
||||||
{
|
|
||||||
$shortCode = 'abc123';
|
|
||||||
$shortCodeToUrl = $this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(
|
|
||||||
EntityDoesNotExistException::class
|
|
||||||
);
|
|
||||||
|
|
||||||
$handler = $this->prophesize(RequestHandlerInterface::class);
|
|
||||||
$handle = $handler->handle(Argument::any())->willReturn(new Response());
|
|
||||||
|
|
||||||
$this->notFoundOptions->enableRedirection = true;
|
|
||||||
$this->notFoundOptions->redirectTo = 'https://shlink.io';
|
|
||||||
|
|
||||||
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode);
|
|
||||||
$resp = $this->action->process($request, $handler->reveal());
|
|
||||||
|
|
||||||
$this->assertEquals(302, $resp->getStatusCode());
|
|
||||||
$this->assertEquals('https://shlink.io', $resp->getHeaderLine('Location'));
|
|
||||||
$shortCodeToUrl->shouldHaveBeenCalledOnce();
|
|
||||||
$handle->shouldNotHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function visitIsNotTrackedIfDisableParamIsProvided(): void
|
public function visitIsNotTrackedIfDisableParamIsProvided(): void
|
||||||
{
|
{
|
||||||
|
95
module/Core/test/Config/DeprecatedConfigParserTest.php
Normal file
95
module/Core/test/Config/DeprecatedConfigParserTest.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\Core\Config;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shlinkio\Shlink\Core\Config\DeprecatedConfigParser;
|
||||||
|
|
||||||
|
use function array_merge;
|
||||||
|
|
||||||
|
class DeprecatedConfigParserTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var DeprecatedConfigParser */
|
||||||
|
private $postProcessor;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->postProcessor = new DeprecatedConfigParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function returnsConfigAsIsIfNewValueIsDefined(): void
|
||||||
|
{
|
||||||
|
$config = [
|
||||||
|
'not_found_redirects' => [
|
||||||
|
'invalid_short_url' => 'somewhere',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = ($this->postProcessor)($config);
|
||||||
|
|
||||||
|
$this->assertEquals($config, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function doesNotProvideNewConfigIfOldOneIsDefinedButDisabled(): void
|
||||||
|
{
|
||||||
|
$config = [
|
||||||
|
'url_shortener' => [
|
||||||
|
'not_found_short_url' => [
|
||||||
|
'enable_redirection' => false,
|
||||||
|
'redirect_to' => 'somewhere',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = ($this->postProcessor)($config);
|
||||||
|
|
||||||
|
$this->assertEquals($config, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function mapsOldConfigToNewOneWhenOldOneIsEnabled(): void
|
||||||
|
{
|
||||||
|
$config = [
|
||||||
|
'url_shortener' => [
|
||||||
|
'not_found_short_url' => [
|
||||||
|
'enable_redirection' => true,
|
||||||
|
'redirect_to' => 'somewhere',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$expected = array_merge($config, [
|
||||||
|
'not_found_redirects' => [
|
||||||
|
'invalid_short_url' => 'somewhere',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ($this->postProcessor)($config);
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function definesNewConfigAsNullIfOldOneIsEnabledWithNoRedirectValue(): void
|
||||||
|
{
|
||||||
|
$config = [
|
||||||
|
'url_shortener' => [
|
||||||
|
'not_found_short_url' => [
|
||||||
|
'enable_redirection' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$expected = array_merge($config, [
|
||||||
|
'not_found_redirects' => [
|
||||||
|
'invalid_short_url' => null,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ($this->postProcessor)($config);
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $result);
|
||||||
|
}
|
||||||
|
}
|
@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,17 @@ namespace ShlinkioTest\Shlink\Core\Response;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||||
|
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
||||||
use Zend\Diactoros\Response;
|
use Zend\Diactoros\Response;
|
||||||
use Zend\Diactoros\ServerRequest;
|
use Zend\Diactoros\ServerRequest;
|
||||||
|
use Zend\Diactoros\ServerRequestFactory;
|
||||||
|
use Zend\Diactoros\Uri;
|
||||||
|
use Zend\Expressive\Router\Route;
|
||||||
|
use Zend\Expressive\Router\RouteResult;
|
||||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||||
|
|
||||||
class NotFoundHandlerTest extends TestCase
|
class NotFoundHandlerTest extends TestCase
|
||||||
@ -18,11 +26,15 @@ class NotFoundHandlerTest extends TestCase
|
|||||||
private $delegate;
|
private $delegate;
|
||||||
/** @var ObjectProphecy */
|
/** @var ObjectProphecy */
|
||||||
private $renderer;
|
private $renderer;
|
||||||
|
/** @var NotFoundRedirectOptions */
|
||||||
|
private $redirectOptions;
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->renderer = $this->prophesize(TemplateRendererInterface::class);
|
$this->renderer = $this->prophesize(TemplateRendererInterface::class);
|
||||||
$this->delegate = new NotFoundHandler($this->renderer->reveal());
|
$this->redirectOptions = new NotFoundRedirectOptions();
|
||||||
|
|
||||||
|
$this->delegate = new NotFoundHandler($this->renderer->reveal(), $this->redirectOptions, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,4 +59,84 @@ class NotFoundHandlerTest extends TestCase
|
|||||||
yield 'application/x-json' => [Response\JsonResponse::class, 'application/x-json', 0];
|
yield 'application/x-json' => [Response\JsonResponse::class, 'application/x-json', 0];
|
||||||
yield 'text/html' => [Response\HtmlResponse::class, 'text/html', 1];
|
yield 'text/html' => [Response\HtmlResponse::class, 'text/html', 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideRedirects
|
||||||
|
*/
|
||||||
|
public function expectedRedirectionIsReturnedDependingOnTheCase(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
string $expectedRedirectTo
|
||||||
|
): void {
|
||||||
|
$this->redirectOptions->invalidShortUrl = 'invalidShortUrl';
|
||||||
|
$this->redirectOptions->regular404 = 'regular404';
|
||||||
|
$this->redirectOptions->baseUrl = 'baseUrl';
|
||||||
|
|
||||||
|
$resp = $this->delegate->handle($request);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Response\RedirectResponse::class, $resp);
|
||||||
|
$this->assertEquals($expectedRedirectTo, $resp->getHeaderLine('Location'));
|
||||||
|
$this->renderer->render(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideRedirects(): iterable
|
||||||
|
{
|
||||||
|
yield 'base URL with trailing slash' => [
|
||||||
|
ServerRequestFactory::fromGlobals()->withUri(new Uri('/')),
|
||||||
|
'baseUrl',
|
||||||
|
];
|
||||||
|
yield 'base URL without trailing slash' => [
|
||||||
|
ServerRequestFactory::fromGlobals()->withUri(new Uri('')),
|
||||||
|
'baseUrl',
|
||||||
|
];
|
||||||
|
yield 'regular 404' => [
|
||||||
|
ServerRequestFactory::fromGlobals()->withUri(new Uri('/foo/bar')),
|
||||||
|
'regular404',
|
||||||
|
];
|
||||||
|
yield 'invalid short URL' => [
|
||||||
|
ServerRequestFactory::fromGlobals()
|
||||||
|
->withAttribute(
|
||||||
|
RouteResult::class,
|
||||||
|
RouteResult::fromRoute(
|
||||||
|
new Route(
|
||||||
|
'',
|
||||||
|
$this->prophesize(MiddlewareInterface::class)->reveal(),
|
||||||
|
['GET'],
|
||||||
|
RedirectAction::class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->withUri(new Uri('/abc123')),
|
||||||
|
'invalidShortUrl',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideTemplates
|
||||||
|
*/
|
||||||
|
public function properErrorTemplateIsRendered(ServerRequestInterface $request, string $expectedTemplate): void
|
||||||
|
{
|
||||||
|
$request = $request->withHeader('Accept', 'text/html');
|
||||||
|
$render = $this->renderer->render($expectedTemplate)->willReturn('');
|
||||||
|
|
||||||
|
$resp = $this->delegate->handle($request);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Response\HtmlResponse::class, $resp);
|
||||||
|
$render->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideTemplates(): iterable
|
||||||
|
{
|
||||||
|
$request = ServerRequestFactory::fromGlobals();
|
||||||
|
|
||||||
|
yield [$request, NotFoundHandler::NOT_FOUND_TEMPLATE];
|
||||||
|
yield [
|
||||||
|
$request->withAttribute(
|
||||||
|
RouteResult::class,
|
||||||
|
RouteResult::fromRoute(new Route('', $this->prophesize(MiddlewareInterface::class)->reveal()))
|
||||||
|
),
|
||||||
|
NotFoundHandler::INVALID_SHORT_CODE_TEMPLATE,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user