Файл: library/XenForo/FrontController.php
Строк: 983
<?php
/**
* Class to manage most of the flow of a request to a XenForo page.
*
* This class: resolves a URL to a route, loads a specified controller, executes an action
* in that controller, loads the view, renders the view, and outputs the response.
*
* Most dependent objects can be injected
*
* @package XenForo_Mvc
*/
class XenForo_FrontController
{
/**
* An object that is able to load the dependencies needed to use this front controller.
*
* @var XenForo_Dependencies_Abstract
*/
protected $_dependencies;
/**
* Request object.
*
* @see setRequest()
* @var Zend_Controller_Request_Http
*/
protected $_request;
/**
* Response object.
*
* @see setResponse()
* @var Zend_Controller_Response_Http
*/
protected $_response;
/**
* Controls whether calling {@link run()} prints the response via the {@link $_response} object.
* If set to false, the response is returned instead.
*
* @see setSendResponse()
* @var boolean
*/
protected $_sendResponse = true;
/**
* Constructor. Sets up dependencies.
*
* @param XenForo_Dependencies_Abstract
*/
public function __construct(XenForo_Dependencies_Abstract $dependencies)
{
$this->_dependencies = $dependencies;
}
/**
* Setter for {@link $_request}.
*
* @param Zend_Controller_Request_Http
*/
public function setRequest(Zend_Controller_Request_Http $request)
{
$this->_request = $request;
}
/**
* Setter for {@link $_response}.
*
* @param Zend_Controller_Response_Http
*/
public function setResponse(Zend_Controller_Response_Http $response)
{
$this->_response = $response;
}
/**
* @return Zend_Controller_Request_Http
*/
public function getRequest()
{
return $this->_request;
}
/**
* @return Zend_Controller_Response_Http
*/
public function getResponse()
{
return $this->_response;
}
/**
* @return XenForo_Dependencies_Abstract
*/
public function getDependencies()
{
return $this->_dependencies;
}
/**
* Setter for {@link $_sendResponse}.
*
* @param boolean
*/
public function setSendResponse($sendResponse)
{
$this->_sendResponse = (bool)$sendResponse;
}
/**
* Runs the request, handling from routing straight through to response output.
* Primary method to be used by the external API.
*
* @return string|null Returns a string if {@link $_sendResponse} is false
*/
public function run()
{
ob_start();
XenForo_Application::set('fc', $this);
$this->setup();
$this->setRequestPaths();
$showDebugOutput = $this->showDebugOutput();
$this->_dependencies->preLoadData();
XenForo_CodeEvent::fire('front_controller_pre_route', array($this));
$routeMatch = $this->route();
XenForo_CodeEvent::fire('front_controller_pre_dispatch', array($this, &$routeMatch));
$controllerResponse = $this->dispatch($routeMatch);
if (!$controllerResponse)
{
XenForo_Error::noControllerResponse($routeMatch, $this->_request);
exit;
}
$viewRenderer = $this->_getViewRenderer($routeMatch->getResponseType());
if (!$viewRenderer)
{
// note: should only happen if there's an error getting the default renderer, which should never happen :)
XenForo_Error::noViewRenderer($this->_request);
exit;
}
$containerParams = array(
'majorSection' => $routeMatch->getMajorSection(),
'minorSection' => $routeMatch->getMinorSection()
);
XenForo_CodeEvent::fire('front_controller_pre_view',
array($this, &$controllerResponse, &$viewRenderer, &$containerParams)
);
$content = $this->renderView($controllerResponse, $viewRenderer, $containerParams);
if ($showDebugOutput)
{
$content = $this->renderDebugOutput($content);
}
$bufferedContents = ob_get_contents();
ob_end_clean();
if ($bufferedContents !== '' && is_string($content))
{
if (preg_match('#<body[^>]*>#sU', $content, $match))
{
$content = str_replace($match[0], $match[0] . $bufferedContents, $content);
}
else
{
$content = $bufferedContents . $content;
}
}
XenForo_CodeEvent::fire('front_controller_post_view', array($this, &$content));
if ($this->_sendResponse)
{
$headers = $this->_response->getHeaders();
$isText = false;
foreach ($headers AS $header)
{
if ($header['name'] == 'Content-Type')
{
if (strpos($header['value'], 'text/') === 0)
{
$isText = true;
}
break;
}
}
if ($isText && is_string($content) && $content)
{
$extraHeaders = XenForo_Application::gzipContentIfSupported($content);
foreach ($extraHeaders AS $extraHeader)
{
$this->_response->setHeader($extraHeader[0], $extraHeader[1], $extraHeader[2]);
}
}
if (is_string($content) && $content && !ob_get_level() && XenForo_Application::get('config')->enableContentLength)
{
if ($this->_response->getHttpResponseCode() >= 400
&& strpos($this->_request->getServer('HTTP_USER_AGENT', ''), 'IEMobile') !== false
)
{
// Windows mobile bug - 400+ errors cause the standard browser error
// to be output if a content length is sent. ...Err, what?
}
else
{
$this->_response->setHeader('Content-Length', strlen($content), true);
}
}
$this->_response->sendHeaders();
if ($content instanceof XenForo_FileOutput)
{
$content->output();
}
else
{
echo $content;
}
}
else
{
return $content;
}
}
/**
* Sets up the default version of objects needed to run.
*/
public function setup()
{
if (!$this->_request)
{
$this->_request = new Zend_Controller_Request_Http();
}
if (!$this->_response)
{
$this->_response = new Zend_Controller_Response_Http();
}
}
/**
* Sets the request paths for this request.
*/
public function setRequestPaths()
{
if (!XenForo_Application::isRegistered('requestPaths'))
{
$requestPaths = XenForo_Application::getRequestPaths($this->_request);
XenForo_Application::set('requestPaths', $requestPaths);
}
}
/**
* Determines if the full debug output show be shown. This usually depends
* on application configuration and a request param.
*
* @return boolean
*/
public function showDebugOutput()
{
return ($this->_request->getParam('_debug') && XenForo_Application::debugMode());
}
/**
* Sends the request to the router for routing.
*
* @param string|null $routePath
*
* @return XenForo_RouteMatch
*/
public function route($routePath = null)
{
$return = $this->_dependencies->route($this->_request, $routePath);
if (!$return || !$return->getControllerName())
{
list($controllerName, $action) = $this->_dependencies->getNotFoundErrorRoute();
if (!$return)
{
$return = new XenForo_RouteMatch();
}
$return->setControllerName($controllerName);
$return->setAction($action);
}
return $return;
}
/**
* Executes the controller dispatch loop.
*
* @param XenForo_RouteMatch $routeMatch
*
* @return XenForo_ControllerResponse_Abstract|null Null will only occur if error handling is broken
*/
public function dispatch(XenForo_RouteMatch &$routeMatch)
{
$reroute = array(
'controllerName' => $routeMatch->getControllerName(),
'action' => $routeMatch->getAction()
);
$allowReroute = true;
do
{
if (isset($reroute['path']))
{
$routeMatch = $this->route($reroute['path']);
$reroute = array(
'controllerName' => $routeMatch->getControllerName(),
'action' => $routeMatch->getAction()
);
}
$controllerResponse = null;
$controllerName = $reroute['controllerName'];
$action = str_replace(array('-', '/'), ' ', strtolower($reroute['action']));
$action = str_replace(' ', '', ucwords($action));
if ($action === '')
{
$action = 'Index';
}
$reroute = false;
$controller = $this->_getValidatedController($controllerName, $action, $routeMatch);
if ($controller)
{
try
{
try
{
$controller->preDispatch($action, $controllerName);
$controllerResponse = $controller->{'action' . $action}();
}
catch (XenForo_ControllerResponse_Exception $e)
{
$controllerResponse = $e->getControllerResponse();
}
$controller->postDispatch($controllerResponse, $controllerName, $action);
$reroute = $this->_handleControllerResponse($controllerResponse, $controllerName, $action);
}
catch (Exception $e)
{
// this is a bit hacky, but it's a selective catch so it's a strange case
if ($e instanceof XenForo_Exception && $e->isUserPrintable())
{
$controllerResponse = $this->_getErrorResponseFromException($e);
$controller->postDispatch($controllerResponse, $controllerName, $action);
}
else
{
if (!$allowReroute)
{
break;
}
$reroute = $this->_rerouteServerError($e);
$allowReroute = false;
XenForo_Error::logException($e);
}
}
$responseType = $controller->getResponseType();
$this->_dependencies->mergeViewStateChanges($controller->getViewStateChanges());
}
else
{
if (!$allowReroute)
{
break;
}
$reroute = $this->_rerouteNotFound($controllerName, $action);
$allowReroute = false;
}
}
while ($reroute);
if ($controllerResponse instanceof XenForo_ControllerResponse_Abstract)
{
$controllerResponse->controllerName = $controllerName;
$controllerResponse->controllerAction = $action;
}
return $controllerResponse;
}
/**
* Called when a printable exception occurs, to get the controller response object.
*
* @param Exception Exception that occurred
*
* @return XenForo_ControllerResponse_Abstract
*/
protected function _getErrorResponseFromException(Exception $e)
{
if (method_exists($e, 'getMessages'))
{
$message = $e->getMessages();
}
else
{
$message = $e->getMessage();
}
$controllerResponse = new XenForo_ControllerResponse_Error();
$controllerResponse->errorText = $message;
return $controllerResponse;
}
/**
* Loads the controller only if it and the specified action have been validated as callable.
*
* @param string Name of the controller to load
* @param string Name of the action to run
* @param XenForo_RouteMatch Route match for this request (may not match controller)
*
* @return XenForo_Controller|null
*/
protected function _getValidatedController($controllerName, $action, XenForo_RouteMatch $routeMatch)
{
$newControllerName = XenForo_Application::resolveDynamicClass($controllerName, 'controller');
if ($newControllerName)
{
$controller = new $newControllerName($this->_request, $this->_response, $routeMatch);
if (method_exists($controller, 'action' . $action) && $this->_dependencies->allowControllerDispatch($controller, $action))
{
return $controller;
}
}
return null;
}
/**
* Handles a controller response to determine if something failed or a reroute is needed.
*
* @param mixed Exceptions will be thrown if is not {@link XenForo_ControllerResponse_Abstract}
* @param string Name of the controller that generated this response
* @param string Name of the action that generated this response
*
* @return false|array False if no reroute is needed. Array with keys controllerName and action/path if needed.
*/
protected function _handleControllerResponse($controllerResponse, $controllerName, $action)
{
if (!$controllerResponse)
{
throw new XenForo_Exception("No controller response from $controllerName::action$action");
}
else if (!($controllerResponse instanceof XenForo_ControllerResponse_Abstract))
{
throw new XenForo_Exception("Invalid controller response from $controllerName::action$action");
}
else if ($controllerResponse instanceof XenForo_ControllerResponse_Reroute)
{
if ($controllerResponse->controllerName == $controllerName && strtolower($controllerResponse->action) == strtolower($action))
{
throw new XenForo_Exception("Cannot reroute controller to itself ($controllerName::action$action)");
}
return array(
'controllerName' => $controllerResponse->controllerName,
'action' => $controllerResponse->action
);
}
else if ($controllerResponse instanceof XenForo_ControllerResponse_ReroutePath)
{
if (!$controllerResponse->path)
{
throw new XenForo_Exception("Cannot reroute to path '{$controllerResponse->path}''");
}
return array(
'path' => $controllerResponse->path
);
}
return false;
}
/**
* Returns information about how to reroute if a server error occurs.
*
* @param Exception Exception object that triggered the error
*
* @return array Reroute array
*/
protected function _rerouteServerError(Exception $e)
{
$this->_request->setParam('_exception', $e);
list($controllerName, $action) = $this->_dependencies->getServerErrorRoute();
return array(
'controllerName' => $controllerName,
'action' => $action
);
}
/**
* Returns information about how to reroute if a page not found error occurs.
*
* @param string Controller name
* @param string Action
*
* @return array Reroute array
*/
protected function _rerouteNotFound($controllerName, $action)
{
$this->_request->setParams(array(
'_controllerName' => $controllerName,
'_action' => $action
));
list($controllerName, $action) = $this->_dependencies->getNotFoundErrorRoute();
return array(
'controllerName' => $controllerName,
'action' => $action
);
}
/**
* Gets the view renderer for the specified response type.
*
* @param string Response type (eg, html, xml, json)
*
* @return XenForo_ViewRenderer_Abstract
*/
protected function _getViewRenderer($responseType)
{
return $this->_dependencies->getViewRenderer($this->_response, $responseType, $this->_request);
}
/**
* Renders the view.
*
* @param XenForo_ControllerResponse_Abstract Controller response object from last controller
* @param XenForo_ViewRenderer_Abstract View renderer for specified response type
* @param array Extra container params (probably "sections" from routing)
*
* @return string View output
*/
public function renderView(XenForo_ControllerResponse_Abstract $controllerResponse, XenForo_ViewRenderer_Abstract $viewRenderer, array $containerParams = array())
{
$this->_dependencies->preRenderView($controllerResponse);
$this->_response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true);
if ($controllerResponse->responseCode)
{
$this->_response->setHttpResponseCode($controllerResponse->responseCode);
if (!empty($controllerResponse->containerParams['allowHeader']))
{
$this->_response->setHeader('Allow', $controllerResponse->containerParams['allowHeader']);
}
}
if ($controllerResponse instanceof XenForo_ControllerResponse_Error)
{
$innerContent = $viewRenderer->renderError($controllerResponse->errorText);
}
else if ($controllerResponse instanceof XenForo_ControllerResponse_Message)
{
$innerContent = $viewRenderer->renderMessage($controllerResponse->message);
}
else if ($controllerResponse instanceof XenForo_ControllerResponse_View)
{
$viewRenderer->addJsonParams($controllerResponse->jsonParams);
$innerContent = $viewRenderer->renderView(
$controllerResponse->viewName, $controllerResponse->params, $controllerResponse->templateName,
$controllerResponse->subView
);
}
else if ($controllerResponse instanceof XenForo_ControllerResponse_Redirect)
{
$target = XenForo_Link::convertUriToAbsoluteUri($controllerResponse->redirectTarget, true);
$innerContent = $viewRenderer->renderRedirect(
$controllerResponse->redirectType,
$target,
$controllerResponse->redirectMessage,
$controllerResponse->redirectParams
);
}
else
{
// generally shouldn't happen
$innerContent = false;
}
if ($innerContent === false || $innerContent === null)
{
$innerContent = $viewRenderer->renderUnrepresentable();
}
if ($viewRenderer->getNeedsContainer())
{
$specificContainerParams = XenForo_Application::mapMerge(
$containerParams,
$controllerResponse->containerParams
);
$containerParams = $this->_dependencies->getEffectiveContainerParams($specificContainerParams, $this->_request);
return $viewRenderer->renderContainer($innerContent, $containerParams);
}
else
{
return $innerContent;
}
}
/**
* Renders page-level debugging output and replaces the original view content
* with it. Alternatively, it could inject itself into the view content.
*
* @param string $originalContent Original, rendered view content
*
* @return string Replacement rendered view content
*/
public function renderDebugOutput($originalContent)
{
$this->_response->clearHeaders();
$this->_response->setHttpResponseCode(200);
$this->_response->setHeader('Content-Type', 'text/html; charset=UTF-8', true);
$this->_response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true);
return XenForo_Debug::getDebugPageWrapperHtml(
XenForo_Debug::getDebugHtml()
);
}
}