PageRenderTime 92ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/library/XenForo/FrontController.php

https://github.com/hanguyenhuu/DTUI_201105
PHP | 590 lines | 362 code | 68 blank | 160 comment | 53 complexity | 2d1f722e682ef6e7b6b5735eeb79cfb3 MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, BSD-3-Clause
  1. <?php
  2. /**
  3. * Class to manage most of the flow of a request to a XenForo page.
  4. *
  5. * This class: resolves a URL to a route, loads a specified controller, executes an action
  6. * in that controller, loads the view, renders the view, and outputs the response.
  7. *
  8. * Most dependent objects can be injected
  9. *
  10. * @package XenForo_Mvc
  11. */
  12. class XenForo_FrontController
  13. {
  14. /**
  15. * An object that is able to load the dependencies needed to use this front controller.
  16. *
  17. * @var XenForo_Dependencies_Abstract
  18. */
  19. protected $_dependencies;
  20. /**
  21. * Request object.
  22. *
  23. * @see setRequest()
  24. * @var Zend_Controller_Request_Http
  25. */
  26. protected $_request;
  27. /**
  28. * Response object.
  29. *
  30. * @see setResponse()
  31. * @var Zend_Controller_Response_Http
  32. */
  33. protected $_response;
  34. /**
  35. * Controls whether calling {@link run()} prints the response via the {@link $_response} object.
  36. * If set to false, the response is returned instead.
  37. *
  38. * @see setSendResponse()
  39. * @var boolean
  40. */
  41. protected $_sendResponse = true;
  42. /**
  43. * Constructor. Sets up dependencies.
  44. *
  45. * @param XenForo_Dependencies_Abstract
  46. */
  47. public function __construct(XenForo_Dependencies_Abstract $dependencies)
  48. {
  49. $this->_dependencies = $dependencies;
  50. }
  51. /**
  52. * Setter for {@link $_request}.
  53. *
  54. * @param Zend_Controller_Request_Http
  55. */
  56. public function setRequest(Zend_Controller_Request_Http $request)
  57. {
  58. $this->_request = $request;
  59. }
  60. /**
  61. * Setter for {@link $_response}.
  62. *
  63. * @param Zend_Controller_Response_Http
  64. */
  65. public function setResponse(Zend_Controller_Response_Http $response)
  66. {
  67. $this->_response = $response;
  68. }
  69. /**
  70. * @return Zend_Controller_Request_Http
  71. */
  72. public function getRequest()
  73. {
  74. return $this->_request;
  75. }
  76. /**
  77. * @return Zend_Controller_Response_Http
  78. */
  79. public function getResponse()
  80. {
  81. return $this->_response;
  82. }
  83. /**
  84. * @return XenForo_Dependencies_Abstract
  85. */
  86. public function getDependencies()
  87. {
  88. return $this->_dependencies;
  89. }
  90. /**
  91. * Setter for {@link $_sendResponse}.
  92. *
  93. * @param boolean
  94. */
  95. public function setSendResponse($sendResponse)
  96. {
  97. $this->_sendResponse = (bool)$sendResponse;
  98. }
  99. /**
  100. * Runs the request, handling from routing straight through to response output.
  101. * Primary method to be used by the external API.
  102. *
  103. * @return string|null Returns a string if {@link $_sendResponse} is false
  104. */
  105. public function run()
  106. {
  107. ob_start();
  108. $this->setup();
  109. $this->setRequestPaths();
  110. $showDebugOutput = $this->showDebugOutput();
  111. $this->_dependencies->preLoadData();
  112. XenForo_CodeEvent::fire('front_controller_pre_route', array($this));
  113. $routeMatch = $this->route();
  114. XenForo_CodeEvent::fire('front_controller_pre_dispatch', array($this, &$routeMatch));
  115. $controllerResponse = $this->dispatch($routeMatch);
  116. if (!$controllerResponse)
  117. {
  118. XenForo_Error::noControllerResponse($routeMatch, $this->_request);
  119. exit;
  120. }
  121. $viewRenderer = $this->_getViewRenderer($routeMatch->getResponseType());
  122. if (!$viewRenderer)
  123. {
  124. // note: should only happen if there's an error getting the default renderer, which should never happen :)
  125. XenForo_Error::noViewRenderer($this->_request);
  126. exit;
  127. }
  128. $containerParams = array(
  129. 'majorSection' => $routeMatch->getMajorSection(),
  130. 'minorSection' => $routeMatch->getMinorSection()
  131. );
  132. XenForo_CodeEvent::fire('front_controller_pre_view',
  133. array($this, &$controllerResponse, &$viewRenderer, &$containerParams)
  134. );
  135. $content = $this->renderView($controllerResponse, $viewRenderer, $containerParams);
  136. if ($showDebugOutput)
  137. {
  138. $content = $this->renderDebugOutput($content);
  139. }
  140. $bufferedContents = ob_get_contents();
  141. ob_end_clean();
  142. if ($bufferedContents !== '')
  143. {
  144. $content = $bufferedContents . $content;
  145. }
  146. XenForo_CodeEvent::fire('front_controller_post_view', array($this, &$content));
  147. if ($this->_sendResponse)
  148. {
  149. $headers = $this->_response->getHeaders();
  150. $isText = false;
  151. foreach ($headers AS $header)
  152. {
  153. if ($header['name'] == 'Content-Type')
  154. {
  155. if (strpos($header['value'], 'text/') === 0)
  156. {
  157. $isText = true;
  158. }
  159. break;
  160. }
  161. }
  162. if ($isText && is_string($content) && $content)
  163. {
  164. $extraHeaders = XenForo_Application::gzipContentIfSupported($content);
  165. foreach ($extraHeaders AS $extraHeader)
  166. {
  167. $this->_response->setHeader($extraHeader[0], $extraHeader[1], $extraHeader[2]);
  168. }
  169. }
  170. if (is_string($content) && $content && !ob_get_level() && XenForo_Application::get('config')->enableContentLength)
  171. {
  172. $this->_response->setHeader('Content-Length', strlen($content), true);
  173. }
  174. $this->_response->sendHeaders();
  175. if ($content instanceof XenForo_FileOutput)
  176. {
  177. $content->output();
  178. }
  179. else
  180. {
  181. echo $content;
  182. }
  183. }
  184. else
  185. {
  186. return $content;
  187. }
  188. }
  189. /**
  190. * Sets up the default version of objects needed to run.
  191. */
  192. public function setup()
  193. {
  194. if (!$this->_request)
  195. {
  196. $this->_request = new Zend_Controller_Request_Http();
  197. }
  198. if (!$this->_response)
  199. {
  200. $this->_response = new Zend_Controller_Response_Http();
  201. }
  202. }
  203. /**
  204. * Sets the request paths for this request.
  205. */
  206. public function setRequestPaths()
  207. {
  208. $requestPaths = XenForo_Application::getRequestPaths($this->_request);
  209. XenForo_Application::set('requestPaths', $requestPaths);
  210. }
  211. /**
  212. * Determines if the full debug output show be shown. This usually depends
  213. * on application configuration and a request param.
  214. *
  215. * @return boolean
  216. */
  217. public function showDebugOutput()
  218. {
  219. return ($this->_request->getParam('_debug') && XenForo_Application::debugMode());
  220. }
  221. /**
  222. * Sends the request to the router for routing.
  223. *
  224. * @return XenForo_RouteMatch
  225. */
  226. public function route()
  227. {
  228. $return = $this->_dependencies->route($this->_request);
  229. if (!$return || !$return->getControllerName())
  230. {
  231. list($controllerName, $action) = $this->_dependencies->getNotFoundErrorRoute();
  232. $return->setControllerName($controllerName);
  233. $return->setAction($action);
  234. }
  235. return $return;
  236. }
  237. /**
  238. * Executes the controller dispatch loop.
  239. *
  240. * @param XenForo_RouteMatch $routeMatch
  241. *
  242. * @return XenForo_ControllerResponse_Abstract|null Null will only occur if error handling is broken
  243. */
  244. public function dispatch(XenForo_RouteMatch $routeMatch)
  245. {
  246. $reroute = array(
  247. 'controllerName' => $routeMatch->getControllerName(),
  248. 'action' => $routeMatch->getAction()
  249. );
  250. $allowReroute = true;
  251. do
  252. {
  253. $controllerResponse = null;
  254. $controllerName = $reroute['controllerName'];
  255. $action = str_replace(array('-', '/'), ' ', strtolower($reroute['action']));
  256. $action = str_replace(' ', '', ucwords($action));
  257. if ($action === '')
  258. {
  259. $action = 'Index';
  260. }
  261. $reroute = false;
  262. $controller = $this->_getValidatedController($controllerName, $action, $routeMatch);
  263. if ($controller)
  264. {
  265. try
  266. {
  267. try
  268. {
  269. $controller->preDispatch($action);
  270. $controllerResponse = $controller->{'action' . $action}();
  271. }
  272. catch (XenForo_ControllerResponse_Exception $e)
  273. {
  274. $controllerResponse = $e->getControllerResponse();
  275. }
  276. $controller->postDispatch($controllerResponse, $controllerName, $action);
  277. $reroute = $this->_handleControllerResponse($controllerResponse, $controllerName, $action);
  278. }
  279. catch (Exception $e)
  280. {
  281. // this is a bit hacky, but it's a selective catch so it's a strange case
  282. if ($e instanceof XenForo_Exception && $e->isUserPrintable())
  283. {
  284. $controllerResponse = $this->_getErrorResponseFromException($e);
  285. $controller->postDispatch($controllerResponse, $controllerName, $action);
  286. }
  287. else
  288. {
  289. if (!$allowReroute)
  290. {
  291. break;
  292. }
  293. $reroute = $this->_rerouteServerError($e);
  294. $allowReroute = false;
  295. XenForo_Error::logException($e);
  296. }
  297. }
  298. $responseType = $controller->getResponseType();
  299. $this->_dependencies->mergeViewStateChanges($controller->getViewStateChanges());
  300. }
  301. else
  302. {
  303. if (!$allowReroute)
  304. {
  305. break;
  306. }
  307. $reroute = $this->_rerouteNotFound($controllerName, $action);
  308. $allowReroute = false;
  309. }
  310. }
  311. while ($reroute);
  312. if ($controllerResponse instanceof XenForo_ControllerResponse_Abstract)
  313. {
  314. $controllerResponse->controllerName = $controllerName;
  315. $controllerResponse->controllerAction = $action;
  316. }
  317. return $controllerResponse;
  318. }
  319. /**
  320. * Called when a printable exception occurs, to get the controller response object.
  321. *
  322. * @param Exception Exception that occurred
  323. *
  324. * @return XenForo_ControllerResponse_Abstract
  325. */
  326. protected function _getErrorResponseFromException(Exception $e)
  327. {
  328. if (method_exists($e, 'getMessages'))
  329. {
  330. $message = $e->getMessages();
  331. }
  332. else
  333. {
  334. $message = $e->getMessage();
  335. }
  336. $controllerResponse = new XenForo_ControllerResponse_Error();
  337. $controllerResponse->errorText = $message;
  338. return $controllerResponse;
  339. }
  340. /**
  341. * Loads the controller only if it and the specified action have been validated as callable.
  342. *
  343. * @param string Name of the controller to load
  344. * @param string Name of the action to run
  345. * @param XenForo_RouteMatch Route match for this request (may not match controller)
  346. *
  347. * @return XenForo_Controller|null
  348. */
  349. protected function _getValidatedController($controllerName, $action, XenForo_RouteMatch $routeMatch)
  350. {
  351. $controllerName = XenForo_Application::resolveDynamicClass($controllerName, 'controller');
  352. if ($controllerName)
  353. {
  354. $controller = new $controllerName($this->_request, $this->_response, $routeMatch);
  355. if (method_exists($controller, 'action' . $action) && $this->_dependencies->allowControllerDispatch($controller, $action))
  356. {
  357. return $controller;
  358. }
  359. }
  360. return null;
  361. }
  362. /**
  363. * Handles a controller response to determine if something failed or a reroute is needed.
  364. *
  365. * @param mixed Exceptions will be thrown if is not {@link XenForo_ControllerResponse_Abstract}
  366. * @param string Name of the controller that generated this response
  367. * @param string Name of the action that generated this response
  368. *
  369. * @return false|array False if no reroute is needed. Array with keys controllerName and action if needed.
  370. */
  371. protected function _handleControllerResponse($controllerResponse, $controllerName, $action)
  372. {
  373. if (!$controllerResponse)
  374. {
  375. throw new XenForo_Exception("No controller response from $controllerName::action$action");
  376. }
  377. else if (!($controllerResponse instanceof XenForo_ControllerResponse_Abstract))
  378. {
  379. throw new XenForo_Exception("Invalid controller response from $controllerName::action$action");
  380. }
  381. else if ($controllerResponse instanceof XenForo_ControllerResponse_Reroute)
  382. {
  383. if ($controllerResponse->controllerName == $controllerName && strtolower($controllerResponse->action) == strtolower($action))
  384. {
  385. throw new XenForo_Exception("Cannot reroute controller to itself ($controllerName::action$action)");
  386. }
  387. return array(
  388. 'controllerName' => $controllerResponse->controllerName,
  389. 'action' => $controllerResponse->action
  390. );
  391. }
  392. return false;
  393. }
  394. /**
  395. * Returns information about how to reroute if a server error occurs.
  396. *
  397. * @param Exception Exception object that triggered the error
  398. *
  399. * @return array Reroute array
  400. */
  401. protected function _rerouteServerError(Exception $e)
  402. {
  403. $this->_request->setParam('_exception', $e);
  404. list($controllerName, $action) = $this->_dependencies->getServerErrorRoute();
  405. return array(
  406. 'controllerName' => $controllerName,
  407. 'action' => $action
  408. );
  409. }
  410. /**
  411. * Returns information about how to reroute if a page not found error occurs.
  412. *
  413. * @param string Controller name
  414. * @param string Action
  415. *
  416. * @return array Reroute array
  417. */
  418. protected function _rerouteNotFound($controllerName, $action)
  419. {
  420. $this->_request->setParams(array(
  421. '_controllerName' => $controllerName,
  422. '_action' => $action
  423. ));
  424. list($controllerName, $action) = $this->_dependencies->getNotFoundErrorRoute();
  425. return array(
  426. 'controllerName' => $controllerName,
  427. 'action' => $action
  428. );
  429. }
  430. /**
  431. * Gets the view renderer for the specified response type.
  432. *
  433. * @param string Response type (eg, html, xml, json)
  434. *
  435. * @return XenForo_ViewRenderer_Abstract
  436. */
  437. protected function _getViewRenderer($responseType)
  438. {
  439. return $this->_dependencies->getViewRenderer($this->_response, $responseType, $this->_request);
  440. }
  441. /**
  442. * Renders the view.
  443. *
  444. * @param XenForo_ControllerResponse_Abstract Controller response object from last controller
  445. * @param XenForo_ViewRenderer_Abstract View renderer for specified response type
  446. * @param array Extra container params (probably "sections" from routing)
  447. *
  448. * @return string View output
  449. */
  450. public function renderView(XenForo_ControllerResponse_Abstract $controllerResponse, XenForo_ViewRenderer_Abstract $viewRenderer, array $containerParams = array())
  451. {
  452. $this->_dependencies->preRenderView($controllerResponse);
  453. $this->_response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true);
  454. if ($controllerResponse->responseCode)
  455. {
  456. $this->_response->setHttpResponseCode($controllerResponse->responseCode);
  457. }
  458. if ($controllerResponse instanceof XenForo_ControllerResponse_Error)
  459. {
  460. $innerContent = $viewRenderer->renderError($controllerResponse->errorText);
  461. }
  462. else if ($controllerResponse instanceof XenForo_ControllerResponse_Message)
  463. {
  464. $innerContent = $viewRenderer->renderMessage($controllerResponse->message);
  465. }
  466. else if ($controllerResponse instanceof XenForo_ControllerResponse_View)
  467. {
  468. $innerContent = $viewRenderer->renderView(
  469. $controllerResponse->viewName, $controllerResponse->params, $controllerResponse->templateName,
  470. $controllerResponse->subView
  471. );
  472. }
  473. else if ($controllerResponse instanceof XenForo_ControllerResponse_Redirect)
  474. {
  475. $target = XenForo_Link::convertUriToAbsoluteUri($controllerResponse->redirectTarget);
  476. $innerContent = $viewRenderer->renderRedirect(
  477. $controllerResponse->redirectType,
  478. $target,
  479. $controllerResponse->redirectMessage,
  480. $controllerResponse->redirectParams
  481. );
  482. }
  483. else
  484. {
  485. // generally shouldn't happen
  486. $innerContent = false;
  487. }
  488. if ($innerContent === false || $innerContent === null)
  489. {
  490. $innerContent = $viewRenderer->renderUnrepresentable();
  491. }
  492. if ($viewRenderer->getNeedsContainer())
  493. {
  494. $specificContainerParams = XenForo_Application::mapMerge(
  495. $containerParams,
  496. $controllerResponse->containerParams
  497. );
  498. $containerParams = $this->_dependencies->getEffectiveContainerParams($specificContainerParams, $this->_request);
  499. return $viewRenderer->renderContainer($innerContent, $containerParams);
  500. }
  501. else
  502. {
  503. return $innerContent;
  504. }
  505. }
  506. /**
  507. * Renders page-level debugging output and replaces the original view content
  508. * with it. Alternatively, it could inject itself into the view content.
  509. *
  510. * @param string $originalContent Original, rendered view content
  511. *
  512. * @return string Replacement rendered view content
  513. */
  514. public function renderDebugOutput($originalContent)
  515. {
  516. $this->_response->clearHeaders();
  517. $this->_response->setHttpResponseCode(200);
  518. $this->_response->setHeader('Content-Type', 'text/html; charset=UTF-8', true);
  519. $this->_response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true);
  520. return XenForo_Debug::getDebugHtml();
  521. }
  522. }