[FEATURE] Improve creation of URL query strings from arrays 79/55079/40
authorStefan Neufeind <typo3.neufeind@speedpartner.de>
Mon, 18 Dec 2017 18:46:47 +0000 (19:46 +0100)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Mon, 5 Nov 2018 20:43:20 +0000 (21:43 +0100)
Adds a new method HttpUtility::buildQueryString() using
http_build_query() instead of reimplementing the encoding-process like
the old method GeneralUtility::implodeArrayForUrl() did.

As the parameter $rawurlencodeParamName of implodeArrayForUrl() was set
to "false" by default and used in several places without manually
setting it to "true" using that method could lead to potentially unsafe
non-encoded parameter names.

Some unit-tests had wrong URLs with non-encoded braces [...], which were
adapted to be properly escaped as well.

Resolves: #83334
Releases: master
Change-Id: Ifbaad912f0d658671356dc7bdf1579dacff272df
Reviewed-on: https://review.typo3.org/55079
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
37 files changed:
typo3/sysext/backend/Classes/Controller/ContentElement/NewContentElementController.php
typo3/sysext/backend/Classes/Controller/EditDocumentController.php
typo3/sysext/backend/Classes/Controller/LinkBrowserController.php
typo3/sysext/backend/Classes/Routing/UriBuilder.php
typo3/sysext/backend/Classes/Template/DocumentTemplate.php
typo3/sysext/backend/Classes/Template/ModuleTemplate.php
typo3/sysext/backend/Classes/Tree/View/ElementBrowserFolderTreeView.php
typo3/sysext/backend/Classes/Tree/View/ElementBrowserPageTreeView.php
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/backend/Classes/View/PageLayoutView.php
typo3/sysext/core/Classes/Database/QueryView.php
typo3/sysext/core/Classes/TypoScript/TemplateService.php
typo3/sysext/core/Classes/Utility/GeneralUtility.php
typo3/sysext/core/Classes/Utility/HttpUtility.php
typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-83334-AddImprovedBuildQueryString.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Utility/HttpUtilityTest.php
typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php
typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
typo3/sysext/felogin/Classes/Controller/FrontendLoginController.php
typo3/sysext/felogin/Tests/Unit/Controller/FrontendLoginControllerTest.php
typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php
typo3/sysext/frontend/Classes/Plugin/AbstractPlugin.php
typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php
typo3/sysext/indexed_search/Classes/Indexer.php
typo3/sysext/install/Classes/ViewHelpers/Uri/ActionViewHelper.php
typo3/sysext/recordlist/Classes/Browser/FileBrowser.php
typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php
typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php
typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
typo3/sysext/recordlist/Classes/Tree/View/ElementBrowserPageTreeView.php
typo3/sysext/recordlist/Classes/Tree/View/RecordBrowserPageTreeView.php
typo3/sysext/recordlist/Classes/View/FolderUtilityRenderer.php
typo3/sysext/workspaces/Classes/Controller/PreviewController.php
typo3/sysext/workspaces/Classes/Preview/PreviewUriBuilder.php

