10000 Work around parse_url() bug (bis) · symfony/symfony@80257ea · GitHub
[go: up one dir, main page]

Skip to content

Commit 80257ea

Browse files
Work around parse_url() bug (bis)
1 parent fde6ae9 commit 80257ea

File tree

12 files changed

+60
- files changed
+60
-35
lines changed

src/Symfony/Component/DomCrawler/Tests/UriResolverTest.php

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ public static function provideResolverTests()
8787

8888
['http://', 'http://localhost', 'http://'],
8989
['/foo:123', 'http://localhost', 'http://localhost/foo:123'],
90+
['foo:123', 'http://localhost/', 'foo:123'],
91+
['foo/bar:1/baz', 'http://localhost/', 'http://localhost/foo/bar:1/baz'],
9092
];
9193
}
9294
}

src/Symfony/Component/DomCrawler/UriResolver.php

+1-5
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,8 @@ public static function resolve(string $uri, ?string $baseUri): string
3232
{
3333
$uri = trim($uri);
3434

35-
if (false === ($scheme = parse_url($uri, \PHP_URL_SCHEME)) && '/' === ($uri[0] ?? '')) {
36-
$scheme = parse_url($uri.'#', \PHP_URL_SCHEME);
37-
}
38-
3935
// absolute URL?
40-
if (null !== $scheme) {
36+
if (null !== parse_url(\strlen($uri) !== strcspn($uri, '?#') ? $uri : $uri.'#', \PHP_URL_SCHEME)) {
4137
return $uri;
4238
}
4339

src/Symfony/Component/HttpClient/CurlHttpClient.php

+6-3
Original file line numberDiff line numberDiff line change
@@ -421,8 +421,9 @@ private static function createRedirectResolver(array $options, string $host): \C
421421
}
422422
}
423423

