PageRenderTime 38ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/pkp/classes/core/PKPComponentRouter.inc.php

https://github.com/lib-uoguelph-ca/ocs
PHP | 487 lines | 182 code | 72 blank | 233 comment | 39 complexity | 120a4577f6c54e40c7cfe98425acca6a MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. /**
  3. * @file classes/core/PKPComponentRouter.inc.php
  4. *
  5. * Copyright (c) 2000-2012 John Willinsky
  6. * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING.
  7. *
  8. * @class PKPComponentRouter
  9. * @ingroup core
  10. *
  11. * @brief Class mapping an HTTP request to a component handler operation.
  12. *
  13. * We are using an RPC style URL-to-endpoint mapping. Our approach follows
  14. * a simple "convention-over-configuration" paradigm. If necessary the
  15. * router can be subclassed to implement more complex URL-to-endpoint mappings.
  16. *
  17. * For servers with path info enabled the component URL has the following elements:
  18. *
  19. * .../index.php/context1/context2/$$$call$$$/path/to/handler-class/operation-name?arg1=...&arg2=...
  20. *
  21. * where "$$$call$$$" is a non-mutable literal string and "path/to" is
  22. * by convention the directory path below the components folder leading to the
  23. * component. The next element ("handler-class" in this example) will be mapped to a
  24. * component class file by "camelizing" the string to "HandlerClassHandler" and adding
  25. * ".inc.php" to the end. The "operation-name" is transformed to "operationName"
  26. * and represents the name of the handler method to be called. Finally "arg1", "arg2",
  27. * etc. are parameters to be passed along to the handler method.
  28. *
  29. * For servers with path info disabled the component URL looks like this:
  30. *
  31. * .../index.php?component=path.to.handler-class&op=operation-name&arg1=...&arg2=...
  32. *
  33. * The router will sanitize the request URL to a certain amount to make sure that
  34. * random code inclusions are prevented. User authorization and parameter validation
  35. * are however not the router's concern. These must be implemented on handler level.
  36. *
  37. * NB: Component and operation names may only contain a-z, 0-9 and hyphens. Numbers
  38. * are not allowed at the beginning of a name or after a hyphen.
  39. *
  40. * NB: Component handlers must implement an initialize() method that will be called
  41. * before the request is routed. The initialization method must enforce authorization
  42. * and request validation.
  43. */
  44. // $Id$
  45. // The string to be found in the URL to mark this request as a component request
  46. define('COMPONENT_ROUTER_PATHINFO_MARKER', '$$$call$$$');
  47. // The parameter to be found in the query string for servers with path info disabled
  48. define('COMPONENT_ROUTER_PARAMETER_MARKER', 'component');
  49. // This is the maximum directory depth allowed within the component directory. Set
  50. // it to something reasonable to avoid DoS or overflow attacks
  51. define ('COMPONENT_ROUTER_PARTS_MAXDEPTH', 5);
  52. // This is the maximum/minimum length of the name of a sub-directory or
  53. // handler class name.
  54. define ('COMPONENT_ROUTER_PARTS_MAXLENGTH', 50);
  55. define ('COMPONENT_ROUTER_PARTS_MINLENGTH', 2);
  56. import('core.PKPRouter');
  57. import('core.Request');
  58. class PKPComponentRouter extends PKPRouter {
  59. //
  60. // Internal state cache variables
  61. // NB: Please do not access directly but
  62. // only via their respective getters/setters
  63. //
  64. /** @var string the requested component handler */
  65. var $_component;
  66. /** @var string the requested operation */
  67. var $_op;
  68. /** @var array the rpc service endpoint parts from the request */
  69. var $_rpcServiceEndpointParts = false;
  70. /** @var callable the rpc service endpoint the request was routed to */
  71. var $_rpcServiceEndpoint = false;
  72. /**
  73. * Determines whether this router can route the given request.
  74. * @param $request PKPRequest
  75. * @return boolean true, if the router supports this request, otherwise false
  76. */
  77. function supports(&$request) {
  78. // See whether we can resolve the request to
  79. // a valid service endpoint.
  80. return is_callable($this->getRpcServiceEndpoint($request));
  81. }
  82. /**
  83. * Routes the given request to a page handler
  84. * @param $request PKPRequest
  85. */
  86. function route(&$request) {
  87. // Determine the requested service endpoint.
  88. $rpcServiceEndpoint =& $this->getRpcServiceEndpoint($request);
  89. assert(is_callable($rpcServiceEndpoint));
  90. // Retrieve RPC arguments from the request.
  91. $args =& $request->getUserVars();
  92. assert(is_array($args));
  93. // Remove the caller-parameter (if present)
  94. if (isset($args[COMPONENT_ROUTER_PARAMETER_MARKER])) unset($args[COMPONENT_ROUTER_PARAMETER_MARKER]);
  95. // Authorize and validate the request
  96. if (!$rpcServiceEndpoint[0]->validate(null, $request)) {
  97. // Components must always validate otherwise this is
  98. // either a programming error or somebody trying to
  99. // directly call a component. In both cases a fatal
  100. // error is the approprate response.
  101. fatalError('Permission denied!');
  102. }
  103. // Initialize the handler
  104. $rpcServiceEndpoint[0]->initialize($request);
  105. // Call the service endpoint.
  106. $result = call_user_func($rpcServiceEndpoint, $args, $request);
  107. echo $result;
  108. }
  109. /**
  110. * Retrieve the requested component from the request.
  111. *
  112. * NB: This can be a component that not actually exists
  113. * in the code base.
  114. *
  115. * @param $request PKPRequest
  116. * @return string the requested component or an empty string
  117. * if none can be found.
  118. */
  119. function getRequestedComponent(&$request) {
  120. if (is_null($this->_component)) {
  121. $this->_component = '';
  122. // Retrieve the service endpoint parts from the request.
  123. if (is_null($rpcServiceEndpointParts = $this->_getValidatedServiceEndpointParts($request))) {
  124. // Endpoint parts cannot be found in the request
  125. return '';
  126. }
  127. // Pop off the operation part
  128. array_pop($rpcServiceEndpointParts);
  129. // Construct the fully qualified component class name from the rest of it.
  130. $handlerClassName = String::camelize(array_pop($rpcServiceEndpointParts), CAMEL_CASE_HEAD_UP).'Handler';
  131. // camelize remaining endpoint parts
  132. $camelizedRpcServiceEndpointParts = array();
  133. foreach ( $rpcServiceEndpointParts as $part) {
  134. $camelizedRpcServiceEndpointParts[] = String::camelize($part, CAMEL_CASE_HEAD_DOWN);
  135. }
  136. $handlerPackage = implode('.', $camelizedRpcServiceEndpointParts);
  137. $this->_component = $handlerPackage.'.'.$handlerClassName;
  138. }
  139. return $this->_component;
  140. }
  141. /**
  142. * Retrieve the requested operation from the request
  143. *
  144. * NB: This can be an operation that not actually
  145. * exists in the requested component.
  146. *
  147. * @param $request PKPRequest
  148. * @return string the requested operation or an empty string
  149. * if none can be found.
  150. */
  151. function getRequestedOp(&$request) {
  152. if (is_null($this->_op)) {
  153. $this->_op = '';
  154. // Retrieve the service endpoint parts from the request.
  155. if (is_null($rpcServiceEndpointParts = $this->_getValidatedServiceEndpointParts($request))) {
  156. // Endpoint parts cannot be found in the request
  157. return '';
  158. }
  159. // Pop off the operation part
  160. $this->_op = String::camelize(array_pop($rpcServiceEndpointParts), CAMEL_CASE_HEAD_DOWN);
  161. }
  162. return $this->_op;
  163. }
  164. /**
  165. * Get the (validated) RPC service endpoint from the request.
  166. * If no such RPC service endpoint can be constructed then the method
  167. * returns null.
  168. * @param $request PKPRequest the request to be routed
  169. * @return callable an array with the handler instance
  170. * and the handler operation to be called by call_user_func().
  171. */
  172. function &getRpcServiceEndpoint(&$request) {
  173. if ($this->_rpcServiceEndpoint === false) {
  174. // We have not yet resolved this request. Mark the
  175. // state variable so that we don't try again next
  176. // time.
  177. $this->_rpcServiceEndpoint = $nullVar = null;
  178. //
  179. // Component Handler
  180. //
  181. // Retrieve requested component handler
  182. $component = $this->getRequestedComponent($request);
  183. if (empty($component)) return $nullVar;
  184. // Construct the component handler file name and test its existence.
  185. $component = 'controllers.'.$component;
  186. $componentFileName = str_replace('.', '/', $component).'.inc.php';
  187. if (!file_exists($componentFileName) && !file_exists('lib/pkp/'.$componentFileName)) {
  188. // Request to non-existent handler
  189. return $nullVar;
  190. }
  191. // Declare the component handler class.
  192. import($component);
  193. // Check that the component class has really been declared
  194. $componentClassName = substr($component, strrpos($component, '.') + 1);
  195. assert(class_exists($componentClassName));
  196. //
  197. // Operation
  198. //
  199. // Retrieve requested component operation
  200. $op = $this->getRequestedOp($request);
  201. assert(!empty($op));
  202. // Check that the requested operation exists for the handler:
  203. // Lowercase comparison for PHP4 compatibility. Also check the required
  204. // validate() and initialize() methods.
  205. $methods = array_map('strtolower', get_class_methods($componentClassName));
  206. foreach(array(strtolower($op), 'validate', 'initialize') as $requiredMethod) {
  207. if (!in_array($requiredMethod, $methods)) return $nullVar;
  208. }
  209. //
  210. // Callable service endpoint
  211. //
  212. // Instantiate the handler
  213. $componentInstance = new $componentClassName();
  214. // Check that the component instance really is a handler
  215. if (!is_a($componentInstance, 'PKPHandler')) return $nullVar;
  216. // Check that the requested operation is on the
  217. // remote operation whitelist.
  218. if (!in_array($op, $componentInstance->getRemoteOperations())) return $nullVar;
  219. // Construct the callable array
  220. $this->_rpcServiceEndpoint = array($componentInstance, $op);
  221. }
  222. return $this->_rpcServiceEndpoint;
  223. }
  224. /**
  225. * Build a component request URL into PKPApplication.
  226. * @param $request PKPRequest the request to be routed
  227. * @param $context mixed Optional contextual paths
  228. * @param $component string Optional name of page to invoke
  229. * @param $op string Optional name of operation to invoke
  230. * @param $path for compatibility only, not supported for the component router.
  231. * @param $params array Optional set of name => value pairs to pass as user parameters
  232. * @param $anchor string Optional name of anchor to add to URL
  233. * @param $escape boolean Whether or not to escape ampersands for this URL; default false.
  234. * @return string the URL
  235. */
  236. function url(&$request, $newContext = null, $component = null, $op = null, $path = null,
  237. $params = null, $anchor = null, $escape = false) {
  238. assert(is_null($path));
  239. $pathInfoEnabled = $request->isPathInfoEnabled();
  240. //
  241. // Base URL and Context
  242. //
  243. $baseUrlAndContext = $this->_urlGetBaseAndContext(
  244. $request, $this->_urlCanonicalizeNewContext($newContext));
  245. $baseUrl = array_shift($baseUrlAndContext);
  246. $context = $baseUrlAndContext;
  247. //
  248. // Component and Operation
  249. //
  250. // We only support component/op retrieval from the request
  251. // if this request is a component request.
  252. $currentRequestIsAComponentRequest = is_a($request->getRouter(), 'PKPComponentRouter');
  253. if($currentRequestIsAComponentRequest) {
  254. if (empty($component)) $component = $this->getRequestedComponent($request);
  255. if (empty($op)) $op = $this->getRequestedOp($request);
  256. }
  257. assert(!empty($component) && !empty($op));
  258. // Encode the component and operation
  259. $componentParts = explode('.', $component);
  260. $componentName = array_pop($componentParts);
  261. assert(substr($componentName, -7) == 'Handler');
  262. $componentName = String::uncamelize(substr($componentName, 0, -7));
  263. // uncamelize the component parts
  264. $uncamelizedComponentParts = array();
  265. foreach ($componentParts as $part) {
  266. $uncamelizedComponentParts[] = String::uncamelize($part);
  267. }
  268. array_push($uncamelizedComponentParts, $componentName);
  269. $opName = String::uncamelize($op);
  270. //
  271. // Additional query parameters
  272. //
  273. $additionalParameters = $this->_urlGetAdditionalParameters($request, $params);
  274. //
  275. // Anchor
  276. //
  277. $anchor = (empty($anchor) ? '' : '#'.rawurlencode($anchor));
  278. //
  279. // Assemble URL
  280. //
  281. if ($pathInfoEnabled) {
  282. // If path info is enabled then context, page,
  283. // operation and additional path go into the
  284. // path info.
  285. $pathInfoArray = array_merge(
  286. $context,
  287. array(COMPONENT_ROUTER_PATHINFO_MARKER),
  288. $uncamelizedComponentParts,
  289. array($opName)
  290. );
  291. // Query parameters
  292. $queryParametersArray = $additionalParameters;
  293. } else {
  294. // If path info is disabled then context, page,
  295. // operation and additional path are encoded as
  296. // query parameters.
  297. $pathInfoArray = array();
  298. // Query parameters
  299. $queryParametersArray = array_merge(
  300. $context,
  301. array(
  302. COMPONENT_ROUTER_PARAMETER_MARKER.'='.implode('.', $uncamelizedComponentParts),
  303. "op=$opName"
  304. ),
  305. $additionalParameters
  306. );
  307. }
  308. return $this->_urlFromParts($baseUrl, $pathInfoArray, $queryParametersArray, $anchor, $escape);
  309. }
  310. //
  311. // Private helper methods
  312. //
  313. /**
  314. * Get the (validated) RPC service endpoint parts from the request.
  315. * If no such RPC service endpoint parts can be retrieved
  316. * then the method returns null.
  317. * @param $request PKPRequest the request to be routed
  318. * @return array a string array with the RPC service endpoint
  319. * parts as values.
  320. */
  321. function _getValidatedServiceEndpointParts(&$request) {
  322. if ($this->_rpcServiceEndpointParts === false) {
  323. // Mark the internal state variable so this
  324. // will not be called again.
  325. $this->_rpcServiceEndpointParts = null;
  326. // Retrieve service endpoint parts from the request.
  327. if (is_null($rpcServiceEndpointParts = $this->_retrieveServiceEndpointParts($request))) {
  328. // This is not an RPC request
  329. return null;
  330. }
  331. // Validate the service endpoint parts.
  332. if (is_null($rpcServiceEndpointParts = $this->_validateServiceEndpointParts($rpcServiceEndpointParts))) {
  333. // Invalid request
  334. return null;
  335. }
  336. // Assign the validated service endpoint parts
  337. $this->_rpcServiceEndpointParts = $rpcServiceEndpointParts;
  338. }
  339. return $this->_rpcServiceEndpointParts;
  340. }
  341. /**
  342. * Try to retrieve a (non-validated) array with the service
  343. * endpoint parts from the request. See the classdoc for the
  344. * URL patterns supported here.
  345. * @param $request PKPRequest the request to be routed
  346. * @return array an array of (non-validated) service endpoint
  347. * parts or null if the request is not an RPC request.
  348. */
  349. function _retrieveServiceEndpointParts(&$request) {
  350. // URL pattern depends on whether the server has path info
  351. // enabled or not. See classdoc for details.
  352. if ($request->isPathInfoEnabled()) {
  353. if (!isset($_SERVER['PATH_INFO'])) return null;
  354. $pathInfoParts = explode('/', trim($_SERVER['PATH_INFO'], '/'));
  355. // We expect at least the context + the component
  356. // router marker + 3 component parts (path, handler, operation)
  357. $application = $this->getApplication();
  358. $contextDepth = $application->getContextDepth();
  359. if (count($pathInfoParts) < $contextDepth + 4) {
  360. // This path info is too short to be an RPC request
  361. return null;
  362. }
  363. // Check the component router marker
  364. if ($pathInfoParts[$contextDepth] != COMPONENT_ROUTER_PATHINFO_MARKER) {
  365. // This is not an RPC request
  366. return null;
  367. }
  368. // Remove context and component marker from the array
  369. $rpcServiceEndpointParts = array_slice($pathInfoParts, $contextDepth + 1);
  370. } else {
  371. $componentParameter = $request->getUserVar(COMPONENT_ROUTER_PARAMETER_MARKER);
  372. $operationParameter = $request->getUserVar('op');
  373. if (is_null($componentParameter) || is_null($operationParameter)) {
  374. // This is not an RPC request
  375. return null;
  376. }
  377. // Expand the router parameter
  378. $rpcServiceEndpointParts = explode('.', $componentParameter);
  379. // Add the operation
  380. array_push($rpcServiceEndpointParts, $operationParameter);
  381. }
  382. return $rpcServiceEndpointParts;
  383. }
  384. /**
  385. * This method pre-validates the service endpoint parts before
  386. * we try to convert them to a file/method name. This also
  387. * converts all parts to lower case.
  388. * @param $rpcServiceEndpointParts array
  389. * @return array the validated service endpoint parts or null if validation
  390. * does not succeed.
  391. */
  392. function _validateServiceEndpointParts($rpcServiceEndpointParts) {
  393. // Do we have data at all?
  394. if (is_null($rpcServiceEndpointParts) || empty($rpcServiceEndpointParts)
  395. || !is_array($rpcServiceEndpointParts)) return null;
  396. // We require at least three parts: component directory, handler
  397. // and method name.
  398. if (count($rpcServiceEndpointParts) < 3) return null;
  399. // Check that the array dimensions remain within sane limits.
  400. if (count($rpcServiceEndpointParts) > COMPONENT_ROUTER_PARTS_MAXDEPTH) return null;
  401. // Validate the individual endpoint parts.
  402. foreach($rpcServiceEndpointParts as $key => $rpcServiceEndpointPart) {
  403. // Make sure that none of the elements exceeds the length limit.
  404. $partLen = strlen($rpcServiceEndpointPart);
  405. if ($partLen > COMPONENT_ROUTER_PARTS_MAXLENGTH
  406. || $partLen < COMPONENT_ROUTER_PARTS_MINLENGTH) return null;
  407. // Service endpoint URLs are case insensitive.
  408. $rpcServiceEndpointParts[$key] = strtolower($rpcServiceEndpointPart);
  409. // We only allow letters, numbers and the hyphen.
  410. if (!String::regexp_match('/^[a-z0-9-]*$/', $rpcServiceEndpointPart)) return null;
  411. }
  412. return $rpcServiceEndpointParts;
  413. }
  414. }
  415. ?>