Merge pull request #7300 from open-sausages/pulls/4.0/flush-live-backurl

BUG Capture errors after a reload token redirect to login url
This commit is contained in:
Chris Joe 2017-08-24 14:00:33 +12:00 committed by GitHub
commit deec9b411b
5 changed files with 165 additions and 30 deletions

View File

@ -49,7 +49,7 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
try { try {
// Check if a token is requesting a redirect // Check if a token is requesting a redirect
if ($reloadToken) { if ($reloadToken && $reloadToken->reloadRequired()) {
$result = $this->safeReloadWithToken($request, $reloadToken); $result = $this->safeReloadWithToken($request, $reloadToken);
} else { } else {
// If no reload necessary, process application // If no reload necessary, process application
@ -61,7 +61,7 @@ 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 ($reloadToken) { ->thenIfErrored(function () use ($reloadToken) {
if ($reloadToken) { if ($reloadToken && $reloadToken->reloadRequiredIfError()) {
$result = $reloadToken->reloadWithToken(); $result = $reloadToken->reloadWithToken();
$result->output(); $result->output();
} }
@ -87,14 +87,20 @@ class ErrorControlChainMiddleware implements HTTPMiddleware
$request->getSession()->init($request); $request->getSession()->init($request);
// Request with ErrorDirector // Request with ErrorDirector
$result = ErrorDirector::singleton()->handleRequestWithToken($request, $reloadToken, $this->getApplication()->getKernel()); $result = ErrorDirector::singleton()->handleRequestWithToken(
$request,
$reloadToken,
$this->getApplication()->getKernel()
);
if ($result) { if ($result) {
return $result; return $result;
} }
// 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));
$backURL = $request->getURL(). '?' . http_build_query($params);
$loginPage = Director::absoluteURL(Security::config()->get('login_url')); $loginPage = Director::absoluteURL(Security::config()->get('login_url'));
$loginPage .= "?BackURL=" . urlencode($request->getURL()); $loginPage .= "?BackURL=" . urlencode($backURL);
$result = new HTTPResponse(); $result = new HTTPResponse();
$result->redirect($loginPage); $result->redirect($loginPage);
return $result; return $result;

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Core\Startup; namespace SilverStripe\Core\Startup;
use function GuzzleHttp\Psr7\parse_query;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
@ -34,12 +35,19 @@ class ParameterConfirmationToken
protected $request = null; protected $request = null;
/** /**
* The parameter given * The parameter given in the main request
* *
* @var string|null The string value, or null if not provided * @var string|null The string value, or null if not provided
*/ */
protected $parameter = null; protected $parameter = null;
/**
* The parameter given in the backURL
*
* @var string|null
*/
protected $parameterBackURL = null;
/** /**
* The validated and checked token for this parameter * The validated and checked token for this parameter
* *
@ -106,6 +114,7 @@ class ParameterConfirmationToken
// Store the parameter value // Store the parameter value
$this->parameter = $request->getVar($parameterName); $this->parameter = $request->getVar($parameterName);
$this->parameterBackURL = $this->backURLToken($request);
// If the token provided is valid, mark it as such // If the token provided is valid, mark it as such
$token = $request->getVar($parameterName.'token'); $token = $request->getVar($parameterName.'token');
@ -114,6 +123,29 @@ class ParameterConfirmationToken
} }
} }
/**
* Check if this token exists in the BackURL
*
* @param HTTPRequest $request
* @return string Value of token in backurl, or null if not in backurl
*/
protected function backURLToken(HTTPRequest $request)
{
$backURL = $request->getVar('BackURL');
if (!strstr($backURL, '?')) {
return null;
}
// Filter backURL if it contains the given request parameter
list(,$query) = explode('?', $backURL);
$queryArgs = parse_query($query);
$name = $this->getName();
if (isset($queryArgs[$name])) {
return $queryArgs[$name];
}
return null;
}
/** /**
* Get the name of this token * Get the name of this token
* *
@ -135,6 +167,16 @@ class ParameterConfirmationToken
return $this->parameter !== null; return $this->parameter !== null;
} }
/**
* Is the parmeter requested in a BackURL param?
*
* @return bool
*/
public function existsInReferer()
{
return $this->parameterBackURL !== null;
}
/** /**
* Is the necessary token provided for this parameter? * Is the necessary token provided for this parameter?
* A value must be provided for the token * A value must be provided for the token
@ -156,6 +198,18 @@ class ParameterConfirmationToken
return $this->parameterProvided() && !$this->tokenProvided(); return $this->parameterProvided() && !$this->tokenProvided();
} }
/**
* Check if this token is provided either in the backurl, or directly,
* but without a token
*
* @return bool
*/
public function reloadRequiredIfError()
{
// Don't reload if token exists
return $this->reloadRequired() || $this->existsInReferer();
}
/** /**
* Suppress the current parameter by unsetting it from $_GET * Suppress the current parameter by unsetting it from $_GET
*/ */
@ -167,14 +221,18 @@ class ParameterConfirmationToken
/** /**
* Determine the querystring parameters to include * Determine the querystring parameters to include
* *
* @param bool $includeToken Include the token value as well?
* @return array List of querystring parameters with name and token parameters * @return array List of querystring parameters with name and token parameters
*/ */
public function params() public function params($includeToken = true)
{ {
return array( $params = array(
$this->parameterName => $this->parameter, $this->parameterName => $this->parameter,
$this->parameterName.'token' => $this->genToken()
); );
if ($includeToken) {
$params[$this->parameterName . 'token'] = $this->genToken();
}
return $params;
} }
/** /**
@ -190,6 +248,26 @@ class ParameterConfirmationToken
); );
} }
/**
* Get redirection URL
*
* @return string
*/
protected function redirectURL()
{
// If url is encoded via BackURL, defer to home page (prevent redirect to form action)
if ($this->existsInReferer() && !$this->parameterProvided()) {
$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));
}
/** /**
* Forces a reload of the request with the token included * Forces a reload of the request with the token included
* *
@ -197,12 +275,7 @@ class ParameterConfirmationToken
*/ */
public function reloadWithToken() public function reloadWithToken()
{ {
// Merge get params with current url $location = $this->redirectURL();
$params = array_merge($this->request->getVars(), $this->params());
$location = Controller::join_links(
$this->currentURL(),
'?'.http_build_query($params)
);
$locationJS = Convert::raw2js($location); $locationJS = Convert::raw2js($location);
$locationATT = Convert::raw2att($location); $locationATT = Convert::raw2att($location);
$body = <<<HTML $body = <<<HTML
@ -231,7 +304,7 @@ HTML;
foreach ($keys as $key) { foreach ($keys as $key) {
$token = new ParameterConfirmationToken($key, $request); $token = new ParameterConfirmationToken($key, $request);
// Validate this token // Validate this token
if ($token->reloadRequired()) { if ($token->reloadRequired() || $token->reloadRequiredIfError()) {
$token->suppress(); $token->suppress();
$target = $token; $target = $token;
} }

View File

@ -75,6 +75,13 @@ if (!getenv('SS_IGNORE_DOT_ENV')) {
if (!defined('BASE_URL')) { if (!defined('BASE_URL')) {
define('BASE_URL', call_user_func(function () { define('BASE_URL', call_user_func(function () {
// Prefer explicitly provided SS_BASE_URL
$base = getenv('SS_BASE_URL');
if ($base) {
// Strip relative path from SS_BASE_URL
return rtrim(parse_url($base, PHP_URL_PATH), '/');
}
// Determine the base URL by comparing SCRIPT_NAME to SCRIPT_FILENAME and getting common elements // Determine the base URL by comparing SCRIPT_NAME to SCRIPT_FILENAME and getting common elements
// This tends not to work on CLI // This tends not to work on CLI
$path = realpath($_SERVER['SCRIPT_FILENAME']); $path = realpath($_SERVER['SCRIPT_FILENAME']);
@ -87,13 +94,6 @@ if (!defined('BASE_URL')) {
} }
} }
// Fall back to SS_BASE_URL
$base = getenv('SS_BASE_URL');
if ($base) {
// Strip relative path from SS_BASE_URL
return rtrim(parse_url($base, PHP_URL_PATH), '/');
}
// Assume no base_url // Assume no base_url
return ''; return '';
})); }));

View File

@ -29,7 +29,8 @@ class ParameterConfirmationTokenTest extends SapphireTest
$get['parameterconfirmationtokentest_nulltokentoken'] = null; $get['parameterconfirmationtokentest_nulltokentoken'] = null;
$get['parameterconfirmationtokentest_emptytoken'] = '1'; $get['parameterconfirmationtokentest_emptytoken'] = '1';
$get['parameterconfirmationtokentest_emptytokentoken'] = ''; $get['parameterconfirmationtokentest_emptytokentoken'] = '';
$this->request = new HTTPRequest('GET', '/', $get); $get['BackURL'] = 'page?parameterconfirmationtokentest_backtoken=1';
$this->request = new HTTPRequest('GET', 'anotherpage', $get);
$this->request->setSession(new Session([])); $this->request->setSession(new Session([]));
} }
@ -41,6 +42,7 @@ class ParameterConfirmationTokenTest extends SapphireTest
$withoutParameter = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_noparam', $this->request); $withoutParameter = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_noparam', $this->request);
$nullToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_nulltoken', $this->request); $nullToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_nulltoken', $this->request);
$emptyToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_emptytoken', $this->request); $emptyToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_emptytoken', $this->request);
$backToken = new ParameterConfirmationTokenTest_Token('parameterconfirmationtokentest_backtoken', $this->request);
// Check parameter // Check parameter
$this->assertTrue($withoutToken->parameterProvided()); $this->assertTrue($withoutToken->parameterProvided());
@ -49,6 +51,16 @@ class ParameterConfirmationTokenTest extends SapphireTest
$this->assertFalse($withoutParameter->parameterProvided()); $this->assertFalse($withoutParameter->parameterProvided());
$this->assertTrue($nullToken->parameterProvided()); $this->assertTrue($nullToken->parameterProvided());
$this->assertTrue($emptyToken->parameterProvided()); $this->assertTrue($emptyToken->parameterProvided());
$this->assertFalse($backToken->parameterProvided());
// Check backurl
$this->assertFalse($withoutToken->existsInReferer());
$this->assertFalse($emptyParameter->existsInReferer()); // even if empty, it's still provided
$this->assertFalse($withToken->existsInReferer());
$this->assertFalse($withoutParameter->existsInReferer());
$this->assertFalse($nullToken->existsInReferer());
$this->assertFalse($emptyToken->existsInReferer());
$this->assertTrue($backToken->existsInReferer());
// Check token // Check token
$this->assertFalse($withoutToken->tokenProvided()); $this->assertFalse($withoutToken->tokenProvided());
@ -57,6 +69,7 @@ class ParameterConfirmationTokenTest extends SapphireTest
$this->assertFalse($withoutParameter->tokenProvided()); $this->assertFalse($withoutParameter->tokenProvided());
$this->assertFalse($nullToken->tokenProvided()); $this->assertFalse($nullToken->tokenProvided());
$this->assertFalse($emptyToken->tokenProvided()); $this->assertFalse($emptyToken->tokenProvided());
$this->assertFalse($backToken->tokenProvided());
// Check if reload is required // Check if reload is required
$this->assertTrue($withoutToken->reloadRequired()); $this->assertTrue($withoutToken->reloadRequired());
@ -65,6 +78,25 @@ class ParameterConfirmationTokenTest extends SapphireTest
$this->assertFalse($withoutParameter->reloadRequired()); $this->assertFalse($withoutParameter->reloadRequired());
$this->assertTrue($nullToken->reloadRequired()); $this->assertTrue($nullToken->reloadRequired());
$this->assertTrue($emptyToken->reloadRequired()); $this->assertTrue($emptyToken->reloadRequired());
$this->assertFalse($backToken->reloadRequired());
// Check if a reload is required in case of error
$this->assertTrue($withoutToken->reloadRequiredIfError());
$this->assertTrue($emptyParameter->reloadRequiredIfError());
$this->assertFalse($withToken->reloadRequiredIfError());
$this->assertFalse($withoutParameter->reloadRequiredIfError());
$this->assertTrue($nullToken->reloadRequiredIfError());
$this->assertTrue($emptyToken->reloadRequiredIfError());
$this->assertTrue($backToken->reloadRequiredIfError());
// Check redirect url
$home = (BASE_URL ?: '/') . '?';
$current = Controller::join_links(BASE_URL, '/', 'anotherpage'). '?';
$this->assertStringStartsWith($current, $withoutToken->redirectURL());
$this->assertStringStartsWith($current, $emptyParameter->redirectURL());
$this->assertStringStartsWith($current, $nullToken->redirectURL());
$this->assertStringStartsWith($current, $emptyToken->redirectURL());
$this->assertStringStartsWith($home, $backToken->redirectURL());
// Check suppression // Check suppression
$this->assertEquals('value', $this->request->getVar('parameterconfirmationtokentest_notoken')); $this->assertEquals('value', $this->request->getVar('parameterconfirmationtokentest_notoken'));
@ -90,6 +122,25 @@ class ParameterConfirmationTokenTest extends SapphireTest
$this->request $this->request
); );
$this->assertEmpty($token); $this->assertEmpty($token);
// Test backurl token
$token = ParameterConfirmationToken::prepare_tokens(
[ 'parameterconfirmationtokentest_backtoken' ],
$this->request
);
$this->assertEquals('parameterconfirmationtokentest_backtoken', $token->getName());
}
public function dataProviderURLs()
{
return [
[''],
['/'],
['bar'],
['bar/'],
['/bar'],
['/bar/'],
];
} }
/** /**
@ -97,18 +148,18 @@ class ParameterConfirmationTokenTest extends SapphireTest
* *
* There should always be exactly one slash between each part in the result, and any trailing slash * There should always be exactly one slash between each part in the result, and any trailing slash
* should be preserved. * should be preserved.
*
* @dataProvider dataProviderURLs
*/ */
public function testCurrentAbsoluteURLHandlesSlashes() public function testCurrentAbsoluteURLHandlesSlashes($url)
{ {
$this->request->setUrl($url);
$token = new ParameterConfirmationTokenTest_Token( $token = new ParameterConfirmationTokenTest_Token(
'parameterconfirmationtokentest_parameter', 'parameterconfirmationtokentest_parameter',
$this->request $this->request
); );
$expected = rtrim(Controller::join_links(BASE_URL, '/', $url), '/') ?: '/';
foreach (array('', '/', 'bar', 'bar/', '/bar', '/bar/') as $url) { $this->assertEquals($expected, $token->currentURL(), "Invalid redirect for request url $url");
$this->request->setUrl($url);
$expected = rtrim(Controller::join_links(BASE_URL, '/', $url), '/') ?: '/';
$this->assertEquals($expected, $token->currentURL(), "Invalid redirect for request url $url");
}
} }
} }

View File

@ -15,4 +15,9 @@ class ParameterConfirmationTokenTest_Token extends ParameterConfirmationToken im
{ {
return parent::currentURL(); return parent::currentURL();
} }
public function redirectURL()
{
return parent::redirectURL();
}
} }