424-
return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders, $options) {
424+
return static function ($ch, string $location, bool $noContent, bool &$locationHasHost) use (&$redirectHeaders, $options) {
425425
try {
426+
$locationHasHost = false;
426427
$location = self::parseUrl($location);
427428
} catch (InvalidArgumentException $e) {
428429
return null;
@@ -436,8 +437,10 @@ private static function createRedirectResolver(array $options, string $host): \C
436437
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
437438
}
438439

439-
if ($redirectHeaders && $host = parse_url('http:'.$location['authority'], \PHP_URL_HOST)) {
440-
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
440+
$locationHasHost = isset($location['authority']);
441+
442+
if ($redirectHeaders && $locationHasHost) {
443+
$requestHeaders = parse_url($location['authority'], \PHP_URL_HOST) === $redirectHeaders['host'] ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
441444
curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders);
442445
} elseif ($noContent && $redirectHeaders) {
443446
curl_setopt($ch, \CURLOPT_HTTPHEADER, $redirectHeaders['with_auth']);

src/Symfony/Component/HttpClient/HttpClientTrait.php

+17-9
Original file line numberDiff line numberDiff line change
@@ -514,29 +514,37 @@ private static function resolveUrl(array $url, ?array $base, array $queryDefault
514514
*/
515515
private static function parseUrl(string $url, array $query = [], array $allowedSchemes = ['http' => 80, 'https' => 443]): array
516516
{
517-
if (false === $parts = parse_url($url)) {
518-
if ('/' !== ($url[0] ?? '') || false === $parts = parse_url($url.'#')) {
519-
throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url));
520-
}
521-
unset($parts['fragment']);
517+
$tail = '';
518+
519+
if (false === $parts = parse_url(\strlen($url) !== strcspn($url, '?#') ? $url : $url.$tail = '#')) {
520+
throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url));
522521
}
523522

524523
if ($query) {
525524
$parts['query'] = self::mergeQueryString($parts['query'] ?? null, $query, true);
526525
}
527526

527+
$scheme = $parts['scheme'] ?? null;
528+
$host = $parts['host'] ?? null;
529+
530+
if (!$scheme && $host && !str_starts_with($url, '//')) {
531+
$parts = parse_url(':/'.$url.$tail);
532+
$parts['path'] = substr($parts['path'], 2);
533+
$scheme = $host = null;
534+
}
535+
528536
$port = $parts['port'] ?? 0;
529537

530-
if (null !== $scheme = $parts['scheme'] ?? null) {
538+
if (null !== $scheme) {
531539
if (!isset($allowedSchemes[$scheme = strtolower($scheme)])) {
532-
throw new InvalidArgumentException(sprintf('Unsupported scheme in "%s".', $url));
540+
throw new InvalidArgumentException(sprintf('Unsupported scheme in "%s": "%s" expected.', $url, implode('" or "', array_keys($allowedSchemes))));
533541
}
534542

535543
$port = $allowedSchemes[$scheme] === $port ? 0 : $port;
536544
$scheme .= ':';
537545
}
538546

539-
if (null !== $host = $parts['host'] ?? null) {
547+
if (null !== $host) {
540548
if (!\defined('INTL_IDNA_VARIANT_UTS46') && preg_match('/[\x80-\xFF]/', $host)) {
541549
throw new InvalidArgumentException(sprintf('Unsupported IDN "%s", try enabling the "intl" PHP extension or running "composer require symfony/polyfill-intl-idn".', $host));
542550
}
@@ -564,7 +572,7 @@ private static function parseUrl(string $url, array $query = [], array $allowedS
564572
'authority' => null !== $host ? '//'.(isset($parts['user']) ? $parts['user'].(isset($parts['pass']) ? ':'.$parts['pass'] : '').'@' : '').$host : null,
565573
'path' => isset($parts['path'][0]) ? $parts['path'] : null,
566574
'query' => isset($parts['query']) ? '?'.$parts['query'] : null,
567-
'fragment' => isset($parts['fragment']) ? '#'.$parts['fragment'] : null,
575+
'fragment' => isset($parts['fragment']) && !$tail ? '#'.$parts['fragment'] : null,
568576
];
569577
}
570578

src/Symfony/Component/HttpClient/NativeHttpClient.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ private static function createRedirectResolver(array $options, string $host, ?ar
389389
return null;
390390
}
391391

392+
$locationHasHost = isset($url['authority']);
392393
$url = self::resolveUrl($url, $info['url']);
393394
$info['redirect_url'] = implode('', $url);
394395

@@ -424,7 +425,7 @@ private static function createRedirectResolver(array $options, string $host, ?ar
424425

425426
[$host, $port] = self::parseHostPort($url, $info);
426427

427-
if (false !== (parse_url($location.'#', \PHP_URL_HOST) ?? false)) {
428+
if ($locationHasHost) {
428429
// Authorization and Cookie headers MUST NOT follow except for the initial host name
429430
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
430431
$requestHeaders[] = 'Host: '.$host.$port;

src/Symfony/Component/HttpClient/Response/CurlResponse.php

+6-5
Original file line numberDiff line numberDiff line change
@@ -436,17 +436,18 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
436436
$info['http_method'] = 'HEAD' === $info['http_method'] ? 'HEAD' : 'GET';
437437< 57AE code class="diff-text syntax-highlighted-line">
curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $info['http_method']);
438438
}
439+
$locationHasHost = false;
439440

440-
if (null === $info['redirect_url'] = $resolveRedirect($ch, $location, $noContent)) {
441+
if (null === $info['redirect_url'] = $resolveRedirect($ch, $location, $noContent, $locationHasHost)) {
441442
$options['max_redirects'] = curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT);
442443
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
443444
curl_setopt($ch, \CURLOPT_MAXREDIRS, $options['max_redirects']);
444-
} else {
445-
$url = parse_url($location ?? ':');
445+
} elseif ($locationHasHost) {
446+
$url = parse_url($info['redirect_url']);
446447

447-
if (isset($url['host']) && null !== $ip = $multi->dnsCache->hostnames[$url['host'] = strtolower($url['host'])] ?? null) {
448+
if (null !== $ip = $multi->dnsCache->hostnames[$url['host'] = strtolower($url['host'])] ?? null) {
448449
// Populate DNS cache for redirects if needed
449-
$port = $url['port'] ?? ('http' === ($url['scheme'] ?? parse_url(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL), \PHP_URL_SCHEME)) ? 80 : 443);
450+
$port = $url['port'] ?? ('http' === $url['scheme'] ? 80 : 443);
450451
curl_setopt($ch, \CURLOPT_RESOLVE, ["{$url['host']}:$port:$ip"]);
451452
$multi->dnsCache->removals["-{$url['host']}:$port"] = "-{$url['host']}:$port";
452453
}

src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php

+9
Original file line numberDiff line numberDiff line change
@@ -489,4 +489,13 @@ public function testNoPrivateNetworkWithResolve()
489489

490490
$client->request('GET', 'http://symfony.com', ['resolve' => ['symfony.com' => '127.0.0.1']]);
491491
}
492+
493+
public function testNoRedirectWithInvalidLocation()
494+
{
495+
$client = $this->getHttpClient(__FUNCTION__);
496+
497+
$response = $client->request('GET', 'http://localhost:8057/302-no-scheme');
498+
499+
$this->assertSame(302, $response->getStatusCode());
500+
}
492501
}

src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php

+4-3
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public static function provideResolveUrl(): array
102102
[self::RFC3986_BASE, 'g/../h', 'http://a/b/c/h'],
103103
[self::RFC3986_BASE, 'g;x=1/./y', 'http://a/b/c/g;x=1/y'],
104104
[self::RFC3986_BASE, 'g;x=1/../y', 'http://a/b/c/y'],
105+
[self::RFC3986_BASE, 'g/h:123/i', 'http://a/b/c/g/h:123/i'],
105106
// dot-segments in the query or fragment
106107
[self::RFC3986_BASE, 'g?y/./x', 'http://a/b/c/g?y/./x'],
107108
[self::RFC3986_BASE, 'g?y/../x', 'http://a/b/c/g?y/../x'],
@@ -127,14 +128,14 @@ public static function provideResolveUrl(): array
127128
public function testResolveUrlWithoutScheme()
128129
{
129130
$this->expectException(InvalidArgumentException::class);
130-
$this->expectExceptionMessage('Invalid URL: scheme is missing in "//localhost:8080". Did you forget to add "http(s)://"?');
131+
$this->expectExceptionMessage('Unsupported scheme in "localhost:8080": "http" or "https" expected.');
131132
self::resolveUrl(self::parseUrl('localhost:8080'), null);
132133
}
133134

134-
public function testResolveBaseUrlWitoutScheme()
135+
public function testResolveBaseUrlWithoutScheme()
135136
{
136137
$this->expectException(InvalidArgumentException::class);
137-
$this->expectExceptionMessage('Invalid URL: scheme is missing in "//localhost:8081". Did you forget to add "http(s)://"?');
138+
$this->expectExceptionMessage('Unsupported scheme in "localhost:8081": "http" or "https" expected.');
138139
self::resolveUrl(self::parseUrl('/foo'), self::parseUrl('localhost:8081'));
139140
}
140141

src/Symfony/Component/HttpClient/composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"php": ">=7.2.5",
2626
"psr/log": "^1|^2|^3",
2727
"symfony/deprecation-contracts": "^2.1|^3",
28-
"symfony/http-client-contracts": "^2.5.3",
28+
"symfony/http-client-contracts": "^2.5.4",
2929
"symfony/polyfill-php73": "^1.11",
3030
"symfony/polyfill-php80": "^1.16",
3131
"symfony/service-contracts": "^1.0|^2|^3"

src/Symfony/Component/HttpFoundation/Request.php

+4-7
Original file line numberDiff line numberDiff line change
@@ -358,12 +358,7 @@ public static function create(string $uri, string $method = 'GET', array $parame
358358
$server['PATH_INFO'] = '';
359359
$server['REQUEST_METHOD'] = strtoupper($method);
360360

361-
if (false === ($components = parse_url($uri)) && '/' === ($uri[0] ?? '')) {
362-
$components = parse_url($uri.'#');
363-
unset($components['fragment']);
364-
}
365-
366-
if (false === $components) {
361+
if (false === $components = parse_url(\strlen($uri) !== strcspn($uri, '?#') ? $uri : $uri.'#')) {
367362
throw new BadRequestException('Invalid URI.');
368363
}
369364

@@ -386,9 +381,11 @@ public static function create(string $uri, string $method = 'GET', array $parame
386381
if ('https' === $components['scheme']) {
387382
$server['HTTPS'] = 'on';
388383
$server['SERVER_PORT'] = 443;
389-
} else {
384+
} elseif ('http' === $components['scheme']) {
390385
unset($server['HTTPS']);
391386
$server['SERVER_PORT'] = 80;
387+
} else {
388+
throw new BadRequestException('Invalid URI: http(s) scheme expected.');
392389
}
393390
}
394391

src/Symfony/Component/HttpFoundation/Tests/RequestTest.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,8 @@ public function testCreateWithRequestUri()
310310
* ["foo\u0000"]
311311
* [" foo"]
312312
* ["foo "]
313-
* [":"]
313+
* ["//"]
314+
* ["foo:bar"]
314315
*/
315316
public function testCreateWithBadRequestUri(string $uri)
316317
{

src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php

+6
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@
9898
}
9999
break;
100100

101+
case '/302-no-scheme':
102+
if (!isset($vars['HTTP_AUTHORIZATION'])) {
103+
header('Location: localhost:8067', true, 302);
104+
}
105+
break;
106+
101107
case '/302/relative':
102108
header('Location: ..', true, 302);
103109
break;

0 commit comments

Comments
 (0)
0