mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Implement ConfirmationTokenChain to handle multiple tokens at once
This commit is contained in:
parent
214e28127f
commit
5563537cc8
@ -15,7 +15,7 @@ use SilverStripe\Security\RandomGenerator;
|
|||||||
*
|
*
|
||||||
* @internal This class is designed specifically for use pre-startup and may change without warning
|
* @internal This class is designed specifically for use pre-startup and may change without warning
|
||||||
*/
|
*/
|
||||||
abstract class ConfirmationToken
|
abstract class AbstractConfirmationToken
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var HTTPRequest
|
* @var HTTPRequest
|
||||||
@ -173,6 +173,16 @@ HTML;
|
|||||||
*/
|
*/
|
||||||
abstract public function params($includeToken = true);
|
abstract public function params($includeToken = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
abstract public function getRedirectUrlBase();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
abstract public function getRedirectUrlParams();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get redirection URL
|
* Get redirection URL
|
||||||
*
|
*
|
178
src/Core/Startup/ConfirmationTokenChain.php
Normal file
178
src/Core/Startup/ConfirmationTokenChain.php
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Startup;
|
||||||
|
|
||||||
|
use SilverStripe\Control\Controller;
|
||||||
|
use SilverStripe\Control\Director;
|
||||||
|
use SilverStripe\Control\HTTPResponse;
|
||||||
|
use SilverStripe\Core\Convert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param AbstractConfirmationToken $token
|
||||||
|
*/
|
||||||
|
public function pushToken(AbstractConfirmationToken $token)
|
||||||
|
{
|
||||||
|
$this->tokens[] = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect all tokens that require a redirect
|
||||||
|
*
|
||||||
|
* @return \Generator
|
||||||
|
*/
|
||||||
|
protected function filteredTokens()
|
||||||
|
{
|
||||||
|
foreach ($this->tokens as $token) {
|
||||||
|
if ($token->reloadRequired() || $token->reloadRequiredIfError()) {
|
||||||
|
yield $token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function suppressionRequired()
|
||||||
|
{
|
||||||
|
foreach ($this->tokens as $token) {
|
||||||
|
if ($token->reloadRequired()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = [];
|
||||||
|
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 = iterator_to_array($this->filteredTokens(), false);
|
||||||
|
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 = [];
|
||||||
|
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 Controller::join_links($this->getRedirectUrlBase(), '?' . $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HTTPResponse
|
||||||
|
*/
|
||||||
|
public function reloadWithTokens()
|
||||||
|
{
|
||||||
|
$location = $this->redirectURL();
|
||||||
|
$locationJS = Convert::raw2js($location);
|
||||||
|
$locationATT = Convert::raw2att($location);
|
||||||
|
$body = <<<HTML
|
||||||
|
<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>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
$result = new HTTPResponse($body);
|
||||||
|
$result->redirect($location);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
@ -34,20 +34,18 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param HTTPRequest $request
|
* @param HTTPRequest $request
|
||||||
* @return ConfirmationToken|null
|
* @return ConfirmationTokenChain
|
||||||
*/
|
*/
|
||||||
protected function prepareConfirmationTokenIfRequired(HTTPRequest $request)
|
protected function prepareConfirmationTokenChain(HTTPRequest $request)
|
||||||
{
|
{
|
||||||
$token = URLConfirmationToken::prepare_tokens(['dev/build'], $request);
|
$chain = new ConfirmationTokenChain();
|
||||||
|
$chain->pushToken(new URLConfirmationToken('dev/build', $request));
|
||||||
|
|
||||||
if (!$token) {
|
foreach (['isTest', 'isDev', 'flush'] as $parameter) {
|
||||||
$token = ParameterConfirmationToken::prepare_tokens(
|
$chain->pushToken(new ParameterConfirmationToken($parameter, $request));
|
||||||
['isTest', 'isDev', 'flush'],
|
|
||||||
$request
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $token;
|
return $chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function process(HTTPRequest $request, callable $next)
|
public function process(HTTPRequest $request, callable $next)
|
||||||
@ -55,19 +53,21 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
|
|||||||
$result = null;
|
$result = null;
|
||||||
|
|
||||||
// Prepare tokens and execute chain
|
// Prepare tokens and execute chain
|
||||||
$confirmationToken = $this->prepareConfirmationTokenIfRequired($request);
|
$confirmationTokenChain = $this->prepareConfirmationTokenChain($request);
|
||||||
$chain = new ErrorControlChain();
|
$errorControlChain = new ErrorControlChain();
|
||||||
$chain
|
$errorControlChain
|
||||||
->then(function () use ($request, $chain, $confirmationToken, $next, &$result) {
|
->then(function () use ($request, $errorControlChain, $confirmationTokenChain, $next, &$result) {
|
||||||
// If no redirection is necessary then we can disable error supression
|
if ($confirmationTokenChain->suppressionRequired()) {
|
||||||
if (!$confirmationToken) {
|
$confirmationTokenChain->suppressTokens();
|
||||||
$chain->setSuppression(false);
|
} else {
|
||||||
|
// If no redirection is necessary then we can disable error supression
|
||||||
|
$errorControlChain->setSuppression(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if a token is requesting a redirect
|
// Check if a token is requesting a redirect
|
||||||
if ($confirmationToken && $confirmationToken->reloadRequired()) {
|
if ($confirmationTokenChain && $confirmationTokenChain->reloadRequired()) {
|
||||||
$result = $this->safeReloadWithToken($request, $confirmationToken);
|
$result = $this->safeReloadWithTokens($request, $confirmationTokenChain);
|
||||||
} else {
|
} else {
|
||||||
// If no reload necessary, process application
|
// If no reload necessary, process application
|
||||||
$result = call_user_func($next, $request);
|
$result = call_user_func($next, $request);
|
||||||
@ -77,10 +77,16 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway
|
// Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway
|
||||||
->thenIfErrored(function () use ($confirmationToken) {
|
->thenIfErrored(function () use ($confirmationTokenChain) {
|
||||||
if ($confirmationToken && $confirmationToken->reloadRequiredIfError()) {
|
if ($confirmationTokenChain && $confirmationTokenChain->reloadRequiredIfError()) {
|
||||||
$result = $confirmationToken->reloadWithToken();
|
try {
|
||||||
$result->output();
|
// Reload requires manual boot
|
||||||
|
$this->getApplication()->getKernel()->boot(false);
|
||||||
|
} finally {
|
||||||
|
// Given we're in an error state here, try to continue even if the kernel boot fails
|
||||||
|
$result = $confirmationTokenChain->reloadWithTokens();
|
||||||
|
$result->output();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
->execute();
|
->execute();
|
||||||
@ -92,10 +98,10 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
|
|||||||
* or authentication is impossible.
|
* or authentication is impossible.
|
||||||
*
|
*
|
||||||
* @param HTTPRequest $request
|
* @param HTTPRequest $request
|
||||||
* @param ParameterConfirmationToken $reloadToken
|
* @param ConfirmationTokenChain $confirmationTokenChain
|
||||||
* @return HTTPResponse
|
* @return HTTPResponse
|
||||||
*/
|
*/
|
||||||
protected function safeReloadWithToken(HTTPRequest $request, $reloadToken)
|
protected function safeReloadWithTokens(HTTPRequest $request, ConfirmationTokenChain $confirmationTokenChain)
|
||||||
{
|
{
|
||||||
// Safe reload requires manual boot
|
// Safe reload requires manual boot
|
||||||
$this->getApplication()->getKernel()->boot(false);
|
$this->getApplication()->getKernel()->boot(false);
|
||||||
@ -104,9 +110,9 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
|
|||||||
$request->getSession()->init($request);
|
$request->getSession()->init($request);
|
||||||
|
|
||||||
// Request with ErrorDirector
|
// Request with ErrorDirector
|
||||||
$result = ErrorDirector::singleton()->handleRequestWithToken(
|
$result = ErrorDirector::singleton()->handleRequestWithTokenChain(
|
||||||
$request,
|
$request,
|
||||||
$reloadToken,
|
$confirmationTokenChain,
|
||||||
$this->getApplication()->getKernel()
|
$this->getApplication()->getKernel()
|
||||||
);
|
);
|
||||||
if ($result) {
|
if ($result) {
|
||||||
@ -114,8 +120,8 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fail and redirect the user to the login page
|
// Fail and redirect the user to the login page
|
||||||
$params = array_merge($request->getVars(), $reloadToken->params(false));
|
$params = array_merge($request->getVars(), $confirmationTokenChain->params(false));
|
||||||
$backURL = $reloadToken->currentURL() . '?' . http_build_query($params);
|
$backURL = $confirmationTokenChain->getRedirectUrlBase() . '?' . http_build_query($params);
|
||||||
$loginPage = Director::absoluteURL(Security::config()->get('login_url'));
|
$loginPage = Director::absoluteURL(Security::config()->get('login_url'));
|
||||||
$loginPage .= "?BackURL=" . urlencode($backURL);
|
$loginPage .= "?BackURL=" . urlencode($backURL);
|
||||||
$result = new HTTPResponse();
|
$result = new HTTPResponse();
|
||||||
|
@ -21,18 +21,21 @@ class ErrorDirector extends Director
|
|||||||
* Redirect with token if allowed, or null if not allowed
|
* Redirect with token if allowed, or null if not allowed
|
||||||
*
|
*
|
||||||
* @param HTTPRequest $request
|
* @param HTTPRequest $request
|
||||||
* @param ConfirmationToken $token
|
* @param ConfirmationTokenChain $confirmationTokenChain
|
||||||
* @param Kernel $kernel
|
* @param Kernel $kernel
|
||||||
* @return null|HTTPResponse Redirection response, or null if not able to redirect
|
* @return null|HTTPResponse Redirection response, or null if not able to redirect
|
||||||
*/
|
*/
|
||||||
public function handleRequestWithToken(HTTPRequest $request, ConfirmationToken $token, Kernel $kernel)
|
public function handleRequestWithTokenChain(
|
||||||
{
|
HTTPRequest $request,
|
||||||
|
ConfirmationTokenChain $confirmationTokenChain,
|
||||||
|
Kernel $kernel
|
||||||
|
) {
|
||||||
Injector::inst()->registerService($request, HTTPRequest::class);
|
Injector::inst()->registerService($request, HTTPRequest::class);
|
||||||
|
|
||||||
// Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin
|
// Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin
|
||||||
$reload = function (HTTPRequest $request) use ($token, $kernel) {
|
$reload = function (HTTPRequest $request) use ($confirmationTokenChain, $kernel) {
|
||||||
if ($kernel->getEnvironment() === Kernel::DEV || !Security::database_is_ready() || Permission::check('ADMIN')) {
|
if ($kernel->getEnvironment() === Kernel::DEV || !Security::database_is_ready() || Permission::check('ADMIN')) {
|
||||||
return $token->reloadWithToken();
|
return $confirmationTokenChain->reloadWithTokens();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
namespace SilverStripe\Core\Startup;
|
namespace SilverStripe\Core\Startup;
|
||||||
|
|
||||||
use SilverStripe\Control\Controller;
|
use SilverStripe\Control\Controller;
|
||||||
|
use SilverStripe\Control\Director;
|
||||||
use SilverStripe\Control\HTTPRequest;
|
use SilverStripe\Control\HTTPRequest;
|
||||||
use SilverStripe\Control\HTTPResponse;
|
use SilverStripe\Control\HTTPResponse;
|
||||||
use SilverStripe\Core\Convert;
|
use SilverStripe\Core\Convert;
|
||||||
@ -15,7 +16,7 @@ use SilverStripe\Security\RandomGenerator;
|
|||||||
*
|
*
|
||||||
* @internal This class is designed specifically for use pre-startup and may change without warning
|
* @internal This class is designed specifically for use pre-startup and may change without warning
|
||||||
*/
|
*/
|
||||||
class ParameterConfirmationToken extends ConfirmationToken
|
class ParameterConfirmationToken extends AbstractConfirmationToken
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The name of the parameter
|
* The name of the parameter
|
||||||
@ -141,18 +142,21 @@ class ParameterConfirmationToken extends ConfirmationToken
|
|||||||
return $params;
|
return $params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRedirectUrlBase()
|
||||||
|
{
|
||||||
|
return ($this->existsInReferer() && !$this->parameterProvided()) ? Director::baseURL() : $this->currentURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRedirectUrlParams()
|
||||||
|
{
|
||||||
|
return ($this->existsInReferer() && !$this->parameterProvided())
|
||||||
|
? $this->params()
|
||||||
|
: array_merge($this->request->getVars(), $this->params());
|
||||||
|
}
|
||||||
|
|
||||||
protected function redirectURL()
|
protected function redirectURL()
|
||||||
{
|
{
|
||||||
// If url is encoded via BackURL, defer to home page (prevent redirect to form action)
|
$query = http_build_query($this->getRedirectUrlParams());
|
||||||
if ($this->existsInReferer() && !$this->parameterProvided()) {
|
return Controller::join_links($this->getRedirectUrlBase(), '?' . $query);
|
||||||
$url = BASE_URL ?: '/';
|
|
||||||
$params = $this->params();
|
|
||||||
} else {
|
|
||||||
$url = $this->currentURL();
|
|
||||||
$params = array_merge($this->request->getVars(), $this->params());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge get params with current url
|
|
||||||
return Controller::join_links($url, '?' . http_build_query($params));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ use SilverStripe\Control\HTTPRequest;
|
|||||||
*
|
*
|
||||||
* @internal This class is designed specifically for use pre-startup and may change without warning
|
* @internal This class is designed specifically for use pre-startup and may change without warning
|
||||||
*/
|
*/
|
||||||
class URLConfirmationToken extends ConfirmationToken
|
class URLConfirmationToken extends AbstractConfirmationToken
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
@ -60,7 +60,7 @@ class URLConfirmationToken extends ConfirmationToken
|
|||||||
*/
|
*/
|
||||||
protected function getURLExistsInBackURL(HTTPRequest $request)
|
protected function getURLExistsInBackURL(HTTPRequest $request)
|
||||||
{
|
{
|
||||||
$backURL = $request->getVar('BackURL');
|
$backURL = ltrim($request->getVar('BackURL'), '/');
|
||||||
return (strpos($backURL, $this->urlToCheck) === 0);
|
return (strpos($backURL, $this->urlToCheck) === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,18 +119,21 @@ class URLConfirmationToken extends ConfirmationToken
|
|||||||
return Controller::join_links(Director::baseURL(), $this->currentURL);
|
return Controller::join_links(Director::baseURL(), $this->currentURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRedirectUrlBase()
|
||||||
|
{
|
||||||
|
return ($this->urlExistsInBackURL && !$this->urlMatches()) ? Director::baseURL() : $this->currentURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRedirectUrlParams()
|
||||||
|
{
|
||||||
|
return ($this->urlExistsInBackURL && !$this->urlMatches())
|
||||||
|
? $this->params()
|
||||||
|
: array_merge($this->request->getVars(), $this->params());
|
||||||
|
}
|
||||||
|
|
||||||
protected function redirectURL()
|
protected function redirectURL()
|
||||||
{
|
{
|
||||||
// If url is encoded via BackURL, defer to home page (prevent redirect to form action)
|
$query = http_build_query($this->getRedirectUrlParams());
|
||||||
if ($this->urlExistsInBackURL && !$this->urlMatches()) {
|
return Controller::join_links($this->getRedirectUrlBase(), '?' . $query);
|
||||||
$url = BASE_URL ?: '/';
|
|
||||||
$params = $this->params();
|
|
||||||
} else {
|
|
||||||
$url = $this->currentURL();
|
|
||||||
$params = array_merge($this->request->getVars(), $this->params());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge get params with current url
|
|
||||||
return Controller::join_links($url, '?' . http_build_query($params));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
185
tests/php/Core/Startup/ConfirmationTokenChainTest.php
Normal file
185
tests/php/Core/Startup/ConfirmationTokenChainTest.php
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Startup;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Startup\ConfirmationTokenChain;
|
||||||
|
use SilverStripe\Core\Startup\ParameterConfirmationToken;
|
||||||
|
use SilverStripe\Core\Startup\URLConfirmationToken;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
|
||||||
|
class ConfirmationTokenChainTest extends SapphireTest
|
||||||
|
{
|
||||||
|
protected function getTokenRequiringReload($requiresReload = true, $extraMethods = [])
|
||||||
|
{
|
||||||
|
$methods = array_merge(['reloadRequired'], $extraMethods);
|
||||||
|
$mock = $this->createPartialMock(ParameterConfirmationToken::class, $methods);
|
||||||
|
$mock->expects($this->any())
|
||||||
|
->method('reloadRequired')
|
||||||
|
->will($this->returnValue($requiresReload));
|
||||||
|
return $mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTokenRequiringReloadIfError($requiresReload = true, $extraMethods = [])
|
||||||
|
{
|
||||||
|
$methods = array_merge(['reloadRequired', 'reloadRequiredIfError'], $extraMethods);
|
||||||
|
$mock = $this->createPartialMock(ParameterConfirmationToken::class, $methods);
|
||||||
|
$mock->expects($this->any())
|
||||||
|
->method('reloadRequired')
|
||||||
|
->will($this->returnValue(false));
|
||||||
|
$mock->expects($this->any())
|
||||||
|
->method('reloadRequiredIfError')
|
||||||
|
->will($this->returnValue($requiresReload));
|
||||||
|
return $mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFilteredTokens()
|
||||||
|
{
|
||||||
|
$chain = new ConfirmationTokenChain();
|
||||||
|
$chain->pushToken($tokenRequiringReload = $this->getTokenRequiringReload());
|
||||||
|
$chain->pushToken($tokenNotRequiringReload = $this->getTokenRequiringReload(false));
|
||||||
|
$chain->pushToken($tokenRequiringReloadIfError = $this->getTokenRequiringReloadIfError());
|
||||||
|
$chain->pushToken($tokenNotRequiringReloadIfError = $this->getTokenRequiringReloadIfError(false));
|
||||||
|
|
||||||
|
$reflectionMethod = new \ReflectionMethod(ConfirmationTokenChain::class, 'filteredTokens');
|
||||||
|
$reflectionMethod->setAccessible(true);
|
||||||
|
$tokens = iterator_to_array($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');
|
||||||
|
$this->assertContains($tokenRequiringReloadIfError, $tokens, 'Token requiring a reload on error was not returned');
|
||||||
|
$this->assertNotContains($tokenNotRequiringReloadIfError, $tokens, 'Token not requiring a reload on error 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->getTokenRequiringReloadIfError(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');
|
||||||
|
|
||||||
|
$chain = new ConfirmationTokenChain();
|
||||||
|
$chain->pushToken($this->getTokenRequiringReloadIfError());
|
||||||
|
$this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSuppressTokens()
|
||||||
|
{
|
||||||
|
$mockToken = $this->getTokenRequiringReload(true, ['suppress']);
|
||||||
|
$mockToken->expects($this->once())
|
||||||
|
->method('suppress');
|
||||||
|
$secondMockToken = $this->getTokenRequiringReloadIfError(true, ['suppress']);
|
||||||
|
$secondMockToken->expects($this->once())
|
||||||
|
->method('suppress');
|
||||||
|
|
||||||
|
$chain = new ConfirmationTokenChain();
|
||||||
|
$chain->pushToken($mockToken);
|
||||||
|
$chain->pushToken($secondMockToken);
|
||||||
|
$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 testReloadRequiredIfError()
|
||||||
|
{
|
||||||
|
$mockToken = $this->getTokenRequiringReloadIfError(true);
|
||||||
|
$secondMockToken = $this->getTokenRequiringReloadIfError(false);
|
||||||
|
|
||||||
|
$chain = new ConfirmationTokenChain();
|
||||||
|
$chain->pushToken($mockToken);
|
||||||
|
$chain->pushToken($secondMockToken);
|
||||||
|
$this->assertTrue($chain->reloadRequiredIfError());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParams()
|
||||||
|
{
|
||||||
|
$mockToken = $this->getTokenRequiringReload(true, ['params']);
|
||||||
|
$mockToken->expects($this->once())
|
||||||
|
->method('params')
|
||||||
|
->with($this->isTrue())
|
||||||
|
->will($this->returnValue(['mockTokenParam' => '1']));
|
||||||
|
$secondMockToken = $this->getTokenRequiringReload(true, ['params']);
|
||||||
|
$secondMockToken->expects($this->once())
|
||||||
|
->method('params')
|
||||||
|
->with($this->isTrue())
|
||||||
|
->will($this->returnValue(['secondMockTokenParam' => '2']));
|
||||||
|
|
||||||
|
$chain = new ConfirmationTokenChain();
|
||||||
|
$chain->pushToken($mockToken);
|
||||||
|
$chain->pushToken($secondMockToken);
|
||||||
|
$this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->params(true));
|
||||||
|
|
||||||
|
$mockToken = $this->getTokenRequiringReload(true, ['params']);
|
||||||
|
$mockToken->expects($this->once())
|
||||||
|
->method('params')
|
||||||
|
->with($this->isFalse())
|
||||||
|
->will($this->returnValue(['mockTokenParam' => '1']));
|
||||||
|
|
||||||
|
$chain = new ConfirmationTokenChain();
|
||||||
|
$chain->pushToken($mockToken);
|
||||||
|
$this->assertEquals(['mockTokenParam' => '1'], $chain->params(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetRedirectUrlBase()
|
||||||
|
{
|
||||||
|
$mockUrlToken = $this->createPartialMock(URLConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']);
|
||||||
|
$mockUrlToken->expects($this->any())
|
||||||
|
->method('reloadRequired')
|
||||||
|
->will($this->returnValue(true));
|
||||||
|
$mockUrlToken->expects($this->any())
|
||||||
|
->method('getRedirectUrlBase')
|
||||||
|
->will($this->returnValue('url-base'));
|
||||||
|
|
||||||
|
$mockParameterToken = $this->createPartialMock(ParameterConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']);
|
||||||
|
$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, ['getRedirectUrlParams']);
|
||||||
|
$mockToken->expects($this->once())
|
||||||
|
->method('getRedirectUrlParams')
|
||||||
|
->will($this->returnValue(['mockTokenParam' => '1']));
|
||||||
|
|
||||||
|
$secondMockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']);
|
||||||
|
$secondMockToken->expects($this->once())
|
||||||
|
->method('getRedirectUrlParams')
|
||||||
|
->will($this->returnValue(['secondMockTokenParam' => '2']));
|
||||||
|
|
||||||
|
$chain = new ConfirmationTokenChain();
|
||||||
|
$chain->pushToken($mockToken);
|
||||||
|
$chain->pushToken($secondMockToken);
|
||||||
|
$this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->getRedirectUrlParams());
|
||||||
|
}
|
||||||
|
}
|
@ -121,4 +121,56 @@ class ErrorControlChainMiddlewareTest extends SapphireTest
|
|||||||
$this->assertNotContains('?devbuildtoken=', $location);
|
$this->assertNotContains('?devbuildtoken=', $location);
|
||||||
$this->assertContains('Security/login', $location);
|
$this->assertContains('Security/login', $location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testLiveBuildAndFlushAdmin()
|
||||||
|
{
|
||||||
|
// Mock admin
|
||||||
|
$adminID = $this->logInWithPermission('ADMIN');
|
||||||
|
$this->logOut();
|
||||||
|
|
||||||
|
// Mock app
|
||||||
|
$app = new HTTPApplication(new BlankKernel(BASE_PATH));
|
||||||
|
$app->getKernel()->setEnvironment(Kernel::LIVE);
|
||||||
|
|
||||||
|
// Test being logged in as admin
|
||||||
|
$chain = new ErrorControlChainMiddleware($app);
|
||||||
|
$request = new HTTPRequest('GET', '/dev/build/', ['flush' => '1']);
|
||||||
|
$request->setSession(new Session(['loggedInAs' => $adminID]));
|
||||||
|
$result = $chain->process($request, function () {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->assertInstanceOf(HTTPResponse::class, $result);
|
||||||
|
$location = $result->getHeader('Location');
|
||||||
|
$this->assertContains('/dev/build', $location);
|
||||||
|
$this->assertContains('flush=1', $location);
|
||||||
|
$this->assertContains('devbuildtoken=', $location);
|
||||||
|
$this->assertContains('flushtoken=', $location);
|
||||||
|
$this->assertNotContains('Security/login', $location);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLiveBuildAndFlushUnauthenticated()
|
||||||
|
{
|
||||||
|
// Mock app
|
||||||
|
$app = new HTTPApplication(new BlankKernel(BASE_PATH));
|
||||||
|
$app->getKernel()->setEnvironment(Kernel::LIVE);
|
||||||
|
|
||||||
|
// Test being logged in as no one
|
||||||
|
Security::setCurrentUser(null);
|
||||||
|
$chain = new ErrorControlChainMiddleware($app);
|
||||||
|
$request = new HTTPRequest('GET', '/dev/build', ['flush' => '1']);
|
||||||
|
$request->setSession(new Session(['loggedInAs' => 0]));
|
||||||
|
$result = $chain->process($request, function () {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should be directed to login, not to flush
|
||||||
|
$this->assertInstanceOf(HTTPResponse::class, $result);
|
||||||
|
$location = $result->getHeader('Location');
|
||||||
|
$this->assertNotContains('/dev/build', $location);
|
||||||
|
$this->assertNotContains('flush=1', $location);
|
||||||
|
$this->assertNotContains('devbuildtoken=', $location);
|
||||||
|
$this->assertNotContains('flushtoken=', $location);
|
||||||
|
$this->assertContains('Security/login', $location);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user