index 6d7ed6b..896fb98 100644 (file)
@@ -32,6 +32,7 @@ use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 
 /**
@@ -601,7 +602,7 @@ class NewContentElementController
                         $tempGetVars['defVals']['tt_content']
                     );
                     unset($tempGetVars['defVals']['tt_content']);
-                    $wizardItems[$key]['params'] = GeneralUtility::implodeArrayForUrl('', $tempGetVars);
+                    $wizardItems[$key]['params'] = HttpUtility::buildQueryString($tempGetVars, '&');
                 }
             }
             // If tt_content_defValues are defined...:
index 70cfcd1..1efa519 100644 (file)
@@ -925,10 +925,7 @@ class EditDocumentController
         $this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
         // Set other internal variables:
         $this->R_URL_getvars['returnUrl'] = $this->retUrl;
-        $this->R_URI = $this->R_URL_parts['path'] . '?' . ltrim(GeneralUtility::implodeArrayForUrl(
-            '',
-            $this->R_URL_getvars
-        ), '&');
+        $this->R_URI = $this->R_URL_parts['path'] . HttpUtility::buildQueryString($this->R_URL_getvars, '?');
 
         // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
         $this->MCONF['name'] = 'xMOD_alt_doc.php';
@@ -1056,14 +1053,14 @@ class EditDocumentController
 
         if (!empty($previewConfiguration['useCacheHash'])) {
             $cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
-            $fullLinkParameters = GeneralUtility::implodeArrayForUrl('', array_merge($linkParameters, ['id' => $previewPageId]));
+            $fullLinkParameters = HttpUtility::buildQueryString(array_merge($linkParameters, ['id' => $previewPageId]), '&');
             $cacheHashParameters = $cacheHashCalculator->getRelevantParameters($fullLinkParameters);
             $linkParameters['cHash'] = $cacheHashCalculator->calculateCacheHash($cacheHashParameters);
         } else {
             $linkParameters['no_cache'] = 1;
         }
 
-        return GeneralUtility::implodeArrayForUrl('', $linkParameters, '', false, true);
+        return HttpUtility::buildQueryString($linkParameters, '&');
     }
 
     /**
@@ -2595,7 +2592,7 @@ class EditDocumentController
             'edit,defVals,overrideVals,columnsOnly,noView,workspace',
             $this->R_URL_getvars
         );
-        $this->storeUrl = GeneralUtility::implodeArrayForUrl('', $this->storeArray);
+        $this->storeUrl = HttpUtility::buildQueryString($this->storeArray, '&');
         $this->storeUrlMd5 = md5($this->storeUrl);
     }
 
index 3f6da45..fe13424 100644 (file)
@@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Http\JsonResponse;
 use TYPO3\CMS\Core\LinkHandling\LinkService;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
 use TYPO3\CMS\Recordlist\Controller\AbstractLinkBrowserController;
 
@@ -136,7 +137,7 @@ class LinkBrowserController extends AbstractLinkBrowserController
         $formEngineParameters['fieldChangeFunc'] = $this->parameters['fieldChangeFunc'];
         $formEngineParameters['fieldChangeFuncHash'] = GeneralUtility::hmac(serialize($this->parameters['fieldChangeFunc']));
 
-        $parameters['data-add-on-params'] .= GeneralUtility::implodeArrayForUrl('P', $formEngineParameters);
+        $parameters['data-add-on-params'] .= HttpUtility::buildQueryString(['P' => $formEngineParameters], '&');
 
         return $parameters;
     }
index 3cacfce..2336459 100644 (file)
@@ -20,6 +20,7 @@ use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 
 /**
@@ -159,7 +160,7 @@ class UriBuilder implements SingletonInterface
      */
     protected function buildUri($parameters, $referenceType)
     {
-        $uri = 'index.php?' . ltrim(GeneralUtility::implodeArrayForUrl('', $parameters, '', false, true), '&');
+        $uri = 'index.php' . HttpUtility::buildQueryString($parameters, '?');
         if ($referenceType === self::ABSOLUTE_PATH) {
             $uri = PathUtility::getAbsoluteWebPath(Environment::getBackendPath() . '/' . $uri);
         } else {
index dfae8a0..970e8e2 100644 (file)
@@ -28,6 +28,7 @@ use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 
 /**
@@ -399,8 +400,11 @@ function jumpToUrl(URL) {
     public function makeShortcutUrl($gvList, $setList)
     {
         $GET = GeneralUtility::_GET();
-        $storeArray = array_merge(GeneralUtility::compileSelectedGetVarsFromArray($gvList, $GET), ['SET' => GeneralUtility::compileSelectedGetVarsFromArray($setList, (array)$GLOBALS['SOBE']->MOD_SETTINGS)]);
-        return GeneralUtility::implodeArrayForUrl('', $storeArray);
+        $storeArray = array_merge(
+            GeneralUtility::compileSelectedGetVarsFromArray($gvList, $GET),
+            ['SET' => GeneralUtility::compileSelectedGetVarsFromArray($setList, (array)$GLOBALS['SOBE']->MOD_SETTINGS)]
+        );
+        return HttpUtility::buildQueryString($storeArray, '&');
     }
 
     /**
index d1d5652..6875734 100644 (file)
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Messaging\FlashMessageService;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 
@@ -572,7 +573,7 @@ class ModuleTemplate
      * - SET[] variables a stored in $GLOBALS["SOBE"]->MOD_SETTINGS for backend
      * modules
      *
-     * @return string
+     * @return string GET-parameters for the shortcut-url only(!). String starts with '&'
      * @internal
      */
     public function makeShortcutUrl($gvList, $setList)
@@ -582,7 +583,7 @@ class ModuleTemplate
             GeneralUtility::compileSelectedGetVarsFromArray($gvList, $getParams),
             ['SET' => GeneralUtility::compileSelectedGetVarsFromArray($setList, (array)$GLOBALS['SOBE']->MOD_SETTINGS)]
         );
-        return GeneralUtility::implodeArrayForUrl('', $storeArray);
+        return HttpUtility::buildQueryString($storeArray, '&');
     }
 
     /**
index 585d1e2..792bdf4 100644 (file)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Backend\Tree\View;
 
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
 
 /**
@@ -63,8 +64,10 @@ class ElementBrowserFolderTreeView extends FolderTreeView
 
         // Wrap icon in link (in ElementBrowser only the "titlelink" is used).
         if ($this->ext_IconMode === 'titlelink') {
-            $parameters = GeneralUtility::implodeArrayForUrl('', $this->linkParameterProvider->getUrlParameters(['identifier' => $folderObject->getCombinedIdentifier()]));
-            $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim($parameters, '&')) . ');';
+            $parameters = HttpUtility::buildQueryString(
+                $this->linkParameterProvider->getUrlParameters(['identifier' => $folderObject->getCombinedIdentifier()])
+            );
+            $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . $parameters) . ');';
             $theFolderIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
         }
 
@@ -81,8 +84,10 @@ class ElementBrowserFolderTreeView extends FolderTreeView
      */
     public function wrapTitle($title, $folderObject, $bank = 0)
     {
-        $parameters = GeneralUtility::implodeArrayForUrl('', $this->linkParameterProvider->getUrlParameters(['identifier' => $folderObject->getCombinedIdentifier()]));
-        return '<a href="#" onclick="return jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim($parameters, '&'))) . ');">' . $title . '</a>';
+        $parameters = HttpUtility::buildQueryString(
+            $this->linkParameterProvider->getUrlParameters(['identifier' => $folderObject->getCombinedIdentifier()])
+        );
+        return '<a href="#" onclick="return jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($this->getThisScript() . $parameters)) . ');">' . $title . '</a>';
     }
 
     /**
@@ -127,7 +132,7 @@ class ElementBrowserFolderTreeView extends FolderTreeView
         $name = $bMark ? ' name=' . $bMark : '';
         $urlParameters = $this->linkParameterProvider->getUrlParameters([]);
         $urlParameters['PM'] = $cmd;
-        $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&')) . ',' . GeneralUtility::quoteJSvalue($anchor) . ');';
+        $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . HttpUtility::buildQueryString($urlParameters)) . ',' . GeneralUtility::quoteJSvalue($anchor) . ');';
         return '<a href="#"' . htmlspecialchars($name) . ' onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
     }
 
index ab6780b..986528d 100644 (file)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Backend\Tree\View;
 
 use TYPO3\CMS\Core\LinkHandling\LinkService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Frontend\Page\PageRepository;
 use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
 
@@ -118,7 +119,7 @@ class ElementBrowserPageTreeView extends BrowseTreeView
                 $classAttr .= ' active';
             }
             $urlParameters = $this->linkParameterProvider->getUrlParameters(['pid' => (int)$treeItem['row']['uid']]);
-            $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&')) . ');';
+            $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . HttpUtility::buildQueryString($urlParameters)) . ');';
             $cEbullet = $this->ext_isLinkable($treeItem['row']['doktype'], $treeItem['row']['uid'])
                 ? '<a href="#" class="list-tree-show" onclick="' . htmlspecialchars($aOnClick) . '"><i class="fa fa-caret-square-o-right"></i></a>'
                 : '';
@@ -176,7 +177,7 @@ class ElementBrowserPageTreeView extends BrowseTreeView
         $name = $bMark ? ' name=' . $bMark : '';
         $urlParameters = $this->linkParameterProvider->getUrlParameters([]);
         $urlParameters['PM'] = $cmd;
-        $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&')) . ',' . GeneralUtility::quoteJSvalue($anchor) . ');';
+        $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . HttpUtility::buildQueryString($urlParameters)) . ',' . GeneralUtility::quoteJSvalue($anchor) . ');';
         return '<a class="list-tree-control ' . ($isOpen ? 'list-tree-control-open' : 'list-tree-control-closed')
             . '" href="#"' . htmlspecialchars($name) . ' onclick="' . htmlspecialchars($aOnClick) . '"><i class="fa"></i></a>';
     }
index eeeee6b..cf3c6b2 100644 (file)
@@ -52,6 +52,7 @@ use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Core\Versioning\VersionState;
@@ -3091,7 +3092,7 @@ class BackendUtility
      * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=...
      * @param string $addParams Additional parameters to pass to the script.
      * @param string $script The script to send the &id to, if empty it's automatically found
-     * @return string The completes script URL
+     * @return string The complete script URL
      */
     protected static function buildScriptUrl($mainParams, $addParams, $script = '')
     {
@@ -3107,7 +3108,7 @@ class BackendUtility
             $scriptUrl = (string)$uriBuilder->buildUriFromRoutePath($routePath, $mainParams);
             $scriptUrl .= $addParams;
         } else {
-            $scriptUrl = $script . '?' . GeneralUtility::implodeArrayForUrl('', $mainParams) . $addParams;
+            $scriptUrl = $script . HttpUtility::buildQueryString($mainParams, '?') . $addParams;
         }
 
         return $scriptUrl;
