From 598edd91341f389d7b919ec1201e03d2aba4d284 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Wed, 26 Sep 2018 17:33:32 +0100 Subject: [PATCH] [SS-2018-019] Add confirmation token to dev/build --- core/startup/AbstractConfirmationToken.php | 218 ++++++++++++++++++ core/startup/ConfirmationTokenChain.php | 163 +++++++++++++ core/startup/ParameterConfirmationToken.php | 205 +++------------- core/startup/URLConfirmationToken.php | 104 +++++++++ main.php | 39 +++- .../startup/ConfirmationTokenChainTest.php | 141 +++++++++++ .../core/startup/URLConfirmationTokenTest.php | 78 +++++++ 7 files changed, 760 insertions(+), 188 deletions(-) create mode 100644 core/startup/AbstractConfirmationToken.php create mode 100644 core/startup/ConfirmationTokenChain.php create mode 100644 core/startup/URLConfirmationToken.php create mode 100644 tests/core/startup/ConfirmationTokenChainTest.php create mode 100644 tests/core/startup/URLConfirmationTokenTest.php diff --git a/core/startup/AbstractConfirmationToken.php b/core/startup/AbstractConfirmationToken.php new file mode 100644 index 000000000..8fbaf9d76 --- /dev/null +++ b/core/startup/AbstractConfirmationToken.php @@ -0,0 +1,218 @@ +reloadRequired()) { + $token->suppress(); + $target = $token; + } + } + return $target; + } + + /** + * Generate a local filesystem path to store a token + * + * @param $token + * @return string + */ + protected function pathForToken($token) { + return TEMP_FOLDER . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token); + } + + /** + * Generate a new random token and store it + * + * @return string Token name + */ + protected function genToken() { + // Generate a new random token (as random as possible) + require_once(dirname(dirname(dirname(__FILE__))).'/security/RandomGenerator.php'); + + $rg = new RandomGenerator(); + $token = $rg->randomToken('md5'); + + // Store a file in the session save path (safer than /tmp, as open_basedir might limit that) + file_put_contents($this->pathForToken($token), $token); + + return $token; + } + + /** + * Is the necessary token provided for this parameter? + * A value must be provided for the token + * + * @return bool + */ + public function tokenProvided() { + return !empty($this->token); + } + + /** + * Validate a token + * + * @param string $token + * @return boolean True if the token is valid + */ + protected function checkToken($token) { + if (!$token) { + return false; + } + + $file = $this->pathForToken($token); + $content = null; + + if (file_exists($file)) { + $content = file_get_contents($file); + unlink($file); + } + + return $content === $token; + } + + /** + * Get redirect url, excluding querystring + * + * @return string + */ + protected function currentAbsoluteURL() { + global $url; + + // Preserve BC - this has been moved from ParameterConfirmationToken + require_once(dirname(__FILE__).'/ParameterConfirmationToken.php'); + if (isset(ParameterConfirmationToken::$alternateBaseURL)) { + self::$alternateBaseURL = ParameterConfirmationToken::$alternateBaseURL; + } + + // Are we http or https? Replicates Director::is_https() without its dependencies/ + $proto = 'http'; + // See https://en.wikipedia.org/wiki/List_of_HTTP_header_fields + // See https://support.microsoft.com/?kbID=307347 + $headerOverride = false; + if(TRUSTED_PROXY) { + $headers = (defined('SS_TRUSTED_PROXY_PROTOCOL_HEADER')) ? array(SS_TRUSTED_PROXY_PROTOCOL_HEADER) : null; + if(!$headers) { + // Backwards compatible defaults + $headers = array('HTTP_X_FORWARDED_PROTO', 'HTTP_X_FORWARDED_PROTOCOL', 'HTTP_FRONT_END_HTTPS'); + } + foreach($headers as $header) { + $headerCompareVal = ($header === 'HTTP_FRONT_END_HTTPS' ? 'on' : 'https'); + if(!empty($_SERVER[$header]) && strtolower($_SERVER[$header]) == $headerCompareVal) { + $headerOverride = true; + break; + } + } + } + + if($headerOverride) { + $proto = 'https'; + } else if((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) { + $proto = 'https'; + } else if(isset($_SERVER['SSL'])) { + $proto = 'https'; + } + + $parts = array_filter(array( + // What's our host + $_SERVER['HTTP_HOST'], + // SilverStripe base + self::$alternateBaseURL !== null ? self::$alternateBaseURL : BASE_URL, + // And URL including base script (eg: if it's index.php/page/url/) + (defined('BASE_SCRIPT_URL') ? '/' . BASE_SCRIPT_URL : '') . $url, + )); + + // Join together with protocol into our current absolute URL, avoiding duplicated "/" characters + return "$proto://" . preg_replace('#/{2,}#', '/', implode('/', $parts)); + } + + /** + * Forces a reload of the request with the token included + */ + public function reloadWithToken() { + require_once(dirname(dirname(__FILE__)).'/Convert.php'); + $location = $this->redirectURL(); + $locationJS = Convert::raw2js($location); + $locationATT = Convert::raw2att($location); + + if (headers_sent()) { + echo " + + +You are being redirected. If you are not redirected soon, click here to continue +"; + } else { + header("location: {$location}", true, 302); + } + + die; + } + + /** + * Is this parameter requested without a valid token? + * + * @return bool True if the parameter is given without a valid token + */ + abstract public function reloadRequired(); + + /** + * Suppress the current parameter for the duration of this request + */ + abstract public function suppress(); + + /** + * Determine the querystring parameters to include + * + * @param bool $includeToken Include the token value? + * @return array List of querystring parameters, possibly including token parameter + */ + abstract public function params($includeToken = true); + + /** + * @return string + */ + abstract public function getRedirectUrlBase(); + + /** + * @return array + */ + abstract public function getRedirectUrlParams(); + + /** + * Get redirection URL + * + * @return string + */ + abstract protected function redirectURL(); +} diff --git a/core/startup/ConfirmationTokenChain.php b/core/startup/ConfirmationTokenChain.php new file mode 100644 index 000000000..9938285c0 --- /dev/null +++ b/core/startup/ConfirmationTokenChain.php @@ -0,0 +1,163 @@ +tokens[] = $token; + } + + /** + * Collect all tokens that require a redirect + * + * @return array + */ + protected function filteredTokens() { + $result = array(); + foreach ($this->tokens as $token) { + if ($token->reloadRequired()) { + $result[] = $token; + } + } + + return $result; + } + + /** + * @return bool + */ + public function suppressionRequired() { + return (count($this->filteredTokens()) !== 0); + } + + /** + * Suppress URLs & GET vars from tokens that require a redirect + */ + public function suppressTokens() { + foreach ($this->filteredTokens() as $token) { + $token->suppress(); + } + } + + /** + * @return bool + */ + public function reloadRequired() { + foreach ($this->tokens as $token) { + if ($token->reloadRequired()) { + return true; + } + } + + return false; + } + + /** + * @return bool + */ + public function reloadRequiredIfError() { + foreach ($this->tokens as $token) { + if ($token->reloadRequiredIfError()) { + return true; + } + } + + return false; + } + + /** + * @param bool $includeToken + * @return array + */ + public function params($includeToken = true) { + $params = array(); + foreach ($this->tokens as $token) { + $params = array_merge($params, $token->params($includeToken)); + } + + return $params; + } + + /** + * Fetch the URL we want to redirect to, excluding query string parameters. This may + * be the same URL (with a token to be added outside this method), or to a different + * URL if the current one has been suppressed + * + * @return string + */ + public function getRedirectUrlBase() { + // URLConfirmationTokens may alter the URL to suppress the URL they're protecting, + // so we need to ensure they're inspected last and therefore take priority + $tokens = $this->filteredTokens(); + usort($tokens, function ($a, $b) { + return ($a instanceof URLConfirmationToken) ? 1 : 0; + }); + + $urlBase = Director::baseURL(); + foreach ($tokens as $token) { + $urlBase = $token->getRedirectUrlBase(); + } + + return $urlBase; + } + + /** + * Collate GET vars from all token providers that need to apply a token + * + * @return array + */ + public function getRedirectUrlParams() { + $params = array(); + foreach ($this->filteredTokens() as $token) { + $params = array_merge($params, $token->getRedirectUrlParams()); + } + + return $params; + } + + /** + * @return string + */ + protected function redirectURL() { + $params = http_build_query($this->getRedirectUrlParams()); + return $this->getRedirectUrlBase() . '?' . $params; + } + + /** + * Forces a reload of the request with the applicable tokens included + */ + public function reloadWithToken() { + require_once(dirname(dirname(__FILE__)).'/Convert.php'); + $location = $this->redirectURL(); + $locationJS = Convert::raw2js($location); + $locationATT = Convert::raw2att($location); + + if (headers_sent()) { + echo " + + +You are being redirected. If you are not redirected soon, click here to continue +"; + } else { + header("location: {$location}", true, 302); + } + + die; + } +} diff --git a/core/startup/ParameterConfirmationToken.php b/core/startup/ParameterConfirmationToken.php index 44ae82332..77c609475 100644 --- a/core/startup/ParameterConfirmationToken.php +++ b/core/startup/ParameterConfirmationToken.php @@ -1,19 +1,17 @@ randomToken('md5'); - - // Store a file in the session save path (safer than /tmp, as open_basedir might limit that) - file_put_contents($this->pathForToken($token), $token); - - return $token; - } - - /** - * Validate a token - * - * @param string $token - * @return boolean True if the token is valid - */ - protected function checkToken($token) { - if(!$token) { - return false; - } - - $file = $this->pathForToken($token); - $content = null; - - if (file_exists($file)) { - $content = file_get_contents($file); - unlink($file); - } - - return $content == $token; - } - /** * Create a new ParameterConfirmationToken * @@ -107,143 +55,46 @@ class ParameterConfirmationToken { return $this->parameterName; } - /** - * Is the parameter requested? - * ?parameter and ?parameter=1 are both considered requested - * - * @return bool - */ public function parameterProvided() { return $this->parameter !== null; } - /** - * Is the necessary token provided for this parameter? - * A value must be provided for the token - * - * @return bool - */ - public function tokenProvided() { - return !empty($this->token); - } - - /** - * Is this parameter requested without a valid token? - * - * @return bool True if the parameter is given without a valid token - */ public function reloadRequired() { return $this->parameterProvided() && !$this->tokenProvided(); } - /** - * Suppress the current parameter by unsetting it from $_GET - */ public function suppress() { unset($_GET[$this->parameterName]); } - /** - * Determine the querystring parameters to include - * - * @return array List of querystring parameters with name and token parameters - */ - public function params() { - return array( + public function params($includeToken = true) { + $params = array( $this->parameterName => $this->parameter, - $this->parameterName.'token' => $this->genToken() ); + if ($includeToken) { + $params[$this->parameterName . 'token'] = $this->genToken(); + } + return $params; } - /** What to use instead of BASE_URL. Must not contain protocol or host. @var string */ - static public $alternateBaseURL = null; - - protected function currentAbsoluteURL() { - global $url; - - // Are we http or https? Replicates Director::is_https() without its dependencies/ - $proto = 'http'; - // See https://en.wikipedia.org/wiki/List_of_HTTP_header_fields - // See https://support.microsoft.com/?kbID=307347 - $headerOverride = false; - if(TRUSTED_PROXY) { - $headers = (defined('SS_TRUSTED_PROXY_PROTOCOL_HEADER')) ? array(SS_TRUSTED_PROXY_PROTOCOL_HEADER) : null; - if(!$headers) { - // Backwards compatible defaults - $headers = array('HTTP_X_FORWARDED_PROTO', 'HTTP_X_FORWARDED_PROTOCOL', 'HTTP_FRONT_END_HTTPS'); - } - foreach($headers as $header) { - $headerCompareVal = ($header === 'HTTP_FRONT_END_HTTPS' ? 'on' : 'https'); - if(!empty($_SERVER[$header]) && strtolower($_SERVER[$header]) == $headerCompareVal) { - $headerOverride = true; - break; - } - } - } - - if($headerOverride) { - $proto = 'https'; - } else if((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) { - $proto = 'https'; - } else if(isset($_SERVER['SSL'])) { - $proto = 'https'; - } - - $parts = array_filter(array( - // What's our host - $_SERVER['HTTP_HOST'], - // SilverStripe base - self::$alternateBaseURL !== null ? self::$alternateBaseURL : BASE_URL, - // And URL including base script (eg: if it's index.php/page/url/) - (defined('BASE_SCRIPT_URL') ? '/' . BASE_SCRIPT_URL : '') . $url, - )); - - // Join together with protocol into our current absolute URL, avoiding duplicated "/" characters - return "$proto://" . preg_replace('#/{2,}#', '/', implode('/', $parts)); + public function getRedirectUrlBase() { + return (!$this->parameterProvided()) ? Director::baseURL() : $this->currentAbsoluteURL(); } - /** - * Forces a reload of the request with the token included - * This method will terminate the script with `die` - */ - public function reloadWithToken() { - $location = $this->currentAbsoluteURL(); + public function getRedirectUrlParams() { + $params = (!$this->parameterProvided()) + ? $this->params() + : array_merge($_GET, $this->params()); - // What's our GET params (ensuring they include the original parameter + a new token) - $params = array_merge($_GET, $this->params()); - unset($params['url']); - - if ($params) $location .= '?'.http_build_query($params); - - // And redirect - if (headers_sent()) { - echo " - - -You are being redirected. If you are not redirected soon, click here to continue the flush -"; + if (isset($params['url'])) { + unset($params['url']); } - else header('location: '.$location, true, 302); - die; + + return $params; } - /** - * Given a list of token names, suppress all tokens that have not been validated, and - * return the non-validated token with the highest priority - * - * @param array $keys List of token keys in ascending priority (low to high) - * @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority - */ - public static function prepare_tokens($keys) { - $target = null; - foreach($keys as $key) { - $token = new ParameterConfirmationToken($key); - // Validate this token - if($token->reloadRequired()) { - $token->suppress(); - $target = $token; - } - } - return $target; + protected function redirectURL() { + $query = http_build_query($this->getRedirectUrlParams()); + return $this->getRedirectUrlBase() . '?' . $query; } } diff --git a/core/startup/URLConfirmationToken.php b/core/startup/URLConfirmationToken.php new file mode 100644 index 000000000..4e7c0b981 --- /dev/null +++ b/core/startup/URLConfirmationToken.php @@ -0,0 +1,104 @@ +urlToCheck = $urlToCheck; + global $url; + // Strip leading/trailing slashes + $this->currentURL = preg_replace(array('/\/+/','/^\//', '/\/$/'), array('/','',''), $url); + $this->tokenParameterName = preg_replace('/[^a-z0-9]/i', '', $urlToCheck) . 'token'; + + // If the token provided is valid, mark it as such + $token = isset($_GET[$this->tokenParameterName]) ? $_GET[$this->tokenParameterName] : null; + if ($this->checkToken($token)) { + $this->token = $token; + } + } + + /** + * @return bool + */ + protected function urlMatches() { + return ($this->currentURL === $this->urlToCheck); + } + + /** + * @return string + */ + public function getURLToCheck() { + return $this->urlToCheck; + } + + public function reloadRequired() { + return $this->urlMatches() && !$this->tokenProvided(); + } + + public function suppress() { + $_SERVER['REQUEST_URI'] = '/'; + $_GET['url'] = $_REQUEST['url'] = '/'; + } + + public function params($includeToken = true) { + $params = array(); + if ($includeToken) { + $params[$this->tokenParameterName] = $this->genToken(); + } + + return $params; + } + + public function currentURL() { + return Director::baseURL() . $this->currentURL; + } + + public function getRedirectUrlBase() { + return (!$this->urlMatches()) ? Director::baseURL() : $this->currentURL(); + } + + public function getRedirectUrlParams() { + $params = (!$this->urlMatches()) + ? $this->params() + : array_merge($_GET, $this->params()); + + if (isset($params['url'])) { + unset($params['url']); + } + + return $params; + } + + protected function redirectURL() { + $query = http_build_query($this->getRedirectUrlParams()); + return $this->getRedirectUrlBase() . '?' . $query; + } +} diff --git a/main.php b/main.php index e9fa011ee..236473472 100644 --- a/main.php +++ b/main.php @@ -121,16 +121,28 @@ if (substr(strtolower($url), 0, strlen(BASE_URL)) == strtolower(BASE_URL)) $url /** * Include SilverStripe's core code */ +require_once('core/startup/ConfirmationTokenChain.php'); require_once('core/startup/ErrorControlChain.php'); require_once('core/startup/ParameterConfirmationToken.php'); +require_once('core/startup/URLConfirmationToken.php'); // Prepare tokens and execute chain -$reloadToken = ParameterConfirmationToken::prepare_tokens(array('isTest', 'isDev', 'flush')); +$confirmationTokenChain = new ConfirmationTokenChain(); +$confirmationTokenChain->pushToken(new URLConfirmationToken('dev/build')); + +foreach (array('isTest', 'isDev', 'flush') as $parameter) { + $confirmationTokenChain->pushToken(new ParameterConfirmationToken($parameter)); +} + $chain = new ErrorControlChain(); $chain - ->then(function($chain) use ($reloadToken) { - // If no redirection is necessary then we can disable error supression - if (!$reloadToken) $chain->setSuppression(false); + ->then(function($chain) use ($confirmationTokenChain) { + if ($confirmationTokenChain->suppressionRequired()) { + $confirmationTokenChain->suppressTokens(); + } else { + // If no redirection is necessary then we can disable error supression + $chain->setSuppression(false); + } // Load in core require_once('core/Core.php'); @@ -141,7 +153,7 @@ $chain if ($databaseConfig) DB::connect($databaseConfig); // Check if a token is requesting a redirect - if (!$reloadToken) return; + if (!$confirmationTokenChain->reloadRequired()) return; // Otherwise, we start up the session if needed if(!isset($_SESSION) && Session::request_contains_session_id()) { @@ -150,19 +162,24 @@ $chain // Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin if (Director::isDev() || !Security::database_is_ready() || Permission::check('ADMIN')) { - return $reloadToken->reloadWithToken(); + return $confirmationTokenChain->reloadWithToken(); } // Fail and redirect the user to the login page - $loginPage = Director::absoluteURL(Config::inst()->get('Security', 'login_url')); - $loginPage .= "?BackURL=" . urlencode($_SERVER['REQUEST_URI']); + $params = array_merge($_GET, $confirmationTokenChain->params(false)); + if (isset($params['url'])) { + unset($params['url']); + } + $backURL = $confirmationTokenChain->getRedirectUrlBase() . '?' . http_build_query($params); + $loginPage = Director::absoluteURL(Security::config()->get('login_url')); + $loginPage .= "?BackURL=" . urlencode($backURL); header('location: '.$loginPage, true, 302); die; }) // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway - ->thenIfErrored(function() use ($reloadToken){ - if ($reloadToken) { - $reloadToken->reloadWithToken(); + ->thenIfErrored(function() use ($confirmationTokenChain){ + if ($confirmationTokenChain->reloadRequired()) { + $confirmationTokenChain->reloadWithToken(); } }) ->execute(); diff --git a/tests/core/startup/ConfirmationTokenChainTest.php b/tests/core/startup/ConfirmationTokenChainTest.php new file mode 100644 index 000000000..fad9ea2c4 --- /dev/null +++ b/tests/core/startup/ConfirmationTokenChainTest.php @@ -0,0 +1,141 @@ +getMockBuilder('ParameterConfirmationToken') + ->disableOriginalConstructor() + ->setMethods($methods) + ->getMock(); + + $mock->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue($requiresReload)); + + return $mock; + } + + public function testFilteredTokens() { + $chain = new ConfirmationTokenChain(); + $chain->pushToken($tokenRequiringReload = $this->getTokenRequiringReload()); + $chain->pushToken($tokenNotRequiringReload = $this->getTokenRequiringReload(false)); + + $reflectionMethod = new ReflectionMethod('ConfirmationTokenChain', 'filteredTokens'); + $reflectionMethod->setAccessible(true); + $tokens = $reflectionMethod->invoke($chain); + + $this->assertContains($tokenRequiringReload, $tokens, 'Token requiring a reload was not returned'); + $this->assertNotContains($tokenNotRequiringReload, $tokens, 'Token not requiring a reload was returned'); + } + + public function testSuppressionRequired() { + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReload(false)); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReload()); + $this->assertTrue($chain->suppressionRequired(), 'Suppression not marked as required'); + } + + public function testSuppressTokens() { + $mockToken = $this->getTokenRequiringReload(true, array('suppress')); + $mockToken->expects($this->once()) + ->method('suppress'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->suppressTokens(); + } + + public function testReloadRequired() { + $mockToken = $this->getTokenRequiringReload(true); + $secondMockToken = $this->getTokenRequiringReload(false); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertTrue($chain->reloadRequired()); + } + + public function testParams() { + $mockToken = $this->getTokenRequiringReload(true, array('params')); + $mockToken->expects($this->once()) + ->method('params') + ->with($this->isTrue()) + ->will($this->returnValue(array('mockTokenParam' => '1'))); + $secondMockToken = $this->getTokenRequiringReload(true, array('params')); + $secondMockToken->expects($this->once()) + ->method('params') + ->with($this->isTrue()) + ->will($this->returnValue(array('secondMockTokenParam' => '2'))); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertEquals(array('mockTokenParam' => '1', 'secondMockTokenParam' => '2'), $chain->params(true)); + + $mockToken = $this->getTokenRequiringReload(true, array('params')); + $mockToken->expects($this->once()) + ->method('params') + ->with($this->isFalse()) + ->will($this->returnValue(array('mockTokenParam' => '1'))); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $this->assertEquals(array('mockTokenParam' => '1'), $chain->params(false)); + } + + public function testGetRedirectUrlBase() { + $mockUrlToken = $this->getMockBuilder('URLConfirmationToken') + ->disableOriginalConstructor() + ->setMethods(array('reloadRequired', 'getRedirectUrlBase')) + ->getMock(); + $mockUrlToken->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(true)); + $mockUrlToken->expects($this->any()) + ->method('getRedirectUrlBase') + ->will($this->returnValue('url-base')); + + $mockParameterToken = $this->getMockBuilder('ParameterConfirmationToken') + ->disableOriginalConstructor() + ->setMethods(array('reloadRequired', 'getRedirectUrlBase')) + ->getMock(); + $mockParameterToken->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(true)); + $mockParameterToken->expects($this->any()) + ->method('getRedirectUrlBase') + ->will($this->returnValue('parameter-base')); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockParameterToken); + $chain->pushToken($mockUrlToken); + $this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority'); + + // Push them in reverse order to check priority still correct + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockUrlToken); + $chain->pushToken($mockParameterToken); + $this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority'); + } + + public function testGetRedirectUrlParams() { + $mockToken = $this->getTokenRequiringReload(true, array('getRedirectUrlParams')); + $mockToken->expects($this->once()) + ->method('getRedirectUrlParams') + ->will($this->returnValue(array('mockTokenParam' => '1'))); + + $secondMockToken = $this->getTokenRequiringReload(true, array('getRedirectUrlParams')); + $secondMockToken->expects($this->once()) + ->method('getRedirectUrlParams') + ->will($this->returnValue(array('secondMockTokenParam' => '2'))); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertEquals(array('mockTokenParam' => '1', 'secondMockTokenParam' => '2'), $chain->getRedirectUrlParams()); + } +} diff --git a/tests/core/startup/URLConfirmationTokenTest.php b/tests/core/startup/URLConfirmationTokenTest.php new file mode 100644 index 000000000..90fea2063 --- /dev/null +++ b/tests/core/startup/URLConfirmationTokenTest.php @@ -0,0 +1,78 @@ +originalURL = $url; + $this->originalGetVars = $_GET; + } + + public function tearDown() { + parent::tearDown(); + global $url; + $url = $this->originalURL; + $_GET = $this->originalGetVars; + } + + public function testValidToken() { + global $url; + $url = Controller::join_links(BASE_URL, '/', 'token/test/url'); + $_GET = array('tokentesturltoken' => 'value', 'url' => $url); + + $validToken = new URLConfirmationTokenTest_StubValidToken('token/test/url'); + $this->assertTrue($validToken->urlMatches()); + $this->assertTrue($validToken->tokenProvided()); // Actually forced to true for this test + $this->assertFalse($validToken->reloadRequired()); + $this->assertStringStartsWith(Controller::join_links(BASE_URL, '/', 'token/test/url'), $validToken->redirectURL()); + } + + public function testTokenWithTrailingSlashInUrl() { + global $url; + $url = Controller::join_links(BASE_URL, '/', 'trailing/slash/url/'); + $_GET = array('url' => $url); + + $trailingSlash = new URLConfirmationTokenTest_StubToken('trailing/slash/url'); + $this->assertTrue($trailingSlash->urlMatches()); + $this->assertFalse($trailingSlash->tokenProvided()); + $this->assertTrue($trailingSlash->reloadRequired()); + $this->assertContains('trailing/slash/url', $trailingSlash->redirectURL()); + $this->assertContains('trailingslashurltoken', $trailingSlash->redirectURL()); + } + + public function testUrlSuppressionWhenTokenMissing() + { + global $url; + $url = Controller::join_links(BASE_URL, '/', 'test/url/'); + $_GET = array('url' => $url); + + $token = new URLConfirmationTokenTest_StubToken('test/url'); + $token->suppress(); + $this->assertEquals('/', $_GET['url']); + } +}