diff --git a/composer.json b/composer.json index ac879f30d51..0d5332dafa5 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ "symfony/yaml": "^7.0|^8.0", "theiconic/name-parser": "^1.2", "tpetry/laravel-query-expressions": "^1.5", - "twig/twig": "~3.27.0", + "twig/twig": "~3.28.0", "voku/portable-ascii": "^2.0", "web-auth/webauthn-lib": "~5.2.4", "webonyx/graphql-php": "~15.33.1", diff --git a/composer.lock b/composer.lock index 479c0d280bf..23467cb5a8a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "957fbd0073e1fd52ec602dd39d65d563", + "content-hash": "0ef83319916bb437c54ea78d760df08f", "packages": [ { "name": "bacon/bacon-qr-code", @@ -411,7 +411,7 @@ }, { "name": "craftcms/cms-assets", - "version": "6.x-dev", + "version": "dev-6.x-5.11", "dist": { "type": "path", "url": "./cms-assets", @@ -8669,16 +8669,16 @@ }, { "name": "twig/twig", - "version": "v3.27.1", + "version": "v3.28.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74" + "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/ae2071bffb38f04847fc0864d730c94b9cb8ab74", - "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", + "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", "shasum": "" }, "require": { @@ -8733,7 +8733,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.27.1" + "source": "https://github.com/twigphp/Twig/tree/v3.28.0" }, "funding": [ { @@ -8745,7 +8745,7 @@ "type": "tidelift" } ], - "time": "2026-05-30T17:09:26+00:00" + "time": "2026-07-03T20:44:34+00:00" }, { "name": "vlucas/phpdotenv", @@ -15010,9 +15010,9 @@ "ext-pdo": "*", "ext-zip": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.5" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.9.0" } diff --git a/src/Support/Url.php b/src/Support/Url.php index f5c65d837e6..65ef5b6d84c 100644 --- a/src/Support/Url.php +++ b/src/Support/Url.php @@ -131,22 +131,50 @@ public static function urlWithParams(string $url, array|string $params): string * Removes a query string param from a URL. */ public static function removeParam(string $url, string $param): string + { + return static::removeParams($url, [$param]); + } + + /** + * Removes query string params from a URL. + */ + public static function removeParams(string $url, array $params): string { // Extract any params/fragment from the base URL - [$url, $params, $fragment] = self::_extractParams($url); + [$url, $urlParams, $fragment] = self::_extractParams($url); - // Remove the param - unset($params[$param]); + // Remove the params + foreach ($params as $param) { + unset($urlParams[$param]); + } // Rebuild - if (($query = static::buildQuery($params)) !== '') { - $url .= '?'.$query; - } - if ($fragment !== null) { - $url .= '#'.$fragment; + return self::_buildUrl($url, $urlParams, $fragment); + } + + /** + * Removes all query string params from a URL. + * + * @param string[] $except Any params that should be left alone + */ + public static function removeAllParams(string $url, array $except = []): string + { + // Extract any params/fragment from the base URL + [$url, $params, $fragment] = self::_extractParams($url); + + // Remove the params + if (! empty($except)) { + foreach (array_keys($params) as $param) { + if (! in_array($param, $except)) { + unset($params[$param]); + } + } + } else { + $params = []; } - return $url; + // Rebuild + return self::_buildUrl($url, $params, $fragment); } /** @@ -242,13 +270,17 @@ public static function rootRelativeUrl(string $url): string /** * Returns either a control panel or a site URL, depending on the request type. + * + * @param array|string|false|null $params The query params to add to the URL. If `false`, any existing params will be removed. */ - public static function url(string $path = '', array|string|null $params = null, ?string $scheme = null): string + public static function url(string $path = '', array|string|false|null $params = null, ?string $scheme = null): string { // Return $path if it appears to be an absolute URL. if (static::isFullUrl($path)) { if ($params) { $path = static::urlWithParams($path, $params); + } elseif ($params === false) { + $path = static::removeAllParams($path); } if ($scheme !== null) { @@ -273,7 +305,7 @@ public static function url(string $path = '', array|string|null $params = null, $scheme = 'https'; } - return self::_createUrl($path, $params, $scheme, $cpUrl); + return self::_createUrl($path, $params ?: null, $scheme, $cpUrl); } /** diff --git a/tests/Feature/Support/URLTest.php b/tests/Feature/Support/URLTest.php index bc0f39a694f..4450bd87300 100644 --- a/tests/Feature/Support/URLTest.php +++ b/tests/Feature/Support/URLTest.php @@ -216,6 +216,61 @@ ], ]); + test('removes multiple query params from a URL', function (string $expected, string $url, array $params) { + expect(Url::removeParams($url, $params))->toBe($expected); + })->with([ + 'removes-multiple' => [ + 'https://craftcms.com/?bar=2#anchor', + 'https://craftcms.com/?foo=1&bar=2&baz=3#anchor', + ['foo', 'baz'], + ], + 'removes-all-listed' => [ + 'https://craftcms.com/#anchor', + 'https://craftcms.com/?foo=1&bar=2#anchor', + ['foo', 'bar'], + ], + 'keeps-url-when-params-are-missing' => [ + 'https://craftcms.com/?foo=1', + 'https://craftcms.com/?foo=1', + ['bar', 'baz'], + ], + 'empty-params' => [ + 'https://craftcms.com/?foo=1', + 'https://craftcms.com/?foo=1', + [], + ], + ]); + + test('removes all query params from a URL', function (string $expected, string $url, array $except) { + expect(Url::removeAllParams($url, $except))->toBe($expected); + })->with([ + 'removes-all' => [ + 'https://craftcms.com/', + 'https://craftcms.com/?foo=1&bar=2', + [], + ], + 'keeps-fragment' => [ + 'https://craftcms.com/#anchor', + 'https://craftcms.com/?foo=1&bar=2#anchor', + [], + ], + 'except' => [ + 'https://craftcms.com/?bar=2', + 'https://craftcms.com/?foo=1&bar=2&baz=3', + ['bar'], + ], + 'except-multiple' => [ + 'https://craftcms.com/?foo=1&baz=3', + 'https://craftcms.com/?foo=1&bar=2&baz=3', + ['foo', 'baz'], + ], + 'except-missing' => [ + 'https://craftcms.com/', + 'https://craftcms.com/?foo=1&bar=2', + ['baz'], + ], + ]); + test('adds token params to a URL', function (string $expected, string $url, string $token) { Cms::config()->useSslOnTokenizedUrls = true; @@ -390,6 +445,13 @@ ['{siteUrl}endpoint?returnUrl=https%3A%2F%2Fexample.test%2Fadmin%2Fentries%3Fsite%3D{handle}', 'endpoint', ['returnUrl' => 'https://example.test/admin/entries?site={handle}'], 'https', null], ]); + it('removes all params from a full URL when params is false', function () { + swapUrlRequest('https://localhost/news'); + + expect(Url::url('https://craftcms.com/?x-craft-preview=foo&test=bar', false)) + ->toBe('https://craftcms.com/'); + }); + it('creates action URLs', function () { swapUrlRequest('https://localhost/news'); diff --git a/yii2-adapter/legacy/helpers/UrlHelper.php b/yii2-adapter/legacy/helpers/UrlHelper.php index ddcd3dd8d14..b25700fe03e 100644 --- a/yii2-adapter/legacy/helpers/UrlHelper.php +++ b/yii2-adapter/legacy/helpers/UrlHelper.php @@ -1,6 +1,8 @@ + * * @since 3.0.0 * @deprecated 6.0.0 use {@see Url} instead. */ @@ -21,7 +24,6 @@ class UrlHelper extends Url /** * Returns a CP referral URL. * - * @return string|null * @since 5.9.0 * @deprecated in 5.10.0 */ @@ -40,7 +42,7 @@ public static function cpReferralUrl(): ?string } // Make sure the CP referred it - if (!str_starts_with($referrer, self::baseCpUrl())) { + if (!str_starts_with($referrer, static::baseCpUrl())) { return null; } diff --git a/yii2-adapter/legacy/web/DbSession.php b/yii2-adapter/legacy/web/DbSession.php new file mode 100644 index 00000000000..0935fa5e34a --- /dev/null +++ b/yii2-adapter/legacy/web/DbSession.php @@ -0,0 +1,37 @@ + + * + * @since 5.11.0 + * + * @mixin SessionBehavior + */ +class DbSession extends \yii\web\DbSession +{ + /** + * {@inheritdoc} + */ + public function has($key): bool + { + // don't open the session if the headers were already sent + if (!$this->getIsActive() && headers_sent()) { + return isset($_SESSION[$key]); + } + + return parent::has($key); + } +}