index 0999044..25d90c9 100644 (file)
@@ -3798,10 +3798,7 @@ class PageLayoutView implements LoggerAwareInterface
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
             $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters);
         } else {
-            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . '?' . ltrim(
-                    GeneralUtility::implodeArrayForUrl('', $urlParameters),
-                    '&'
-                );
+            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . HttpUtility::buildQueryString($urlParameters, '?');
         }
         return $url;
     }
index ee176a5..2e81f3a 100644 (file)
@@ -31,6 +31,7 @@ use TYPO3\CMS\Core\Utility\CsvUtility;
 use TYPO3\CMS\Core\Utility\DebugUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 
 /**
  * Class used in module tools/dbint (advanced search) and which may hold code specific for that module
@@ -707,7 +708,7 @@ class QueryView
                     ]
                 ],
                 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
-                    . GeneralUtility::implodeArrayForUrl('SET', (array)GeneralUtility::_POST('SET'))
+                    . HttpUtility::buildQueryString(['SET' => (array)GeneralUtility::_POST('SET')], '&')
             ]);
             $out .= '<a class="btn btn-default" href="' . htmlspecialchars($url) . '">'
                 . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
index 7e99e55..a1427f0 100644 (file)
@@ -34,6 +34,7 @@ use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
@@ -1596,7 +1597,7 @@ class TemplateService
         $LD['no_cache'] = $no_cache ? '&no_cache=1' : '';
         // linkVars
         if ($addParams) {
-            $LD['linkVars'] = GeneralUtility::implodeArrayForUrl('', GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '', false, true);
+            $LD['linkVars'] = HttpUtility::buildQueryString(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '&');
         } else {
             $LD['linkVars'] = $this->getTypoScriptFrontendController()->linkVars;
         }
index fe4dd8e..d1d7576 100644 (file)
@@ -2620,12 +2620,11 @@ class GeneralUtility
                 unset($params[$key]);
             }
         }
-        $pString = self::implodeArrayForUrl('', $params);
-        return $pString ? $parts . '?' . ltrim($pString, '&') : $parts;
+        return $parts . HttpUtility::buildQueryString($params, '?');
     }
 
     /**
-     * Takes a full URL, $url, possibly with a querystring and overlays the $getParams arrays values onto the quirystring, packs it all together and returns the URL again.
+     * Takes a full URL, $url, possibly with a querystring and overlays the $getParams arrays values onto the querystring, packs it all together and returns the URL again.
      * So basically it adds the parameters in $getParams to an existing URL, $url
      *
      * @param string $url URL string
@@ -2640,10 +2639,8 @@ class GeneralUtility
             parse_str($parts['query'], $getP);
         }
         ArrayUtility::mergeRecursiveWithOverrule($getP, $getParams);
-        $uP = explode('?', $url);
-        $params = self::implodeArrayForUrl('', $getP);
-        $outurl = $uP[0] . ($params ? '?' . substr($params, 1) : '');
-        return $outurl;
+        [$url] = explode('?', $url);
+        return $url . HttpUtility::buildQueryString($getP, '?');
     }
 
     /**
index 29901e4..a137144 100644 (file)
@@ -146,4 +146,36 @@ class HttpUtility
             (isset($urlParts['query']) ? '?' . $urlParts['query'] : '') .
             (isset($urlParts['fragment']) ? '#' . $urlParts['fragment'] : '');
     }
+
+    /**
+     * Implodes a multidimensional array of query parameters to a string of GET parameters (eg. param[key][key2]=value2&param[key][key3]=value3)
+     * and properly encodes parameter names as well as values. Spaces are encoded as %20
+     *
+     * @param array $parameters The (multidimensional) array of query parameters with values
+     * @param string $prependCharacter If the created query string is not empty, prepend this character "?" or "&" else no prepend
+     * @param bool $skipEmptyParameters If true, empty parameters (blank string, empty array, null) are removed.
+     * @return string Imploded result, for example param[key][key2]=value2&param[key][key3]=value3
+     * @see explodeUrl2Array()
+     */
+    public static function buildQueryString(array $parameters, string $prependCharacter = '', bool $skipEmptyParameters = false): string
+    {
+        if (empty($parameters)) {
+            return '';
+        }
+
+        if ($skipEmptyParameters) {
+            // This callback filters empty strings, array and null but keeps zero integers
+            $parameters = ArrayUtility::filterRecursive(
+                $parameters,
+                function ($item) {
+                    return $item !== '' && $item !== [] && $item !== null;
+                }
+            );
+        }
+
+        $queryString = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
+        $prependCharacter = $prependCharacter === '?' || $prependCharacter === '&' ? $prependCharacter : '';
+
+        return $queryString && $prependCharacter ? $prependCharacter . $queryString : $queryString;
+    }
 }
