Skip to content

Commit 4ebfe9e

Browse files
committed
Improve the handling of query parameters and fragments
1 parent e1a589c commit 4ebfe9e

File tree

2 files changed

+76
-5
lines changed

2 files changed

+76
-5
lines changed

wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use wcf\system\page\handler\ILookupPageHandler;
1212
use wcf\system\page\handler\IMenuPageHandler;
1313
use wcf\system\WCF;
14+
use wcf\util\Url;
1415

1516
/**
1617
* Represents a menu item.
@@ -111,11 +112,7 @@ private function appendUrlParameters(string $url): string
111112
return $url;
112113
}
113114

114-
if (\str_contains($url, '?')) {
115-
return $url .= '&' . $this->urlParameters;
116-
}
117-
118-
return $url .= '?' . $this->urlParameters;
115+
return Url::withQueryString($url, $this->urlParameters);
119116
}
120117

121118
/**

wcfsetup/install/files/lib/util/Url.class.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace wcf\util;
44

5+
use GuzzleHttp\Psr7\Uri;
6+
use Psr\Http\Message\UriInterface;
7+
58
/**
69
* Generic wrapper around `parse_url()`.
710
*
@@ -216,4 +219,75 @@ public static function getHostnameMatcher(array $hostnames): callable
216219
return false;
217220
};
218221
}
222+
223+
/**
224+
* Appends a query string and an optional fragment to an existing URI.
225+
*
226+
* @since 6.3
227+
*/
228+
public static function withQueryString(string|Uri $uri, string $queryString): UriInterface
229+
{
230+
if (\is_string($uri)) {
231+
$uri = new Uri($uri);
232+
}
233+
234+
if ($queryString === '') {
235+
return $uri;
236+
}
237+
238+
$anchorPosition = \mb_strpos($queryString, '#');
239+
if ($anchorPosition !== false) {
240+
$anchor = \mb_substr($queryString, $anchorPosition + 1);
241+
$queryString = \mb_substr($queryString, 0, $anchorPosition);
242+
243+
$uri = $uri->withFragment($anchor);
244+
}
245+
246+
if ($queryString === '') {
247+
return $uri;
248+
}
249+
250+
\parse_str($queryString, $parts);
251+
252+
$flattenedParts = [];
253+
foreach ($parts as $key => $value) {
254+
self::flattenQueryKey($key, $value, $flattenedParts);
255+
}
256+
257+
return $uri->withQueryValues($uri, $flattenedParts);
258+
}
259+
260+
/**
261+
* PHP’s `parse_str()` splits a query string into an array but nested keys
262+
* using the square bracket notation are parsed into nested arrays. This
263+
* conflicts with `Uri::withQueryValues()` that expects a one-dimensional
264+
* array with keys using the bracket notation.
265+
*
266+
* This method recursively processes the value with each child key added to
267+
* the `$key` value using the bracket notation.
268+
*
269+
* @param string|array<string, string> $value
270+
* @param array<string, string> &$flattenedParts
271+
* @since 6.3
272+
*/
273+
private static function flattenQueryKey(string $key, string|array $value, array &$flattenedParts): void
274+
{
275+
if (\is_string($value)) {
276+
$flattenedParts[$key] = $value;
277+
278+
return;
279+
}
280+
281+
foreach ($value as $subKey => $subValue) {
282+
self::flattenQueryKey(
283+
\sprintf(
284+
'%s[%s]',
285+
$key,
286+
$subKey
287+
),
288+
$subValue,
289+
$flattenedParts,
290+
);
291+
}
292+
}
219293
}

0 commit comments

Comments
 (0)