From 3eddacdff81752fb018d5286f000744c33c05e78 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 3 Nov 2018 11:37:43 +0100 Subject: [PATCH 1/7] Created options to enable redirection to external page when short code is not found --- config/autoload/url-shortener.global.php | 4 ++ module/Core/config/dependencies.config.php | 9 ++++- module/Core/src/Options/AppOptionsFactory.php | 31 -------------- .../Options/DeleteShortUrlsOptionsFactory.php | 31 -------------- .../src/Options/NotFoundShortUrlOptions.php | 40 +++++++++++++++++++ .../test/Options/AppOptionsFactoryTest.php | 31 -------------- 6 files changed, 51 insertions(+), 95 deletions(-) delete mode 100644 module/Core/src/Options/AppOptionsFactory.php delete mode 100644 module/Core/src/Options/DeleteShortUrlsOptionsFactory.php create mode 100644 module/Core/src/Options/NotFoundShortUrlOptions.php delete mode 100644 module/Core/test/Options/AppOptionsFactoryTest.php diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index dd268cb7..5b941ee4 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -13,6 +13,10 @@ return [ ], 'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS), 'validate_url' => true, + 'not_found_short_url' => [ + 'enable_redirection' => false, + 'redirect_to' => null, + ], ], ]; diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 31fd1297..665d0894 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -16,8 +16,9 @@ return [ 'dependencies' => [ 'factories' => [ - Options\AppOptions::class => Options\AppOptionsFactory::class, - Options\DeleteShortUrlsOptions::class => Options\DeleteShortUrlsOptionsFactory::class, + Options\AppOptions::class => ConfigAbstractFactory::class, + Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class, + Options\NotFoundShortUrlOptions::class => ConfigAbstractFactory::class, NotFoundHandler::class => ConfigAbstractFactory::class, // Services @@ -40,6 +41,10 @@ return [ ConfigAbstractFactory::class => [ NotFoundHandler::class => [TemplateRendererInterface::class], + Options\AppOptions::class => ['config.app_options'], + Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'], + Options\NotFoundShortUrlOptions::class => ['config.url_shortener.not_found_short_url'], + // Services Service\UrlShortener::class => [ 'httpClient', diff --git a/module/Core/src/Options/AppOptionsFactory.php b/module/Core/src/Options/AppOptionsFactory.php deleted file mode 100644 index 0df25a2a..00000000 --- a/module/Core/src/Options/AppOptionsFactory.php +++ /dev/null @@ -1,31 +0,0 @@ -has('config') ? $container->get('config') : []; - return new AppOptions($config['app_options'] ?? []); - } -} diff --git a/module/Core/src/Options/DeleteShortUrlsOptionsFactory.php b/module/Core/src/Options/DeleteShortUrlsOptionsFactory.php deleted file mode 100644 index fab7cfee..00000000 --- a/module/Core/src/Options/DeleteShortUrlsOptionsFactory.php +++ /dev/null @@ -1,31 +0,0 @@ -has('config') ? $container->get('config') : []; - return new DeleteShortUrlsOptions($config['delete_short_urls'] ?? []); - } -} diff --git a/module/Core/src/Options/NotFoundShortUrlOptions.php b/module/Core/src/Options/NotFoundShortUrlOptions.php new file mode 100644 index 00000000..8e461240 --- /dev/null +++ b/module/Core/src/Options/NotFoundShortUrlOptions.php @@ -0,0 +1,40 @@ +enableRedirection; + } + + protected function enableRedirection(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; + } +} diff --git a/module/Core/test/Options/AppOptionsFactoryTest.php b/module/Core/test/Options/AppOptionsFactoryTest.php deleted file mode 100644 index b9bbe498..00000000 --- a/module/Core/test/Options/AppOptionsFactoryTest.php +++ /dev/null @@ -1,31 +0,0 @@ -factory = new AppOptionsFactory(); - } - - /** - * @test - */ - public function serviceIsCreated() - { - $instance = $this->factory->__invoke(new ServiceManager([]), ''); - $this->assertInstanceOf(AppOptions::class, $instance); - } -} From 358b2b661e4d4f79fae2fe99f3bd7a35a952c607 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 3 Nov 2018 11:40:57 +0100 Subject: [PATCH 2/7] Deprecated ci composer command, since it does the same as check, but slower --- .travis.yml | 2 +- composer.json | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 24ebfc96..085989bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ install: script: - mkdir build - - composer ci + - composer check after_success: - rm -f build/clover.xml diff --git a/composer.json b/composer.json index 5a704aa7..9b8c2027 100644 --- a/composer.json +++ b/composer.json @@ -88,17 +88,15 @@ }, "scripts": { "check": [ - "@cs", - "@stan", - "@test", - "@infect" - ], - "ci": [ "@cs", "@stan", "@test:ci", "@infect:ci" ], + "ci": [ + "echo \"This command is DEPRECATED. Use check instead\"", + "@check" + ], "cs": "phpcs", "cs:fix": "phpcbf", From 313927827d752497b71b33e7efb058a4cb4b9969 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 3 Nov 2018 12:10:02 +0100 Subject: [PATCH 3/7] Updated RedirectAction so that it makes use of the not found short url options --- module/Core/config/dependencies.config.php | 1 + .../src/Action/AbstractTrackingAction.php | 14 ++--- module/Core/src/Action/PixelAction.php | 11 +++- module/Core/src/Action/RedirectAction.php | 38 +++++++++++++- .../src/Options/NotFoundShortUrlOptions.php | 8 +-- .../Core/test/Action/RedirectActionTest.php | 51 +++++++++++++++---- 6 files changed, 100 insertions(+), 23 deletions(-) diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 665d0894..7042b19a 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -63,6 +63,7 @@ return [ Service\UrlShortener::class, Service\VisitsTracker::class, Options\AppOptions::class, + Options\NotFoundShortUrlOptions::class, 'Logger_Shlink', ], Action\PixelAction::class => [ diff --git a/module/Core/src/Action/AbstractTrackingAction.php b/module/Core/src/Action/AbstractTrackingAction.php index 670aa908..21ab0b49 100644 --- a/module/Core/src/Action/AbstractTrackingAction.php +++ b/module/Core/src/Action/AbstractTrackingAction.php @@ -9,7 +9,6 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Model\Visitor; @@ -20,8 +19,6 @@ use function array_key_exists; abstract class AbstractTrackingAction implements MiddlewareInterface { - use ErrorResponseBuilderTrait; - /** * @var UrlShortenerInterface */ @@ -74,12 +71,17 @@ abstract class AbstractTrackingAction implements MiddlewareInterface $this->visitTracker->track($shortCode, Visitor::fromRequest($request)); } - return $this->createResp($url->getLongUrl()); + return $this->createSuccessResp($url->getLongUrl()); } catch (InvalidShortCodeException | EntityDoesNotExistException $e) { $this->logger->warning('An error occurred while tracking short code. {e}', ['e' => $e]); - return $this->buildErrorResponse($request, $handler); + return $this->createErrorResp($request, $handler); } } - abstract protected function createResp(string $longUrl): ResponseInterface; + abstract protected function createSuccessResp(string $longUrl): ResponseInterface; + + abstract protected function createErrorResp( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface; } diff --git a/module/Core/src/Action/PixelAction.php b/module/Core/src/Action/PixelAction.php index 5f2c797a..fff85ce3 100644 --- a/module/Core/src/Action/PixelAction.php +++ b/module/Core/src/Action/PixelAction.php @@ -4,12 +4,21 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Action; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Common\Response\PixelResponse; class PixelAction extends AbstractTrackingAction { - protected function createResp(string $longUrl): ResponseInterface + protected function createSuccessResp(string $longUrl): ResponseInterface { return new PixelResponse(); } + + protected function createErrorResp( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + return new PixelResponse(); + } } diff --git a/module/Core/src/Action/RedirectAction.php b/module/Core/src/Action/RedirectAction.php index 4654e1fe..83c4312c 100644 --- a/module/Core/src/Action/RedirectAction.php +++ b/module/Core/src/Action/RedirectAction.php @@ -4,14 +4,50 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Action; use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface; +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; class RedirectAction extends AbstractTrackingAction { - protected function createResp(string $longUrl): Response + 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 { // Return a redirect response to the long URL. // Use a temporary redirect to make sure browsers always hit the server for analytics purposes return new RedirectResponse($longUrl); } + + protected function createErrorResp( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): Response { + if ($this->notFoundOptions->isRedirectionEnabled()) { + return new RedirectResponse($this->notFoundOptions->getRedirectTo()); + } + + return $this->buildErrorResponse($request, $handler); + } } diff --git a/module/Core/src/Options/NotFoundShortUrlOptions.php b/module/Core/src/Options/NotFoundShortUrlOptions.php index 8e461240..871c8eff 100644 --- a/module/Core/src/Options/NotFoundShortUrlOptions.php +++ b/module/Core/src/Options/NotFoundShortUrlOptions.php @@ -21,18 +21,18 @@ class NotFoundShortUrlOptions extends AbstractOptions return $this->enableRedirection; } - protected function enableRedirection(bool $enableRedirection = true): self + protected function setEnableRedirection(bool $enableRedirection = true): self { $this->enableRedirection = $enableRedirection; return $this; } - public function getRedirectTo(): ?string + public function getRedirectTo(): string { - return $this->redirectTo; + return $this->redirectTo ?? ''; } - protected function setRedirectTo(string $redirectTo): self + protected function setRedirectTo(?string $redirectTo): self { $this->redirectTo = $redirectTo; return $this; diff --git a/module/Core/test/Action/RedirectActionTest.php b/module/Core/test/Action/RedirectActionTest.php index 91e1b4d6..5de290f7 100644 --- a/module/Core/test/Action/RedirectActionTest.php +++ b/module/Core/test/Action/RedirectActionTest.php @@ -5,13 +5,12 @@ namespace ShlinkioTest\Shlink\Core\Action; use PHPUnit\Framework\TestCase; use Prophecy\Argument; -use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Action\RedirectAction; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; -use Shlinkio\Shlink\Core\Options\AppOptions; +use Shlinkio\Shlink\Core\Options; use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\VisitsTracker; use ShlinkioTest\Shlink\Common\Util\TestUtils; @@ -23,25 +22,31 @@ class RedirectActionTest extends TestCase /** * @var RedirectAction */ - protected $action; + private $action; /** * @var ObjectProphecy */ - protected $urlShortener; + private $urlShortener; /** * @var ObjectProphecy */ - protected $visitTracker; + private $visitTracker; + /** + * @var Options\NotFoundShortUrlOptions + */ + private $notFoundOptions; public function setUp() { $this->urlShortener = $this->prophesize(UrlShortener::class); $this->visitTracker = $this->prophesize(VisitsTracker::class); + $this->notFoundOptions = new Options\NotFoundShortUrlOptions(); $this->action = new RedirectAction( $this->urlShortener->reveal(), $this->visitTracker->reveal(), - new AppOptions(['disableTrackParam' => 'foobar']) + new Options\AppOptions(['disableTrackParam' => 'foobar']), + $this->notFoundOptions ); } @@ -76,14 +81,38 @@ class RedirectActionTest extends TestCase ->shouldBeCalledTimes(1); $this->visitTracker->track(Argument::cetera())->shouldNotBeCalled(); - $delegate = $this->prophesize(RequestHandlerInterface::class); - /** @var MethodProphecy $process */ - $process = $delegate->handle(Argument::any())->willReturn(new Response()); + $handler = $this->prophesize(RequestHandlerInterface::class); + $handle = $handler->handle(Argument::any())->willReturn(new Response()); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); - $this->action->process($request, $delegate->reveal()); + $this->action->process($request, $handler->reveal()); - $process->shouldHaveBeenCalledTimes(1); + $handle->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + public function redirectToCustomUrlIsReturnedIfConfiguredSoAndShortUrlIsNotFound() + { + $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 = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $resp = $this->action->process($request, $handler->reveal()); + + $this->assertEquals(302, $resp->getStatusCode()); + $this->assertEquals('https://shlink.io', $resp->getHeaderLine('Location')); + $shortCodeToUrl->shouldHaveBeenCalledTimes(1); + $handle->shouldNotHaveBeenCalled(); } /** From 32fcdd9d94fd4a054f0a1a58b6ecfb128f71ffea Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 3 Nov 2018 12:15:25 +0100 Subject: [PATCH 4/7] Ensured phpcov is run with phpdbg in travis pipeline --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 085989bb..247e7bfe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ script: after_success: - rm -f build/clover.xml - - vendor/bin/phpcov merge build --clover build/clover.xml + - phpdbg -qrr vendor/bin/phpcov merge build --clover build/clover.xml - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover build/clover.xml From 057f88a36af32b6713090f8e6cb3dabaf27ce39e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 4 Nov 2018 11:58:35 +0100 Subject: [PATCH 5/7] Added new not found short url config to installer --- .../Plugin/UrlShortenerConfigCustomizer.php | 21 +++++++++++++++++++ .../src/Model/CustomizableAppConfig.php | 15 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/module/Installer/src/Config/Plugin/UrlShortenerConfigCustomizer.php b/module/Installer/src/Config/Plugin/UrlShortenerConfigCustomizer.php index 6eb5fabf..0e5d4a00 100644 --- a/module/Installer/src/Config/Plugin/UrlShortenerConfigCustomizer.php +++ b/module/Installer/src/Config/Plugin/UrlShortenerConfigCustomizer.php @@ -19,11 +19,15 @@ class UrlShortenerConfigCustomizer implements ConfigCustomizerInterface public const HOSTNAME = 'HOSTNAME'; public const CHARS = 'CHARS'; public const VALIDATE_URL = 'VALIDATE_URL'; + public const ENABLE_NOT_FOUND_REDIRECTION = 'ENABLE_NOT_FOUND_REDIRECTION'; + public const NOT_FOUND_REDIRECT_TO = 'NOT_FOUND_REDIRECT_TO'; private const EXPECTED_KEYS = [ self::SCHEMA, self::HOSTNAME, self::CHARS, self::VALIDATE_URL, + self::ENABLE_NOT_FOUND_REDIRECTION, + self::NOT_FOUND_REDIRECT_TO, ]; public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void @@ -38,6 +42,11 @@ class UrlShortenerConfigCustomizer implements ConfigCustomizerInterface $io->title('URL SHORTENER'); foreach ($keysToAskFor as $key) { + // Skip not found redirect URL when the user decided not to redirect + if ($key === self::NOT_FOUND_REDIRECT_TO && ! $urlShortener[self::ENABLE_NOT_FOUND_REDIRECTION]) { + continue; + } + $urlShortener[$key] = $this->ask($io, $key); } $appConfig->setUrlShortener($urlShortener); @@ -60,6 +69,18 @@ class UrlShortenerConfigCustomizer implements ConfigCustomizerInterface ) ?: 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'); + case self::ENABLE_NOT_FOUND_REDIRECTION: + return $io->confirm( + 'Do you want to enable a redirection to a custom URL when a user hits an invalid short URL? ' . + '(If not enabled, the user will see a default "404 not found" page)', + false + ); + case self::NOT_FOUND_REDIRECT_TO: + return $this->askRequired( + $io, + 'redirect URL', + 'Custom URL to redirect to when a user hits an invalid short URL' + ); } return ''; diff --git a/module/Installer/src/Model/CustomizableAppConfig.php b/module/Installer/src/Model/CustomizableAppConfig.php index 2e307f3f..f347519f 100644 --- a/module/Installer/src/Model/CustomizableAppConfig.php +++ b/module/Installer/src/Model/CustomizableAppConfig.php @@ -146,6 +146,16 @@ final class CustomizableAppConfig implements ArraySerializableInterface UrlShortenerConfigCustomizer::HOSTNAME => ['url_shortener', 'domain', 'hostname'], UrlShortenerConfigCustomizer::CHARS => ['url_shortener', 'shortcode_chars'], UrlShortenerConfigCustomizer::VALIDATE_URL => ['url_shortener', 'validate_url'], + UrlShortenerConfigCustomizer::ENABLE_NOT_FOUND_REDIRECTION => [ + 'url_shortener', + 'not_found_short_url', + 'enable_redirection', + ], + UrlShortenerConfigCustomizer::NOT_FOUND_REDIRECT_TO => [ + 'url_shortener', + 'not_found_short_url', + 'redirect_to', + ], ], $pathCollection)); } @@ -191,6 +201,11 @@ final class CustomizableAppConfig implements ArraySerializableInterface ], 'shortcode_chars' => $this->urlShortener[UrlShortenerConfigCustomizer::CHARS] ?? '', 'validate_url' => $this->urlShortener[UrlShortenerConfigCustomizer::VALIDATE_URL] ?? true, + 'not_found_short_url' => [ + 'enable_redirection' => + $this->urlShortener[UrlShortenerConfigCustomizer::ENABLE_NOT_FOUND_REDIRECTION] ?? false, + 'redirect_to' => $this->urlShortener[UrlShortenerConfigCustomizer::NOT_FOUND_REDIRECT_TO] ?? null, + ], ], ]; From a71245b8833a839aae883bd84ee1d8dccf1d5bf9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 4 Nov 2018 12:05:22 +0100 Subject: [PATCH 6/7] Improved UrlShortenerConfigCustomizerTest covering new config options --- .../UrlShortenerConfigCustomizerTest.php | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/module/Installer/test/Config/Plugin/UrlShortenerConfigCustomizerTest.php b/module/Installer/test/Config/Plugin/UrlShortenerConfigCustomizerTest.php index f5facbbd..2942248a 100644 --- a/module/Installer/test/Config/Plugin/UrlShortenerConfigCustomizerTest.php +++ b/module/Installer/test/Config/Plugin/UrlShortenerConfigCustomizerTest.php @@ -46,10 +46,12 @@ class UrlShortenerConfigCustomizerTest extends TestCase 'HOSTNAME' => 'asked', 'CHARS' => 'asked', 'VALIDATE_URL' => true, + 'ENABLE_NOT_FOUND_REDIRECTION' => true, + 'NOT_FOUND_REDIRECT_TO' => 'asked', ], $config->getUrlShortener()); - $ask->shouldHaveBeenCalledTimes(2); + $ask->shouldHaveBeenCalledTimes(3); $choice->shouldHaveBeenCalledTimes(1); - $confirm->shouldHaveBeenCalledTimes(1); + $confirm->shouldHaveBeenCalledTimes(2); } /** @@ -64,6 +66,8 @@ class UrlShortenerConfigCustomizerTest extends TestCase $config->setUrlShortener([ 'SCHEMA' => 'foo', 'HOSTNAME' => 'foo', + 'ENABLE_NOT_FOUND_REDIRECTION' => true, + 'NOT_FOUND_REDIRECT_TO' => 'foo', ]); $this->plugin->process($this->io->reveal(), $config); @@ -73,6 +77,8 @@ class UrlShortenerConfigCustomizerTest extends TestCase 'HOSTNAME' => 'foo', 'CHARS' => 'asked', 'VALIDATE_URL' => false, + 'ENABLE_NOT_FOUND_REDIRECTION' => true, + 'NOT_FOUND_REDIRECT_TO' => 'foo', ], $config->getUrlShortener()); $choice->shouldNotHaveBeenCalled(); $ask->shouldHaveBeenCalledTimes(1); @@ -94,6 +100,8 @@ class UrlShortenerConfigCustomizerTest extends TestCase 'HOSTNAME' => 'foo', 'CHARS' => 'foo', 'VALIDATE_URL' => true, + 'ENABLE_NOT_FOUND_REDIRECTION' => true, + 'NOT_FOUND_REDIRECT_TO' => 'foo', ]); $this->plugin->process($this->io->reveal(), $config); @@ -103,9 +111,41 @@ class UrlShortenerConfigCustomizerTest extends TestCase 'HOSTNAME' => 'foo', 'CHARS' => 'foo', 'VALIDATE_URL' => true, + 'ENABLE_NOT_FOUND_REDIRECTION' => true, + 'NOT_FOUND_REDIRECT_TO' => 'foo', ], $config->getUrlShortener()); $choice->shouldNotHaveBeenCalled(); $ask->shouldNotHaveBeenCalled(); $confirm->shouldNotHaveBeenCalled(); } + + /** + * @test + */ + public function redirectUrlOptionIsNotAskedIfAnswerToPreviousQuestionIsNo() + { + $ask = $this->io->ask(Argument::cetera())->willReturn('asked'); + $confirm = $this->io->confirm(Argument::cetera())->willReturn(false); + + $config = new CustomizableAppConfig(); + $config->setUrlShortener([ + 'SCHEMA' => 'foo', + 'HOSTNAME' => 'foo', + 'CHARS' => 'foo', + 'VALIDATE_URL' => true, + ]); + + $this->plugin->process($this->io->reveal(), $config); + + $this->assertTrue($config->hasUrlShortener()); + $this->assertEquals([ + 'SCHEMA' => 'foo', + 'HOSTNAME' => 'foo', + 'CHARS' => 'foo', + 'VALIDATE_URL' => true, + 'ENABLE_NOT_FOUND_REDIRECTION' => false, + ], $config->getUrlShortener()); + $ask->shouldNotHaveBeenCalled(); + $confirm->shouldHaveBeenCalledTimes(1); + } } From 05abe49d8b1988f02cb6bc8b09151fbfbdf58f95 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 4 Nov 2018 12:11:36 +0100 Subject: [PATCH 7/7] Updated changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe069713..d6215679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), #### Added -* *Nothing* +* [#236](https://github.com/shlinkio/shlink/issues/236) Added option to define a redirection to a custom URL when a user hits an invalid short URL. + + It only affects URLs matched as "short URL" where the short code is invalid, not any 404 that happens in the app. For example, a request to the path `/foo/bar` will keep returning a 404. + + This new option will be asked by the installer both for new shlink installations and for any previous shlink version which is updated. #### Changed