diff --git a/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-83334-AddImprovedBuildQueryString.rst b/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-83334-AddImprovedBuildQueryString.rst
new file mode 100644 (file)
index 0000000..d9408a7
--- /dev/null
@@ -0,0 +1,23 @@
+.. include:: ../../Includes.txt
+
+========================================================
+Feature: #83334 - Add improved building of query strings
+========================================================
+
+See :issue:`83334`
+
+Description
+===========
+
+The new method :php:`\TYPO3\CMS\Core\Utility\HttpUtility::buildQueryString()` has been added as an enhancement to the `PHP function`_ :php:`http_build_query()`
+to implode multidimensional parameter arrays, properly encode parameter names as well as values with an optional prepend of :php:`?` or :php:`&` if the query
+string is not empty and skipping empty parameters.
+
+.. _`PHP function`: https://secure.php.net/manual/de/function.http-build-query.php
+
+Impact
+======
+
+Parameter arrays can be safely transformed into HTTP GET query strings using the new method.
+
+.. index:: PHP-API, NotScanned
index 8d9b4f7..2e5cc8b 100644 (file)
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\Utility;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
@@ -29,7 +30,7 @@ class HttpUtilityTest extends UnitTestCase
      */
     public function isUrlBuiltCorrectly(array $urlParts, $expected)
     {
-        $url = \TYPO3\CMS\Core\Utility\HttpUtility::buildUrl($urlParts);
+        $url = HttpUtility::buildUrl($urlParts);
         $this->assertEquals($expected, $url);
     }
 
@@ -61,4 +62,87 @@ class HttpUtilityTest extends UnitTestCase
             ]
         ];
     }
