Ensure query parameters are preserved verbatim when forwarded to long URL

This commit is contained in:
Alejandro Celaya 2024-10-10 11:28:31 +02:00
parent a8e4b2fceb
commit 1773e6ecae
5 changed files with 50 additions and 19 deletions

View file

@ -18,7 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
* *Nothing* * *Nothing*
### Fixed ### Fixed
* *Nothing* * [#2213](https://github.com/shlinkio/shlink/issues/2213) Fix spaces being replaced with underscores in query parameter names, when forwarded from short URL to long URL.
## [4.2.1] - 2024-10-04 ## [4.2.1] - 2024-10-04

View file

@ -7,7 +7,7 @@ return [
'rabbitmq' => [ 'rabbitmq' => [
'enabled' => true, 'enabled' => true,
'host' => 'shlink_rabbitmq', 'host' => 'shlink_rabbitmq',
'port' => '5672', 'port' => 5672,
'user' => 'rabbit', 'user' => 'rabbit',
'password' => 'rabbit', 'password' => 'rabbit',
], ],

View file

@ -28,11 +28,15 @@ readonly class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderI
?string $extraPath = null, ?string $extraPath = null,
): string { ): string {
$uri = new Uri($this->redirectionResolver->resolveLongUrl($shortUrl, $request)); $uri = new Uri($this->redirectionResolver->resolveLongUrl($shortUrl, $request));
$currentQuery = $request->getQueryParams();
$shouldForwardQuery = $shortUrl->forwardQuery(); $shouldForwardQuery = $shortUrl->forwardQuery();
$baseQueryString = $uri->getQuery(); $baseQueryString = $uri->getQuery();
$basePath = $uri->getPath(); $basePath = $uri->getPath();
// Get current query by manually parsing query string, instead of using $request->getQueryParams().
// That prevents some weird PHP logic in which some characters in param names are converted to ensure resulting
// names are valid variable names.
$currentQuery = Query::parse($request->getUri()->getQuery());
return $uri return $uri
->withQuery($shouldForwardQuery ? $this->resolveQuery($baseQueryString, $currentQuery) : $baseQueryString) ->withQuery($shouldForwardQuery ? $this->resolveQuery($baseQueryString, $currentQuery) : $baseQueryString)
->withPath($this->resolvePath($basePath, $extraPath)) ->withPath($this->resolvePath($basePath, $extraPath))

View file

@ -122,4 +122,22 @@ class RedirectTest extends ApiTestCase
self::assertEquals(302, $response->getStatusCode()); self::assertEquals(302, $response->getStatusCode());
self::assertEquals($longUrl, $response->getHeaderLine('Location')); self::assertEquals($longUrl, $response->getHeaderLine('Location'));
} }
#[Test]
public function queryParametersAreProperlyForwarded(): void
{
$slug = 'forward-query-params';
$this->callApiWithKey('POST', '/short-urls', [
RequestOptions::JSON => [
'longUrl' => 'https://example.com',
'customSlug' => $slug,
'forwardQuery' => true,
],
]);
$response = $this->callShortUrl($slug, [RequestOptions::QUERY => ['foo bar' => '123']]);
self::assertEquals(302, $response->getStatusCode());
self::assertEquals('https://example.com?foo%20bar=123', $response->getHeaderLine('Location'));
}
} }

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\ShortUrl\Helper; namespace ShlinkioTest\Shlink\Core\ShortUrl\Helper;
use Laminas\Diactoros\ServerRequestFactory; use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Uri;
use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\Attributes\TestWith;
@ -53,62 +54,70 @@ class ShortUrlRedirectionBuilderTest extends TestCase
public static function provideData(): iterable public static function provideData(): iterable
{ {
$request = static fn (array $query = []) => ServerRequestFactory::fromGlobals()->withQueryParams($query); $request = static fn (string $query = '') => ServerRequestFactory::fromGlobals()->withUri(
(new Uri())->withQuery($query),
);
yield ['https://example.com/foo/bar?some=thing', $request(), null, true]; yield ['https://example.com/foo/bar?some=thing', $request(), null, true];
yield ['https://example.com/foo/bar?some=thing', $request(), null, null]; yield ['https://example.com/foo/bar?some=thing', $request(), null, null];
yield ['https://example.com/foo/bar?some=thing', $request(), null, false]; yield ['https://example.com/foo/bar?some=thing', $request(), null, false];
yield ['https://example.com/foo/bar?some=thing&else', $request(['else' => null]), null, true]; yield ['https://example.com/foo/bar?some=thing&else', $request('else'), null, true];
yield ['https://example.com/foo/bar?some=thing&foo=bar', $request(['foo' => 'bar']), null, true]; yield ['https://example.com/foo/bar?some=thing&foo=bar', $request('foo=bar'), null, true];
yield ['https://example.com/foo/bar?some=thing&foo=bar', $request(['foo' => 'bar']), null, null]; yield ['https://example.com/foo/bar?some=thing&foo=bar', $request('foo=bar'), null, null];
yield ['https://example.com/foo/bar?some=thing', $request(['foo' => 'bar']), null, false]; yield ['https://example.com/foo/bar?some=thing', $request('foo=bar'), null, false];
yield ['https://example.com/foo/bar?some=thing&123=foo', $request(['123' => 'foo']), null, true]; yield ['https://example.com/foo/bar?some=thing&123=foo', $request('123=foo'), null, true];
yield ['https://example.com/foo/bar?some=thing&456=foo', $request([456 => 'foo']), null, true]; yield ['https://example.com/foo/bar?some=thing&456=foo', $request('456=foo'), null, true];
yield ['https://example.com/foo/bar?some=thing&456=foo', $request([456 => 'foo']), null, null]; yield ['https://example.com/foo/bar?some=thing&456=foo', $request('456=foo'), null, null];
yield ['https://example.com/foo/bar?some=thing', $request([456 => 'foo']), null, false]; yield ['https://example.com/foo/bar?some=thing', $request('456=foo'), null, false];
yield [ yield [
'https://example.com/foo/bar?some=overwritten&foo=bar', 'https://example.com/foo/bar?some=overwritten&foo=bar',
$request(['foo' => 'bar', 'some' => 'overwritten']), $request('foo=bar&some=overwritten'),
null, null,
true, true,
]; ];
yield [ yield [
'https://example.com/foo/bar?some=overwritten', 'https://example.com/foo/bar?some=overwritten',
$request(['foobar' => 'notrack', 'some' => 'overwritten']), $request('foobar=notrack&some=overwritten'),
null, null,
true, true,
]; ];
yield [ yield [
'https://example.com/foo/bar?some=overwritten', 'https://example.com/foo/bar?some=overwritten',
$request(['foobar' => 'notrack', 'some' => 'overwritten']), $request('foobar=notrack&some=overwritten'),
null, null,
null, null,
]; ];
yield [ yield [
'https://example.com/foo/bar?some=thing', 'https://example.com/foo/bar?some=thing',
$request(['foobar' => 'notrack', 'some' => 'overwritten']), $request('foobar=notrack&some=overwritten'),
null, null,
false, false,
]; ];
yield ['https://example.com/foo/bar/something/else-baz?some=thing', $request(), '/something/else-baz', true]; yield ['https://example.com/foo/bar/something/else-baz?some=thing', $request(), '/something/else-baz', true];
yield [ yield [
'https://example.com/foo/bar/something/else-baz?some=thing&hello=world', 'https://example.com/foo/bar/something/else-baz?some=thing&hello=world',
$request(['hello' => 'world']), $request('hello=world',),
'/something/else-baz', '/something/else-baz',
true, true,
]; ];
yield [ yield [
'https://example.com/foo/bar/something/else-baz?some=thing&hello=world', 'https://example.com/foo/bar/something/else-baz?some=thing&hello=world',
$request(['hello' => 'world']), $request('hello=world',),
'/something/else-baz', '/something/else-baz',
null, null,
]; ];
yield [ yield [
'https://example.com/foo/bar/something/else-baz?some=thing', 'https://example.com/foo/bar/something/else-baz?some=thing',
$request(['hello' => 'world']), $request('hello=world',),
'/something/else-baz', '/something/else-baz',
false, false,
]; ];
yield [
'https://example.com/foo/bar/something/else-baz?some=thing&parameter%20with%20spaces=world',
$request('parameter with spaces=world',),
'/something/else-baz',
true,
];
} }
/** /**