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);
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 |