mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
Created middleware which ensures trailing slash and multi-segment features work properly together
This commit is contained in:
@@ -8,6 +8,7 @@ use Fig\Http\Message\RequestMethodInterface;
|
|||||||
use RKA\Middleware\IpAddress;
|
use RKA\Middleware\IpAddress;
|
||||||
use Shlinkio\Shlink\Core\Action as CoreAction;
|
use Shlinkio\Shlink\Core\Action as CoreAction;
|
||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Middleware\TrimTrailingSlashMiddleware;
|
||||||
use Shlinkio\Shlink\Rest\Action;
|
use Shlinkio\Shlink\Rest\Action;
|
||||||
use Shlinkio\Shlink\Rest\ConfigProvider;
|
use Shlinkio\Shlink\Rest\ConfigProvider;
|
||||||
use Shlinkio\Shlink\Rest\Middleware;
|
use Shlinkio\Shlink\Rest\Middleware;
|
||||||
@@ -19,6 +20,8 @@ return (static function (): array {
|
|||||||
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
||||||
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||||
|
|
||||||
|
// TODO This should be based on config, not the env var
|
||||||
$shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false) ? '[/]' : '';
|
$shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false) ? '[/]' : '';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -97,6 +100,7 @@ return (static function (): array {
|
|||||||
'path' => sprintf('/{shortCode}%s', $shortUrlRouteSuffix),
|
'path' => sprintf('/{shortCode}%s', $shortUrlRouteSuffix),
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
IpAddress::class,
|
IpAddress::class,
|
||||||
|
TrimTrailingSlashMiddleware::class,
|
||||||
CoreAction\RedirectAction::class,
|
CoreAction\RedirectAction::class,
|
||||||
],
|
],
|
||||||
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
|
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ return (static function (): array {
|
|||||||
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
|
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
|
||||||
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false),
|
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false),
|
||||||
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false),
|
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false),
|
||||||
|
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ return [
|
|||||||
],
|
],
|
||||||
'auto_resolve_titles' => true,
|
'auto_resolve_titles' => true,
|
||||||
// 'multi_segment_slugs_enabled' => true,
|
// 'multi_segment_slugs_enabled' => true,
|
||||||
|
// 'trailing_slash_enabled' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ return [
|
|||||||
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => ConfigAbstractFactory::class,
|
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
|
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
|
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
|
||||||
|
ShortUrl\Middleware\TrimTrailingSlashMiddleware::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Tag\TagService::class => ConfigAbstractFactory::class,
|
Tag\TagService::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
@@ -154,6 +155,7 @@ return [
|
|||||||
Util\RedirectResponseHelper::class,
|
Util\RedirectResponseHelper::class,
|
||||||
Options\UrlShortenerOptions::class,
|
Options\UrlShortenerOptions::class,
|
||||||
],
|
],
|
||||||
|
ShortUrl\Middleware\TrimTrailingSlashMiddleware::class => [Options\UrlShortenerOptions::class],
|
||||||
|
|
||||||
EventDispatcher\PublishingUpdatesGenerator::class => [
|
EventDispatcher\PublishingUpdatesGenerator::class => [
|
||||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ final class UrlShortenerOptions
|
|||||||
public readonly bool $autoResolveTitles = false,
|
public readonly bool $autoResolveTitles = false,
|
||||||
public readonly bool $appendExtraPath = false,
|
public readonly bool $appendExtraPath = false,
|
||||||
public readonly bool $multiSegmentSlugsEnabled = false,
|
public readonly bool $multiSegmentSlugsEnabled = false,
|
||||||
|
public readonly bool $trailingSlashEnabled = false,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
|
|
||||||
|
use function rtrim;
|
||||||
|
|
||||||
|
class TrimTrailingSlashMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
private const SHORT_CODE_ATTR = 'shortCode';
|
||||||
|
|
||||||
|
public function __construct(private readonly UrlShortenerOptions $options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
return $handler->handle($this->resolveRequest($request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveRequest(ServerRequestInterface $request): ServerRequestInterface
|
||||||
|
{
|
||||||
|
// If multi-segment slugs are enabled together with trailing slashes, the "shortCode" attribute will include
|
||||||
|
// ending slashes that we need to trim for a proper short code matching
|
||||||
|
|
||||||
|
/** @var string|null $shortCode */
|
||||||
|
$shortCode = $request->getAttribute(self::SHORT_CODE_ATTR);
|
||||||
|
$shouldTrimSlash = $shortCode !== null && $this->options->trailingSlashEnabled;
|
||||||
|
|
||||||
|
return $shouldTrimSlash ? $request->withAttribute(self::SHORT_CODE_ATTR, rtrim($shortCode, '/')) : $request;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\Core\ShortUrl\Middleware;
|
||||||
|
|
||||||
|
use Laminas\Diactoros\Response;
|
||||||
|
use Laminas\Diactoros\ServerRequestFactory;
|
||||||
|
use PHPUnit\Framework\Assert;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Middleware\TrimTrailingSlashMiddleware;
|
||||||
|
|
||||||
|
use function Functional\compose;
|
||||||
|
use function Functional\const_function;
|
||||||
|
|
||||||
|
class TrimTrailingSlashMiddlewareTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private ObjectProphecy $requestHandler;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->requestHandler = $this->prophesize(RequestHandlerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideRequests
|
||||||
|
*/
|
||||||
|
public function returnsExpectedResponse(
|
||||||
|
bool $trailingSlashEnabled,
|
||||||
|
ServerRequestInterface $inputRequest,
|
||||||
|
callable $assertions,
|
||||||
|
): void {
|
||||||
|
$arg = compose($assertions, const_function(true));
|
||||||
|
|
||||||
|
$this->requestHandler->handle(Argument::that($arg))->willReturn(new Response());
|
||||||
|
$this->middleware($trailingSlashEnabled)->process($inputRequest, $this->requestHandler->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideRequests(): iterable
|
||||||
|
{
|
||||||
|
yield 'trailing slash disabled' => [
|
||||||
|
false,
|
||||||
|
$inputReq = ServerRequestFactory::fromGlobals(),
|
||||||
|
function (ServerRequestInterface $request) use ($inputReq): void {
|
||||||
|
Assert::assertSame($inputReq, $request);
|
||||||
|
},
|
||||||
|
];
|
||||||
|
yield 'trailing slash enabled without shortCode attr' => [
|
||||||
|
true,
|
||||||
|
$inputReq = ServerRequestFactory::fromGlobals(),
|
||||||
|
function (ServerRequestInterface $request) use ($inputReq): void {
|
||||||
|
Assert::assertSame($inputReq, $request);
|
||||||
|
},
|
||||||
|
];
|
||||||
|
yield 'trailing slash enabled with null shortCode attr' => [
|
||||||
|
true,
|
||||||
|
$inputReq = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', null),
|
||||||
|
function (ServerRequestInterface $request) use ($inputReq): void {
|
||||||
|
Assert::assertSame($inputReq, $request);
|
||||||
|
},
|
||||||
|
];
|
||||||
|
yield 'trailing slash enabled with non-null shortCode attr' => [
|
||||||
|
true,
|
||||||
|
$inputReq = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'foo//'),
|
||||||
|
function (ServerRequestInterface $request) use ($inputReq): void {
|
||||||
|
Assert::assertNotSame($inputReq, $request);
|
||||||
|
Assert::assertEquals('foo', $request->getAttribute('shortCode'));
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function middleware(bool $trailingSlashEnabled = false): TrimTrailingSlashMiddleware
|
||||||
|
{
|
||||||
|
return new TrimTrailingSlashMiddleware(new UrlShortenerOptions(trailingSlashEnabled: $trailingSlashEnabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user