diff --git a/control/Director.php b/control/Director.php index f069e2cc8..7122453fa 100644 --- a/control/Director.php +++ b/control/Director.php @@ -16,6 +16,21 @@ */ class Director implements TemplateGlobalProvider { + /** + * Specifies this url is relative to the base + */ + const BASE = 'BASE'; + + /** + * Specifies this url is relative to the site root + */ + const ROOT = 'ROOT'; + + /** + * specifies this url is relative to the current request + */ + const REQUEST = 'REQUEST'; + static private $urlParams; static private $rules = array(); @@ -428,36 +443,56 @@ class Director implements TemplateGlobalProvider { /** * Turns the given URL into an absolute URL. - * By default non-site root relative urls will be evaluated relative to the current request. + * By default non-site root relative urls will be evaluated relative to the current base_url * * @param string $url URL To transform to absolute - * @param bool $relativeToSiteBase Flag indicating if non-site root relative urls should be - * evaluated relative to the site BaseURL instead of the current url. + * @param string $relativeParent Method to use for evaluating relative urls. + * Either one of BASE (baseurl), ROOT (site root), or REQUEST (requested page). + * Defaults to BASE, which is the same behaviour as template url resolution. + * Igored if the url is absolute or site root. * @return string The fully qualified URL */ - public static function absoluteURL($url, $relativeToSiteBase = false) { - if(!isset($_SERVER['REQUEST_URI'])) return false; - - //a url of . or ./ is the same as an empty url - if ($url == '.' || $url == './') { - $url = ''; + public static function absoluteURL($url, $relativeParent = self::BASE) { + if(is_bool($relativeParent)) { + // Deprecate old boolean second parameter + Deprecation::notice('5.0', 'Director::absoluteURL takes an explicit parent for relative url'); + $relativeParent = $relativeParent ? self::BASE : self::REQUEST; } - - if(strpos($url,'/') === false && !$relativeToSiteBase) { - //if there's no URL we want to force a trailing slash on the link - if (!$url) { - $url = '/'; + + // Check if there is already a protocol given + if(preg_match('/^http(s?):\/\//', $url)) { + return $url; + } + + // Absolute urls without protocol are added + // E.g. //google.com -> http://google.com + if(strpos($url, '//') === 0) { + return self::protocol() . substr($url, 2); + } + + // Determine method for mapping the parent to this relative url + if($relativeParent === self::ROOT || self::is_root_relative_url($url)) { + // Root relative urls always should be evaluated relative to the root + $parent = self::protocolAndHost(); + + } elseif($relativeParent === self::REQUEST) { + // Request relative urls rely on the REQUEST_URI param (old default behaviour) + if(!isset($_SERVER['REQUEST_URI'])) { + return false; } - $url = Controller::join_links(dirname($_SERVER['REQUEST_URI'] . 'x'), $url); + $parent = dirname($_SERVER['REQUEST_URI'] . 'x'); + + } else { + // Default to respecting site base_url + $parent = self::absoluteBaseURL(); } - if(substr($url,0,4) != "http") { - if(strpos($url, '/') !== 0) $url = Director::baseURL() . $url; - // Sometimes baseURL() can return a full URL instead of just a path - if(substr($url,0,4) != "http") $url = self::protocolAndHost() . $url; + // Map empty urls to relative slash and join to base + if(empty($url) || $url === '.' || $url === './') { + $url = '/'; } + return Controller::join_links($parent, $url); - return $url; } /** @@ -615,7 +650,7 @@ class Director implements TemplateGlobalProvider { // Allow for the accidental inclusion whitespace and // in the URL $url = trim(preg_replace('#([^:])//#', '\\1/', $url)); - $base1 = self::absoluteBaseURL(); + $base1 = self::absoluteBaseURL(); $baseDomain = substr($base1, strlen(self::protocol())); // Only bother comparing the URL to the absolute version if $url looks like a URL. @@ -667,10 +702,22 @@ class Director implements TemplateGlobalProvider { * @return bool */ public static function is_absolute($path) { + if(empty($path)) return false; if($path[0] == '/' || $path[0] == '\\') return true; return preg_match('/^[a-zA-Z]:[\\\\\/]/', $path) == 1; } + /** + * Determine if the url is root relative (i.e. starts with /, but not with //) + * SilverStripe considers root relative urls as a subset of relative urls + * + * @param string $url + * @return bool True if this url is root relative + */ + public static function is_root_relative_url($url) { + return strpos($url, '/') === 0 && strpos($url, '//') !== 0; + } + /** * Checks if a given URL is absolute (e.g. starts with 'http://' etc.). * URLs beginning with "//" are treated as absolute, as browsers take this to mean @@ -700,8 +747,8 @@ class Director implements TemplateGlobalProvider { // Base check for existence of a host on a compliant URL parse_url($url, PHP_URL_HOST) // Check for more than one leading slash without a protocol. - // While not a RFC compliant absolute URL, it is completed to a valid URL by some browsers, - // and hence a potential security risk. Single leading slashes are not an issue though. + // While not a RFC compliant absolute URL, it is completed to a valid URL by some browsers, + // and hence a potential security risk. Single leading slashes are not an issue though. || preg_match('%^\s*/{2,}%', $url) || ( // If a colon is found, check if it's part of a valid scheme definition @@ -713,13 +760,13 @@ class Director implements TemplateGlobalProvider { } /** - * Checks if a given URL is relative by checking {@link is_absolute_url()}. + * Checks if a given URL is relative (or root relative) by checking {@link is_absolute_url()} * * @param string $url * @return boolean */ public static function is_relative_url($url) { - return (!Director::is_absolute_url($url)); + return !static::is_absolute_url($url); } /** @@ -747,7 +794,7 @@ class Director implements TemplateGlobalProvider { /** * Takes a $_SERVER data array and extracts HTTP request headers. * - * @param array $data + * @param array $server * @return array */ public static function extract_request_headers(array $server) { @@ -790,9 +837,14 @@ class Director implements TemplateGlobalProvider { /** * Returns the Absolute URL of the site root. + * + * @return string */ public static function absoluteBaseURL() { - return Director::absoluteURL(Director::baseURL()); + return self::absoluteURL( + self::baseURL(), + self::ROOT + ); } /** diff --git a/tests/control/DirectorTest.php b/tests/control/DirectorTest.php index ab65e2161..b68810ec6 100644 --- a/tests/control/DirectorTest.php +++ b/tests/control/DirectorTest.php @@ -99,36 +99,50 @@ class DirectorTest extends SapphireTest { $_SERVER['REQUEST_URI'] = "$rootURL/mysite/sub-page/"; Config::inst()->update('Director', 'alternate_base_url', '/mysite/'); - //test empty URL - $this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL('')); + //test empty / local urls + foreach(array('', './', '.') as $url) { + $this->assertEquals("$rootURL/mysite/", Director::absoluteURL($url, Director::BASE)); + $this->assertEquals("$rootURL/", Director::absoluteURL($url, Director::ROOT)); + $this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL($url, Director::REQUEST)); + } - //test absolute - / + // Test site root url $this->assertEquals("$rootURL/", Director::absoluteURL('/')); - //test relative - $this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL('./')); - $this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL('.')); + // Test Director::BASE + $this->assertEquals($rootURL, Director::absoluteURL($rootURL, Director::BASE)); + $this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com', Director::BASE)); + $this->assertEquals("$rootURL/test", Director::absoluteURL("$rootURL/test", Director::BASE)); + $this->assertEquals("$rootURL/root", Director::absoluteURL("/root", Director::BASE)); + $this->assertEquals("$rootURL/root/url", Director::absoluteURL("/root/url", Director::BASE)); - // Test already absolute url - $this->assertEquals($rootURL, Director::absoluteURL($rootURL)); - $this->assertEquals($rootURL, Director::absoluteURL($rootURL, true)); - $this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com')); - $this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com', true)); - $this->assertEquals("$rootURL/test", Director::absoluteURL("$rootURL/test")); - $this->assertEquals("$rootURL/test", Director::absoluteURL("$rootURL/test", true)); + // Test Director::ROOT + $this->assertEquals($rootURL, Director::absoluteURL($rootURL, Director::ROOT)); + $this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com', Director::ROOT)); + $this->assertEquals("$rootURL/test", Director::absoluteURL("$rootURL/test", Director::ROOT)); + $this->assertEquals("$rootURL/root", Director::absoluteURL("/root", Director::ROOT)); + $this->assertEquals("$rootURL/root/url", Director::absoluteURL("/root/url", Director::ROOT)); - // Test relative to base - $this->assertEquals("$rootURL/mysite/test", Director::absoluteURL("test", true)); - $this->assertEquals("$rootURL/mysite/test/url", Director::absoluteURL("test/url", true)); - $this->assertEquals("$rootURL/root", Director::absoluteURL("/root", true)); - $this->assertEquals("$rootURL/root/url", Director::absoluteURL("/root/url", true)); + // Test Director::REQUEST + $this->assertEquals($rootURL, Director::absoluteURL($rootURL, Director::REQUEST)); + $this->assertEquals('http://www.mytest.com', Director::absoluteURL('http://www.mytest.com', Director::REQUEST)); + $this->assertEquals("$rootURL/test", Director::absoluteURL("$rootURL/test", Director::REQUEST)); + $this->assertEquals("$rootURL/root", Director::absoluteURL("/root", Director::REQUEST)); + $this->assertEquals("$rootURL/root/url", Director::absoluteURL("/root/url", Director::REQUEST)); + + // Test evaluating relative urls relative to base (default) + $this->assertEquals("$rootURL/mysite/test", Director::absoluteURL("test")); + $this->assertEquals("$rootURL/mysite/test/url", Director::absoluteURL("test/url")); + $this->assertEquals("$rootURL/mysite/test", Director::absoluteURL("test", Director::BASE)); + $this->assertEquals("$rootURL/mysite/test/url", Director::absoluteURL("test/url", Director::BASE)); + + // Test evaluting relative urls relative to root + $this->assertEquals("$rootURL/test", Director::absoluteURL("test", Director::ROOT)); + $this->assertEquals("$rootURL/test/url", Director::absoluteURL("test/url", Director::ROOT)); // Test relative to requested page - $this->assertEquals("$rootURL/mysite/sub-page/test", Director::absoluteURL("test")); - // Legacy behaviour resolves this to $rootURL/mysite/test/url - //$this->assertEquals("$rootURL/mysite/sub-page/test/url", Director::absoluteURL("test/url")); - $this->assertEquals("$rootURL/root", Director::absoluteURL("/root")); - $this->assertEquals("$rootURL/root/url", Director::absoluteURL("/root/url")); + $this->assertEquals("$rootURL/mysite/sub-page/test", Director::absoluteURL("test", Director::REQUEST)); + $this->assertEquals("$rootURL/mysite/sub-page/test/url", Director::absoluteURL("test/url", Director::REQUEST)); // Test that javascript links are not left intact $this->assertStringStartsNotWith('javascript', Director::absoluteURL('javascript:alert("attack")')); @@ -157,17 +171,20 @@ class DirectorTest extends SapphireTest { $_SERVER['REQUEST_URI'] = "http://www.example.org/sub-page/"; $this->assertEquals('http://www.example.org/', Director::baseURL()); $this->assertEquals('http://www.example.org/', Director::absoluteBaseURL()); - $this->assertEquals('http://www.example.org/sub-page/', Director::absoluteURL('')); - $this->assertEquals('http://www.example.org/', Director::absoluteURL('', true)); - /* - * See Legacy behaviour in testAbsoluteURL - sub-pages with '/' in the string are not correctly evaluated + $this->assertEquals('http://www.example.org/sub-page/', Director::absoluteURL('', Director::REQUEST)); + $this->assertEquals('http://www.example.org/', Director::absoluteURL('', Director::BASE)); + $this->assertEquals('http://www.example.org/', Director::absoluteURL('', Director::ROOT)); $this->assertEquals( 'http://www.example.org/sub-page/subfolder/test', - Director::absoluteURL('subfolder/test') - );*/ + Director::absoluteURL('subfolder/test', Director::REQUEST) + ); $this->assertEquals( 'http://www.example.org/subfolder/test', - Director::absoluteURL('subfolder/test', true) + Director::absoluteURL('subfolder/test', Director::ROOT) + ); + $this->assertEquals( + 'http://www.example.org/subfolder/test', + Director::absoluteURL('subfolder/test', Director::BASE) ); // Setting it to false restores functionality