@@ -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 *
0 commit comments