+
+    /**
+     * Data provider for buildQueryString
+     *
+     * @return array
+     */
+    public function queryStringDataProvider()
+    {
+        $valueArray = ['one' => '√', 'two' => 2];
+
+        return [
+            'Empty input' => ['foo', [], ''],
+            'String parameters' => ['foo', $valueArray, 'foo%5Bone%5D=%E2%88%9A&foo%5Btwo%5D=2'],
+            'Nested array parameters' => ['foo', [$valueArray], 'foo%5B0%5D%5Bone%5D=%E2%88%9A&foo%5B0%5D%5Btwo%5D=2'],
+            'Keep blank parameters' => ['foo', ['one' => '√', ''], 'foo%5Bone%5D=%E2%88%9A&foo%5B0%5D=']
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider queryStringDataProvider
+     * @param string $name
+     * @param array $input
+     * @param string $expected
+     */
+    public function buildQueryStringBuildsValidParameterString($name, array $input, $expected)
+    {
+        if ($name === '') {
+            $this->assertSame($expected, HttpUtility::buildQueryString($input));
+        } else {
+            $this->assertSame($expected, HttpUtility::buildQueryString([$name => $input]));
+        }
+    }
+
+    /**
+     * @test
+     */
+    public function buildQueryStringCanSkipEmptyParameters()
+    {
+        $input = ['one' => '√', ''];
+        $expected = 'foo%5Bone%5D=%E2%88%9A';
+        $this->assertSame($expected, HttpUtility::buildQueryString(['foo' => $input], '', true));
+    }
+
+    /**
+     * @test
+     */
+    public function buildQueryStringCanUrlEncodeKeyNames()
+    {
+        $input = ['one' => '√', ''];
+        $expected = 'foo%5Bone%5D=%E2%88%9A&foo%5B0%5D=';
+        $this->assertSame($expected, HttpUtility::buildQueryString(['foo' => $input]));
+    }
+
+    /**
+     * @test
+     */
+    public function buildQueryStringCanUrlEncodeKeyNamesMultidimensional()
+    {
+        $input = ['one' => ['two' => ['three' => '√']], ''];
+        $expected = 'foo%5Bone%5D%5Btwo%5D%5Bthree%5D=%E2%88%9A&foo%5B0%5D=';
+        $this->assertSame($expected, HttpUtility::buildQueryString(['foo' => $input]));
+    }
+
+    /**
+     * @test
+     */
+    public function buildQueryStringSkipsLeadingCharacterOnEmptyParameters()
+    {
+        $input = [];
+        $expected = '';
+        $this->assertSame($expected, HttpUtility::buildQueryString($input, '?', true));
+    }
+
+    /**
+     * @test
+     */
+    public function buildQueryStringSkipsLeadingCharacterOnCleanedEmptyParameters()
+    {
+        $input = ['one' => ''];
+        $expected = '';
+        $this->assertSame($expected, HttpUtility::buildQueryString(['foo' => $input], '?', true));
+    }
 }
index 9eeb313..c9c5171 100644 (file)
@@ -18,6 +18,7 @@ use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
 use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Extbase\Mvc\Request;
 use TYPO3\CMS\Extbase\Mvc\Web\Request as WebRequest;
 
@@ -731,7 +732,7 @@ class UriBuilder
         if (!empty($this->arguments)) {
             $arguments = $this->convertDomainObjectsToIdentityArrays($this->arguments);
             $this->lastArguments = $arguments;
-            $typolinkConfiguration['additionalParams'] = GeneralUtility::implodeArrayForUrl(null, $arguments);
+            $typolinkConfiguration['additionalParams'] = HttpUtility::buildQueryString($arguments, '&');
         }
         if ($this->addQueryString === true) {
             $typolinkConfiguration['addQueryString'] = 1;
index 38a36fc..d52c2ee 100644 (file)
@@ -623,7 +623,7 @@ class UriBuilderTest extends UnitTestCase
     {
         $this->uriBuilder->setTargetPageUid(123);
         $this->uriBuilder->setArguments(['foo' => 'bar', 'baz' => ['extbase' => 'fluid']]);
-        $expectedConfiguration = ['parameter' => 123, 'useCacheHash' => 1, 'additionalParams' => '&foo=bar&baz[extbase]=fluid'];
+        $expectedConfiguration = ['parameter' => 123, 'useCacheHash' => 1, 'additionalParams' => '&foo=bar&baz%5Bextbase%5D=fluid'];
         $actualConfiguration = $this->uriBuilder->_call('buildTypolinkConfiguration');
         $this->assertEquals($expectedConfiguration, $actualConfiguration);
     }
@@ -664,7 +664,7 @@ class UriBuilderTest extends UnitTestCase
         $mockDomainObject2->_set('uid', '321');
         $this->uriBuilder->setTargetPageUid(123);
         $this->uriBuilder->setArguments(['someDomainObject' => $mockDomainObject1, 'baz' => ['someOtherDomainObject' => $mockDomainObject2]]);
-        $expectedConfiguration = ['parameter' => 123, 'useCacheHash' => 1, 'additionalParams' => '&someDomainObject=123&baz[someOtherDomainObject]=321'];
+        $expectedConfiguration = ['parameter' => 123, 'useCacheHash' => 1, 'additionalParams' => '&someDomainObject=123&baz%5BsomeOtherDomainObject%5D=321'];
         $actualConfiguration = $this->uriBuilder->_call('buildTypolinkConfiguration');
         $this->assertEquals($expectedConfiguration, $actualConfiguration);
     }
index b7ac122..5095f3a 100644 (file)
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Frontend\Plugin\AbstractPlugin;
 
 /**
@@ -907,19 +908,14 @@ class FrontendLoginController extends AbstractPlugin implements LoggerAwareInter
      */
     protected function getPageLink($label, $piVars, $returnUrl = false)
     {
-        $additionalParams = '';
-        if (!empty($piVars)) {
-            foreach ($piVars as $key => $val) {
-                $additionalParams .= '&' . $key . '=' . $val;
-            }
-        }
+        $additionalParams = is_array($piVars) && !empty($piVars) ? $piVars : [];
         // Should GETvars be preserved?
         if ($this->conf['preserveGETvars']) {
-            $additionalParams .= $this->getPreserveGetVars();
+            $additionalParams = array_merge_recursive($additionalParams, $this->getPreserveGetVars());
         }
         $this->conf['linkConfig.']['parameter'] = $this->frontendController->id;
-        if ($additionalParams) {
-            $this->conf['linkConfig.']['additionalParams'] = $additionalParams;
+        if (!empty($additionalParams)) {
+            $this->conf['linkConfig.']['additionalParams'] = HttpUtility::buildQueryString($additionalParams, '&');
         }
         if ($returnUrl) {
             return htmlspecialchars($this->cObj->typoLink_URL($this->conf['linkConfig.']));
@@ -933,7 +929,7 @@ class FrontendLoginController extends AbstractPlugin implements LoggerAwareInter
      * Supports multi-dimensional GET-vars.
      * Some hardcoded values are dropped.
      *
-     * @return string additionalParams-string
+     * @return array additionalParams-array
      */
     protected function getPreserveGetVars()
     {
@@ -954,8 +950,7 @@ class FrontendLoginController extends AbstractPlugin implements LoggerAwareInter
             parse_str(implode('=1&', $preserveQueryStringProperties) . '=1', $preserveQueryParts);
             $preserveQueryParts = \TYPO3\CMS\Core\Utility\ArrayUtility::intersectRecursive($getVars, $preserveQueryParts);
         }
-        $parameters = GeneralUtility::implodeArrayForUrl('', $preserveQueryParts);
-        return $parameters;
+        return $preserveQueryParts;
     }
 
     /**
index e72dfb7..48e8e99 100644 (file)
@@ -322,7 +322,7 @@ class FrontendLoginControllerTest extends UnitTestCase
                     'id' => 42,
                 ],
                 '',
-                '',
+                [],
             ],
             'simple additional parameter is not preserved if not specified in preservedGETvars' => [
                 [
@@ -330,7 +330,7 @@ class FrontendLoginControllerTest extends UnitTestCase
                     'special' => 23,
                 ],
                 '',
-                '',
+                [],
             ],
             'all params except ignored ones are preserved if preservedGETvars is set to "all"' => [
                 [
@@ -344,14 +344,21 @@ class FrontendLoginControllerTest extends UnitTestCase
                     ],
                 ],
                 'all',
-                '&special1=23&special2[foo]=bar',
+                [
+                    'special1' => 23,
+                    'special2' => [
+                        'foo' => 'bar',
+                    ],
+                ]
             ],
             'preserve single parameter' => [
                 [
                     'L' => 42,
                 ],
                 'L',
-                '&L=42'
+                [
+                    'L' => 42,
+                ],
             ],
             'preserve whole parameter array' => [
                 [
@@ -364,7 +371,15 @@ class FrontendLoginControllerTest extends UnitTestCase
                     ],
                 ],
                 'L,tx_someext',
-                '&L=3&tx_someext[foo]=simple&tx_someext[bar][baz]=simple',
+                [
+                    'L' => 3,
+                    'tx_someext' => [
+                        'foo' => 'simple',
+                        'bar' => [
+                            'baz' => 'simple',
+                        ],
+                    ],
+                ],
             ],
             'preserve part of sub array' => [
                 [
@@ -377,7 +392,14 @@ class FrontendLoginControllerTest extends UnitTestCase
                     ],
                 ],
                 'L,tx_someext[bar]',
-                '&L=3&tx_someext[bar][baz]=simple',
+                [
+                    'L' => 3,
+                    'tx_someext' => [
+                        'bar' => [
+                            'baz' => 'simple',
+                        ],
+                    ],
+                ],
             ],
             'preserve keys on different levels' => [
                 [
@@ -394,18 +416,23 @@ class FrontendLoginControllerTest extends UnitTestCase
                     ],
                 ],
                 'L,tx_ext2,tx_ext3[bar]',
-                '&L=3&tx_ext2[foo]=simple&tx_ext3[bar][baz]=simple',
+                [
+                    'L' => 3,
+                    'tx_ext2' => [
+                        'foo' => 'simple',
+                    ],
+                    'tx_ext3' => [
+                        'bar' => [
+                            'baz' => 'simple',
+                        ],
+                    ],
+                ],
             ],
             'preserved value that does not exist in get' => [
                 [],
-                'L,foo[bar]',
-                ''
-            ],
-            'url params are encoded' => [
-                ['tx_ext1' => 'param with spaces and \\ %<>& /'],
-                'L,tx_ext1',
-                '&tx_ext1=param%20with%20spaces%20and%20%5C%20%25%3C%3E%26%20%2F'
-            ],
+                'L,foo%5Bbar%5D',
+                [],
+             ],
         ];
     }
 
index 789f3f7..f0a64d4 100644 (file)
@@ -53,6 +53,7 @@ use TYPO3\CMS\Core\TypoScript\TypoScriptService;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\DebugUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MailUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
@@ -5564,7 +5565,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
         }
         if (is_array($urlParameters)) {
             if (!empty($urlParameters)) {
-                $conf['additionalParams'] .= GeneralUtility::implodeArrayForUrl('', $urlParameters);
+                $conf['additionalParams'] .= HttpUtility::buildQueryString($urlParameters, '&');
             }
         } else {
             $conf['additionalParams'] .= $urlParameters;
@@ -5865,7 +5866,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
             $newQueryArray = $currentQueryArray;
         }
         ArrayUtility::mergeRecursiveWithOverrule($newQueryArray, $overruleQueryArguments, $forceOverruleArguments);
-        return GeneralUtility::implodeArrayForUrl('', $newQueryArray, '', false, true);
+        return HttpUtility::buildQueryString($newQueryArray, '&');
     }
 
     /***********************************************
index 0e29f5d..80f4994 100644 (file)
@@ -2255,7 +2255,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         if ($this->cHash && is_array($GET)) {
             // Make sure we use the page uid and not the page alias
             $GET['id'] = $this->id;
-            $this->cHash_array = $this->cacheHash->getRelevantParameters(GeneralUtility::implodeArrayForUrl('', $GET));
+            $this->cHash_array = $this->cacheHash->getRelevantParameters(HttpUtility::buildQueryString($GET));
             $cHash_calc = $this->cacheHash->calculateCacheHash($this->cHash_array);
             if (!hash_equals($cHash_calc, $this->cHash)) {
                 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']) {
@@ -2271,7 +2271,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             }
         } elseif (is_array($GET)) {
             // No cHash is set, check if that is correct
-            if ($this->cacheHash->doParametersRequireCacheHash(GeneralUtility::implodeArrayForUrl('', $GET))) {
+            if ($this->cacheHash->doParametersRequireCacheHash(HttpUtility::buildQueryString($GET))) {
                 $this->reqCHash();
             }
         }
@@ -3084,7 +3084,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                         // Error: This key must not be an array!
                         continue;
                     }
-                    $value = GeneralUtility::implodeArrayForUrl($parameterName, $value);
+                    $value = HttpUtility::buildQueryString([$parameterName => $value], '&');
                 }
                 $this->linkVars .= $value;
             }
index 7f0a18c..5a9ea04 100644 (file)
@@ -23,6 +23,7 @@ use Psr\Http\Server\RequestHandlerInterface;
 use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Frontend\Controller\ErrorController;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
@@ -102,7 +103,7 @@ class PageArgumentValidator implements MiddlewareInterface
         if ($this->controller->cHash) {
             // Make sure we use the page uid and not the page alias
             $queryParams['id'] = $this->controller->id;
-            $this->controller->cHash_array = $this->cacheHashCalculator->getRelevantParameters(GeneralUtility::implodeArrayForUrl('', $queryParams));
+            $this->controller->cHash_array = $this->cacheHashCalculator->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
             $cHash_calc = $this->cacheHashCalculator->calculateCacheHash($this->controller->cHash_array);
             if (!hash_equals($cHash_calc, $this->controller->cHash)) {
                 // Early return to trigger the error controller
@@ -113,7 +114,7 @@ class PageArgumentValidator implements MiddlewareInterface
                 $this->getTimeTracker()->setTSlogMessage('The incoming cHash "' . $this->controller->cHash . '" and calculated cHash "' . $cHash_calc . '" did not match, so caching was disabled. The fieldlist used was "' . implode(',', array_keys($this->controller->cHash_array)) . '"', 2);
             }
             // No cHash is set, check if that is correct
-        } elseif ($this->cacheHashCalculator->doParametersRequireCacheHash(GeneralUtility::implodeArrayForUrl('', $queryParams))) {
+        } elseif ($this->cacheHashCalculator->doParametersRequireCacheHash(HttpUtility::buildQueryString($queryParams))) {
             // Will disable caching
             $this->controller->reqCHash();
         }
index 4b2d1da..a39bced 100644 (file)
@@ -24,6 +24,7 @@ use TYPO3\CMS\Core\Localization\LocalizationFactory;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
@@ -384,7 +385,7 @@ class AbstractPlugin
         $conf['useCacheHash'] = $this->pi_USER_INT_obj ? 0 : $cache;
         $conf['no_cache'] = $this->pi_USER_INT_obj ? 0 : !$cache;
         $conf['parameter'] = $altPageId ? $altPageId : ($this->pi_tmpPageId ? $this->pi_tmpPageId : $this->frontendController->id);
-        $conf['additionalParams'] = $this->conf['parent.']['addParams'] . GeneralUtility::implodeArrayForUrl('', $urlParameters, '', true) . $this->pi_moreParams;
+        $conf['additionalParams'] = $this->conf['parent.']['addParams'] . HttpUtility::buildQueryString($urlParameters, '&', true) . $this->pi_moreParams;
         return $this->cObj->typoLink($str, $conf);
     }
 
index a8d7caf..eae8018 100644 (file)
@@ -31,6 +31,7 @@ use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\RootlineUtility;
 use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver;
@@ -474,9 +475,8 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         ) {
             $currentQueryArray = [];
             parse_str(GeneralUtility::getIndpEnv('QUERY_STRING'), $currentQueryArray);
-            $currentQueryParams = GeneralUtility::implodeArrayForUrl('', $currentQueryArray, '', false, true);
 
-            if (!trim($currentQueryParams)) {
+            if (empty($currentQueryArray)) {
                 list(, $URLparams) = explode('?', $url);
                 list($URLparams) = explode('#', (string)$URLparams);
                 parse_str($URLparams . $LD['orig_type'], $URLparamsArray);
@@ -753,7 +753,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         $LD['no_cache'] = $no_cache ? '&no_cache=1' : '';
         // linkVars
         if ($addParams) {
-            $LD['linkVars'] = GeneralUtility::implodeArrayForUrl('', GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '', false, true);
+            $LD['linkVars'] = HttpUtility::buildQueryString(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '&');
         } else {
             $LD['linkVars'] = $this->getTypoScriptFrontendController()->linkVars;
         }
index ef4132e..f8f69c5 100644 (file)
@@ -400,7 +400,7 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
                     'foo' => [ 1, 2, 'f' => [ 4, 5 ] ],
                     'blub' => 123
                 ],
-                '&L=1&foo[0]=1&foo[1]=2&foo[f][0]=4&foo[f][1]=5'
+                '&L=1&foo%5B0%5D=1&foo%5B1%5D=2&foo%5Bf%5D%5B0%5D=4&foo%5Bf%5D%5B1%5D=5'
             ],
             'nested variables' => [
                 'bar|foo(1-2)',
index 374cccb..d641838 100644 (file)
@@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
@@ -390,7 +391,7 @@ class Indexer
         if ($createCHash) {
             /* @var \TYPO3\CMS\Frontend\Page\CacheHashCalculator $cacheHash */
             $cacheHash = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\CacheHashCalculator::class);
-            $this->conf['cHash'] = $cacheHash->generateForParameters(GeneralUtility::implodeArrayForUrl('', $cHash_array));
+            $this->conf['cHash'] = $cacheHash->generateForParameters(HttpUtility::buildQueryString($cHash_array));
         } else {
             $this->conf['cHash'] = '';
         }
index 512d476..ea1d44d 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Install\ViewHelpers\Uri;
  */
 
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
 use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
 use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
@@ -79,9 +80,13 @@ class ActionViewHelper extends AbstractViewHelper
         }
 
         return GeneralUtility::getIndpEnv('TYPO3_REQUEST_SCRIPT')
