mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch '3.6' into 3.7
This commit is contained in:
commit
a7d511e739
218
core/startup/AbstractConfirmationToken.php
Normal file
218
core/startup/AbstractConfirmationToken.php
Normal file
@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Shared functionality for token-based authentication of potentially dangerous URLs or query
|
||||
* string parameters
|
||||
*
|
||||
* @internal This class is designed specifically for use pre-startup and may change without warning
|
||||
*/
|
||||
abstract class AbstractConfirmationToken {
|
||||
|
||||
/**
|
||||
* The validated and checked token for this parameter
|
||||
*
|
||||
* @var string|null A string value, or null if either not provided or invalid
|
||||
*/
|
||||
protected $token = null;
|
||||
|
||||
/**
|
||||
* What to use instead of BASE_URL. Must not contain protocol or host.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $alternateBaseURL = null;
|
||||
|
||||
/**
|
||||
* 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 static 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 static($key);
|
||||
// Validate this token
|
||||
if ($token->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 "
|
||||
<script>location.href='{$locationJS}';</script>
|
||||
<noscript><meta http-equiv='refresh' content='0; url={$locationATT}'></noscript>
|
||||
You are being redirected. If you are not redirected soon, <a href='{$locationATT}'>click here to continue</a>
|
||||
";
|
||||
} 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();
|
||||
}
|
163
core/startup/ConfirmationTokenChain.php
Normal file
163
core/startup/ConfirmationTokenChain.php
Normal file
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
require_once(dirname(dirname(dirname(__FILE__))).'/view/TemplateGlobalProvider.php');
|
||||
require_once(dirname(dirname(dirname(__FILE__))).'/control/Director.php');
|
||||
|
||||
/**
|
||||
* A chain of confirmation tokens to be validated on each request. This allows the application to
|
||||
* check multiple tokens at once without having to potentially redirect the user for each of them
|
||||
*
|
||||
* @internal This class is designed specifically for use pre-startup and may change without warning
|
||||
*/
|
||||
class ConfirmationTokenChain
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $tokens = array();
|
||||
|
||||
/**
|
||||
* @param AbstractConfirmationToken $token
|
||||
*/
|
||||
public function pushToken(AbstractConfirmationToken $token) {
|
||||
$this->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 "
|
||||
<script>location.href='{$locationJS}';</script>
|
||||
<noscript><meta http-equiv='refresh' content='0; url={$locationATT}'></noscript>
|
||||
You are being redirected. If you are not redirected soon, <a href='{$locationATT}'>click here to continue</a>
|
||||
";
|
||||
} else {
|
||||
header("location: {$location}", true, 302);
|
||||
}
|
||||
|
||||
die;
|
||||
}
|
||||
}
|
@ -1,19 +1,17 @@
|
||||
<?php
|
||||
|
||||
require_once(dirname(__FILE__).'/AbstractConfirmationToken.php');
|
||||
require_once(dirname(dirname(dirname(__FILE__))).'/view/TemplateGlobalProvider.php');
|
||||
require_once(dirname(dirname(dirname(__FILE__))).'/control/Director.php');
|
||||
|
||||
/**
|
||||
* Class ParameterConfirmationToken
|
||||
* This is used to protect dangerous GET parameters that need to be detected early in the request
|
||||
* lifecycle by generating a one-time-use token & redirecting with that token included in the
|
||||
* redirected URL
|
||||
*
|
||||
* When you need to use a dangerous GET parameter that needs to be set before core/Core.php is
|
||||
* established, this class takes care of allowing some other code of confirming the parameter,
|
||||
* by generating a one-time-use token & redirecting with that token included in the redirected URL
|
||||
*
|
||||
* WARNING: This class is experimental and designed specifically for use pre-startup in main.php
|
||||
* It will likely be heavily refactored before the release of 3.2
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage misc
|
||||
* @internal This class is designed specifically for use pre-startup and may change without warning
|
||||
*/
|
||||
class ParameterConfirmationToken {
|
||||
class ParameterConfirmationToken extends AbstractConfirmationToken {
|
||||
|
||||
/**
|
||||
* The name of the parameter
|
||||
@ -29,56 +27,6 @@ class ParameterConfirmationToken {
|
||||
*/
|
||||
protected $parameter = null;
|
||||
|
||||
/**
|
||||
* The validated and checked token for this parameter
|
||||
*
|
||||
* @var string|null A string value, or null if either not provided or invalid
|
||||
*/
|
||||
protected $token = null;
|
||||
|
||||
protected function pathForToken($token) {
|
||||
return TEMP_FOLDER.'/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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 "
|
||||
<script>location.href='$location';</script>
|
||||
<noscript><meta http-equiv='refresh' content='0; url=$location'></noscript>
|
||||
You are being redirected. If you are not redirected soon, <a href='$location'>click here to continue the flush</a>
|
||||
";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
104
core/startup/URLConfirmationToken.php
Normal file
104
core/startup/URLConfirmationToken.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
require_once(dirname(__FILE__).'/AbstractConfirmationToken.php');
|
||||
require_once(dirname(dirname(dirname(__FILE__))).'/control/Director.php');
|
||||
require_once(dirname(dirname(dirname(__FILE__))).'/view/TemplateGlobalProvider.php');
|
||||
|
||||
/**
|
||||
* This is used to protect dangerous URLs that need to be detected early in the request lifecycle
|
||||
* by generating a one-time-use token & redirecting with that token included in the redirected URL
|
||||
*
|
||||
* @internal This class is designed specifically for use pre-startup and may change without warning
|
||||
*/
|
||||
class URLConfirmationToken extends AbstractConfirmationToken {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $urlToCheck;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $currentURL;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $tokenParameterName;
|
||||
|
||||
/**
|
||||
* @param string $urlToCheck URL to check
|
||||
*/
|
||||
public function __construct($urlToCheck)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
39
main.php
39
main.php
@ -124,16 +124,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');
|
||||
@ -144,7 +156,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()) {
|
||||
@ -153,19 +165,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();
|
||||
|
141
tests/core/startup/ConfirmationTokenChainTest.php
Normal file
141
tests/core/startup/ConfirmationTokenChainTest.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
class ConfirmationTokenChainTest extends SapphireTest {
|
||||
|
||||
protected function getTokenRequiringReload($requiresReload = true, $extraMethods = array()) {
|
||||
$methods = array_merge(array('reloadRequired'), $extraMethods);
|
||||
$mock = $this->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());
|
||||
}
|
||||
}
|
78
tests/core/startup/URLConfirmationTokenTest.php
Normal file
78
tests/core/startup/URLConfirmationTokenTest.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
class URLConfirmationTokenTest_StubToken extends URLConfirmationToken implements TestOnly {
|
||||
public function urlMatches() {
|
||||
return parent::urlMatches();
|
||||
}
|
||||
|
||||
public function currentURL() {
|
||||
return parent::currentURL();
|
||||
}
|
||||
|
||||
public function redirectURL() {
|
||||
return parent::redirectURL();
|
||||
}
|
||||
}
|
||||
|
||||
class URLConfirmationTokenTest_StubValidToken extends URLConfirmationTokenTest_StubToken {
|
||||
protected function checkToken($token) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class URLConfirmationTokenTest extends SapphireTest {
|
||||
|
||||
protected $originalURL;
|
||||
|
||||
protected $originalGetVars;
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
global $url;
|
||||
$this->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']);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user