"ModelAdmin" (defined in mysite/_config.php) * - ModelAdmin will determine that SearchForm is controlled by a Form object returned by $this->SearchForm(), and pass control to that. * Matching $url_handlers: "$Action" => "$Action" (defined in RequestHandler class) * - Form will determine that field/Groups is controlled by the Groups field, a TreeMultiselectField, and pass control to that. * Matching $url_handlers: 'field/$FieldName!' => 'handleField' (defined in Form class) * - 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. * Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class) * * {@link RequestHandler::handleRequest()} is where this behaviour is implemented. */ class RequestHandler extends ViewableData { protected $request = null; /** * 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. */ static $url_handlers = array( '$Action' => '$Action', ); /** * 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: * * * array( * 'someaction', // someaction can be accessed by anyone, any time * 'otheraction' => true, // So can otheraction * 'restrictedaction' => 'ADMIN', // restrictedaction can only be people with ADMIN privilege * 'complexaction' '->canComplexAction' // complexaction can only be accessed if $this->canComplexAction() returns true * ); * */ static $allowed_actions = null; /** * 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 $params The parameters taken from the parsed URL of the parent url handler * @param $request The {@link HTTPRequest} object that is reponsible for distributing URL parsing * @uses HTTPRequest * @uses HTTPRequest->match() * @return HTTPResponse|RequestHandler|string|array */ function handleRequest($request) { $this->request = $request; // $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance $handlerClass = ($this->class) ? $this->class : get_class($this); // We stop after RequestHandler; in other words, at ViewableData while($handlerClass && $handlerClass != 'ViewableData') { $urlHandlers = Object::get_static($handlerClass, 'url_handlers'); if($urlHandlers) foreach($urlHandlers as $rule => $action) { if(isset($_REQUEST['debug_request'])) Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class"); if($params = $request->match($rule, true)) { // FIXME: This unnecessary coupling was added to fix a bug in Image_Uploader. if($this instanceof Controller) $this->urlParams = $request->allParams(); if(isset($_REQUEST['debug_request'])) { Debug::message("Rule '$rule' matched to action '$action' on $this->class. Latest request params: " . var_export($request->latestParams(), true)); } // Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action', if($action[0] == '$') $action = $params[substr($action,1)]; if($this->checkAccessAction($action)) { if(!$action) { if(isset($_REQUEST['debug_request'])) Debug::message("Action not set; using default action method name 'index'"); $action = "index"; } else if(!is_string($action)) { user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR); } $result = $this->$action($request); } else { return $this->httpError(403, "Action '$action' isn't allowed on class $this->class"); } 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 if(!$request->isEmptyPattern($rule) && is_object($result) && $result instanceof RequestHandler) { $returnValue = $result->handleRequest($request); // 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 } else if($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(400, "I can't handle sub-URLs of a $this->class object."); } return $this; } } $handlerClass = get_parent_class($handlerClass); } // If nothing matches, return this object return $this; } /** * Check that the given action is allowed to be called from a URL. * It will interrogate {@link self::$allowed_actions} to determine this. */ function checkAccessAction($action) { $action = strtolower($action); $allowedActions = Object::combined_static($this->class, 'allowed_actions'); $newAllowedActions = array(); // merge in any $allowed_actions from extensions if($this->extension_instances) foreach($this->extension_instances as $extension) { if($extAccess = $extension->stat('allowed_actions')) { $allowedActions = array_merge($allowedActions, $extAccess); } } if($action == 'index') return true; if($allowedActions) { foreach($allowedActions as $key => $value) { $newAllowedActions[strtolower($key)] = strtolower($value); } $allowedActions = $newAllowedActions; if(isset($allowedActions[$action])) { $test = $allowedActions[$action]; if($test === true) { return true; } elseif(substr($test, 0, 2) == '->') { return $this->{substr($test, 2)}(); } elseif(Permission::check($test)) { return true; } } elseif((($key = array_search($action, $allowedActions)) !== false) && is_numeric($key)) { return true; } } if($allowedActions === null || !$this->uninherited('allowed_actions')) { // If no allowed_actions are provided, then we should only let through actions that aren't handled by magic methods // we test this by calling the unmagic method_exists and comparing it to the magic $this->hasMethod(). This will // still let through actions that are handled by templates. return method_exists($this, $action) || !$this->hasMethod($action); } return false; } /** * Throw an HTTP error instead of performing the normal processing * @todo This doesn't work properly right now. :-( */ function httpError($errorCode, $errorMessage = null) { $r = new HTTPResponse(); $r->setBody($errorMessage); $r->setStatuscode($errorCode); return $r; } /** * Returns the HTTPRequest object that this controller is using. * * @return HTTPRequest */ function getRequest() { return $this->request; } }