-            . '?'
-            . GeneralUtility::implodeArrayForUrl('install', $arguments)
-            . GeneralUtility::implodeArrayForUrl('', $additionalParams)
+            . HttpUtility::buildQueryString(
+                array_merge(
+                    ['install' => $arguments],
+                    $additionalParams
+                ),
+                '?'
+            )
             . ($section ? '#' . $section : '');
     }
 }
index 829d619..a69c42b 100644 (file)
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\ProcessedFile;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
 use TYPO3\CMS\Recordlist\View\FolderUtilityRenderer;
@@ -417,7 +418,7 @@ class FileBrowser extends AbstractElementBrowser implements ElementBrowserInterf
             $_MOD_MENU = ['displayThumbs' => ''];
             $_MCONF['name'] = 'file_list';
             $_MOD_SETTINGS = BackendUtility::getModuleData($_MOD_MENU, GeneralUtility::_GP('SET'), $_MCONF['name']);
-            $addParams = GeneralUtility::implodeArrayForUrl('', $this->getUrlParameters(['identifier' => $this->selectedFolder->getCombinedIdentifier()]));
+            $addParams = HttpUtility::buildQueryString($this->getUrlParameters(['identifier' => $this->selectedFolder->getCombinedIdentifier()]), '&');
             $thumbNailCheck = '<div class="checkbox" style="padding:5px 0 15px 0"><label for="checkDisplayThumbs">'
                 . BackendUtility::getFuncCheck(
                     '',
index 42541bd..bf90c57 100644 (file)
@@ -25,6 +25,7 @@ use TYPO3\CMS\Core\Http\HtmlResponse;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Recordlist\LinkHandler\LinkHandlerInterface;
 
 /**
@@ -385,8 +386,8 @@ abstract class AbstractLinkBrowserController
             if ($configuration['addParams']) {
                 $addParams = $configuration['addParams'];
             } else {
-                $parameters = GeneralUtility::implodeArrayForUrl('', $this->getUrlParameters(['act' => $identifier]));
-                $addParams = 'onclick="jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue('?' . ltrim($parameters, '&'))) . ');return false;"';
+                $parameters = HttpUtility::buildQueryString($this->getUrlParameters(['act' => $identifier]), '?');
+                $addParams = 'onclick="jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($parameters)) . ');return false;"';
             }
             $menuDef[$identifier] = [
                 'isActive' => $isActive,
@@ -587,7 +588,7 @@ abstract class AbstractLinkBrowserController
         $parameters['params']['allowedExtensions'] = $this->parameters['params']['allowedExtensions'] ?? '';
         $parameters['params']['blindLinkOptions'] = $this->parameters['params']['blindLinkOptions'] ?? '';
         $parameters['params']['blindLinkFields'] = $this->parameters['params']['blindLinkFields'] ?? '';
-        $addPassOnParams = GeneralUtility::implodeArrayForUrl('P', $parameters);
+        $addPassOnParams = HttpUtility::buildQueryString(['P' => $parameters], '&');
 
         $attributes = $this->displayedLinkHandler->getBodyTagAttributes();
         return array_merge(
index 8387556..0ca066b 100644 (file)
@@ -1174,7 +1174,7 @@ class AbstractDatabaseRecordList extends AbstractRecordList
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
             $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters);
         } else {
-            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . '?' . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&');
+            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . HttpUtility::buildQueryString($urlParameters, '?');
         }
         return $url;
     }
index 22562a5..9da79ba 100644 (file)
@@ -3777,10 +3777,7 @@ class DatabaseRecordList
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
             $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters);
         } else {
-            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . '?' . ltrim(
-                    GeneralUtility::implodeArrayForUrl('', $urlParameters),
-                    '&'
-                );
+            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . HttpUtility::buildQueryString($urlParameters, '?');
         }
         return $url;
     }
index 5f20932..589f0d6 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Recordlist\Tree\View;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 
 /**
  * Extension class for the TBE record browser
@@ -55,7 +56,7 @@ class ElementBrowserPageTreeView extends \TYPO3\CMS\Backend\Tree\View\ElementBro
             return $out;
         }
 
-        $parameters = GeneralUtility::implodeArrayForUrl('', $this->linkParameterProvider->getUrlParameters(['pid' => $v['uid']]));
-        return '<a href="#" onclick="return jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim($parameters, '&'))) . ');">' . $title . '</a>';
+        $parameters = HttpUtility::buildQueryString($this->linkParameterProvider->getUrlParameters(['pid' => $v['uid']]));
+        return '<a href="#" onclick="return jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($this->getThisScript() . $parameters)) . ');">' . $title . '</a>';
     }
 }
index 5a8dded..01aa108 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Recordlist\Tree\View;
 
 use TYPO3\CMS\Backend\Tree\View\ElementBrowserPageTreeView;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 
 /**
  * Specific page tree for the record link handler.
@@ -100,7 +101,7 @@ class RecordBrowserPageTreeView extends ElementBrowserPageTreeView
     public function wrapTitle($title, $record, $ext_pArrPages = false)
     {
         $urlParameters = $this->linkParameterProvider->getUrlParameters(['pid' => (int)$record['uid']]);
-        $url = $this->getThisScript() . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&');
+        $url = $this->getThisScript() . HttpUtility::buildQueryString($urlParameters);
         $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($url) . ');';
 
         return '<span class="list-tree-title"><a href="#" onclick="' . htmlspecialchars($aOnClick) . '">'
index a6cf0df..4b5962b 100644 (file)
@@ -20,6 +20,7 @@ use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
 
 /**
@@ -85,12 +86,12 @@ class FolderUtilityRenderer
             . htmlspecialchars($folderObject->getCombinedIdentifier()) . '" />';
 
         // Make footer of upload form, including the submit button:
-        $redirectValue = $this->parameterProvider->getScriptUrl() . GeneralUtility::implodeArrayForUrl(
-            '',
-            $this->parameterProvider->getUrlParameters(
-                ['identifier' => $folderObject->getCombinedIdentifier()]
-            )
-        );
+        $redirectValue = $this->parameterProvider->getScriptUrl() . HttpUtility::buildQueryString(
+                $this->parameterProvider->getUrlParameters(
+                    ['identifier' => $folderObject->getCombinedIdentifier()]
+                ),
+                '&'
+            );
         $markup[] = '<input type="hidden" name="data[newfolder][' . $a . '][redirect]" value="' . htmlspecialchars($redirectValue) . '" />';
 
         $markup[] = '</div></form>';
@@ -149,10 +150,10 @@ class FolderUtilityRenderer
                 . htmlspecialchars($combinedIdentifier) . '" />';
             $markup[] = '<input type="hidden" name="data[upload][' . $a . '][data]" value="' . $a . '" />';
         }
-        $redirectValue = $this->parameterProvider->getScriptUrl() . GeneralUtility::implodeArrayForUrl(
-            '',
-            $this->parameterProvider->getUrlParameters(['identifier' => $combinedIdentifier])
-        );
+        $redirectValue = $this->parameterProvider->getScriptUrl() . HttpUtility::buildQueryString(
+                $this->parameterProvider->getUrlParameters(['identifier' => $combinedIdentifier]),
+                '&'
+            );
         $markup[] = '<input type="hidden" name="data[upload][1][redirect]" value="' . htmlspecialchars($redirectValue) . '" />';
 
         if (!empty($fileExtList)) {
@@ -238,7 +239,7 @@ class FolderUtilityRenderer
     public function getFileSearchField($searchWord)
     {
         $action = $this->parameterProvider->getScriptUrl()
-            . GeneralUtility::implodeArrayForUrl('', $this->parameterProvider->getUrlParameters([]));
+            . HttpUtility::buildQueryString($this->parameterProvider->getUrlParameters([]), '&');
 
         $markup = [];
         $markup[] = '<form method="post" action="' . htmlspecialchars($action) . '" style="padding-bottom: 15px;">';
index fb75f42..7602327 100644 (file)
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Http\HtmlResponse;
 use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 use TYPO3\CMS\Workspaces\Service\StagesService;
 use TYPO3\CMS\Workspaces\Service\WorkspaceService;
@@ -121,7 +122,7 @@ class PreviewController
         unset($queryParameters['route'], $queryParameters['token'], $queryParameters['previewWS']);
 
         // Assemble a query string from the retrieved parameters
-        $queryString = GeneralUtility::implodeArrayForUrl('', $queryParameters);
+        $queryString = HttpUtility::buildQueryString($queryParameters, '&');
 
         // fetch the next and previous stage
         $workspaceItemsArray = $this->workspaceService->selectVersionsInWorkspace(
index 38a1242..324ee34 100644 (file)
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Versioning\VersionState;
 use TYPO3\CMS\Workspaces\Service\WorkspaceService;
 
@@ -82,7 +83,7 @@ class PreviewUriBuilder
                 'id' => $uid,
                 'L' => $languageId
             ];
-            return BackendUtility::getViewDomain($uid) . '/index.php?' . GeneralUtility::implodeArrayForUrl('', $linkParams);
+            return BackendUtility::getViewDomain($uid) . '/index.php?' . HttpUtility::buildQueryString($linkParams);
         }
     }