From 32f7b4fbf6e979806dd0fc5d903c19010369178f Mon Sep 17 00:00:00 2001
From: Alejandro Celaya <alejandrocelaya@gmail.com>
Date: Thu, 15 Jul 2021 16:54:54 +0200
Subject: [PATCH] Created new middleware that redirects to short URLs with an
 extra path

---
 .../autoload/middleware-pipeline.global.php   |  1 +
 module/Core/config/dependencies.config.php    |  8 ++
 .../src/Action/AbstractTrackingAction.php     |  2 +-
 .../Helper/ShortUrlRedirectionBuilder.php     |  4 +-
 .../ExtraPathRedirectMiddleware.php           | 74 +++++++++++++++++++
 5 files changed, 86 insertions(+), 3 deletions(-)
 create mode 100644 module/Core/src/ShortUrl/Middleware/ExtraPathRedirectMiddleware.php

diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php
index c60e1ba7..0466ebc5 100644
--- a/config/autoload/middleware-pipeline.global.php
+++ b/config/autoload/middleware-pipeline.global.php
@@ -68,6 +68,7 @@ return [
                 // This middleware is in front of tracking actions explicitly. Putting here for orphan visits tracking
                 IpAddress::class,
                 Core\ErrorHandler\NotFoundTypeResolverMiddleware::class,
+                Core\ShortUrl\Middleware\ExtraPathRedirectMiddleware::class,
                 Core\ErrorHandler\NotFoundTrackerMiddleware::class,
                 Core\ErrorHandler\NotFoundRedirectHandler::class,
                 Core\ErrorHandler\NotFoundTemplateHandler::class,
diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php
index 34de226d..baecce9f 100644
--- a/module/Core/config/dependencies.config.php
+++ b/module/Core/config/dependencies.config.php
@@ -55,6 +55,7 @@ return [
             ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => ConfigAbstractFactory::class,
             ShortUrl\Helper\ShortUrlRedirectionBuilder::class => ConfigAbstractFactory::class,
             ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
+            ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
 
             Mercure\MercureUpdatesGenerator::class => ConfigAbstractFactory::class,
 
@@ -139,6 +140,13 @@ return [
         ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => [Util\UrlValidator::class],
         ShortUrl\Helper\ShortUrlRedirectionBuilder::class => [Options\TrackingOptions::class],
         ShortUrl\Transformer\ShortUrlDataTransformer::class => [ShortUrl\Helper\ShortUrlStringifier::class],
+        ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => [
+            Service\ShortUrl\ShortUrlResolver::class,
+            Visit\VisitsTracker::class,
+            ShortUrl\Helper\ShortUrlRedirectionBuilder::class,
+            Util\RedirectResponseHelper::class,
+            Options\UrlShortenerOptions::class,
+        ],
 
         Mercure\MercureUpdatesGenerator::class => [
             ShortUrl\Transformer\ShortUrlDataTransformer::class,
diff --git a/module/Core/src/Action/AbstractTrackingAction.php b/module/Core/src/Action/AbstractTrackingAction.php
index b0f3d6ee..554c1844 100644
--- a/module/Core/src/Action/AbstractTrackingAction.php
+++ b/module/Core/src/Action/AbstractTrackingAction.php
@@ -43,7 +43,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMet
             }
 
             return $this->createSuccessResp($shortUrl, $request);
-        } catch (ShortUrlNotFoundException $e) {
+        } catch (ShortUrlNotFoundException) {
             return $this->createErrorResp($request, $handler);
         }
     }
diff --git a/module/Core/src/ShortUrl/Helper/ShortUrlRedirectionBuilder.php b/module/Core/src/ShortUrl/Helper/ShortUrlRedirectionBuilder.php
index 1c45698f..43ea4993 100644
--- a/module/Core/src/ShortUrl/Helper/ShortUrlRedirectionBuilder.php
+++ b/module/Core/src/ShortUrl/Helper/ShortUrlRedirectionBuilder.php
@@ -28,7 +28,7 @@ class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderInterface
             ->__toString();
     }
 
-    private function resolveQuery(Uri $uri, array $currentQuery): string
+    private function resolveQuery(Uri $uri, array $currentQuery): ?string
     {
         $hardcodedQuery = Query::parse($uri->getQuery() ?? '');
 
@@ -39,7 +39,7 @@ class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderInterface
 
         $mergedQuery = array_merge($hardcodedQuery, $currentQuery);
 
-        return empty($mergedQuery) ? '' : Query::build($mergedQuery);
+        return empty($mergedQuery) ? null : Query::build($mergedQuery);
     }
 
     private function resolvePath(Uri $uri, ?string $extraPath): string
diff --git a/module/Core/src/ShortUrl/Middleware/ExtraPathRedirectMiddleware.php b/module/Core/src/ShortUrl/Middleware/ExtraPathRedirectMiddleware.php
new file mode 100644
index 00000000..401c7a0a
--- /dev/null
+++ b/module/Core/src/ShortUrl/Middleware/ExtraPathRedirectMiddleware.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shlinkio\Shlink\Core\ShortUrl\Middleware;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\UriInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
+use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
+use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
+use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
+use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
+use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlRedirectionBuilderInterface;
+use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
+use Shlinkio\Shlink\Core\Visit\VisitsTrackerInterface;
+
+use function array_pad;
+use function explode;
+use function sprintf;
+use function trim;
+
+class ExtraPathRedirectMiddleware implements MiddlewareInterface
+{
+    public function __construct(
+        private ShortUrlResolverInterface $resolver,
+        private VisitsTrackerInterface $visitTracker,
+        private ShortUrlRedirectionBuilderInterface $redirectionBuilder,
+        private RedirectResponseHelperInterface $redirectResponseHelper,
+        private UrlShortenerOptions $urlShortenerOptions,
+    ) {
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        /** @var NotFoundType|null $notFoundType */
+        $notFoundType = $request->getAttribute(NotFoundType::class);
+
+        // We'll apply this logic only if actively opted in and current URL is potentially /{shortCode}/[...]
+        if (! $notFoundType?->isRegularNotFound() || ! $this->urlShortenerOptions->appendExtraPath()) {
+            return $handler->handle($request);
+        }
+
+        $uri = $request->getUri();
+        $query = $request->getQueryParams();
+        [$potentialShortCode, $extraPath] = $this->resolvePotentialShortCodeAndExtraPath($uri);
+        $identifier = ShortUrlIdentifier::fromShortCodeAndDomain($potentialShortCode, $uri->getAuthority());
+
+        try {
+            $shortUrl = $this->resolver->resolveEnabledShortUrl($identifier);
+
+            // TODO Track visit
+
+            $longUrl = $this->redirectionBuilder->buildShortUrlRedirect($shortUrl, $query, $extraPath);
+            return $this->redirectResponseHelper->buildRedirectResponse($longUrl);
+        } catch (ShortUrlNotFoundException) {
+            return $handler->handle($request);
+        }
+    }
+
+    /**
+     * @return array{0: string, 1: string|null}
+     */
+    private function resolvePotentialShortCodeAndExtraPath(UriInterface $uri): array
+    {
+        $pathParts = explode('/', trim($uri->getPath(), '/'), 2);
+        [$potentialShortCode, $extraPath] = array_pad($pathParts, 2, null);
+
+        return [$potentialShortCode, $extraPath === null ? null : sprintf('/%s', $extraPath)];
+    }
+}