PageRenderTime 53ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/6_crm/sapphire/core/control/RequestHandler.php

https://github.com/neopba/silverstripe-book
PHP | 225 lines | 115 code | 22 blank | 88 comment | 39 complexity | fdaf7a024f83bfd643a4f4f8ac4043e8 MD5 | raw file
  1. <?php
  2. /**
  3. * This class is the base class of any Sapphire object that can be used to handle HTTP requests.
  4. *
  5. * Any RequestHandler object can be made responsible for handling its own segment of the URL namespace.
  6. * The {@link Director} begins the URL parsing process; it will parse the beginning of the URL to identify which
  7. * controller is being used. It will then call {@link handleRequest()} on that Controller, passing it the parameters that it
  8. * parsed from the URL, and the {@link HTTPRequest} that contains the remainder of the URL to be parsed.
  9. *
  10. * You can use ?debug_request=1 to view information about the different components and rule matches for a specific URL.
  11. *
  12. * In Sapphire, URL parsing is distributed throughout the object graph. For example, suppose that we have a search form
  13. * that contains a {@link TreeMultiSelectField} named "Groups". We want to use ajax to load segments of this tree as they are needed
  14. * rather than downloading the tree right at the beginning. We could use this URL to get the tree segment that appears underneath
  15. * Group #36: "admin/crm/SearchForm/field/Groups/treesegment/36"
  16. * - Director will determine that admin/crm is controlled by a new ModelAdmin object, and pass control to that.
  17. * Matching Director Rule: "admin/crm" => "ModelAdmin" (defined in mysite/_config.php)
  18. * - ModelAdmin will determine that SearchForm is controlled by a Form object returned by $this->SearchForm(), and pass control to that.
  19. * Matching $url_handlers: "$Action" => "$Action" (defined in RequestHandler class)
  20. * - Form will determine that field/Groups is controlled by the Groups field, a TreeMultiselectField, and pass control to that.
  21. * Matching $url_handlers: 'field/$FieldName!' => 'handleField' (defined in Form class)
  22. * - 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.
  23. * Matching $url_handlers: "$Action/$ID" => "handleItem" (defined in TreeMultiSelectField class)
  24. *
  25. * {@link RequestHandler::handleRequest()} is where this behaviour is implemented.
  26. */
  27. class RequestHandler extends ViewableData {
  28. protected $request = null;
  29. /**
  30. * The default URL handling rules. This specifies that the next component of the URL corresponds to a method to
  31. * be called on this RequestHandlingData object.
  32. *
  33. * The keys of this array are parse rules. See {@link HTTPRequest::match()} for a description of the rules available.
  34. *
  35. * The values of the array are the method to be called if the rule matches. If this value starts with a '$', then the
  36. * named parameter of the parsed URL wil be used to determine the method name.
  37. */
  38. static $url_handlers = array(
  39. '$Action' => '$Action',
  40. );
  41. /**
  42. * Define a list of action handling methods that are allowed to be called directly by URLs.
  43. * The variable should be an array of action names. This sample shows the different values that it can contain:
  44. *
  45. * <code>
  46. * array(
  47. * 'someaction', // someaction can be accessed by anyone, any time
  48. * 'otheraction' => true, // So can otheraction
  49. * 'restrictedaction' => 'ADMIN', // restrictedaction can only be people with ADMIN privilege
  50. * 'complexaction' '->canComplexAction' // complexaction can only be accessed if $this->canComplexAction() returns true
  51. * );
  52. * </code>
  53. */
  54. static $allowed_actions = null;
  55. /**
  56. * Handles URL requests.
  57. *
  58. * - ViewableData::handleRequest() iterates through each rule in {@link self::$url_handlers}.
  59. * - If the rule matches, the named method will be called.
  60. * - If there is still more URL to be processed, then handleRequest() is called on the object that that method returns.
  61. *
  62. * Once all of the URL has been processed, the final result is returned. However, if the final result is an array, this
  63. * array is interpreted as being additional template data to customise the 2nd to last result with, rather than an object
  64. * in its own right. This is most frequently used when a Controller's action will return an array of data with which to
  65. * customise the controller.
  66. *
  67. * @param $params The parameters taken from the parsed URL of the parent url handler
  68. * @param $request The {@link HTTPRequest} object that is reponsible for distributing URL parsing
  69. * @uses HTTPRequest
  70. * @uses HTTPRequest->match()
  71. * @return HTTPResponse|RequestHandler|string|array
  72. */
  73. function handleRequest($request) {
  74. $this->request = $request;
  75. // $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance
  76. $handlerClass = ($this->class) ? $this->class : get_class($this);
  77. // We stop after RequestHandler; in other words, at ViewableData
  78. while($handlerClass && $handlerClass != 'ViewableData') {
  79. // Todo: ajshort's stat rewriting could be useful here.
  80. $urlHandlers = eval("return $handlerClass::\$url_handlers;");
  81. if($urlHandlers) foreach($urlHandlers as $rule => $action) {
  82. if(isset($_REQUEST['debug_request'])) Debug::message("Testing '$rule' with '" . $request->remaining() . "' on $this->class");
  83. if($params = $request->match($rule, true)) {
  84. // FIXME: This unnecessary coupling was added to fix a bug in Image_Uploader.
  85. if($this instanceof Controller) $this->urlParams = $request->allParams();
  86. if(isset($_REQUEST['debug_request'])) {
  87. Debug::message("Rule '$rule' matched to action '$action' on $this->class. Latest request params: " . var_export($request->latestParams(), true));
  88. }
  89. // Actions can reference URL parameters, eg, '$Action/$ID/$OtherID' => '$Action',
  90. if($action[0] == '$') $action = $params[substr($action,1)];
  91. if($this->checkAccessAction($action)) {
  92. if(!$action) {
  93. if(isset($_REQUEST['debug_request'])) Debug::message("Action not set; using default action method name 'index'");
  94. $action = "index";
  95. } else if(!is_string($action)) {
  96. user_error("Non-string method name: " . var_export($action, true), E_USER_ERROR);
  97. }
  98. $result = $this->$action($request);
  99. } else {
  100. return $this->httpError(403, "Action '$action' isn't allowed on class $this->class");
  101. }
  102. if($result instanceof HTTPResponse && $result->isError()) {
  103. if(isset($_REQUEST['debug_request'])) Debug::message("Rule resulted in HTTP error; breaking");
  104. return $result;
  105. }
  106. // If we return a RequestHandler, call handleRequest() on that, even if there is no more URL to parse.
  107. // It might have its own handler. However, we only do this if we haven't just parsed an empty rule ourselves,
  108. // to prevent infinite loops
  109. if(!$request->isEmptyPattern($rule) && is_object($result) && $result instanceof RequestHandler) {
  110. $returnValue = $result->handleRequest($request);
  111. // Array results can be used to handle
  112. if(is_array($returnValue)) $returnValue = $this->customise($returnValue);
  113. return $returnValue;
  114. // If we return some other data, and all the URL is parsed, then return that
  115. } else if($request->allParsed()) {
  116. return $result;
  117. // But if we have more content on the URL and we don't know what to do with it, return an error.
  118. } else {
  119. return $this->httpError(400, "I can't handle sub-URLs of a $this->class object.");
  120. }
  121. return $this;
  122. }
  123. }
  124. $handlerClass = get_parent_class($handlerClass);
  125. }
  126. // If nothing matches, return this object
  127. return $this;
  128. }
  129. /**
  130. * Check that the given action is allowed to be called from a URL.
  131. * It will interrogate {@link self::$allowed_actions} to determine this.
  132. */
  133. function checkAccessAction($action) {
  134. // Collate self::$allowed_actions from this class and all parent classes
  135. $access = null;
  136. $className = ($this->class) ? $this->class : get_class($this);
  137. while($className && $className != 'RequestHandler') {
  138. // Merge any non-null parts onto $access.
  139. $accessPart = eval("return $className::\$allowed_actions;");
  140. if($accessPart != null) $access = array_merge((array)$access, $accessPart);
  141. // Build an array of parts for checking if part[0] == part[1], which means that this class doesn't directly define it.
  142. $accessParts[] = $accessPart;
  143. $className = get_parent_class($className);
  144. }
  145. // Add $allowed_actions from extensions
  146. if($this->extension_instances) {
  147. foreach($this->extension_instances as $inst) {
  148. $accessPart = $inst->stat('allowed_actions');
  149. if($accessPart !== null) $access = array_merge((array)$access, $accessPart);
  150. }
  151. }
  152. if($action == 'index') return true;
  153. // Make checkAccessAction case-insensitive
  154. $action = strtolower($action);
  155. if($access) {
  156. foreach($access as $k => $v) $newAccess[strtolower($k)] = strtolower($v);
  157. $access = $newAccess;
  158. if(isset($access[$action])) {
  159. $test = $access[$action];
  160. if($test === true) return true;
  161. if(substr($test,0,2) == '->') {
  162. $funcName = substr($test,2);
  163. return $this->$funcName();
  164. }
  165. if(Permission::check($test)) return true;
  166. } else if((($key = array_search($action, $access)) !== false) && is_numeric($key)) {
  167. return true;
  168. }
  169. }
  170. if($access === null || (isset($accessParts[1]) && $accessParts[0] === $accessParts[1])) {
  171. // If no allowed_actions are provided, then we should only let through actions that aren't handled by magic methods
  172. // we test this by calling the unmagic method_exists and comparing it to the magic $this->hasMethod(). This will
  173. // still let through actions that are handled by templates.
  174. return method_exists($this, $action) || !$this->hasMethod($action);
  175. }
  176. return false;
  177. }
  178. /**
  179. * Throw an HTTP error instead of performing the normal processing
  180. * @todo This doesn't work properly right now. :-(
  181. */
  182. function httpError($errorCode, $errorMessage = null) {
  183. $r = new HTTPResponse();
  184. $r->setBody($errorMessage);
  185. $r->setStatuscode($errorCode);
  186. return $r;
  187. }
  188. /**
  189. * Returns the HTTPRequest object that this controller is using.
  190. *
  191. * @return HTTPRequest
  192. */
  193. function getRequest() {
  194. return $this->request;
  195. }
  196. }