From 2d91723934abac1b9d202eae3827de015b7c927a Mon Sep 17 00:00:00 2001 From: Robert Korulczyk Date: Thu, 24 Nov 2016 14:16:18 +0100 Subject: [PATCH] Add support for creating protocol-relative URLs. Improve phpdoc for protocol-relative URL creation. Remove useless `is_string()` check. --- framework/CHANGELOG.md | 1 + framework/helpers/BaseUrl.php | 88 +++++++++++++++++--------- framework/web/UrlManager.php | 11 ++-- tests/framework/helpers/UrlTest.php | 6 ++ tests/framework/web/UrlManagerTest.php | 6 ++ 5 files changed, 76 insertions(+), 36 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 3aed7bb2ccb..7e4f34c0c00 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -35,6 +35,7 @@ Yii Framework 2 Change Log - Enh #12015: Changed visibility `yii\db\ActiveQueryTrait::createModels()` from private to protected (ArekX, dynasource) - Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006) - Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul) +- Enh #12738: Added support for creating protocol-relative URLs in `UrlManager::createAbsoluteUrl()` and `Url` helper methods (rob006) - Enh #12748: Added Migration tool automatic generation reference column for foreignKey (MKiselev) - Enh #12748: Migration generator now tries to fetch reference column name for foreignKey from schema if it's not set explicitly (MKiselev) - Enh #12750: `yii\widgets\ListView::itemOptions` can be a closure now (webdevsega, silverfire) diff --git a/framework/helpers/BaseUrl.php b/framework/helpers/BaseUrl.php index 0134ec961da..69b240456a6 100644 --- a/framework/helpers/BaseUrl.php +++ b/framework/helpers/BaseUrl.php @@ -86,8 +86,9 @@ class BaseUrl * @param bool|string $scheme the URI scheme to use in the generated URL: * * - `false` (default): generating a relative URL. - * - `true`: returning an absolute base URL whose scheme is the same as that in [[\yii\web\UrlManager::hostInfo]]. - * - string: generating an absolute URL with the specified scheme (either `http` or `https`). + * - `true`: returning an absolute base URL whose scheme is the same as that in [[\yii\web\UrlManager::$hostInfo]]. + * - string: generating an absolute URL with the specified scheme (either `http`, `https` or empty string + * for protocol-relative URL). * * @return string the generated URL * @throws InvalidParamException a relative route is given while there is no active controller @@ -97,7 +98,7 @@ public static function toRoute($route, $scheme = false) $route = (array) $route; $route[0] = static::normalizeRoute($route[0]); - if ($scheme) { + if ($scheme !== false) { return static::getUrlManager()->createAbsoluteUrl($route, is_string($scheme) ? $scheme : null); } else { return static::getUrlManager()->createUrl($route); @@ -160,8 +161,8 @@ protected static function normalizeRoute($route) * - an empty string: the currently requested URL will be returned; * - a normal string: it will be returned as is. * - * When `$scheme` is specified (either a string or true), an absolute URL with host info (obtained from - * [[\yii\web\UrlManager::hostInfo]]) will be returned. If `$url` is already an absolute URL, its scheme + * When `$scheme` is specified (either a string or `true`), an absolute URL with host info (obtained from + * [[\yii\web\UrlManager::$hostInfo]]) will be returned. If `$url` is already an absolute URL, its scheme * will be replaced with the specified one. * * Below are some examples of using this method: @@ -190,6 +191,9 @@ protected static function normalizeRoute($route) * * // https://www.example.com/images/logo.gif * echo Url::to('@web/images/logo.gif', 'https'); + * + * // //www.example.com/images/logo.gif + * echo Url::to('@web/images/logo.gif', ''); * ``` * * @@ -197,8 +201,9 @@ protected static function normalizeRoute($route) * @param bool|string $scheme the URI scheme to use in the generated URL: * * - `false` (default): generating a relative URL. - * - `true`: returning an absolute base URL whose scheme is the same as that in [[\yii\web\UrlManager::hostInfo]]. - * - string: generating an absolute URL with the specified scheme (either `http` or `https`). + * - `true`: returning an absolute base URL whose scheme is the same as that in [[\yii\web\UrlManager::$hostInfo]]. + * - string: generating an absolute URL with the specified scheme (either `http`, `https` or empty string + * for protocol-relative URL). * * @return string the generated URL * @throws InvalidParamException a relative route is given while there is no active controller @@ -214,23 +219,46 @@ public static function to($url = '', $scheme = false) $url = Yii::$app->getRequest()->getUrl(); } - if (!$scheme) { + if ($scheme === false) { return $url; } - if (strncmp($url, '//', 2) === 0) { - // e.g. //hostname/path/to/resource - return is_string($scheme) ? "$scheme:$url" : $url; - } - if (($pos = strpos($url, ':')) === false || !ctype_alpha(substr($url, 0, $pos))) { // turn relative URL into absolute $url = static::getUrlManager()->getHostInfo() . '/' . ltrim($url, '/'); } - if (is_string($scheme) && ($pos = strpos($url, ':')) !== false) { - // replace the scheme with the specified one - $url = $scheme . substr($url, $pos); + return static::ensureScheme($url, $scheme); + } + + /** + * Normalize URL by ensuring that it use specified scheme. + * + * If URL is relative or scheme is not string, normalization is skipped. + * + * @param string $url the URL to process + * @param string $scheme the URI scheme used in URL (e.g. `http` or `https`). Use empty string to + * create protocol-relative URL (e.g. `//example.com/path`) + * @return string the processed URL + * @since 2.0.11 + */ + public static function ensureScheme($url, $scheme) + { + if (static::isRelative($url) || !is_string($scheme)) { + return $url; + } + + if (substr($url, 0, 2) === '//') { + // e.g. //example.com/path/to/resource + return $scheme === '' ? $url : "$scheme:$url"; + } + + if (($pos = strpos($url, '://')) !== false) { + if ($scheme === '') { + $url = substr($url, $pos + 1); + } else { + $url = $scheme . substr($url, $pos); + } } return $url; @@ -241,19 +269,19 @@ public static function to($url = '', $scheme = false) * @param bool|string $scheme the URI scheme to use in the returned base URL: * * - `false` (default): returning the base URL without host info. - * - `true`: returning an absolute base URL whose scheme is the same as that in [[\yii\web\UrlManager::hostInfo]]. - * - string: returning an absolute base URL with the specified scheme (either `http` or `https`). + * - `true`: returning an absolute base URL whose scheme is the same as that in [[\yii\web\UrlManager::$hostInfo]]. + * - string: returning an absolute base URL with the specified scheme (either `http`, `https` or empty string + * for protocol-relative URL). * @return string */ public static function base($scheme = false) { $url = static::getUrlManager()->getBaseUrl(); - if ($scheme) { + if ($scheme !== false) { $url = static::getUrlManager()->getHostInfo() . $url; - if (is_string($scheme) && ($pos = strpos($url, '://')) !== false) { - $url = $scheme . substr($url, $pos); - } + $url = static::ensureScheme($url, $scheme); } + return $url; } @@ -320,8 +348,9 @@ public static function canonical() * @param bool|string $scheme the URI scheme to use for the returned URL: * * - `false` (default): returning a relative URL. - * - `true`: returning an absolute base URL whose scheme is the same as that in [[\yii\web\UrlManager::hostInfo]]. - * - string: returning an absolute URL with the specified scheme (either `http` or `https`). + * - `true`: returning an absolute base URL whose scheme is the same as that in [[\yii\web\UrlManager::$hostInfo]]. + * - string: returning an absolute URL with the specified scheme (either `http`, `https` or empty string + * for protocol-relative URL). * * @return string home URL */ @@ -329,11 +358,9 @@ public static function home($scheme = false) { $url = Yii::$app->getHomeUrl(); - if ($scheme) { + if ($scheme !== false) { $url = static::getUrlManager()->getHostInfo() . $url; - if (is_string($scheme) && ($pos = strpos($url, '://')) !== false) { - $url = $scheme . substr($url, $pos); - } + $url = static::ensureScheme($url, $scheme); } return $url; @@ -387,8 +414,9 @@ public static function isRelative($url) * @param bool|string $scheme the URI scheme to use in the generated URL: * * - `false` (default): generating a relative URL. - * - `true`: returning an absolute base URL whose scheme is the same as that in [[\yii\web\UrlManager::hostInfo]]. - * - string: generating an absolute URL with the specified scheme (either `http` or `https`). + * - `true`: returning an absolute base URL whose scheme is the same as that in [[\yii\web\UrlManager::$hostInfo]]. + * - string: generating an absolute URL with the specified scheme (either `http`, `https` or empty string + * for protocol-relative URL). * * @return string the generated URL * @since 2.0.3 diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 3072c58e217..87f2a723918 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -11,6 +11,7 @@ use yii\base\Component; use yii\base\InvalidConfigException; use yii\caching\Cache; +use yii\helpers\Url; /** * UrlManager handles HTTP request parsing and creation of URLs based on a set of rules. @@ -452,8 +453,9 @@ protected function setRuleToCache($cacheKey, UrlRuleInterface $rule) * * @param string|array $params use a string to represent a route (e.g. `site/index`), * or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`). - * @param string $scheme the scheme to use for the url (either `http` or `https`). If not specified - * the scheme of the current request will be used. + * @param string|null $scheme the scheme to use for the URL (either `http`, `https` or empty string + * for protocol-relative URL). + * If not specified the scheme of the current request will be used. * @return string the created URL * @see createUrl() */ @@ -464,11 +466,8 @@ public function createAbsoluteUrl($params, $scheme = null) if (strpos($url, '://') === false) { $url = $this->getHostInfo() . $url; } - if (is_string($scheme) && ($pos = strpos($url, '://')) !== false) { - $url = $scheme . substr($url, $pos); - } - return $url; + return Url::ensureScheme($url, $scheme); } /** diff --git a/tests/framework/helpers/UrlTest.php b/tests/framework/helpers/UrlTest.php index 1320b0cfc0f..391d894c737 100644 --- a/tests/framework/helpers/UrlTest.php +++ b/tests/framework/helpers/UrlTest.php @@ -71,6 +71,7 @@ public function testToRoute() $this->assertEquals('/base/index.php?r=', Url::toRoute('/')); $this->assertEquals('http://example.com/base/index.php?r=page%2Fview', Url::toRoute('', true)); $this->assertEquals('https://example.com/base/index.php?r=page%2Fview', Url::toRoute('', 'https')); + $this->assertEquals('//example.com/base/index.php?r=page%2Fview', Url::toRoute('', '')); // If the route contains no slashes at all, it is considered to be an action ID of the current controller and // will be prepended with uniqueId; @@ -78,6 +79,7 @@ public function testToRoute() $this->assertEquals('/base/index.php?r=page%2Fedit&id=20', Url::toRoute(['edit', 'id' => 20])); $this->assertEquals('http://example.com/base/index.php?r=page%2Fedit&id=20', Url::toRoute(['edit', 'id' => 20], true)); $this->assertEquals('https://example.com/base/index.php?r=page%2Fedit&id=20', Url::toRoute(['edit', 'id' => 20], 'https')); + $this->assertEquals('//example.com/base/index.php?r=page%2Fedit&id=20', Url::toRoute(['edit', 'id' => 20], '')); // If the route has no leading slash, it is considered to be a route relative // to the current module and will be prepended with the module's uniqueId. @@ -86,6 +88,7 @@ public function testToRoute() $this->assertEquals('/base/index.php?r=stats%2Fuser%2Fview&id=42', Url::toRoute(['user/view', 'id' => 42])); $this->assertEquals('http://example.com/base/index.php?r=stats%2Fuser%2Fview&id=42', Url::toRoute(['user/view', 'id' => 42], true)); $this->assertEquals('https://example.com/base/index.php?r=stats%2Fuser%2Fview&id=42', Url::toRoute(['user/view', 'id' => 42], 'https')); + $this->assertEquals('//example.com/base/index.php?r=stats%2Fuser%2Fview&id=42', Url::toRoute(['user/view', 'id' => 42], '')); // alias support \Yii::setAlias('@userView', 'user/view'); @@ -181,6 +184,7 @@ public function testTo() $this->assertEquals('#test', Url::to('@web5')); $this->assertEquals('http://example.com/#test', Url::to('@web5', true)); $this->assertEquals('https://example.com/#test', Url::to('@web5', 'https')); + $this->assertEquals('//example.com/#test', Url::to('@web5', '')); //In case there is no controller, throw an exception $this->removeMockedAction(); @@ -228,6 +232,7 @@ public function testBase() $this->assertEquals('/base', Url::base()); $this->assertEquals('http://example.com/base', Url::base(true)); $this->assertEquals('https://example.com/base', Url::base('https')); + $this->assertEquals('//example.com/base', Url::base('')); } public function testHome() @@ -235,6 +240,7 @@ public function testHome() $this->assertEquals('/base/index.php', Url::home()); $this->assertEquals('http://example.com/base/index.php', Url::home(true)); $this->assertEquals('https://example.com/base/index.php', Url::home('https')); + $this->assertEquals('//example.com/base/index.php', Url::home('')); } public function testCanonical() diff --git a/tests/framework/web/UrlManagerTest.php b/tests/framework/web/UrlManagerTest.php index 0176d66e00b..12f062db2b4 100644 --- a/tests/framework/web/UrlManagerTest.php +++ b/tests/framework/web/UrlManagerTest.php @@ -247,9 +247,15 @@ public function testCreateAbsoluteUrl() $url = $manager->createAbsoluteUrl(['post/view', 'id' => 1, 'title' => 'sample post'], 'https'); $this->assertEquals('https://www.example.com?r=post%2Fview&id=1&title=sample+post', $url); + $url = $manager->createAbsoluteUrl(['post/view', 'id' => 1, 'title' => 'sample post'], ''); + $this->assertEquals('//www.example.com?r=post%2Fview&id=1&title=sample+post', $url); + $manager->hostInfo = 'https://www.example.com'; $url = $manager->createAbsoluteUrl(['post/view', 'id' => 1, 'title' => 'sample post'], 'http'); $this->assertEquals('http://www.example.com?r=post%2Fview&id=1&title=sample+post', $url); + + $url = $manager->createAbsoluteUrl(['post/view', 'id' => 1, 'title' => 'sample post'], ''); + $this->assertEquals('//www.example.com?r=post%2Fview&id=1&title=sample+post', $url); } public function testCreateAbsoluteUrlWithSuffix()