diff --git a/CHANGELOG-5.11.md b/CHANGELOG-5.11.md new file mode 100644 index 00000000000..44d94590972 --- /dev/null +++ b/CHANGELOG-5.11.md @@ -0,0 +1,10 @@ +# Release Notes for Craft CMS 5.11 (WIP) + +### Development +- The `params` argument of the `url()` Twig function now accepts `false` to remove all params from the passed-in URL. ([#19102](https://github.com/craftcms/cms/pull/19102)) +- Added `craft\web\DbSession`, which should be used instead of `yii\web\DbSession` to prevent “headers already sent” warnings from getting logged. ([#19139](https://github.com/craftcms/cms/issues/19139)) + +### Extensibility +- Added `craft\helpers\UrlHelper::removeParams()`. ([#19102](https://github.com/craftcms/cms/pull/19102)) +- Added `craft\helpers\UrlHelper::removeAllParams()`. ([#19102](https://github.com/craftcms/cms/pull/19102)) +- The `$params` argument of `craft\helpers\UrlHelper::url()` now accepts `false` to remove all params from the passed-in URL. ([#19102](https://github.com/craftcms/cms/pull/19102)) diff --git a/src/helpers/UrlHelper.php b/src/helpers/UrlHelper.php index fd4aefa9d08..e27013eb31e 100644 --- a/src/helpers/UrlHelper.php +++ b/src/helpers/UrlHelper.php @@ -155,12 +155,61 @@ public static function urlWithParams(string $url, array|string $params): string * @since 3.2.2 */ public static function removeParam(string $url, string $param): string + { + return static::removeParams($url, [$param]); + } + + /** + * Removes query string params from a URL. + * + * @param string $url + * @param string[] $params + * @return string + * @since 5.11.0 + */ + public static function removeParams(string $url, array $params): string + { + // Extract any params/fragment from the base URL + [$url, $urlParams, $fragment] = self::_extractParams($url); + + // Remove the params + foreach ($params as $param) { + unset($urlParams[$param]); + } + + // Rebuild + if (($query = static::buildQuery($urlParams)) !== '') { + $url .= '?' . $query; + } + if ($fragment !== null) { + $url .= '#' . $fragment; + } + return $url; + } + + /** + * Removes all query string params from a URL. + * + * @param string $url + * @param string[] $except Any params that should be left alone + * @return string + * @since 5.11.0 + */ + 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 param - unset($params[$param]); + // Remove the params + if (!empty($except)) { + foreach (array_keys($params) as $param) { + if (!in_array($param, $except)) { + unset($params[$param]); + } + } + } else { + $params = []; + } // Rebuild if (($query = static::buildQuery($params)) !== '') { @@ -275,18 +324,20 @@ public static function rootRelativeUrl(string $url): string * Returns either a control panel or a site URL, depending on the request type. * * @param string $path - * @param array|string|null $params + * @param array|string|false|null $params The query params to add to the URL. If `false`, any existing params will be removed. * @param string|null $scheme * @param bool|null $showScriptName Whether the script name (index.php) should be included in the URL. * By default (null) it will defer to the `omitScriptNameInUrls` config setting. * @return string */ - public static function url(string $path = '', array|string|null $params = null, ?string $scheme = null, ?bool $showScriptName = null): string + public static function url(string $path = '', array|string|false|null $params = null, ?string $scheme = null, ?bool $showScriptName = 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) { @@ -312,7 +363,7 @@ public static function url(string $path = '', array|string|null $params = null, $scheme = 'https'; } - return self::_createUrl($path, $params, $scheme, $cpUrl, showScriptName: $showScriptName); + return self::_createUrl($path, $params ?: null, $scheme, $cpUrl, showScriptName: $showScriptName); } /** @@ -596,12 +647,12 @@ 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; } // to ensure we're comparing uris strip base cp url and query string from the referrer first - $referrerFullUri = ltrim(StringHelper::removeLeft($referrer, self::baseCpUrl()), '/'); + $referrerFullUri = ltrim(StringHelper::removeLeft($referrer, static::baseCpUrl()), '/'); $referrerFullUri = substr($referrerFullUri, 0, strpos($referrerFullUri, '?') ?: null); // Make sure it didn't refer itself diff --git a/src/web/DbSession.php b/src/web/DbSession.php new file mode 100644 index 00000000000..6963edb30f1 --- /dev/null +++ b/src/web/DbSession.php @@ -0,0 +1,33 @@ + + * @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); + } +} diff --git a/tests/unit/helpers/UrlHelperTest.php b/tests/unit/helpers/UrlHelperTest.php index 6eb9c23ce71..c30e86627a9 100644 --- a/tests/unit/helpers/UrlHelperTest.php +++ b/tests/unit/helpers/UrlHelperTest.php @@ -158,6 +158,39 @@ public function testStripQueryString(string $expected, string $url): void self::assertSame($expected, UrlHelper::stripQueryString($url)); } + /** + * @dataProvider removeParamDataProvider + * @param string $expected + * @param string $url + * @param string $param + */ + public function testRemoveParam(string $expected, string $url, string $param): void + { + self::assertSame($expected, UrlHelper::removeParam($url, $param)); + } + + /** + * @dataProvider removeParamsDataProvider + * @param string $expected + * @param string $url + * @param string[] $params + */ + public function testRemoveParams(string $expected, string $url, array $params): void + { + self::assertSame($expected, UrlHelper::removeParams($url, $params)); + } + + /** + * @dataProvider removeAllParamsDataProvider + * @param string $expected + * @param string $url + * @param string[] $except + */ + public function testRemoveAllParams(string $expected, string $url, array $except = []): void + { + self::assertSame($expected, UrlHelper::removeAllParams($url, $except)); + } + /** * @dataProvider encodeParamsDataProvider */ @@ -192,11 +225,11 @@ public function testRootRelativeUrl(string $expected, string $url): void * @dataProvider urlFunctionDataProvider * @param string $expected * @param string $path - * @param array|null $params + * @param array|string|false|null $params * @param string|null $scheme * @param bool|null $showScriptName */ - public function testUrlFunction(string $expected, string $path = '', ?array $params = null, ?string $scheme = null, ?bool $showScriptName = null): void + public function testUrlFunction(string $expected, string $path = '', array|string|false|null $params = null, ?string $scheme = null, ?bool $showScriptName = null): void { $scheme ??= 'https'; $expected = $this->_prepExpectedUrl($expected, $scheme); @@ -435,6 +468,129 @@ public static function stripQueryStringDataProvider(): array ]; } + /** + * Tests for UrlHelper::removeParam() method + * + * @return array + */ + public static function removeParamDataProvider(): array + { + return [ + 'basic' => [ + self::ABSOLUTE_URL_HTTPS . '?param2=value2', + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2', + 'param1', + ], + 'last-param' => [ + self::ABSOLUTE_URL_HTTPS, + self::ABSOLUTE_URL_HTTPS . '?param1=value1', + 'param1', + ], + 'missing-param' => [ + self::ABSOLUTE_URL_HTTPS . '?param1=value1', + self::ABSOLUTE_URL_HTTPS . '?param1=value1', + 'param2', + ], + 'no-params' => [ + self::ABSOLUTE_URL_HTTPS, + self::ABSOLUTE_URL_HTTPS, + 'param1', + ], + 'keeps-fragment' => [ + self::ABSOLUTE_URL_HTTPS . '?param2=value2#anchor', + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2#anchor', + 'param1', + ], + ]; + } + + /** + * Tests for UrlHelper::removeParams() method + * + * @return array + */ + public static function removeParamsDataProvider(): array + { + return [ + 'multiple' => [ + self::ABSOLUTE_URL_HTTPS . '?param2=value2', + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2¶m3=value3', + ['param1', 'param3'], + ], + 'all-params' => [ + self::ABSOLUTE_URL_HTTPS, + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2', + ['param1', 'param2'], + ], + 'missing-params' => [ + self::ABSOLUTE_URL_HTTPS . '?param1=value1', + self::ABSOLUTE_URL_HTTPS . '?param1=value1', + ['param2', 'param3'], + ], + 'empty-params' => [ + self::ABSOLUTE_URL_HTTPS . '?param1=value1', + self::ABSOLUTE_URL_HTTPS . '?param1=value1', + [], + ], + 'no-params' => [ + self::ABSOLUTE_URL_HTTPS, + self::ABSOLUTE_URL_HTTPS, + ['param1'], + ], + 'keeps-fragment' => [ + self::ABSOLUTE_URL_HTTPS . '?param2=value2#anchor', + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2¶m3=value3#anchor', + ['param1', 'param3'], + ], + ]; + } + + /** + * Tests for UrlHelper::removeAllParams() method + * + * @return array + */ + public static function removeAllParamsDataProvider(): array + { + return [ + 'basic' => [ + self::ABSOLUTE_URL_HTTPS, + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2', + [], + ], + 'except' => [ + self::ABSOLUTE_URL_HTTPS . '?param2=value2', + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2¶m3=value3', + ['param2'], + ], + 'except-multiple' => [ + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m3=value3', + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2¶m3=value3', + ['param1', 'param3'], + ], + 'except-missing' => [ + self::ABSOLUTE_URL_HTTPS, + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2', + ['param3'], + ], + 'no-params' => [ + self::ABSOLUTE_URL_HTTPS, + self::ABSOLUTE_URL_HTTPS, + [], + ], + 'keeps-fragment' => [ + self::ABSOLUTE_URL_HTTPS . '#anchor', + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2#anchor', + [], + ], + 'except-keeps-fragment' => [ + self::ABSOLUTE_URL_HTTPS . '?param2=value2#anchor', + self::ABSOLUTE_URL_HTTPS . '?param1=value1¶m2=value2#anchor', + ['param2'], + ], + ]; + } + /** * Tests for UrlHelper::urlWithParams() method * @@ -716,6 +872,11 @@ public static function urlFunctionDataProvider(): array ['returnUrl' => 'https://example.test/admin/entries?site={handle}'], 'https', ], + 'remove-params' => [ + self::ABSOLUTE_URL_HTTPS, + self::ABSOLUTE_URL_HTTPS . '?x-craft-preview=foo&test=bar', + false, + ], ]; }