From 7e9abf5405f1b75e1634d0eb7f3e995b7d177152 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 31 Jul 2025 12:38:31 +0200 Subject: [PATCH 1/6] Additional parameter for internal menu items --- XSD/menuItem.xsd | 1 + .../database/update_com.woltlab.wcf_6.3.php | 20 +++++++++++++++++++ .../lib/acp/form/MenuItemAddForm.class.php | 9 +++++++++ .../lib/data/menu/item/MenuItem.class.php | 5 +++-- ...enuItemPackageInstallationPlugin.class.php | 18 ++++++++++++++++- wcfsetup/install/lang/de.xml | 3 +++ wcfsetup/install/lang/en.xml | 3 +++ wcfsetup/setup/db/install.sql | 3 ++- 8 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3.php diff --git a/XSD/menuItem.xsd b/XSD/menuItem.xsd index e4c2860998b..cbffdd536fa 100644 --- a/XSD/menuItem.xsd +++ b/XSD/menuItem.xsd @@ -43,6 +43,7 @@ + diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3.php new file mode 100644 index 00000000000..ea1d3d737a3 --- /dev/null +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3.php @@ -0,0 +1,20 @@ + + */ + +use wcf\system\database\table\column\NotNullVarchar255DatabaseTableColumn; +use wcf\system\database\table\PartialDatabaseTable; + +return [ + PartialDatabaseTable::create('wcf1_menu_item') + ->columns([ + NotNullVarchar255DatabaseTableColumn::create('additionalInternalURL') + ->defaultValue(''), + ]), +]; diff --git a/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php b/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php index 2ab89fc61dd..ef7f142d144 100644 --- a/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php @@ -210,6 +210,15 @@ protected function createForm() ->fieldId('pageID') ->values(\array_keys($pageHandlers)) ), + TextFormField::create('additionalInternalURL') + ->label('wcf.acp.menu.item.additionalInternalURL') + ->description('wcf.acp.menu.item.additionalInternalURL.description') + ->maximumLength(255) + ->addDependency( + ValueFormFieldDependency::create('isInternalLinkDependency') + ->fieldId('isInternalLink') + ->values([1]) + ), TextFormField::create('externalURL') ->label('wcf.acp.menu.item.externalURL') ->maximumLength(255) diff --git a/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php b/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php index c845af84e6d..64cee9fd443 100644 --- a/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php +++ b/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php @@ -32,6 +32,7 @@ * @property-read int $isDisabled is `1` if the menu item is disabled and thus not shown in the menu, otherwise `0` * @property-read int $originIsSystem is `1` if the menu item has been delivered by a package, otherwise `0` (if the menu item has been created by an admin in the ACP) * @property-read int $packageID id of the package the which delivers the menu item or `1` if it has been created in the ACP + * @property-read string $additionalInternalURL */ class MenuItem extends DatabaseObject implements ITitledObject { @@ -93,12 +94,12 @@ public function getURL() if ($this->pageObjectID) { $handler = $this->getMenuPageHandler(); if ($handler && $handler instanceof ILookupPageHandler) { - return $handler->getLink($this->pageObjectID); + return $handler->getLink($this->pageObjectID) . $this->additionalInternalURL; } } if ($this->pageID) { - return $this->getPage()->getLink(); + return $this->getPage()->getLink() . $this->additionalInternalURL; } else { return WCF::getLanguage()->get($this->externalURL); } diff --git a/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php index 2b3376bd520..1f47ce69d82 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php @@ -155,11 +155,14 @@ protected function prepareImport(array $data) } $externalURL = (!empty($data['elements']['externalURL'])) ? $data['elements']['externalURL'] : ''; + $additionalInternalURL = $data['elements']['additionalInternalURL'] ?? ''; if ($pageID === null && empty($externalURL)) { throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' must either have an associated page or an external url set."); } elseif ($pageID !== null && !empty($externalURL)) { throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' can either have an associated page or an external url, but not both."); + } elseif ($pageID === null && !empty($additionalInternalURL)) { + throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' can not have an additional internal URL set if it does not have an associated page."); } return [ @@ -171,6 +174,7 @@ protected function prepareImport(array $data) 'parentItemID' => $parentItemID, 'showOrder' => $this->getItemOrder($menuID, $parentItemID), 'title' => $this->getI18nValues($data['elements']['title']), + 'additionalInternalURL' => $additionalInternalURL, ]; } @@ -390,6 +394,14 @@ protected function addFormFields(IFormDocument $form) ->values(['internal']) ), + TextFormField::create('additionalInternalURL') + ->label('wcf.acp.pip.menuItem.additionalInternalURL') + ->maximumLength(255) + ->addDependency( + ValueFormFieldDependency::create('linkType') + ->fieldId('linkType') + ->values(['internal']) + ), TextFormField::create('externalURL') ->label('wcf.acp.pip.menuItem.externalURL') ->description('wcf.acp.pip.menuItem.externalURL.description') @@ -469,7 +481,7 @@ protected function fetchElementData(\DOMElement $element, $saveData) $data['title'][LanguageFactory::getInstance()->getLanguageByCode($title->getAttribute('language'))->languageID] = $title->nodeValue; } - foreach (['externalURL', 'menu', 'page', 'parent'] as $optionalElementName) { + foreach (['externalURL', 'menu', 'page', 'parent', 'additionalInternalURL'] as $optionalElementName) { $optionalElement = $element->getElementsByTagName($optionalElementName)->item(0); if ($optionalElement !== null) { $data[$optionalElementName] = $optionalElement->nodeValue; @@ -594,6 +606,10 @@ protected function prepareXmlElement(\DOMDocument $document, IFormDocument $form [ 'page' => '', 'externalURL' => '', + 'additionalInternalURL' => [ + 'defaultValue' => '', + 'cdata' => true, + ], 'showOrder' => null, ], $form diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index f335878ed0d..ab4073bb508 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -1230,6 +1230,8 @@ Sie erreichen das Fehlerprotokoll unter: {link controller='ExceptionLogView' isE + + & oder # beginnen.]]> @@ -2547,6 +2549,7 @@ Die Datenbestände werden sorgfältig gepflegt, aber es ist nicht ausgeschlossen {$tableName}-Datenbanktabelle, der für diese Bedingung verwendet wird.]]> {$tableName} ist keine Spalte vom Typ INT.]]> {$tableName}.]]> + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 19a6382374d..c3a79b7016c 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -1206,6 +1206,8 @@ You can access the error log at: {link controller='ExceptionLogView' isEmail=tru + + & or #.]]> @@ -2347,6 +2349,7 @@ If you have already bought the licenses for the listed apps, th + [A-z0-9-_]. In general, the first part of the menu identifier is the package identifier and the second part is the unqualified controller class without the controller type suffixes Form and Page. Example: com.foo.bar.package.Baz]]> diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 0ce54b4e9a9..df11359d5f4 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -835,7 +835,8 @@ CREATE TABLE wcf1_menu_item ( showOrder INT(10) NOT NULL DEFAULT 0, isDisabled TINYINT(1) NOT NULL DEFAULT 0, originIsSystem TINYINT(1) NOT NULL DEFAULT 0, - packageID INT(10) NOT NULL + packageID INT(10) NOT NULL, + additionalInternalURL VARCHAR(255) NOT NULL DEFAULT '' ); DROP TABLE IF EXISTS wcf1_message_embedded_object; From 63aa94a9ef5ce73d8cb964e00eb921fed0f0cdf6 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Wed, 10 Sep 2025 18:40:27 +0200 Subject: [PATCH 2/6] Rename property for additional parameters --- XSD/menuItem.xsd | 2 +- .../update_com.woltlab.wcf_6.2_step1.php | 6 ++++++ .../database/update_com.woltlab.wcf_6.3.php | 20 ------------------- .../lib/acp/form/MenuItemAddForm.class.php | 5 ++--- .../lib/data/menu/item/MenuItem.class.php | 6 +++--- ...enuItemPackageInstallationPlugin.class.php | 14 ++++++------- wcfsetup/install/lang/de.xml | 3 +-- wcfsetup/install/lang/en.xml | 3 +-- wcfsetup/setup/db/install.sql | 2 +- 9 files changed, 22 insertions(+), 39 deletions(-) delete mode 100644 wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3.php diff --git a/XSD/menuItem.xsd b/XSD/menuItem.xsd index cbffdd536fa..83d9989dce3 100644 --- a/XSD/menuItem.xsd +++ b/XSD/menuItem.xsd @@ -43,7 +43,7 @@ - + diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php index 9c2b9b3d872..5bb0d7c6451 100644 --- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php @@ -11,6 +11,7 @@ use wcf\system\database\table\column\IntDatabaseTableColumn; use wcf\system\database\table\column\MediumtextDatabaseTableColumn; use wcf\system\database\table\column\VarcharDatabaseTableColumn; +use wcf\system\database\table\column\NotNullVarchar255DatabaseTableColumn; use wcf\system\database\table\index\DatabaseTableForeignKey; use wcf\system\database\table\index\DatabaseTableIndex; use wcf\system\database\table\PartialDatabaseTable; @@ -74,4 +75,9 @@ ->columns([ MediumtextDatabaseTableColumn::create('exifData'), ]), + PartialDatabaseTable::create('wcf1_menu_item') + ->columns([ + NotNullVarchar255DatabaseTableColumn::create('urlParameters') + ->defaultValue(''), + ]), ]; diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3.php deleted file mode 100644 index ea1d3d737a3..00000000000 --- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ - -use wcf\system\database\table\column\NotNullVarchar255DatabaseTableColumn; -use wcf\system\database\table\PartialDatabaseTable; - -return [ - PartialDatabaseTable::create('wcf1_menu_item') - ->columns([ - NotNullVarchar255DatabaseTableColumn::create('additionalInternalURL') - ->defaultValue(''), - ]), -]; diff --git a/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php b/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php index ef7f142d144..785127a9edf 100644 --- a/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php @@ -210,9 +210,8 @@ protected function createForm() ->fieldId('pageID') ->values(\array_keys($pageHandlers)) ), - TextFormField::create('additionalInternalURL') - ->label('wcf.acp.menu.item.additionalInternalURL') - ->description('wcf.acp.menu.item.additionalInternalURL.description') + TextFormField::create('urlParameters') + ->label('wcf.acp.menu.item.urlParameters') ->maximumLength(255) ->addDependency( ValueFormFieldDependency::create('isInternalLinkDependency') diff --git a/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php b/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php index 64cee9fd443..0c71ec91b58 100644 --- a/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php +++ b/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php @@ -32,7 +32,7 @@ * @property-read int $isDisabled is `1` if the menu item is disabled and thus not shown in the menu, otherwise `0` * @property-read int $originIsSystem is `1` if the menu item has been delivered by a package, otherwise `0` (if the menu item has been created by an admin in the ACP) * @property-read int $packageID id of the package the which delivers the menu item or `1` if it has been created in the ACP - * @property-read string $additionalInternalURL + * @property-read string $urlParameters */ class MenuItem extends DatabaseObject implements ITitledObject { @@ -94,12 +94,12 @@ public function getURL() if ($this->pageObjectID) { $handler = $this->getMenuPageHandler(); if ($handler && $handler instanceof ILookupPageHandler) { - return $handler->getLink($this->pageObjectID) . $this->additionalInternalURL; + return $handler->getLink($this->pageObjectID) . $this->urlParameters; } } if ($this->pageID) { - return $this->getPage()->getLink() . $this->additionalInternalURL; + return $this->getPage()->getLink() . $this->urlParameters; } else { return WCF::getLanguage()->get($this->externalURL); } diff --git a/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php index 1f47ce69d82..f533a27ef48 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php @@ -155,14 +155,14 @@ protected function prepareImport(array $data) } $externalURL = (!empty($data['elements']['externalURL'])) ? $data['elements']['externalURL'] : ''; - $additionalInternalURL = $data['elements']['additionalInternalURL'] ?? ''; + $urlParameters = $data['elements']['urlParameters'] ?? ''; if ($pageID === null && empty($externalURL)) { throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' must either have an associated page or an external url set."); } elseif ($pageID !== null && !empty($externalURL)) { throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' can either have an associated page or an external url, but not both."); - } elseif ($pageID === null && !empty($additionalInternalURL)) { - throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' can not have an additional internal URL set if it does not have an associated page."); + } elseif ($pageID === null && !empty($urlParameters)) { + throw new SystemException("The menu item '" . $data['attributes']['identifier'] . "' can not have an additional URL parameters set if it does not have an associated page."); } return [ @@ -174,7 +174,7 @@ protected function prepareImport(array $data) 'parentItemID' => $parentItemID, 'showOrder' => $this->getItemOrder($menuID, $parentItemID), 'title' => $this->getI18nValues($data['elements']['title']), - 'additionalInternalURL' => $additionalInternalURL, + 'urlParameters' => $urlParameters, ]; } @@ -394,8 +394,8 @@ protected function addFormFields(IFormDocument $form) ->values(['internal']) ), - TextFormField::create('additionalInternalURL') - ->label('wcf.acp.pip.menuItem.additionalInternalURL') + TextFormField::create('urlParameters') + ->label('wcf.acp.pip.menuItem.urlParameters') ->maximumLength(255) ->addDependency( ValueFormFieldDependency::create('linkType') @@ -481,7 +481,7 @@ protected function fetchElementData(\DOMElement $element, $saveData) $data['title'][LanguageFactory::getInstance()->getLanguageByCode($title->getAttribute('language'))->languageID] = $title->nodeValue; } - foreach (['externalURL', 'menu', 'page', 'parent', 'additionalInternalURL'] as $optionalElementName) { + foreach (['externalURL', 'menu', 'page', 'parent', 'urlParameters'] as $optionalElementName) { $optionalElement = $element->getElementsByTagName($optionalElementName)->item(0); if ($optionalElement !== null) { $data[$optionalElementName] = $optionalElement->nodeValue; diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index ab4073bb508..9c16733261e 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -1230,8 +1230,7 @@ Sie erreichen das Fehlerprotokoll unter: {link controller='ExceptionLogView' isE - - & oder # beginnen.]]> + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index c3a79b7016c..56f6939d4cd 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -1206,8 +1206,7 @@ You can access the error log at: {link controller='ExceptionLogView' isEmail=tru - - & or #.]]> + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index df11359d5f4..26acc92cdd3 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -836,7 +836,7 @@ CREATE TABLE wcf1_menu_item ( isDisabled TINYINT(1) NOT NULL DEFAULT 0, originIsSystem TINYINT(1) NOT NULL DEFAULT 0, packageID INT(10) NOT NULL, - additionalInternalURL VARCHAR(255) NOT NULL DEFAULT '' + urlParameters VARCHAR(255) NOT NULL DEFAULT '' ); DROP TABLE IF EXISTS wcf1_message_embedded_object; From f1194e9cbd76121873e91085b690946e349f75e7 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Wed, 10 Sep 2025 19:40:36 +0200 Subject: [PATCH 3/6] Reduce size of the input field --- wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php b/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php index 785127a9edf..580dbe2d7e9 100644 --- a/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/MenuItemAddForm.class.php @@ -217,7 +217,8 @@ protected function createForm() ValueFormFieldDependency::create('isInternalLinkDependency') ->fieldId('isInternalLink') ->values([1]) - ), + ) + ->addFieldClass('medium'), TextFormField::create('externalURL') ->label('wcf.acp.menu.item.externalURL') ->maximumLength(255) From e1a589c2d2d2a6de8489b4c9a8467950c7b1d237 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Wed, 10 Sep 2025 19:41:11 +0200 Subject: [PATCH 4/6] Fix issue when appending the url parameters --- .../files/lib/data/menu/item/MenuItem.class.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php b/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php index 0c71ec91b58..43f6ffbca71 100644 --- a/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php +++ b/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php @@ -94,17 +94,30 @@ public function getURL() if ($this->pageObjectID) { $handler = $this->getMenuPageHandler(); if ($handler && $handler instanceof ILookupPageHandler) { - return $handler->getLink($this->pageObjectID) . $this->urlParameters; + return $this->appendUrlParameters($handler->getLink($this->pageObjectID)); } } if ($this->pageID) { - return $this->getPage()->getLink() . $this->urlParameters; + return $this->appendUrlParameters($this->getPage()->getLink()); } else { return WCF::getLanguage()->get($this->externalURL); } } + private function appendUrlParameters(string $url): string + { + if (!$this->urlParameters) { + return $url; + } + + if (\str_contains($url, '?')) { + return $url .= '&' . $this->urlParameters; + } + + return $url .= '?' . $this->urlParameters; + } + /** * Returns the page that is linked by this menu item. * From 4ebfe9eaaa8f6214c5964fd78a4d70d3ab7ab677 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 13 Sep 2025 11:05:10 +0200 Subject: [PATCH 5/6] Improve the handling of query parameters and fragments --- .../lib/data/menu/item/MenuItem.class.php | 7 +- wcfsetup/install/files/lib/util/Url.class.php | 74 +++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php b/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php index 43f6ffbca71..b7e60c6c808 100644 --- a/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php +++ b/wcfsetup/install/files/lib/data/menu/item/MenuItem.class.php @@ -11,6 +11,7 @@ use wcf\system\page\handler\ILookupPageHandler; use wcf\system\page\handler\IMenuPageHandler; use wcf\system\WCF; +use wcf\util\Url; /** * Represents a menu item. @@ -111,11 +112,7 @@ private function appendUrlParameters(string $url): string return $url; } - if (\str_contains($url, '?')) { - return $url .= '&' . $this->urlParameters; - } - - return $url .= '?' . $this->urlParameters; + return Url::withQueryString($url, $this->urlParameters); } /** diff --git a/wcfsetup/install/files/lib/util/Url.class.php b/wcfsetup/install/files/lib/util/Url.class.php index 8c6cecc54ab..3f9290eff19 100644 --- a/wcfsetup/install/files/lib/util/Url.class.php +++ b/wcfsetup/install/files/lib/util/Url.class.php @@ -2,6 +2,9 @@ namespace wcf\util; +use GuzzleHttp\Psr7\Uri; +use Psr\Http\Message\UriInterface; + /** * Generic wrapper around `parse_url()`. * @@ -216,4 +219,75 @@ public static function getHostnameMatcher(array $hostnames): callable return false; }; } + + /** + * Appends a query string and an optional fragment to an existing URI. + * + * @since 6.3 + */ + public static function withQueryString(string|Uri $uri, string $queryString): UriInterface + { + if (\is_string($uri)) { + $uri = new Uri($uri); + } + + if ($queryString === '') { + return $uri; + } + + $anchorPosition = \mb_strpos($queryString, '#'); + if ($anchorPosition !== false) { + $anchor = \mb_substr($queryString, $anchorPosition + 1); + $queryString = \mb_substr($queryString, 0, $anchorPosition); + + $uri = $uri->withFragment($anchor); + } + + if ($queryString === '') { + return $uri; + } + + \parse_str($queryString, $parts); + + $flattenedParts = []; + foreach ($parts as $key => $value) { + self::flattenQueryKey($key, $value, $flattenedParts); + } + + return $uri->withQueryValues($uri, $flattenedParts); + } + + /** + * PHP’s `parse_str()` splits a query string into an array but nested keys + * using the square bracket notation are parsed into nested arrays. This + * conflicts with `Uri::withQueryValues()` that expects a one-dimensional + * array with keys using the bracket notation. + * + * This method recursively processes the value with each child key added to + * the `$key` value using the bracket notation. + * + * @param string|array $value + * @param array &$flattenedParts + * @since 6.3 + */ + private static function flattenQueryKey(string $key, string|array $value, array &$flattenedParts): void + { + if (\is_string($value)) { + $flattenedParts[$key] = $value; + + return; + } + + foreach ($value as $subKey => $subValue) { + self::flattenQueryKey( + \sprintf( + '%s[%s]', + $key, + $subKey + ), + $subValue, + $flattenedParts, + ); + } + } } From f41ae9ea567bed844ae2bca3a42add281de6f225 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 13 Sep 2025 12:43:27 +0200 Subject: [PATCH 6/6] Consider URL parameters for the active state of menu items --- .../data/menu/item/MenuItemNodeTree.class.php | 29 +++++++--- wcfsetup/install/files/lib/util/Url.class.php | 56 +++++++++---------- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/wcfsetup/install/files/lib/data/menu/item/MenuItemNodeTree.class.php b/wcfsetup/install/files/lib/data/menu/item/MenuItemNodeTree.class.php index eee399551f1..fa7cbba3324 100644 --- a/wcfsetup/install/files/lib/data/menu/item/MenuItemNodeTree.class.php +++ b/wcfsetup/install/files/lib/data/menu/item/MenuItemNodeTree.class.php @@ -4,6 +4,8 @@ use wcf\system\page\PageLocationManager; use wcf\system\request\RequestHandler; +use wcf\system\request\RouteHandler; +use wcf\util\Url; /** * Represents a menu item node tree. @@ -75,17 +77,28 @@ public function __construct($menuID, ?MenuItemList $menuItemList = null, $checkV $activeMenuItems = []; if (!RequestHandler::getInstance()->isACPRequest()) { + $requestParameters = Url::parseQueryString($_SERVER['QUERY_STRING']); + $possibleLocations = PageLocationManager::getInstance()->getLocations(); - $length = \count($possibleLocations); - for ($i = 0; $i < $length; $i++) { - foreach ($menuItemList as $menuItem) { - if ($menuItem->pageID == $possibleLocations[$i]['pageID'] && $menuItem->pageObjectID == $possibleLocations[$i]['pageObjectID']) { - if (!isset($activeMenuItems[$i])) { - $activeMenuItems[$i] = []; - } + for ($i = 0, $length = \count($possibleLocations); $i < $length; $i++) { + foreach ($menuItemList->getObjects() as $menuItem) { + if ($menuItem->pageID !== $possibleLocations[$i]['pageID']) { + continue; + } - $activeMenuItems[$i][] = $menuItem->itemID; + if ($menuItem->pageObjectID !== $possibleLocations[$i]['pageObjectID']) { + continue; } + + if ($menuItem->urlParameters !== '') { + $expectedParameters = Url::parseQueryString($menuItem->urlParameters); + if (\array_diff($expectedParameters, $requestParameters) !== []) { + continue; + } + } + + $activeMenuItems[$i] ??= []; + $activeMenuItems[$i][] = $menuItem->itemID; } } } diff --git a/wcfsetup/install/files/lib/util/Url.class.php b/wcfsetup/install/files/lib/util/Url.class.php index 3f9290eff19..82a5d10d26e 100644 --- a/wcfsetup/install/files/lib/util/Url.class.php +++ b/wcfsetup/install/files/lib/util/Url.class.php @@ -247,47 +247,43 @@ public static function withQueryString(string|Uri $uri, string $queryString): Ur return $uri; } - \parse_str($queryString, $parts); + $parts = self::parseQueryString($queryString); - $flattenedParts = []; - foreach ($parts as $key => $value) { - self::flattenQueryKey($key, $value, $flattenedParts); - } - - return $uri->withQueryValues($uri, $flattenedParts); + return $uri->withQueryValues($uri, $parts); } /** - * PHP’s `parse_str()` splits a query string into an array but nested keys - * using the square bracket notation are parsed into nested arrays. This - * conflicts with `Uri::withQueryValues()` that expects a one-dimensional - * array with keys using the bracket notation. + * Parses a query string into a one-dimensional key-value list. * - * This method recursively processes the value with each child key added to - * the `$key` value using the bracket notation. + * This is a replacement for PHP’s `parse_str()` that has two problems: + * 1. Dots inside the key are replaced by underscores. + * 2. Keys containing square brackets are converted into nested arrays. * - * @param string|array $value - * @param array &$flattenedParts + * @return array * @since 6.3 */ - private static function flattenQueryKey(string $key, string|array $value, array &$flattenedParts): void + public static function parseQueryString(string $queryString): array { - if (\is_string($value)) { - $flattenedParts[$key] = $value; - - return; - } + $components = \explode('&', $queryString); + $keyValueMap = []; + for ($i = 0, $length = \count($components); $i < $length; $i++) { + $component = $components[$i]; + if (\str_contains($component, '=')) { + [$key, $value] = \explode('=', $component, 2); + } else { + $key = $component; + $value = ''; + } - foreach ($value as $subKey => $subValue) { - self::flattenQueryKey( - \sprintf( - '%s[%s]', - $key, - $subKey - ), - $subValue, - $flattenedParts, + $key = \str_replace( + ['%5B', '%5D'], + ['[', ']'], + $key ); + + $keyValueMap[$key] = $value; } + + return $keyValueMap; } }