From 31232ee0243f4ee18018e263d040f0969ee586ad Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Sun, 31 Dec 2023 11:35:30 +0100 Subject: [PATCH] [HttpFoundation] Add `UrlParser` and `Url` --- .../DependencyInjection/EnvVarProcessor.php | 22 ++--- .../Tests/Dumper/PhpDumperTest.php | 4 +- .../Tests/ParameterBag/ParameterBagTest.php | 4 +- .../Component/HttpFoundation/CHANGELOG.md | 1 + .../Exception/Parser/InvalidUrlException.php | 20 +++++ .../Exception/Parser/MissingHostException.php | 20 +++++ .../Parser/MissingSchemeException.php | 20 +++++ .../Tests/UrlParser/UrlParserTest.php | 74 ++++++++++++++++ .../Tests/UrlParser/UrlTest.php | 68 ++++++++++++++ .../HttpFoundation/UrlParser/Url.php | 88 +++++++++++++++++++ .../HttpFoundation/UrlParser/UrlParser.php | 67 ++++++++++++++ .../Mailer/Tests/Transport/DsnTest.php | 6 +- .../Component/Mailer/Tests/TransportTest.php | 4 +- .../Component/Mailer/Transport/Dsn.php | 32 +++---- .../Notifier/Tests/Transport/DsnTest.php | 6 +- .../Component/Notifier/Transport/Dsn.php | 34 ++++--- .../Component/Translation/Provider/Dsn.php | 34 ++++--- .../Translation/Tests/Provider/DsnTest.php | 6 +- 18 files changed, 429 insertions(+), 81 deletions(-) create mode 100644 src/Symfony/Component/HttpFoundation/Exception/Parser/InvalidUrlException.php create mode 100644 src/Symfony/Component/HttpFoundation/Exception/Parser/MissingHostException.php create mode 100644 src/Symfony/Component/HttpFoundation/Exception/Parser/MissingSchemeException.php create mode 100644 src/Symfony/Component/HttpFoundation/Tests/UrlParser/UrlParserTest.php create mode 100644 src/Symfony/Component/HttpFoundation/Tests/UrlParser/UrlTest.php create mode 100644 src/Symfony/Component/HttpFoundation/UrlParser/Url.php create mode 100644 src/Symfony/Component/HttpFoundation/UrlParser/UrlParser.php diff --git a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php index 7376d03595264..d6c2bc9a0193d 100644 --- a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php +++ b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php @@ -14,6 +14,7 @@ use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException; use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\HttpFoundation\UrlParser\UrlParser; /** * @author Nicolas Grekas @@ -287,27 +288,20 @@ public function getEnv(string $prefix, string $name, \Closure $getEnv): mixed } if ('url' === $prefix) { - $parsedEnv = parse_url($env); - - if (false === $parsedEnv) { + try { + $params = UrlParser::parse($env); + } catch (\InvalidArgumentException) { throw new RuntimeException(sprintf('Invalid URL in env var "%s".', $name)); } - if (!isset($parsedEnv['scheme'], $parsedEnv['host'])) { + + if (null === $params->host) { throw new RuntimeException(sprintf('Invalid URL env var "%s": schema and host expected, "%s" given.', $name, $env)); } - $parsedEnv += [ - 'port' => null, - 'user' => null, - 'pass' => null, - 'path' => null, - 'query' => null, - 'fragment' => null, - ]; // remove the '/' separator - $parsedEnv['path'] = '/' === ($parsedEnv['path'] ?? '/') ? '' : substr($parsedEnv['path'], 1); + $params->path = '/' === ($params->path ?? '/') ? '' : substr($params->path, 1); - return $parsedEnv; + return (array) $params; } if ('query_string' === $prefix) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index deb1e23f2b3b1..12becdcd7d1f4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -624,12 +624,12 @@ public function testDumpedUrlEnvParameters() $container = new \Symfony_DI_PhpDumper_Test_UrlParameters(); $this->assertSame([ 'scheme' => 'postgres', + 'user' => 'user', + 'pass' => null, 'host' => 'localhost', 'port' => 5432, - 'user' => 'user', 'path' => 'database', 'query' => 'sslmode=disable', - 'pass' => null, 'fragment' => null, ], $container->getParameter('hello')); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/ParameterBagTest.php b/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/ParameterBagTest.php index 99c2f6a35a296..344c590bc7600 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/ParameterBagTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/ParameterBagTest.php @@ -348,8 +348,8 @@ public function testResolveStringWithSpacesReturnsString($expected, $test, $desc public static function stringsWithSpacesProvider() { return [ - ['bar', '%foo%', 'Parameters must be wrapped by %.'], - ['% foo %', '% foo %', 'Parameters should not have spaces.'], + ['bar', '%foo%', 'Url must be wrapped by %.'], + ['% foo %', '% foo %', 'Url should not have spaces.'], ['{% set my_template = "foo" %}', '{% set my_template = "foo" %}', 'Twig-like strings are not parameters.'], ['50% is less than 100%', '50% is less than 100%', 'Text between % signs is allowed, if there are spaces.'], ]; diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index d4d07411f70e7..4359869862fe6 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `UploadedFile::getClientOriginalPath()` + * Add `UrlParser` and `Url` 7.0 --- diff --git a/src/Symfony/Component/HttpFoundation/Exception/Parser/InvalidUrlException.php b/src/Symfony/Component/HttpFoundation/Exception/Parser/InvalidUrlException.php new file mode 100644 index 0000000000000..31ac602d118b8 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Exception/Parser/InvalidUrlException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception\Parser; + +class InvalidUrlException extends \InvalidArgumentException +{ + public function __construct() + { + parent::__construct('The URL is invalid.'); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Exception/Parser/MissingHostException.php b/src/Symfony/Component/HttpFoundation/Exception/Parser/MissingHostException.php new file mode 100644 index 0000000000000..724659e509be4 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Exception/Parser/MissingHostException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception\Parser; + +class MissingHostException extends \InvalidArgumentException +{ + public function __construct() + { + parent::__construct('The URL must contain a host.'); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Exception/Parser/MissingSchemeException.php b/src/Symfony/Component/HttpFoundation/Exception/Parser/MissingSchemeException.php new file mode 100644 index 0000000000000..e476707b4cf0e --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Exception/Parser/MissingSchemeException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception\Parser; + +class MissingSchemeException extends \InvalidArgumentException +{ + public function __construct() + { + parent::__construct('The URL must contain a scheme.'); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/UrlParser/UrlParserTest.php b/src/Symfony/Component/HttpFoundation/Tests/UrlParser/UrlParserTest.php new file mode 100644 index 0000000000000..77ad89fc8327d --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/UrlParser/UrlParserTest.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\UrlParser; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\UrlParser\UrlParser; +use Symfony\Component\HttpFoundation\Exception\Parser\InvalidUrlException; +use Symfony\Component\HttpFoundation\Exception\Parser\MissingSchemeException; + +class UrlParserTest extends TestCase +{ + public function testInvalidDsn() + { + $this->expectException(InvalidUrlException::class); + $this->expectExceptionMessage('The URL is invalid.'); + + UrlParser::parse('/search:2019'); + } + + public function testMissingScheme() + { + $this->expectException(MissingSchemeException::class); + $this->expectExceptionMessage('The URL must contain a scheme.'); + + UrlParser::parse('://example.com'); + } + + public function testReturnsFullParsedDsn() + { + $parsedDsn = UrlParser::parse('http://user:pass@localhost:8080/path?query=1#fragment'); + + $this->assertSame('http', $parsedDsn->scheme); + $this->assertSame('user', $parsedDsn->user); + $this->assertSame('pass', $parsedDsn->password); + $this->assertSame('localhost', $parsedDsn->host); + $this->assertSame(8080, $parsedDsn->port); + $this->assertSame('/path', $parsedDsn->path); + $this->assertSame('query=1', $parsedDsn->query); + $this->assertSame('fragment', $parsedDsn->fragment); + } + + public function testItDecodesByDefault() + { + $parsedDsn = UrlParser::parse('http://user%20one:p%40ss@localhost:8080/path?query=1#fragment'); + + $this->assertSame('user one', $parsedDsn->user); + $this->assertSame('p@ss', $parsedDsn->password); + } + + public function testDisableDecoding() + { + $parsedDsn = UrlParser::parse('http://user%20one:p%40ss@localhost:8080/path?query=1#fragment', decodeAuth: false); + + $this->assertSame('user%20one', $parsedDsn->user); + $this->assertSame('p%40ss', $parsedDsn->password); + } + + public function testEmptyUserAndPasswordAreSetToNull() + { + $parsedDsn = UrlParser::parse('http://@localhost:8080/path?query=1#fragment'); + + $this->assertNull($parsedDsn->user); + $this->assertNull($parsedDsn->password); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/UrlParser/UrlTest.php b/src/Symfony/Component/HttpFoundation/Tests/UrlParser/UrlTest.php new file mode 100644 index 0000000000000..454508c03177a --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/UrlParser/UrlTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\UrlParser; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\UrlParser\Url; + +class UrlTest extends TestCase +{ + /** + * @dataProvider provideUserAndPass + */ + public function testIsAuthenticated(?string $user, ?string $pass, bool $expected) + { + $params = new Url('http', $user, $pass); + + $this->assertSame($expected, $params->isAuthenticated()); + } + + public function provideUserAndPass() + { + yield 'no user, no pass' => [null, null, false]; + yield 'user, no pass' => ['user', null, true]; + yield 'no user, pass' => [null, 'pass', true]; + yield 'user, pass' => ['user', 'pass', true]; + } + + public function testToString() + { + $params = new Url( + 'http', + 'user', + 'pass', + 'localhost', + 8080, + '/path', + 'query=1', + 'fragment' + ); + + $this->assertSame('http://user:pass@localhost:8080/path?query=1#fragment', (string) $params); + } + + public function testToStringReencode() + { + $params = new Url( + 'http', + 'user one', + 'p@ss', + 'localhost', + 8080, + '/p@th', + 'query=1', + 'fr%40gment%20with%20spaces' + ); + + $this->assertSame('http://user%20one:p%40ss@localhost:8080/p@th?query=1#fr%40gment%20with%20spaces', (string) $params); + } +} diff --git a/src/Symfony/Component/HttpFoundation/UrlParser/Url.php b/src/Symfony/Component/HttpFoundation/UrlParser/Url.php new file mode 100644 index 0000000000000..5e542766ff1ab --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/UrlParser/Url.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\UrlParser; + +/** + * @author Alexandre Daubois + */ +final class Url implements \Stringable +{ + public function __construct( + public string $scheme, + public ?string $user = null, + public ?string $password = null, + public ?string $host = null, + public ?int $port = null, + public ?string $path = null, + public ?string $query = null, + public ?string $fragment = null + ) { + } + + public function isAuthenticated(): bool + { + return null !== $this->user || null !== $this->password; + } + + public function isScheme(string $scheme): bool + { + return $this->scheme === $scheme; + } + + public function __toString(): string + { + $dsn = $this->scheme.'://'; + + if (null !== $this->user) { + $dsn .= rawurlencode($this->user); + } + + if (null !== $this->password) { + $dsn .= ':'.rawurlencode($this->password); + } + + if (null !== $this->user || null !== $this->password) { + $dsn .= '@'; + } + + $dsn .= $this->host; + + if (null !== $this->port) { + $dsn .= ':'.$this->port; + } + + if (null !== $this->path) { + $dsn .= $this->path; + } + + if (null !== $this->query) { + $dsn .= '?'.$this->query; + } + + if (null !== $this->fragment) { + $dsn .= '#'.$this->fragment; + } + + return $dsn; + } + + public function parsedQuery(): array + { + if (null === $this->query) { + return []; + } + + parse_str($this->query, $query); + + return $query; + } +} diff --git a/src/Symfony/Component/HttpFoundation/UrlParser/UrlParser.php b/src/Symfony/Component/HttpFoundation/UrlParser/UrlParser.php new file mode 100644 index 0000000000000..c1f5f843ea18f --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/UrlParser/UrlParser.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\UrlParser; + +use Symfony\Component\HttpFoundation\Exception\Parser\InvalidUrlException; +use Symfony\Component\HttpFoundation\Exception\Parser\MissingHostException; +use Symfony\Component\HttpFoundation\Exception\Parser\MissingSchemeException; + +/** + * @author Alexandre Daubois + */ +final class UrlParser +{ + private function __construct() + { + } + + public static function parse(#[\SensitiveParameter] string $url, bool $requiresHost = false, bool $decodeAuth = true): Url + { + if (false === $params = parse_url($url)) { + throw new InvalidUrlException($url); + } + + if (!isset($params['scheme'])) { + throw new MissingSchemeException(); + } + + if ($requiresHost && !isset($params['host'])) { + throw new MissingHostException(); + } + + $params += [ + 'port' => null, + 'user' => null, + 'pass' => null, + 'path' => null, + 'query' => null, + 'fragment' => null, + ]; + + $auth = [ + 'user' => $params['user'], + 'pass' => $params['pass'], + ]; + + unset($params['user'], $params['pass']); + + if ($decodeAuth) { + $auth = array_map(static fn (?string $param): ?string => \is_string($param) ? rawurldecode($param) : $param, $auth); + } + + return new Url( + ...$params, + user: '' === $auth['user'] ? null : $auth['user'], + password: '' === $auth['pass'] ? null : $auth['pass'], + ); + } +} diff --git a/src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php b/src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php index f0c0a8ffe0fed..606605f664d3e 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php @@ -92,17 +92,17 @@ public static function invalidDsnProvider(): iterable { yield [ 'some://', - 'The mailer DSN is invalid.', + 'The URL is invalid.', ]; yield [ '//sendmail', - 'The mailer DSN must contain a scheme.', + 'The URL must contain a scheme.', ]; yield [ 'file:///some/path', - 'The mailer DSN must contain a host (use "default" by default).', + 'The URL must contain a host (use "default" by default).', ]; } } diff --git a/src/Symfony/Component/Mailer/Tests/TransportTest.php b/src/Symfony/Component/Mailer/Tests/TransportTest.php index f0dd6d4be930e..0153ad959971a 100644 --- a/src/Symfony/Component/Mailer/Tests/TransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/TransportTest.php @@ -92,9 +92,9 @@ public static function fromWrongStringProvider(): iterable { yield 'garbage at the end' => ['dummy://a some garbage here', 'The mailer DSN has some garbage at the end.']; - yield 'not a valid DSN' => ['something not a dsn', 'The mailer DSN must contain a scheme.']; + yield 'not a valid DSN' => ['something not a dsn', 'The URL must contain a scheme.']; - yield 'failover not closed' => ['failover(dummy://a', 'The mailer DSN must contain a scheme.']; + yield 'failover not closed' => ['failover(dummy://a', 'The URL must contain a scheme.']; yield 'not a valid keyword' => ['foobar(dummy://a)', 'The "foobar" keyword is not valid (valid ones are "failover", "roundrobin")']; } diff --git a/src/Symfony/Component/Mailer/Transport/Dsn.php b/src/Symfony/Component/Mailer/Transport/Dsn.php index 4c0fb48c3b389..de872f82f3e95 100644 --- a/src/Symfony/Component/Mailer/Transport/Dsn.php +++ b/src/Symfony/Component/Mailer/Transport/Dsn.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Mailer\Transport; +use Symfony\Component\HttpFoundation\UrlParser\UrlParser; +use Symfony\Component\HttpFoundation\Exception\Parser\MissingHostException; use Symfony\Component\Mailer\Exception\InvalidArgumentException; /** @@ -37,24 +39,22 @@ public function __construct(string $scheme, string $host, string $user = null, # public static function fromString(#[\SensitiveParameter] string $dsn): self { - if (false === $parsedDsn = parse_url($dsn)) { - throw new InvalidArgumentException('The mailer DSN is invalid.'); + try { + $params = UrlParser::parse($dsn, true); + } catch (MissingHostException) { + throw new InvalidArgumentException('The URL must contain a host (use "default" by default).'); + } catch (\InvalidArgumentException $e) { + throw new InvalidArgumentException($e->getMessage()); } - if (!isset($parsedDsn['scheme'])) { - throw new InvalidArgumentException('The mailer DSN must contain a scheme.'); - } - - if (!isset($parsedDsn['host'])) { - throw new InvalidArgumentException('The mailer DSN must contain a host (use "default" by default).'); - } - - $user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; - $password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; - $port = $parsedDsn['port'] ?? null; - parse_str($parsedDsn['query'] ?? '', $query); - - return new self($parsedDsn['scheme'], $parsedDsn['host'], $user, $password, $port, $query); + return new self( + $params->scheme, + $params->host, + $params->user, + $params->password, + $params->port, + $params->parsedQuery() + ); } public function getScheme(): string diff --git a/src/Symfony/Component/Notifier/Tests/Transport/DsnTest.php b/src/Symfony/Component/Notifier/Tests/Transport/DsnTest.php index a75f1608d8090..bad2ffab93184 100644 --- a/src/Symfony/Component/Notifier/Tests/Transport/DsnTest.php +++ b/src/Symfony/Component/Notifier/Tests/Transport/DsnTest.php @@ -155,17 +155,17 @@ public static function invalidDsnProvider(): iterable { yield [ 'some://', - 'The notifier DSN is invalid.', + 'The URL is invalid.', ]; yield [ '//slack', - 'The notifier DSN must contain a scheme.', + 'The URL must contain a scheme.', ]; yield [ 'file:///some/path', - 'The notifier DSN must contain a host (use "default" by default).', + 'The URL must contain a host (use "default" by default).', ]; } diff --git a/src/Symfony/Component/Notifier/Transport/Dsn.php b/src/Symfony/Component/Notifier/Transport/Dsn.php index fa37b6d338eb0..589a234bfc468 100644 --- a/src/Symfony/Component/Notifier/Transport/Dsn.php +++ b/src/Symfony/Component/Notifier/Transport/Dsn.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Notifier\Transport; +use Symfony\Component\HttpFoundation\UrlParser\UrlParser; +use Symfony\Component\HttpFoundation\Exception\Parser\MissingHostException; use Symfony\Component\Notifier\Exception\InvalidArgumentException; use Symfony\Component\Notifier\Exception\MissingRequiredOptionException; @@ -31,27 +33,23 @@ final class Dsn public function __construct(#[\SensitiveParameter] string $dsn) { - $this->originalDsn = $dsn; - - if (false === $parsedDsn = parse_url($dsn)) { - throw new InvalidArgumentException('The notifier DSN is invalid.'); - } - - if (!isset($parsedDsn['scheme'])) { - throw new InvalidArgumentException('The notifier DSN must contain a scheme.'); + try { + $params = UrlParser::parse($dsn, true); + } catch (MissingHostException) { + throw new InvalidArgumentException('The URL must contain a host (use "default" by default).'); + } catch (\InvalidArgumentException $e) { + throw new InvalidArgumentException($e->getMessage()); } - $this->scheme = $parsedDsn['scheme']; - if (!isset($parsedDsn['host'])) { - throw new InvalidArgumentException('The notifier DSN must contain a host (use "default" by default).'); - } - $this->host = $parsedDsn['host']; + $this->originalDsn = $dsn; - $this->user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; - $this->password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; - $this->port = $parsedDsn['port'] ?? null; - $this->path = $parsedDsn['path'] ?? null; - parse_str($parsedDsn['query'] ?? '', $this->options); + $this->scheme = $params->scheme; + $this->host = $params->host; + $this->user = $params->user; + $this->password = $params->password; + $this->port = $params->port; + $this->path = $params->path; + $this->options = $params->parsedQuery(); } public function getScheme(): string diff --git a/src/Symfony/Component/Translation/Provider/Dsn.php b/src/Symfony/Component/Translation/Provider/Dsn.php index af75cb3ae6721..b57fc83717454 100644 --- a/src/Symfony/Component/Translation/Provider/Dsn.php +++ b/src/Symfony/Component/Translation/Provider/Dsn.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Translation\Provider; +use Symfony\Component\HttpFoundation\UrlParser\UrlParser; +use Symfony\Component\HttpFoundation\Exception\Parser\MissingHostException; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\MissingRequiredOptionException; @@ -31,27 +33,23 @@ final class Dsn public function __construct(#[\SensitiveParameter] string $dsn) { - $this->originalDsn = $dsn; - - if (false === $parsedDsn = parse_url($dsn)) { - throw new InvalidArgumentException('The translation provider DSN is invalid.'); - } - - if (!isset($parsedDsn['scheme'])) { - throw new InvalidArgumentException('The translation provider DSN must contain a scheme.'); + try { + $params = UrlParser::parse($dsn, true); + } catch (MissingHostException) { + throw new InvalidArgumentException('The URL must contain a host (use "default" by default).'); + } catch (\InvalidArgumentException $e) { + throw new InvalidArgumentException($e->getMessage()); } - $this->scheme = $parsedDsn['scheme']; - if (!isset($parsedDsn['host'])) { - throw new InvalidArgumentException('The translation provider DSN must contain a host (use "default" by default).'); - } - $this->host = $parsedDsn['host']; + $this->originalDsn = $dsn; - $this->user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; - $this->password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; - $this->port = $parsedDsn['port'] ?? null; - $this->path = $parsedDsn['path'] ?? null; - parse_str($parsedDsn['query'] ?? '', $this->options); + $this->scheme = $params->scheme; + $this->host = $params->host; + $this->user = $params->user; + $this->password = $params->password; + $this->port = $params->port; + $this->path = $params->path; + $this->options = $params->parsedQuery(); } public function getScheme(): string diff --git a/src/Symfony/Component/Translation/Tests/Provider/DsnTest.php b/src/Symfony/Component/Translation/Tests/Provider/DsnTest.php index 54593be96710d..99c052a1861ae 100644 --- a/src/Symfony/Component/Translation/Tests/Provider/DsnTest.php +++ b/src/Symfony/Component/Translation/Tests/Provider/DsnTest.php @@ -155,17 +155,17 @@ public static function invalidDsnProvider(): iterable { yield [ 'some://', - 'The translation provider DSN is invalid.', + 'The URL is invalid.', ]; yield [ '//loco', - 'The translation provider DSN must contain a scheme.', + 'The URL must contain a scheme.', ]; yield [ 'file:///some/path', - 'The translation provider DSN must contain a host (use "default" by default).', + 'The URL must contain a host (use "default" by default).', ]; }