Skip to content

Commit f8eb0e8

Browse files
committed
Merge branch '2.x' into 3.x
* 2.x: * new `Utility\Uri::fromParsed()` method * `Uri::withUserInfo()` : passing user as empty string should remove userInfo (was only removing if `null`) * `Utility\Uri::parseUrl()` now returns components sorted by key
2 parents 0001aec + 734635a commit f8eb0e8

File tree

6 files changed

+214
-109
lines changed

6 files changed

+214
-109
lines changed

src/HttpMessage/Uri.php

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -312,16 +312,17 @@ public function withScheme(string $scheme): static
312312
public function withUserInfo(string $user, ?string $password = null): static
313313
{
314314
$this->assertString($user, 'user');
315-
$info = $user;
316-
if ($password !== null && $password !== '') {
315+
$userInfo = (string) $user; // for versions without type hint in method signature
316+
$password = (string) $password; // for versions without type hint in method signature
317+
if ($userInfo !== '' && $password !== '') {
317318
$this->assertString($password, 'password');
318-
$info .= ':' . $password;
319+
$userInfo .= ':' . $password;
319320
}
320-
if ($info === $this->userInfo) {
321+
if ($userInfo === $this->userInfo) {
321322
return $this;
322323
}
323324
$new = clone $this;
324-
$new->userInfo = $info;
325+
$new->userInfo = $userInfo;
325326
return $new;
326327
}
327328

@@ -458,30 +459,11 @@ public function withFragment(string $fragment): static
458459
*/
459460
private function setUrlParts(array $urlParts)
460461
{
461-
$asserts = \array_intersect_key(array(
462-
'scheme' => 'assertScheme',
463-
), $urlParts);
464-
$filters = \array_intersect_key(array(
465-
'fragment' => 'filterQueryAndFragment',
466-
'host' => 'lowercase',
467-
'path' => 'filterPath',
468-
'port' => 'filterPort',
469-
'query' => 'filterQueryAndFragment',
470-
'scheme' => 'lowercase',
471-
), $urlParts);
472-
foreach ($asserts as $part => $method) {
473-
$val = $urlParts[$part];
474-
$this->{$method}($val);
475-
}
476-
foreach ($filters as $part => $method) {
477-
$val = $urlParts[$part];
478-
$this->{$part} = $this->{$method}($val);
479-
}
480-
if (isset($urlParts['user'])) {
481-
$this->userInfo = (string) $urlParts['user'];
482-
}
483-
if (isset($urlParts['pass'])) {
484-
$this->userInfo .= ':' . $urlParts['pass'];
462+
$uri = UriUtil::fromParsed($urlParts);
463+
$validKeys = ['fragment', 'host', 'path', 'port', 'query', 'scheme', 'userInfo'];
464+
foreach ($validKeys as $key) {
465+
$method = 'get' . \ucfirst($key);
466+
$this->{$key} = $uri->{$method}();
485467
}
486468
}
487469
}

src/HttpMessage/Utility/ParseStr.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ public static function parse($str, array $opts = array()): array
6666
{
6767
$str = (string) $str;
6868
$opts = \array_merge(self::$parseStrOpts, $opts);
69-
$useParseStr = ($opts['convDot'] || \strpos($str, '.') === false)
70-
&& ($opts['convSpace'] || \strpos($str, ' ') === false);
69+
$useParseStr = ($opts['convDot'] || \preg_match('/(.|%2E)/', $str) !== 1)
70+
&& ($opts['convSpace'] || \preg_match('/( |\+|%20)/', $str) !== 1);
7171
if ($useParseStr) {
7272
// there are no spaces or dots in serialized data
7373
// and/or we're not interested in converting them

src/HttpMessage/Utility/Uri.php

Lines changed: 83 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,44 @@ class Uri
3131
*/
3232
public static function fromGlobals(): BdkUri
3333
{
34-
$uri = new BdkUri();
35-
$parts = \array_filter(\array_merge(
34+
$parsed = \array_merge(
3635
array(
3736
'scheme' => isset($_SERVER['HTTPS']) && \filter_var($_SERVER['HTTPS'], FILTER_VALIDATE_BOOLEAN)
3837
? 'https'
3938
: 'http',
4039
),
4140
self::hostPortFromGlobals(),
4241
self::pathQueryFromGlobals()
43-
));
44-
$methods = array(
45-
'host' => 'withHost',
46-
'path' => 'withPath',
47-
'port' => 'withPort',
48-
'query' => 'withQuery',
49-
'scheme' => 'withScheme',
5042
);
51-
foreach ($parts as $name => $value) {
52-
$method = $methods[$name];
43+
return self::fromParsed($parsed);
44+
}
45+
46+
/**
47+
* Get a Uri populated with component values
48+
*
49+
* Username & password accepted in multiple ways (highest precedence first):
50+
* - userInfo ("username:password" or ["username", "password"])
51+
* - user & pass
52+
* - username & password
53+
*
54+
* @param array $parsed Url component values (ie from `parse_url()`)
55+
*
56+
* @return BdkUri
57+
*
58+
* @since x.3.2
59+
*/
60+
public static function fromParsed(array $parsed): BdkUri
61+
{
62+
$uriKeys = ['fragment', 'host', 'path', 'port', 'query', 'scheme', 'userInfo'];
63+
$parsed = \array_intersect_key(self::parsedPartsPrep($parsed), \array_flip($uriKeys));
64+
$parsed = \array_filter($parsed, static function ($val) {
65+
return \in_array($val, array(null, ''), true) === false;
66+
});
67+
$uri = new BdkUri();
68+
foreach ($parsed as $key => $value) {
69+
$method = 'with' . \ucfirst($key);
5370
/** @var BdkUri */
54-
$uri = $uri->{$method}($value);
71+
$uri = \call_user_func_array(array($uri, $method), (array) $value);
5572
}
5673
return $uri;
5774
}
@@ -69,11 +86,9 @@ public static function isCrossOrigin(UriInterface $uri1, UriInterface $uri2): bo
6986
if (\strcasecmp($uri1->getHost(), $uri2->getHost()) !== 0) {
7087
return true;
7188
}
72-
7389
if ($uri1->getScheme() !== $uri2->getScheme()) {
7490
return true;
7591
}
76-
7792
return self::computePort($uri1) !== self::computePort($uri2);
7893
}
7994

@@ -122,8 +137,7 @@ public static function resolve(UriInterface $base, UriInterface $rel): UriInterf
122137
}
123138
if ($rel->getScheme() !== '') {
124139
// rel specified scheme... return rel (with path cleaned up)
125-
return $rel
126-
->withPath(self::pathRemoveDots($rel->getPath()));
140+
return $rel->withPath(self::pathRemoveDots($rel->getPath()));
127141
}
128142
if ($rel->getAuthority() !== '') {
129143
// rel specified "authority"..
@@ -161,6 +175,41 @@ private static function computePort(UriInterface $uri): int
161175
return $uri->getScheme() === 'https' ? 443 : 80;
162176
}
163177

178+
/**
179+
* Converted parsed values to keys used by `fromParsed()`
180+
*
181+
* (`fromParsed()` accepts multiple key names for username & password)
182+
*
183+
* @param array $parsed Url values as you would obtain from from `parse_url()`
184+
*
185+
* @return array
186+
*/
187+
private static function parsedPartsPrep(array $parsed): array
188+
{
189+
$map = array(
190+
'password' => 'pass',
191+
'username' => 'user',
192+
);
193+
\ksort($parsed);
194+
$rename = \array_intersect_key($parsed, $map);
195+
$keysNew = \array_values(\array_intersect_key($map, $rename));
196+
$renamed = \array_combine($keysNew, \array_values($rename));
197+
$parsed = \array_merge(array(
198+
'pass' => '',
199+
'user' => '',
200+
), $renamed, $parsed);
201+
if (\array_key_exists('userInfo', $parsed) === false) {
202+
$parsed['userInfo'] = array($parsed['user'], $parsed['pass']);
203+
}
204+
if (\is_array($parsed['userInfo']) === false) {
205+
$parsed['userInfo'] = \explode(':', (string) $parsed['userInfo'], 2);
206+
}
207+
if ((string) $parsed['userInfo'][0] === '') {
208+
unset($parsed['userInfo']);
209+
}
210+
return $parsed;
211+
}
212+
164213
/**
165214
* Parse URL with latest `parse_url` fixes / behavior
166215
*
@@ -173,9 +222,6 @@ private static function computePort(UriInterface $uri): int
173222
*/
174223
private static function parseUrlPatched(string $url)
175224
{
176-
if (PHP_VERSION_ID >= 80000) {
177-
return \parse_url($url);
178-
}
179225
$hasTempScheme = false;
180226
if (PHP_VERSION_ID < 50500 && \strpos($url, '//') === 0) {
181227
// php 5.4 chokes without the scheme
@@ -186,7 +232,9 @@ private static function parseUrlPatched(string $url)
186232
if ($parts === false) {
187233
return false;
188234
}
235+
\ksort($parts);
189236
if ($hasTempScheme) {
237+
// only applicable for php 5.4
190238
unset($parts['scheme']);
191239
}
192240
return self::parseUrlAddEmpty($parts, $url);
@@ -202,18 +250,12 @@ private static function parseUrlPatched(string $url)
202250
*/
203251
private static function parseUrlAddEmpty(array $parts, string $url): array
204252
{
205-
// @phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder
206-
$default = array(
207-
'scheme' => null,
208-
'host' => null,
209-
'port' => null,
210-
'user' => null,
211-
'pass' => null,
212-
'path' => null,
213-
'query' => \strpos($url, '?') !== false ? '' : null,
253+
$parts = \array_merge(array(
214254
'fragment' => \strpos($url, '#') !== false ? '' : null,
215-
);
216-
return \array_filter(\array_merge($default, $parts), static function ($val) {
255+
'query' => \strpos($url, '?') !== false ? '' : null,
256+
), $parts);
257+
\ksort($parts);
258+
return \array_filter($parts, static function ($val) {
217259
return $val !== null;
218260
});
219261
}
@@ -301,19 +343,17 @@ private static function resolveTargetPath(UriInterface $base, UriInterface $rel)
301343
private static function uriInterfaceToParts(UriInterface $url): array
302344
{
303345
$userInfo = \array_replace(array(null, null), \explode(':', $url->getUserInfo(), 2));
304-
// @phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder
305346
$parts = array(
306-
'scheme' => $url->getScheme(),
307-
'host' => $url->getHost(),
308-
'port' => $url->getPort(),
309-
'user' => $userInfo[0],
310347
'pass' => $userInfo[1],
311-
'path' => $url->getPath(),
312-
'query' => $url->getQuery(),
313-
'fragment' => $url->getFragment(),
348+
'user' => $userInfo[0],
314349
);
350+
foreach (['fragment', 'host', 'path', 'port', 'query', 'scheme'] as $key) {
351+
$method = 'get' . \ucfirst($key);
352+
$parts[$key] = $url->{$method}();
353+
}
354+
\ksort($parts);
315355
return \array_filter($parts, static function ($val) {
316-
return !empty($val);
356+
return \strlen((string) $val) > 0;
317357
});
318358
}
319359

@@ -331,40 +371,20 @@ private static function hostPortFromGlobals(): array
331371
'port' => null,
332372
);
333373
if (isset($_SERVER['HTTP_HOST'])) {
334-
$hostPort = self::hostPortFromHttpHost($_SERVER['HTTP_HOST']);
374+
$parts = \parse_url('http://' . $_SERVER['HTTP_HOST']); // may return false
375+
$parts = \array_merge($hostPort, (array) $parts);
376+
return \array_intersect_key($parts, $hostPort);
335377
} elseif (isset($_SERVER['SERVER_NAME'])) {
336378
$hostPort['host'] = $_SERVER['SERVER_NAME'];
337379
} elseif (isset($_SERVER['SERVER_ADDR'])) {
338380
$hostPort['host'] = $_SERVER['SERVER_ADDR'];
339381
}
340-
if ($hostPort['port'] === null && isset($_SERVER['SERVER_PORT'])) {
382+
if (isset($_SERVER['SERVER_PORT'])) {
341383
$hostPort['port'] = (int) $_SERVER['SERVER_PORT'];
342384
}
343385
return $hostPort;
344386
}
345387

346-
/**
347-
* Get host & port from `$_SERVER['HTTP_HOST']`
348-
*
349-
* @param string $httpHost `$_SERVER['HTTP_HOST']` value
350-
*
351-
* @return array{host:string|null,port:int|null}
352-
*
353-
* @psalm-suppress InvalidReturnType
354-
* @psalm-suppress InvalidReturnStatement
355-
*/
356-
private static function hostPortFromHttpHost($httpHost): array
357-
{
358-
$url = 'http://' . $httpHost;
359-
$partsDefault = array(
360-
'host' => null,
361-
'port' => null,
362-
);
363-
$parts = \parse_url($url) ?: array();
364-
$parts = \array_merge($partsDefault, $parts);
365-
return \array_intersect_key($parts, $partsDefault);
366-
}
367-
368388
/**
369389
* Get request uri and query from `$_SERVER`
370390
*

tests/HttpMessage/ServerRequestTest.php

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,21 @@ public function testConstructWithUri()
6464
$this->assertSame('1.0', $serverRequest->getProtocolVersion());
6565
$this->assertSame('text/html', $serverRequest->getHeaderLine('Content-Type'));
6666

67-
// test options parsing works on constructor
67+
ParseStr::setOpts('convDot', true);
68+
ParseStr::setOpts('convSpace', false);
69+
$serverRequest = $this->createServerRequest(
70+
'GET',
71+
'/some/page?foo=bar&dingle.berry=brown&a%20b=c&d+e=f&g h=i'
72+
);
73+
$this->assertSame(array(
74+
'foo' => 'bar',
75+
'dingle_berry' => 'brown',
76+
'a b' => 'c',
77+
'd e' => 'f',
78+
'g h' => 'i',
79+
), $serverRequest->getQueryParams());
80+
81+
ParseStr::setOpts('convDot', false);
6882
ParseStr::setOpts('convSpace', true);
6983
$serverRequest = $this->createServerRequest(
7084
'GET',
@@ -77,7 +91,9 @@ public function testConstructWithUri()
7791
'd_e' => 'f',
7892
'g_h' => 'i',
7993
), $serverRequest->getQueryParams());
94+
8095
ParseStr::setOpts('convSpace', false);
96+
ParseStr::setOpts('convDot', false);
8197

8298
/*
8399
Test new values replace
@@ -393,10 +409,21 @@ public function testExceptionParsedBody()
393409
->withParsedBody('I am a string');
394410
}
395411

396-
public function testExceptionParseStrOpts()
412+
/**
413+
* @dataProvider providerParseStrOpts
414+
*/
415+
public function testExceptionParseStrOpts($val, $message)
397416
{
398417
$this->expectException('InvalidArgumentException');
399-
$this->expectExceptionMessage('parseStrOpts expects string or array. boolean provided.');
400-
ParseStr::setOpts(false);
418+
$this->expectExceptionMessage($message);
419+
ParseStr::setOpts($val);
420+
}
421+
422+
public function providerParseStrOpts()
423+
{
424+
return array(
425+
'boolean' => array(false, 'parseStrOpts expects string or array. boolean provided.'),
426+
'object' => array(new \stdClass(), 'parseStrOpts expects string or array. stdClass provided.'),
427+
);
401428
}
402429
}

tests/HttpMessage/UriTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,14 @@ public function testWithUserInfoRejectsInvalid($user, $password)
263263
});
264264
}
265265

266+
public function testWithUserInfoRemovesIfUserNameEmpty()
267+
{
268+
$uri = $this->createUri()->withUserInfo('dingus', 'swordfish');
269+
self::assertSame('dingus:swordfish', $uri->getUserInfo());
270+
$uri = $uri->withUserInfo('', 'trout');
271+
self::assertSame('', $uri->getUserInfo());
272+
}
273+
266274
/**
267275
* @param mixed $host
268276
*

0 commit comments

Comments
 (0)