2008-08-09 05:19:54 +02:00
|
|
|
<?php
|
|
|
|
|
2016-08-19 00:51:35 +02:00
|
|
|
namespace SilverStripe\Control;
|
|
|
|
|
2017-06-25 05:12:29 +02:00
|
|
|
use BadMethodCallException;
|
|
|
|
use Exception;
|
2017-02-22 04:14:53 +01:00
|
|
|
use InvalidArgumentException;
|
2017-06-25 05:12:29 +02:00
|
|
|
use ReflectionClass;
|
2018-06-12 07:52:31 +02:00
|
|
|
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
2017-03-02 03:24:38 +01:00
|
|
|
use SilverStripe\Core\ClassInfo;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Core\Config\Config;
|
|
|
|
use SilverStripe\Dev\Debug;
|
2016-06-23 01:37:22 +02:00
|
|
|
use SilverStripe\Security\Permission;
|
2017-06-25 05:12:29 +02:00
|
|
|
use SilverStripe\Security\PermissionFailureException;
|
|
|
|
use SilverStripe\Security\Security;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\View\ViewableData;
|
2016-06-15 06:03:16 +02:00
|
|
|
|
2008-08-09 05:19:54 +02:00
|
|
|
/**
|
2012-03-24 04:38:57 +01:00
|
|
|
* This class is the base class of any SilverStripe object that can be used to handle HTTP requests.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2008-10-30 23:03:21 +01:00
|
|
|
* Any RequestHandler object can be made responsible for handling its own segment of the URL namespace.
|
2008-08-09 05:19:54 +02:00
|
|
|
* The {@link Director} begins the URL parsing process; it will parse the beginning of the URL to identify which
|
2012-09-26 23:34:00 +02:00
|
|
|
* controller is being used. It will then call {@link handleRequest()} on that Controller, passing it the parameters
|
2016-09-09 08:43:05 +02:00
|
|
|
* that it parsed from the URL, and the {@link HTTPRequest} that contains the remainder of the URL to be parsed.
|
2008-10-06 00:16:07 +02:00
|
|
|
*
|
|
|
|
* You can use ?debug_request=1 to view information about the different components and rule matches for a specific URL.
|
2008-08-09 05:19:54 +02:00
|
|
|
*
|
2012-09-26 23:34:00 +02:00
|
|
|
* In SilverStripe, URL parsing is distributed throughout the object graph. For example, suppose that we have a
|
|
|
|
* search form that contains a {@link TreeMultiSelectField} named "Groups". We want to use ajax to load segments of
|
|
|
|
* this tree as they are needed rather than downloading the tree right at the beginning. We could use this URL to get
|
|
|
|
* the tree segment that appears underneath
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2008-10-06 00:16:07 +02:00
|
|
|
* Group #36: "admin/crm/SearchForm/field/Groups/treesegment/36"
|
2008-08-09 05:19:54 +02:00
|
|
|
* - Director will determine that admin/crm is controlled by a new ModelAdmin object, and pass control to that.
|
2008-10-06 00:16:07 +02:00
|
|
|
* Matching Director Rule: "admin/crm" => "ModelAdmin" (defined in mysite/_config.php)
|
2012-09-26 23:34:00 +02:00
|
|
|
* - ModelAdmin will determine that SearchForm is controlled by a Form object returned by $this->SearchForm(), and
|
|
|
|
* pass control to that.
|
2008-10-30 23:03:21 +01:00
|
|
|
* Matching $url_handlers: "$Action" => "$Action" (defined in RequestHandler class)
|
2012-09-26 23:34:00 +02:00
|
|
|
* - Form will determine that field/Groups is controlled by the Groups field, a TreeMultiselectField, and pass
|
|
|
|
* control to that.
|
2008-10-06 00:16:07 +02:00
|
|
|
* Matching $url_handlers: 'field/$FieldName!' => 'handleField' (defined in Form class)
|
2012-09-26 23:34:00 +02:00
|
|
|
* - TreeMultiselectField will determine that treesegment/36 is handled by its treesegment() method. This method
|
|
|
|
* will return an HTML fragment that is output to the screen.
|
2008-10-06 00:16:07 +02:00
|
|
|
* Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class)
|
2008-08-09 05:19:54 +02:00
|
|
|
*
|
2008-10-30 23:03:21 +01:00
|
|
|
* {@link RequestHandler::handleRequest()} is where this behaviour is implemented.
|
2008-08-09 05:19:54 +02:00
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
class RequestHandler extends ViewableData
|
|
|
|
{
|
2017-06-25 08:03:03 +02:00
|
|
|
|
2017-03-02 03:24:38 +01:00
|
|
|
/**
|
|
|
|
* Optional url_segment for this request handler
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var string|null
|
|
|
|
*/
|
|
|
|
private static $url_segment = null;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var HTTPRequest $request The request object that the controller was called with.
|
|
|
|
* Set in {@link handleRequest()}. Useful to generate the {}
|
|
|
|
*/
|
|
|
|
protected $request = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The DataModel for this request
|
|
|
|
*/
|
|
|
|
protected $model = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This variable records whether RequestHandler::__construct()
|
|
|
|
* was called or not. Useful for checking if subclasses have
|
|
|
|
* called parent::__construct()
|
|
|
|
*
|
|
|
|
* @var boolean
|
|
|
|
*/
|
|
|
|
protected $brokenOnConstruct = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The default URL handling rules. This specifies that the next component of the URL corresponds to a method to
|
|
|
|
* be called on this RequestHandlingData object.
|
|
|
|
*
|
|
|
|
* The keys of this array are parse rules. See {@link HTTPRequest::match()} for a description of the rules
|
|
|
|
* available.
|
|
|
|
*
|
|
|
|
* The values of the array are the method to be called if the rule matches. If this value starts with a '$', then
|
|
|
|
* the named parameter of the parsed URL wil be used to determine the method name.
|
|
|
|
* @config
|
|
|
|
*/
|
2018-08-17 15:03:46 +02:00
|
|
|
private static $url_handlers = [
|
2016-11-29 00:31:16 +01:00
|
|
|
'$Action' => '$Action',
|
2018-08-17 15:03:46 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Define a list of action handling methods that are allowed to be called directly by URLs.
|
|
|
|
* The variable should be an array of action names. This sample shows the different values that it can contain:
|
|
|
|
*
|
|
|
|
* <code>
|
2018-08-17 15:03:46 +02:00
|
|
|
* [
|
2016-11-29 00:31:16 +01:00
|
|
|
* // someaction can be accessed by anyone, any time
|
|
|
|
* 'someaction',
|
|
|
|
* // So can otheraction
|
|
|
|
* 'otheraction' => true,
|
|
|
|
* // restrictedaction can only be people with ADMIN privilege
|
|
|
|
* 'restrictedaction' => 'ADMIN',
|
|
|
|
* // complexaction can only be accessed if $this->canComplexAction() returns true
|
2018-08-17 15:03:46 +02:00
|
|
|
* 'complexaction' '->canComplexAction',
|
|
|
|
* ];
|
2016-11-29 00:31:16 +01:00
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* Form getters count as URL actions as well, and should be included in allowed_actions.
|
|
|
|
* Form actions on the other handed (first argument to {@link FormAction()} should NOT be included,
|
|
|
|
* these are handled separately through {@link Form->httpSubmission}. You can control access on form actions
|
|
|
|
* either by conditionally removing {@link FormAction} in the form construction,
|
|
|
|
* or by defining $allowed_actions in your {@link Form} class.
|
|
|
|
* @config
|
|
|
|
*/
|
|
|
|
private static $allowed_actions = null;
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
{
|
|
|
|
$this->brokenOnConstruct = false;
|
|
|
|
|
|
|
|
$this->setRequest(new NullHTTPRequest());
|
|
|
|
|
|
|
|
parent::__construct();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles URL requests.
|
|
|
|
*
|
|
|
|
* - ViewableData::handleRequest() iterates through each rule in {@link self::$url_handlers}.
|
|
|
|
* - If the rule matches, the named method will be called.
|
|
|
|
* - If there is still more URL to be processed, then handleRequest()
|
|
|
|
* is called on the object that that method returns.
|
|
|
|
*
|
|
|
|
* Once all of the URL has been processed, the final result is returned.
|
|
|
|
* However, if the final result is an array, this
|
|
|
|
* array is interpreted as being additional template data to customise the
|
|
|
|
* 2nd to last result with, rather than an object
|
|
|
|
* in its own right. This is most frequently used when a Controller's
|
|
|
|
* action will return an array of data with which to
|
|
|
|
* customise the controller.
|
|
|
|
*
|
|
|
|
* @param HTTPRequest $request The object that is reponsible for distributing URL parsing
|
|
|
|
* @return HTTPResponse|RequestHandler|string|array
|
|
|
|
*/
|
2017-06-22 12:50:45 +02:00
|
|
|
public function handleRequest(HTTPRequest $request)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
// $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance
|
|
|
|
if ($this->brokenOnConstruct) {
|
2017-05-17 07:40:13 +02:00
|
|
|
$handlerClass = static::class;
|
2016-11-29 00:31:16 +01:00
|
|
|
throw new BadMethodCallException(
|
|
|
|
"parent::__construct() needs to be called on {$handlerClass}::__construct()"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->setRequest($request);
|
|
|
|
|
|
|
|
$match = $this->findAction($request);
|
|
|
|
|
|
|
|
// If nothing matches, return this object
|
|
|
|
if (!$match) {
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start to find what action to call. Start by using what findAction returned
|
|
|
|
$action = $match['action'];
|
|
|
|
|
|
|
|
// We used to put "handleAction" as the action on controllers, but (a) this could only be called when
|
|
|
|
// you had $Action in your rule, and (b) RequestHandler didn't have one. $Action is better
|
|
|
|
if ($action == 'handleAction') {
|
|
|
|
// TODO Fix LeftAndMain usage
|
|
|
|
// Deprecation::notice('3.2.0', 'Calling handleAction directly is deprecated - use $Action instead');
|
|
|
|
$action = '$Action';
|
|
|
|
}
|
|
|
|
|
|
|
|
// Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
|
|
|
|
if ($action[0] == '$') {
|
|
|
|
$action = str_replace("-", "_", $request->latestParam(substr($action, 1)));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$action) {
|
|
|
|
if (isset($_REQUEST['debug_request'])) {
|
|
|
|
Debug::message("Action not set; using default action method name 'index'");
|
|
|
|
}
|
|
|
|
$action = "index";
|
|
|
|
} elseif (!is_string($action)) {
|
|
|
|
user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
|
|
|
|
}
|
|
|
|
|
2018-01-16 19:39:30 +01:00
|
|
|
$classMessage = Director::isLive() ? 'on this handler' : 'on class ' . static::class;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
try {
|
|
|
|
if (!$this->hasAction($action)) {
|
|
|
|
return $this->httpError(404, "Action '$action' isn't available $classMessage.");
|
|
|
|
}
|
2018-08-17 15:03:46 +02:00
|
|
|
if (!$this->checkAccessAction($action) || in_array(strtolower($action), ['run', 'doInit'])) {
|
2016-11-29 00:31:16 +01:00
|
|
|
return $this->httpError(403, "Action '$action' isn't allowed $classMessage.");
|
|
|
|
}
|
|
|
|
$result = $this->handleAction($request, $action);
|
|
|
|
} catch (HTTPResponse_Exception $e) {
|
|
|
|
return $e->getResponse();
|
|
|
|
} catch (PermissionFailureException $e) {
|
|
|
|
$result = Security::permissionFailure(null, $e->getMessage());
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($result instanceof HTTPResponse && $result->isError()) {
|
|
|
|
if (isset($_REQUEST['debug_request'])) {
|
|
|
|
Debug::message("Rule resulted in HTTP error; breaking");
|
|
|
|
}
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to
|
|
|
|
// parse. It might have its own handler. However, we only do this if we haven't just parsed an
|
|
|
|
// empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
|
|
|
|
// actions which return themselves to avoid infinite loops.
|
|
|
|
$matchedRuleWasEmpty = $request->isEmptyPattern($match['rule']);
|
2017-03-02 03:24:38 +01:00
|
|
|
if ($this !== $result && !$matchedRuleWasEmpty && ($result instanceof RequestHandler || $result instanceof HasRequestHandler)) {
|
|
|
|
// Expose delegated request handler
|
|
|
|
if ($result instanceof HasRequestHandler) {
|
|
|
|
$result = $result->getRequestHandler();
|
|
|
|
}
|
2017-06-22 12:50:45 +02:00
|
|
|
$returnValue = $result->handleRequest($request);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// Array results can be used to handle
|
|
|
|
if (is_array($returnValue)) {
|
|
|
|
$returnValue = $this->customise($returnValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $returnValue;
|
|
|
|
|
|
|
|
// If we return some other data, and all the URL is parsed, then return that
|
|
|
|
} elseif ($request->allParsed()) {
|
|
|
|
return $result;
|
|
|
|
|
|
|
|
// But if we have more content on the URL and we don't know what to do with it, return an error.
|
|
|
|
} else {
|
|
|
|
return $this->httpError(404, "I can't handle sub-URLs $classMessage.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param HTTPRequest $request
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
protected function findAction($request)
|
|
|
|
{
|
2017-05-17 07:40:13 +02:00
|
|
|
$handlerClass = static::class;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// We stop after RequestHandler; in other words, at ViewableData
|
2017-02-22 04:14:53 +01:00
|
|
|
while ($handlerClass && $handlerClass != ViewableData::class) {
|
2016-11-29 00:31:16 +01:00
|
|
|
$urlHandlers = Config::inst()->get($handlerClass, 'url_handlers', Config::UNINHERITED);
|
|
|
|
|
|
|
|
if ($urlHandlers) {
|
|
|
|
foreach ($urlHandlers as $rule => $action) {
|
|
|
|
if (isset($_REQUEST['debug_request'])) {
|
2017-05-17 07:40:13 +02:00
|
|
|
$class = static::class;
|
|
|
|
$remaining = $request->remaining();
|
|
|
|
Debug::message("Testing '{$rule}' with '{$remaining}' on {$class}");
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($request->match($rule, true)) {
|
|
|
|
if (isset($_REQUEST['debug_request'])) {
|
2017-05-17 07:40:13 +02:00
|
|
|
$class = static::class;
|
|
|
|
$latestParams = var_export($request->latestParams(), true);
|
2016-11-29 00:31:16 +01:00
|
|
|
Debug::message(
|
2018-01-16 19:39:30 +01:00
|
|
|
"Rule '{$rule}' matched to action '{$action}' on {$class}. " . "Latest request params: {$latestParams}"
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-08-17 15:03:46 +02:00
|
|
|
return [
|
|
|
|
'rule' => $rule,
|
|
|
|
'action' => $action,
|
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$handlerClass = get_parent_class($handlerClass);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-05-30 09:42:00 +02:00
|
|
|
/**
|
|
|
|
* @param string $link
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function addBackURLParam($link)
|
|
|
|
{
|
|
|
|
$backURL = $this->getBackURL();
|
|
|
|
if ($backURL) {
|
|
|
|
return Controller::join_links($link, '?BackURL=' . urlencode($backURL));
|
|
|
|
}
|
|
|
|
|
|
|
|
return $link;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Given a request, and an action name, call that action name on this RequestHandler
|
|
|
|
*
|
|
|
|
* Must not raise HTTPResponse_Exceptions - instead it should return
|
|
|
|
*
|
|
|
|
* @param $request
|
|
|
|
* @param $action
|
|
|
|
* @return HTTPResponse
|
|
|
|
*/
|
|
|
|
protected function handleAction($request, $action)
|
|
|
|
{
|
2018-01-16 19:39:30 +01:00
|
|
|
$classMessage = Director::isLive() ? 'on this handler' : 'on class ' . static::class;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
if (!$this->hasMethod($action)) {
|
|
|
|
return new HTTPResponse("Action '$action' isn't available $classMessage.", 404);
|
|
|
|
}
|
|
|
|
|
|
|
|
$res = $this->extend('beforeCallActionHandler', $request, $action);
|
|
|
|
if ($res) {
|
|
|
|
return reset($res);
|
|
|
|
}
|
|
|
|
|
|
|
|
$actionRes = $this->$action($request);
|
|
|
|
|
|
|
|
$res = $this->extend('afterCallActionHandler', $request, $action, $actionRes);
|
|
|
|
if ($res) {
|
|
|
|
return reset($res);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $actionRes;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a array of allowed actions defined on this controller,
|
|
|
|
* any parent classes or extensions.
|
|
|
|
*
|
|
|
|
* Caution: Since 3.1, allowed_actions definitions only apply
|
|
|
|
* to methods on the controller they're defined on,
|
|
|
|
* so it is recommended to use the $class argument
|
|
|
|
* when invoking this method.
|
|
|
|
*
|
|
|
|
* @param string $limitToClass
|
|
|
|
* @return array|null
|
|
|
|
*/
|
|
|
|
public function allowedActions($limitToClass = null)
|
|
|
|
{
|
|
|
|
if ($limitToClass) {
|
2017-02-22 04:14:53 +01:00
|
|
|
$actions = Config::forClass($limitToClass)->get('allowed_actions', true);
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
2017-02-22 04:14:53 +01:00
|
|
|
$actions = $this->config()->get('allowed_actions');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (is_array($actions)) {
|
|
|
|
if (array_key_exists('*', $actions)) {
|
2017-02-22 04:14:53 +01:00
|
|
|
throw new InvalidArgumentException("Invalid allowed_action '*'");
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// convert all keys and values to lowercase to
|
|
|
|
// allow for easier comparison, unless it is a permission code
|
|
|
|
$actions = array_change_key_case($actions, CASE_LOWER);
|
|
|
|
|
|
|
|
foreach ($actions as $key => $value) {
|
|
|
|
if (is_numeric($key)) {
|
|
|
|
$actions[$key] = strtolower($value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $actions;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if this request handler has a specific action,
|
|
|
|
* even if the current user cannot access it.
|
|
|
|
* Includes class ancestry and extensions in the checks.
|
|
|
|
*
|
|
|
|
* @param string $action
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function hasAction($action)
|
|
|
|
{
|
|
|
|
if ($action == 'index') {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't allow access to any non-public methods (inspect instance plus all extensions)
|
2018-08-17 15:13:13 +02:00
|
|
|
$insts = array_merge([$this], (array) $this->getExtensionInstances());
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($insts as $inst) {
|
|
|
|
if (!method_exists($inst, $action)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$r = new ReflectionClass(get_class($inst));
|
|
|
|
$m = $r->getMethod($action);
|
|
|
|
if (!$m || !$m->isPublic()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$action = strtolower($action);
|
|
|
|
$actions = $this->allowedActions();
|
|
|
|
|
|
|
|
// Check if the action is defined in the allowed actions of any ancestry class
|
|
|
|
// as either a key or value. Note that if the action is numeric, then keys are not
|
|
|
|
// searched for actions to prevent actual array keys being recognised as actions.
|
|
|
|
if (is_array($actions)) {
|
|
|
|
$isKey = !is_numeric($action) && array_key_exists($action, $actions);
|
|
|
|
$isValue = in_array($action, $actions, true);
|
|
|
|
if ($isKey || $isValue) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-22 04:14:53 +01:00
|
|
|
$actionsWithoutExtra = $this->config()->get('allowed_actions', true);
|
2016-11-29 00:31:16 +01:00
|
|
|
if (!is_array($actions) || !$actionsWithoutExtra) {
|
|
|
|
if ($action != 'doInit' && $action != 'run' && method_exists($this, $action)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the class that defines the given action, so that we know where to check allowed_actions.
|
|
|
|
*
|
|
|
|
* @param string $actionOrigCasing
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function definingClassForAction($actionOrigCasing)
|
|
|
|
{
|
|
|
|
$action = strtolower($actionOrigCasing);
|
|
|
|
|
|
|
|
$definingClass = null;
|
2018-08-17 15:03:46 +02:00
|
|
|
$insts = array_merge([$this], (array) $this->getExtensionInstances());
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($insts as $inst) {
|
|
|
|
if (!method_exists($inst, $action)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$r = new ReflectionClass(get_class($inst));
|
|
|
|
$m = $r->getMethod($actionOrigCasing);
|
|
|
|
return $m->getDeclaringClass()->getName();
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check that the given action is allowed to be called from a URL.
|
|
|
|
* It will interrogate {@link self::$allowed_actions} to determine this.
|
|
|
|
*
|
|
|
|
* @param string $action
|
|
|
|
* @return bool
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
public function checkAccessAction($action)
|
|
|
|
{
|
|
|
|
$actionOrigCasing = $action;
|
|
|
|
$action = strtolower($action);
|
|
|
|
|
|
|
|
$isAllowed = false;
|
|
|
|
$isDefined = false;
|
|
|
|
|
|
|
|
// Get actions for this specific class (without inheritance)
|
|
|
|
$definingClass = $this->definingClassForAction($actionOrigCasing);
|
|
|
|
$allowedActions = $this->allowedActions($definingClass);
|
|
|
|
|
|
|
|
// check if specific action is set
|
|
|
|
if (isset($allowedActions[$action])) {
|
|
|
|
$isDefined = true;
|
|
|
|
$test = $allowedActions[$action];
|
|
|
|
if ($test === true || $test === 1 || $test === '1') {
|
|
|
|
// TRUE should always allow access
|
|
|
|
$isAllowed = true;
|
|
|
|
} elseif (substr($test, 0, 2) == '->') {
|
|
|
|
// Determined by custom method with "->" prefix
|
2017-05-17 07:40:13 +02:00
|
|
|
list($method, $arguments) = ClassInfo::parse_class_spec(substr($test, 2));
|
2018-08-17 15:03:46 +02:00
|
|
|
$isAllowed = call_user_func_array([$this, $method], $arguments);
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
|
|
|
// Value is a permission code to check the current member against
|
|
|
|
$isAllowed = Permission::check($test);
|
|
|
|
}
|
|
|
|
} elseif (is_array($allowedActions)
|
|
|
|
&& (($key = array_search($action, $allowedActions, true)) !== false)
|
|
|
|
&& is_numeric($key)
|
|
|
|
) {
|
|
|
|
// Allow numeric array notation (search for array value as action instead of key)
|
|
|
|
$isDefined = true;
|
|
|
|
$isAllowed = true;
|
|
|
|
} elseif (is_array($allowedActions) && !count($allowedActions)) {
|
|
|
|
// If defined as empty array, deny action
|
|
|
|
$isAllowed = false;
|
|
|
|
} elseif ($allowedActions === null) {
|
|
|
|
// If undefined, allow action based on configuration
|
2017-03-02 03:24:38 +01:00
|
|
|
$isAllowed = false;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// If we don't have a match in allowed_actions,
|
|
|
|
// whitelist the 'index' action as well as undefined actions based on configuration.
|
|
|
|
if (!$isDefined && ($action == 'index' || empty($action))) {
|
|
|
|
$isAllowed = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $isAllowed;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Throws a HTTP error response encased in a {@link HTTPResponse_Exception}, which is later caught in
|
|
|
|
* {@link RequestHandler::handleAction()} and returned to the user.
|
|
|
|
*
|
|
|
|
* @param int $errorCode
|
|
|
|
* @param string $errorMessage Plaintext error message
|
|
|
|
* @uses HTTPResponse_Exception
|
|
|
|
* @throws HTTPResponse_Exception
|
|
|
|
*/
|
|
|
|
public function httpError($errorCode, $errorMessage = null)
|
|
|
|
{
|
|
|
|
$request = $this->getRequest();
|
|
|
|
|
|
|
|
// Call a handler method such as onBeforeHTTPError404
|
2018-06-05 23:09:37 +02:00
|
|
|
$this->extend("onBeforeHTTPError{$errorCode}", $request, $errorMessage);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// Call a handler method such as onBeforeHTTPError, passing 404 as the first arg
|
2018-06-05 23:09:37 +02:00
|
|
|
$this->extend('onBeforeHTTPError', $errorCode, $request, $errorMessage);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// Throw a new exception
|
|
|
|
throw new HTTPResponse_Exception($errorMessage, $errorCode);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the HTTPRequest object that this controller is using.
|
|
|
|
* Returns a placeholder {@link NullHTTPRequest} object unless
|
|
|
|
* {@link handleAction()} or {@link handleRequest()} have been called,
|
|
|
|
* which adds a reference to an actual {@link HTTPRequest} object.
|
|
|
|
*
|
2017-03-02 03:24:38 +01:00
|
|
|
* @return HTTPRequest
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function getRequest()
|
|
|
|
{
|
|
|
|
return $this->request;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Typically the request is set through {@link handleAction()}
|
|
|
|
* or {@link handleRequest()}, but in some based we want to set it manually.
|
|
|
|
*
|
|
|
|
* @param HTTPRequest $request
|
|
|
|
* @return $this
|
2016-06-23 01:37:22 +02:00
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
public function setRequest($request)
|
|
|
|
{
|
|
|
|
$this->request = $request;
|
|
|
|
return $this;
|
|
|
|
}
|
2017-03-02 03:24:38 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a link to this controller. Overload with your own Link rules if they exist.
|
|
|
|
*
|
|
|
|
* @param string $action Optional action
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function Link($action = null)
|
|
|
|
{
|
|
|
|
// Check configured url_segment
|
|
|
|
$url = $this->config()->get('url_segment');
|
|
|
|
if ($url) {
|
2018-03-21 03:50:31 +01:00
|
|
|
$link = Controller::join_links($url, $action, '/');
|
|
|
|
|
2018-03-20 03:08:29 +01:00
|
|
|
// Give extensions the chance to modify by reference
|
|
|
|
$this->extend('updateLink', $link, $action);
|
2018-03-21 03:50:31 +01:00
|
|
|
return $link;
|
2017-03-02 03:24:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// no link defined by default
|
|
|
|
trigger_error(
|
2018-01-16 19:39:30 +01:00
|
|
|
'Request handler ' . static::class . ' does not have a url_segment defined. ' . 'Relying on this link may be an application error',
|
2017-03-02 03:24:38 +01:00
|
|
|
E_USER_WARNING
|
|
|
|
);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Redirect to the given URL.
|
|
|
|
*
|
|
|
|
* @param string $url
|
|
|
|
* @param int $code
|
|
|
|
* @return HTTPResponse
|
|
|
|
*/
|
|
|
|
public function redirect($url, $code = 302)
|
|
|
|
{
|
|
|
|
$url = Director::absoluteURL($url);
|
|
|
|
$response = new HTTPResponse();
|
|
|
|
return $response->redirect($url, $code);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Safely get the value of the BackURL param, if provided via querystring / posted var
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getBackURL()
|
|
|
|
{
|
|
|
|
$request = $this->getRequest();
|
|
|
|
if (!$request) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$backURL = $request->requestVar('BackURL');
|
|
|
|
// Fall back to X-Backurl header
|
|
|
|
if (!$backURL && $request->isAjax() && $request->getHeader('X-Backurl')) {
|
|
|
|
$backURL = $request->getHeader('X-Backurl');
|
|
|
|
}
|
|
|
|
if (!$backURL) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (Director::is_site_url($backURL)) {
|
|
|
|
return $backURL;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the referer, if it is safely validated as an internal URL
|
|
|
|
* and can be redirected to.
|
|
|
|
*
|
|
|
|
* @internal called from {@see Form::getValidationErrorResponse}
|
|
|
|
* @return string|null
|
|
|
|
*/
|
|
|
|
public function getReturnReferer()
|
|
|
|
{
|
|
|
|
$referer = $this->getReferer();
|
|
|
|
if ($referer && Director::is_site_url($referer)) {
|
|
|
|
return $referer;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get referer
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getReferer()
|
|
|
|
{
|
|
|
|
$request = $this->getRequest();
|
|
|
|
if (!$request) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return $request->getHeader('Referer');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Redirect back. Uses either the HTTP-Referer or a manually set request-variable called "BackURL".
|
|
|
|
* This variable is needed in scenarios where HTTP-Referer is not sent (e.g when calling a page by
|
|
|
|
* location.href in IE). If none of the two variables is available, it will redirect to the base
|
|
|
|
* URL (see {@link Director::baseURL()}).
|
|
|
|
*
|
|
|
|
* @uses redirect()
|
|
|
|
*
|
|
|
|
* @return HTTPResponse
|
|
|
|
*/
|
|
|
|
public function redirectBack()
|
|
|
|
{
|
|
|
|
// Prefer to redirect to ?BackURL, but fall back to Referer header
|
|
|
|
// As a last resort redirect to base url
|
|
|
|
$url = $this->getBackURL()
|
|
|
|
?: $this->getReturnReferer()
|
|
|
|
?: Director::baseURL();
|
|
|
|
|
|
|
|
// Only direct to absolute urls
|
|
|
|
$url = Director::absoluteURL($url);
|
|
|
|
return $this->redirect($url);
|
|
|
|
}
|
2009-06-27 10:48:44 +02:00
|
|
|
}
|