Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Mvc/Dispatcher.php
<?php

namespace XF\Mvc;

use
XF\Http;
use
XF\Mvc\Renderer\AbstractRenderer;
use
XF\Mvc\Reply\AbstractReply;
use
XF\PrintableException;

use function
get_class, is_string, strlen;

class
Dispatcher
{
   
/**
     * @var \XF\App
     */
   
protected $app;

   
/**
     * @var \XF\Http\Request
     */
   
protected $request;

   
/**
     * @var \XF\Mvc\Router
     */
   
protected $router;

    protected
$eventPrefix = 'dispatcher';

   
/**
     * @var \XF\Mvc\Reply\AbstractReply
     */
   
protected $fallbackReply;

    public function
__construct(\XF\App $app, Http\Request $request = null)
    {
       
$this->app = $app;
       
$this->request = $request ? $request : $app->request();
    }

    public function
run($routePath = null)
    {
        if (
$routePath === null)
        {
           
$routePath = $this->request->getRoutePath();
        }

       
$match = $this->route($routePath);

       
$earlyResponse = $this->beforeDispatch($match);
        if (
$earlyResponse)
        {
            return
$earlyResponse;
        }

       
$reply = $this->dispatchLoop($match);

       
$responseType = $reply->getResponseType() ? $reply->getResponseType() : $match->getResponseType();
       
$response = $this->render($reply, $responseType);

        return
$response;
    }

    public function
route($routePath)
    {
       
$match = $this->getRouter()->routeToController($routePath, $this->request);

        if (!(
$match instanceof RouteMatch) || !$match->getController())
        {
           
$match = $this->app->getErrorRoute('DispatchError', [
               
'code' => 'invalid_route',
               
'match' => $match
           
]);
        }

        if (
strlen($routePath) > 1 && substr($routePath, -1, 1) != '/')
        {
           
// this is a route path that does not have a trailing slash which can be ambiguous
            // is it a controller action or is it a URL that contains a string param?
            // if it fails then we should retry the same route path with a trailing slash
           
$match->setPathRetry("$routePath/");
        }

        return
$match;
    }

    protected function
beforeDispatch(RouteMatch $match)
    {
       
$this->app->fire($this->eventPrefix . '_pre_dispatch', [$this, $match]);

        return
$this->app->preDispatch($match);
    }

    public function
dispatchLoop(RouteMatch $match)
    {
       
$i = 1;
       
$attemptErrorReroute = true;
       
$originalMatch = $match;
       
$reply = null;

       
$this->app->fire($this->eventPrefix . '_match', [$this, &$match]);

        do
        {
           
$controllerClass = $match->getController();
           
$action = $match->getAction();
           
$responseType = $match->getResponseType();
           
$sectionContext = $match->getSectionContext();
           
$params = $match->getParameterBag();
           
$controller = null;

            try
            {
               
$reply = $this->dispatchFromMatch($match, $controller, $reply);
            }
            catch (\
Throwable $e)
            {
               
$reply = $this->handleControllerError($e, $attemptErrorReroute, $controller, [
                   
'responseType' => $responseType,
                   
'sectionContext' => $sectionContext,
                   
'action' => $action,
                   
'params' => $params
               
]);
               
$attemptErrorReroute = false;
            }
            catch (\
Exception $e)
            {
               
// this will only be hit in PHP 5.x
               
$reply = $this->handleControllerError($e, $attemptErrorReroute, $controller, [
                   
'responseType' => $responseType,
                   
'sectionContext' => $sectionContext,
                   
'action' => $action,
                   
'params' => $params
               
]);
               
$attemptErrorReroute = false;
            }

            if (!
$reply instanceof Reply\AbstractReply)
            {
               
$reply = new Reply\Reroute(
                   
$this->app->getErrorRoute('DispatchError', [
                       
'code' => 'no_reply',
                       
'controller' => $controllerClass,
                       
'action' => $action
                   
], $responseType)
                );
               
$reply->setSectionContext($sectionContext);
            }

            if (!(
$reply instanceof Reply\Reroute) && $attemptErrorReroute)
            {
               
// if we might be debugging, move this up so that we can display an error instead of the page results.
                // not doing this can hide errors

               
try
                {
                    \
XF::triggerRunOnce(true);
                }
                catch (\
Throwable $e)
                {
                   
$attemptErrorReroute = false;

                   
$reply = new Reply\Reroute(
                       
$this->app->getErrorRoute('Exception', ['exception' => $e], $responseType)
                    );
                   
$reply->setResponseType($responseType);
                   
$reply->setSectionContext($sectionContext);
                }
                catch (\
Exception $e)
                {
                   
// this will only be hit in PHP 5.x
                   
$attemptErrorReroute = false;

                   
$reply = new Reply\Reroute(
                       
$this->app->getErrorRoute('Exception', ['exception' => $e], $responseType)
                    );
                   
$reply->setResponseType($responseType);
                   
$reply->setSectionContext($sectionContext);
                }
            }

            if (
$reply instanceof Reply\Reroute)
            {
               
$match = $reply->getMatch();
                if (!
$match->getResponseType())
                {
                   
$match->setResponseType($responseType);
                }
                if (!
$match->getSectionContext())
                {
                   
$match->setSectionContext($sectionContext);
                }
            }
            else
            {
                break;
            }
        }
        while (
$i++ < 10);

        if (
$reply instanceof Reply\Reroute)
        {
           
// rerouted too many times
           
$reply = new Reply\Error(
               
'An error occurred while the page was being generated. Please try again later.'
           
);
           
$reply->setResponseType($responseType);
           
$reply->setSectionContext($sectionContext);
        }

       
$this->app->postDispatch($reply, $match, $originalMatch);

       
$this->app->fire($this->eventPrefix . '_post_dispatch', [$this, &$reply, $match, $originalMatch]);

        return
$reply;
    }

    protected function
handleControllerError($e, $attemptErrorReroute, $controller, array $state = [])
    {
       
/** @var \Throwable $e */

       
$state = array_replace([
           
'responseType' => null,
           
'sectionContext' => '',
           
'action' => '',
           
'params' => null
       
], $state);

        if (
$attemptErrorReroute)
        {
            \
XF::logException($e, true); // rollback as don't know the state

           
$reply = new Reply\Reroute(
               
$this->app->getErrorRoute('Exception', ['exception' => $e], $state['responseType'])
            );
        }
        else
        {
           
$reply = new Reply\Error(
               
'An error occurred while the page was being generated. Please try again later.'
           
);
        }

       
$reply->setResponseType($state['responseType']);
       
$reply->setSectionContext($state['sectionContext']);

        if (
$controller instanceof \XF\Mvc\Controller)
        {
           
$controller->applyReplyChanges($state['action'], $state['params'] ?: new ParameterBag(), $reply);
        }

        return
$reply;
    }

    public function
dispatchFromMatch(RouteMatch $match, &$controller = null, AbstractReply $previousReply = null)
    {
        return
$this->dispatchClass(
           
$match->getController(),
           
$match->getAction(),
           
$match,
           
$controller,
           
$previousReply
       
);
    }

    public function
dispatchClass(
       
$controllerClass, $action, RouteMatch $match, &$controller = null, AbstractReply $previousReply = null
   
)
    {
       
$params = $match->getParameterBag();
        if (!
$params)
        {
           
$params = new ParameterBag();
        }

       
$responseType = $match->getResponseType();

        if (!
$controllerClass)
        {
            return new
Reply\Reroute(
               
$this->app->getErrorRoute('DispatchError', [
                   
'code' => 'no_controller',
                   
'controller' => $controllerClass,
                   
'action' => is_string($action) ? $action : null,
                   
'match' => $match
               
], $responseType)
            );
        }

       
$controller = $this->app->controller($controllerClass, $this->request);
        if (!
$controller)
        {
            return new
Reply\Reroute(
               
$this->app->getErrorRoute('DispatchError', [
                   
'code' => 'invalid_controller',
                   
'controller' => $controllerClass,
                   
'action' => is_string($action) ? $action : null,
                   
'match' => $match
               
], $responseType)
            );
        }

       
$controller->setupFromMatch($match);
        if (
$previousReply)
        {
           
$controller->setupFromReply($previousReply);
        }

        if (
$action instanceof \Closure)
        {
           
$action = $action($controller, $responseType, $params);
        }
        else
        {
           
$action = preg_replace('#[^a-z0-9]#i', ' ', $action);
           
$action = str_replace(' ', '', ucwords($action));
        }

       
$method = 'action' . $action;
        if (!
is_callable([$controller, $method]))
        {
           
$reply = new Reply\Reroute(
               
$this->app->getErrorRoute('DispatchError', [
                   
'code' => 'invalid_action',
                   
'controller' => $controllerClass,
                   
'action' => $action,
                   
'match' => $match
               
], $responseType)
            );

           
// the original route path failed to resolve to a valid controller action
            // but we have a path to retry - typically the same path but with a trailing slash
            // if the retry path results in a 404 we will fallback to this original invalid_action reply
           
$retryPath = $match->getPathRetry();
            if (
$retryPath)
            {
               
$retryMatch = $this->route($retryPath);
                if (
$retryMatch)
                {
                   
$this->fallbackReply = $reply;
                   
$reply = new Reply\Reroute($retryMatch);
                }
            }

            return
$reply;
        }

        try
        {
           
$controller->preDispatch($action, $params);
           
$reply = $controller->$method($params);

           
// this looks like a retry that failed (404) and we have a fallback reply
            // so return that original reply in order to maintain the expected reply
           
if ($reply instanceof AbstractReply
               
&& $reply->getResponseCode() == 404
               
&& $this->fallbackReply
           
)
            {
               
$reply = $this->fallbackReply;
               
$this->fallbackReply = null;
            }
        }
        catch (
PrintableException $e)
        {
           
$reply = new Reply\Error($e->getMessages());
        }
        catch (
Reply\Exception $e)
        {
           
$reply = $e->getReply();
        }

        if (!
$reply)
        {
           
$reply = new Reply\Reroute(
               
$this->app->getErrorRoute('DispatchError', [
                   
'code' => 'no_reply',
                   
'controller' => $controllerClass,
                   
'action' => $action
               
], $responseType)
            );
        }

       
$controller->postDispatch($action, $params, $reply);

       
$reply->setControllerClass($controllerClass);
       
$reply->setAction($action);

        return
$reply;
    }

    public function
render(AbstractReply $reply, $responseType)
    {
       
$this->app->fire($this->eventPrefix . '_pre_render', [$this, $reply, $responseType]);

       
$this->app->preRender($reply, $responseType);

       
$renderer = $this->app->renderer($responseType);
       
$this->setupRenderer($renderer, $reply);

       
$content = $this->renderReply($renderer, $reply);

       
$content = $this->app->renderPage($content, $reply, $renderer);
       
$content = $renderer->postFilter($content, $reply);

       
$response = $renderer->getResponse();

       
$this->app->fire($this->eventPrefix . '_post_render', [$this, &$content, $reply, $renderer, $response]);

       
$response->body($content);

        return
$response;
    }

    protected function
setupRenderer(AbstractRenderer $renderer, AbstractReply $reply)
    {
       
$renderer->setReply($reply);

       
$renderer->getResponse()->header('Last-Modified', gmdate('D, d M Y H:i:s', \XF::$time) . ' GMT');
       
$renderer->setResponseCode($reply->getResponseCode());
       
$renderer->getTemplater()->setPageParams($reply->getPageParams());
    }

    protected function
renderReply(AbstractRenderer $renderer, AbstractReply $reply)
    {
        if (
$reply instanceof Reply\Error)
        {
            return
$renderer->renderErrors($reply->getErrors());
        }
        else if (
$reply instanceof Reply\Message)
        {
            return
$renderer->renderMessage($reply->getMessage());
        }
        else if (
$reply instanceof Reply\Redirect)
        {
           
$url = $this->request->convertToAbsoluteUri($reply->getUrl());
            return
$renderer->renderRedirect($url, $reply->getType(), $reply->getMessage());
        }
        else if (
$reply instanceof Reply\View)
        {
            return
$this->renderView($renderer, $reply);
        }
        else
        {
            throw new \
InvalidArgumentException("Unknown reply type: " . get_class($reply));
        }
    }

    public function
renderView(AbstractRenderer $renderer, Reply\View $reply)
    {
       
$params = $reply->getParams();

       
$template = $reply->getTemplateName();
        if (
$template && !strpos($template, ':'))
        {
           
$template = $this->app['app.defaultType'] . ':' . $template;
        }

        return
$renderer->renderView($reply->getViewClass(), $template, $params);
    }

   
/**
     * @return Http\Request
     */
   
public function getRequest()
    {
        return
$this->request;
    }

   
/**
     * @return Router
     */
   
public function getRouter()
    {
        if (!
$this->router)
        {
           
$this->router = $this->app->router();
        }

        return
$this->router;
    }

   
/**
     * @param Router $router
     */
   
public function setRouter(Router $router)
    {
       
$this->router = $router;
    }
}