349 lines
8.8 KiB
PHP
349 lines
8.8 KiB
PHP
<?php
|
||
/**
|
||
* @package FOF
|
||
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||
* @license GNU General Public License version 3, or later
|
||
*/
|
||
|
||
namespace FOF40\Dispatcher;
|
||
|
||
defined('_JEXEC') || die;
|
||
|
||
use Exception;
|
||
use FOF40\Container\Container;
|
||
use FOF40\Controller\Controller;
|
||
use FOF40\Dispatcher\Exception\AccessForbidden;
|
||
use FOF40\TransparentAuthentication\TransparentAuthentication;
|
||
|
||
/**
|
||
* A generic MVC dispatcher
|
||
*
|
||
* @property-read \FOF40\Input\Input $input The input object (magic __get returns the Input from the Container)
|
||
*/
|
||
class Dispatcher
|
||
{
|
||
/** @var string The name of the default view, in case none is specified */
|
||
public $defaultView;
|
||
|
||
/** @var array Local cache of the dispatcher configuration */
|
||
protected $config = [];
|
||
|
||
/** @var Container The container we belong to */
|
||
protected $container;
|
||
|
||
/** @var string The view which will be rendered by the dispatcher */
|
||
protected $view;
|
||
|
||
/** @var string The layout for rendering the view */
|
||
protected $layout;
|
||
|
||
/** @var Controller The controller which will be used */
|
||
protected $controller;
|
||
|
||
/** @var bool Is this user transparently logged in? */
|
||
protected $isTransparentlyLoggedIn = false;
|
||
|
||
/**
|
||
* Public constructor
|
||
*
|
||
* The $config array can contain the following optional values:
|
||
* defaultView string The view to render if none is specified in $input
|
||
*
|
||
* Do note that $config is passed to the Controller and through it to the Model and View. Please see these classes
|
||
* for more information on the configuration variables they accept.
|
||
*
|
||
* @param Container $container
|
||
* @param array $config
|
||
*/
|
||
public function __construct(Container $container, array $config = [])
|
||
{
|
||
$this->container = $container;
|
||
|
||
$this->config = $config;
|
||
|
||
$this->defaultView = $container->appConfig->get('dispatcher.defaultView', $this->defaultView);
|
||
|
||
if (isset($config['defaultView']))
|
||
{
|
||
$this->defaultView = $config['defaultView'];
|
||
}
|
||
|
||
$this->supportCustomViewAndTaskParameters();
|
||
|
||
// Get the default values for the view and layout names
|
||
$this->view = $this->input->getCmd('view', null);
|
||
$this->layout = $this->input->getCmd('layout', null);
|
||
|
||
// Not redundant; you may pass an empty but non-null view which is invalid, so we need the fallback
|
||
if (empty($this->view))
|
||
{
|
||
$this->view = $this->defaultView;
|
||
$this->container->input->set('view', $this->view);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Magic get method. Handles magic properties:
|
||
* $this->input mapped to $this->container->input
|
||
*
|
||
* @param string $name The property to fetch
|
||
*
|
||
* @return mixed|null
|
||
*/
|
||
public function __get(string $name)
|
||
{
|
||
// Handle $this->input
|
||
if ($name == 'input')
|
||
{
|
||
return $this->container->input;
|
||
}
|
||
|
||
// Property not found; raise error
|
||
$trace = debug_backtrace();
|
||
trigger_error(
|
||
'Undefined property via __get(): ' . $name .
|
||
' in ' . $trace[0]['file'] .
|
||
' on line ' . $trace[0]['line'],
|
||
E_USER_NOTICE);
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* The main code of the Dispatcher. It spawns the necessary controller and
|
||
* runs it.
|
||
*
|
||
* @return void
|
||
*
|
||
* @throws AccessForbidden When the access is forbidden
|
||
* @throws Exception For displaying an error page
|
||
*/
|
||
public function dispatch(): void
|
||
{
|
||
// Load the translations for this component;
|
||
$this->container->platform->loadTranslations($this->container->componentName);
|
||
|
||
// Perform transparent authentication
|
||
if ($this->container->platform->getUser()->guest)
|
||
{
|
||
$this->transparentAuthenticationLogin();
|
||
}
|
||
|
||
// Get the event names (different for CLI)
|
||
$onBeforeEventName = 'onBeforeDispatch';
|
||
$onAfterEventName = 'onAfterDispatch';
|
||
|
||
if ($this->container->platform->isCli())
|
||
{
|
||
$onBeforeEventName = 'onBeforeDispatchCLI';
|
||
$onAfterEventName = 'onAfterDispatchCLI';
|
||
}
|
||
|
||
try
|
||
{
|
||
$result = $this->triggerEvent($onBeforeEventName);
|
||
$error = '';
|
||
}
|
||
catch (\Exception $e)
|
||
{
|
||
$result = false;
|
||
$error = $e->getMessage();
|
||
}
|
||
|
||
if ($result === false)
|
||
{
|
||
if ($this->container->platform->isCli())
|
||
{
|
||
$this->container->platform->setHeader('Status', '403 Forbidden', true);
|
||
}
|
||
|
||
$this->transparentAuthenticationLogout();
|
||
|
||
$this->container->platform->showErrorPage(new AccessForbidden);
|
||
}
|
||
|
||
// Get and execute the controller
|
||
$view = $this->input->getCmd('view', $this->defaultView);
|
||
$task = $this->input->getCmd('task', 'default');
|
||
|
||
if (empty($task))
|
||
{
|
||
$task = 'default';
|
||
$this->input->set('task', $task);
|
||
}
|
||
|
||
try
|
||
{
|
||
$this->controller = $this->container->factory->controller($view, $this->config);
|
||
$status = $this->controller->execute($task);
|
||
}
|
||
catch (Exception $e)
|
||
{
|
||
$this->container->platform->showErrorPage($e);
|
||
|
||
// Redundant; just to make code sniffers happy
|
||
return;
|
||
}
|
||
|
||
if ($status !== false)
|
||
{
|
||
try
|
||
{
|
||
$this->triggerEvent($onAfterEventName);
|
||
}
|
||
catch (\Exception $e)
|
||
{
|
||
$status = false;
|
||
}
|
||
}
|
||
|
||
if ($status === false)
|
||
{
|
||
if ($this->container->platform->isCli())
|
||
{
|
||
$this->container->platform->setHeader('Status', '403 Forbidden', true);
|
||
}
|
||
|
||
$this->transparentAuthenticationLogout();
|
||
|
||
$this->container->platform->showErrorPage(new AccessForbidden);
|
||
}
|
||
|
||
$this->transparentAuthenticationLogout();
|
||
|
||
$this->controller->redirect();
|
||
}
|
||
|
||
/**
|
||
* Returns a reference to the Controller object currently in use by the dispatcher
|
||
*
|
||
* @return Controller|null
|
||
*/
|
||
public function &getController(): ?Controller
|
||
{
|
||
return $this->controller;
|
||
}
|
||
|
||
/**
|
||
* Triggers an object-specific event. The event runs both locally –if a suitable method exists– and through the
|
||
* Joomla! plugin system. A true/false return value is expected. The first false return cancels the event.
|
||
*
|
||
* EXAMPLE
|
||
* Component: com_foobar, Object name: item, Event: onBeforeDispatch, Arguments: array(123, 456)
|
||
* The event calls:
|
||
* 1. $this->onBeforeDispatch(123, 456)
|
||
* 2. Joomla! plugin event onComFoobarDispatcherBeforeDispatch($this, 123, 456)
|
||
*
|
||
* @param string $event The name of the event, typically named onPredicateVerb e.g. onBeforeKick
|
||
* @param array $arguments The arguments to pass to the event handlers
|
||
*
|
||
* @return bool
|
||
*/
|
||
protected function triggerEvent(string $event, array $arguments = []): bool
|
||
{
|
||
$result = true;
|
||
|
||
// If there is an object method for this event, call it
|
||
if (method_exists($this, $event))
|
||
{
|
||
$result = $this->{$event}(...$arguments);
|
||
}
|
||
|
||
if ($result === false)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// All other event handlers live outside this object, therefore they need to be passed a reference to this
|
||
// objects as the first argument.
|
||
array_unshift($arguments, $this);
|
||
|
||
// If we have an "on" prefix for the event (e.g. onFooBar) remove it and stash it for later.
|
||
$prefix = '';
|
||
|
||
if (substr($event, 0, 2) == 'on')
|
||
{
|
||
$prefix = 'on';
|
||
$event = substr($event, 2);
|
||
}
|
||
|
||
// Get the component/model prefix for the event
|
||
$prefix .= 'Com' . ucfirst($this->container->bareComponentName) . 'Dispatcher';
|
||
|
||
// The event name will be something like onComFoobarItemsBeforeSomething
|
||
$event = $prefix . $event;
|
||
|
||
// Call the Joomla! plugins
|
||
$results = $this->container->platform->runPlugins($event, $arguments);
|
||
|
||
return !in_array(false, $results, true);
|
||
}
|
||
|
||
/**
|
||
* Handles the transparent authentication log in
|
||
*/
|
||
protected function transparentAuthenticationLogin(): void
|
||
{
|
||
/** @var TransparentAuthentication $transparentAuth */
|
||
$transparentAuth = $this->container->transparentAuth;
|
||
$authInfo = $transparentAuth->getTransparentAuthenticationCredentials();
|
||
|
||
if (empty($authInfo))
|
||
{
|
||
return;
|
||
}
|
||
|
||
$this->isTransparentlyLoggedIn = $this->container->platform->loginUser($authInfo);
|
||
}
|
||
|
||
/**
|
||
* Handles the transparent authentication log out
|
||
*/
|
||
protected function transparentAuthenticationLogout(): void
|
||
{
|
||
if (!$this->isTransparentlyLoggedIn)
|
||
{
|
||
return;
|
||
}
|
||
|
||
/** @var TransparentAuthentication $transparentAuth */
|
||
$transparentAuth = $this->container->transparentAuth;
|
||
|
||
if (!$transparentAuth->getLogoutOnExit())
|
||
{
|
||
return;
|
||
}
|
||
|
||
$this->container->platform->logoutUser();
|
||
}
|
||
|
||
/**
|
||
* Adds support for akview/aktask in lieu of view and task.
|
||
*
|
||
* This is for future-proofing FOF in case Joomla assigns special meaning to view and task, e.g. by trying to find a
|
||
* specific controller / task class instead of letting the component's front-end router handle it. If that happens
|
||
* FOF components can have a single Joomla-compatible view/task which launches the Dispatcher and perform internal
|
||
* routing using akview/aktask.
|
||
*
|
||
* @return void
|
||
* @since 3.6.3
|
||
*/
|
||
private function supportCustomViewAndTaskParameters()
|
||
{
|
||
$view = $this->input->getCmd('akview', null);
|
||
$task = $this->input->getCmd('aktask', null);
|
||
|
||
if (!is_null($view))
|
||
{
|
||
$this->input->remove('akview');
|
||
$this->input->set('view', $view);
|
||
}
|
||
|
||
if (!is_null($task))
|
||
{
|
||
$this->input->remove('aktask');
|
||
$this->input->set('task', $task);
|
||
}
|
||
}
|
||
}
|