1253 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			1253 lines
		
	
	
		
			30 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\Controller;
 | ||
| 
 | ||
| defined('_JEXEC') || die;
 | ||
| 
 | ||
| use Exception;
 | ||
| use FOF40\Container\Container;
 | ||
| use FOF40\Controller\Exception\CannotGetName;
 | ||
| use FOF40\Controller\Exception\TaskNotFound;
 | ||
| use FOF40\Model\Model;
 | ||
| use FOF40\View\Exception\AccessForbidden;
 | ||
| use FOF40\View\View;
 | ||
| use Joomla\CMS\Application\CMSApplication;
 | ||
| use Joomla\CMS\Cache\Cache;
 | ||
| use Joomla\CMS\Cache\Exception\CacheExceptionInterface;
 | ||
| use Joomla\CMS\Document\Document;
 | ||
| use Joomla\CMS\Factory as JoomlaFactory;
 | ||
| use Joomla\CMS\Language\Text;
 | ||
| use Joomla\CMS\Log\Log;
 | ||
| use Joomla\CMS\Router\Route;
 | ||
| use Joomla\CMS\Uri\Uri;
 | ||
| use stdClass;
 | ||
| 
 | ||
| /**
 | ||
|  * Class Controller
 | ||
|  *
 | ||
|  * A generic MVC controller implementation
 | ||
|  *
 | ||
|  * @property-read  \FOF40\Input\Input $input  The input object (magic __get returns the Input from the Container)
 | ||
|  */
 | ||
| class Controller
 | ||
| {
 | ||
| 	/**
 | ||
| 	 * Instance container.
 | ||
| 	 *
 | ||
| 	 * @var    Controller
 | ||
| 	 */
 | ||
| 	protected static $instance;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * The name of the controller
 | ||
| 	 *
 | ||
| 	 * @var    array
 | ||
| 	 */
 | ||
| 	protected $name;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * The mapped task that was performed.
 | ||
| 	 *
 | ||
| 	 * @var    string
 | ||
| 	 */
 | ||
| 	protected $doTask;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Bit mask to enable routing through Route on redirects. The value can be:
 | ||
| 	 *
 | ||
| 	 * 0 = never
 | ||
| 	 * 1 = frontend only
 | ||
| 	 * 2 = backend  only
 | ||
| 	 * 3 = always
 | ||
| 	 *
 | ||
| 	 * @var    int
 | ||
| 	 */
 | ||
| 	protected $autoRouting = 0;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Should I protect against state bleedover? When this is enabled the default model's state hash will be
 | ||
| 	 * automatically set to include the controller name i.e. `com_example.controllerName.modelName.` instead of
 | ||
| 	 * `com_example.modelName.`. This will happen ONLY if the preventStateBleedover flag is set, the controller and
 | ||
| 	 * model names are different and the model doesn't set its own hash (or override getHash altogether).
 | ||
| 	 *
 | ||
| 	 * You should only need to enable this feature when you have multiple controllers using the _same_ Model as their
 | ||
| 	 * default. For example, if you have a blog component with Latest and Posts Controllers, both using the Posts Model
 | ||
| 	 * as their default Model the state variables set in the latest posts page would bleed over to the posts page. This
 | ||
| 	 * can include filtering and pagination preferences, resulting in a confusing experience for the user.
 | ||
| 	 *
 | ||
| 	 * Caveat: if you are using a different Controller class for singular / plural view names you will need to override
 | ||
| 	 *         getModel() yourself. Otherwise the state of the singular view would be disjointed from the state of the
 | ||
| 	 *         plural view (since the Controller names are different). That's the reason why this feature is turned off
 | ||
| 	 *         by default.
 | ||
| 	 *
 | ||
| 	 * False = same behavior as FOF 3.0.0 to 3.1.1 inclusive.
 | ||
| 	 *
 | ||
| 	 * @var   bool
 | ||
| 	 */
 | ||
| 	protected $preventStateBleedover = false;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Redirect message.
 | ||
| 	 *
 | ||
| 	 * @var    string
 | ||
| 	 */
 | ||
| 	protected $message;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Redirect message type.
 | ||
| 	 *
 | ||
| 	 * @var    string
 | ||
| 	 */
 | ||
| 	protected $messageType;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Array of class methods
 | ||
| 	 *
 | ||
| 	 * @var    array
 | ||
| 	 */
 | ||
| 	protected $methods;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * The set of search directories for resources (views).
 | ||
| 	 *
 | ||
| 	 * @var    array
 | ||
| 	 */
 | ||
| 	protected $paths;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * URL for redirection.
 | ||
| 	 *
 | ||
| 	 * @var    string|null
 | ||
| 	 */
 | ||
| 	protected $redirect;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Current or most recently performed task.
 | ||
| 	 *
 | ||
| 	 * @var    string
 | ||
| 	 */
 | ||
| 	protected $task;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Array of class methods to call for a given task.
 | ||
| 	 *
 | ||
| 	 * @var    array
 | ||
| 	 */
 | ||
| 	protected $taskMap;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * The current view name; you can override it in the configuration
 | ||
| 	 *
 | ||
| 	 * @var string
 | ||
| 	 */
 | ||
| 	protected $view = '';
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * The current layout; you can override it in the configuration
 | ||
| 	 *
 | ||
| 	 * @var string
 | ||
| 	 */
 | ||
| 	protected $layout;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * A cached copy of the class configuration parameter passed during initialisation
 | ||
| 	 *
 | ||
| 	 * @var array
 | ||
| 	 */
 | ||
| 	protected $config = [];
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Overrides the name of the view's default model
 | ||
| 	 *
 | ||
| 	 * @var string
 | ||
| 	 */
 | ||
| 	protected $modelName;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Overrides the name of the view's default view
 | ||
| 	 *
 | ||
| 	 * @var string
 | ||
| 	 */
 | ||
| 	protected $viewName;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * An array of Model instances known to this Controller
 | ||
| 	 *
 | ||
| 	 * @var   array[Model]
 | ||
| 	 */
 | ||
| 	protected $modelInstances = [];
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * An array of View instances known to this Controller
 | ||
| 	 *
 | ||
| 	 * @var   array[View]
 | ||
| 	 */
 | ||
| 	protected $viewInstances = [];
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * The container attached to this Controller
 | ||
| 	 *
 | ||
| 	 * @var Container
 | ||
| 	 */
 | ||
| 	protected $container;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * The tasks for which caching should be enabled by default
 | ||
| 	 *
 | ||
| 	 * @var array
 | ||
| 	 */
 | ||
| 	protected $cacheableTasks = [];
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * How user group membership affects caching. The values are:
 | ||
| 	 * - 0 : Not taken into account, everyone sees the same page, always
 | ||
| 	 * - 1 : Only user groups are taken into account (default behaviour of FOF 3.0 to 3.4.2)
 | ||
| 	 * - 2 : The user ID itself is taken into account
 | ||
| 	 *
 | ||
| 	 * @var   bool
 | ||
| 	 * @since 3.4.3
 | ||
| 	 */
 | ||
| 	protected $userCaching = 1;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * An associative array for required ACL privileges per task. For example:
 | ||
| 	 * array(
 | ||
| 	 *   'edit' => 'core.edit',
 | ||
| 	 *   'jump' => 'foobar.jump',
 | ||
| 	 *   'alwaysallow' => 'true',
 | ||
| 	 *   'neverallow' => 'false'
 | ||
| 	 * );
 | ||
| 	 *
 | ||
| 	 * You can use the notation '@task' which means 'apply the same privileges as "task"'. If you create a reference
 | ||
| 	 * back to yourself (e.g. 'mytask' => array('@mytask')) it will return TRUE.
 | ||
| 	 *
 | ||
| 	 * @var array
 | ||
| 	 */
 | ||
| 	protected $taskPrivileges = [];
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Enable CSRF protection on selected tasks. The possible values are:
 | ||
| 	 *
 | ||
| 	 * 0    Disabled; no token checks are performed
 | ||
| 	 * 1    Enabled; token checks are always performed
 | ||
| 	 * 2    Only on HTML requests and backend; token checks are always performed in the back-end and in the front-end
 | ||
| 	 * only when format is 'html'
 | ||
| 	 * 3    Only on back-end; token checks are performed only in the back-end
 | ||
| 	 *
 | ||
| 	 * @var    integer
 | ||
| 	 */
 | ||
| 	protected $csrfProtection = 2;
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Public constructor of the Controller class. You can pass the following variables in the $config array:
 | ||
| 	 * name            string  The name of the Controller. Default: auto detect from the class name
 | ||
| 	 * default_task    string  The task to use when none is specified. Default: main
 | ||
| 	 * autoRouting     int     See the autoRouting property
 | ||
| 	 * csrfProtection  int     See the csrfProtection property
 | ||
| 	 * viewName        string  The view name. Default: the same as the controller name
 | ||
| 	 * modelName       string  The model name. Default: the same as the controller name
 | ||
| 	 * viewConfig      array   The configuration overrides for the View.
 | ||
| 	 * modelConfig     array   The configuration overrides for the Model.
 | ||
| 	 *
 | ||
| 	 * @param   Container  $container  The application container
 | ||
| 	 * @param   array      $config     The configuration array
 | ||
| 	 *
 | ||
| 	 * @return  Controller
 | ||
| 	 */
 | ||
| 	public function __construct(Container $container, array $config = [])
 | ||
| 	{
 | ||
| 		// Initialise
 | ||
| 		$this->methods     = [];
 | ||
| 		$this->message     = null;
 | ||
| 		$this->messageType = 'message';
 | ||
| 		$this->paths       = [];
 | ||
| 		$this->redirect    = null;
 | ||
| 		$this->taskMap     = [];
 | ||
| 
 | ||
| 		// Get a local copy of the container
 | ||
| 		$this->container = $container;
 | ||
| 
 | ||
| 		// Determine the methods to exclude from the base class.
 | ||
| 		$xMethods = get_class_methods('\\FOF40\\Controller\\Controller');
 | ||
| 
 | ||
| 		// Get the public methods in this class using reflection.
 | ||
| 		$r        = new \ReflectionClass($this);
 | ||
| 		$rMethods = $r->getMethods(\ReflectionMethod::IS_PUBLIC);
 | ||
| 
 | ||
| 		foreach ($rMethods as $rMethod)
 | ||
| 		{
 | ||
| 			$mName = $rMethod->getName();
 | ||
| 
 | ||
| 			// If the developer screwed up and declared one of the helper method public do NOT make them available as
 | ||
| 			// tasks.
 | ||
| 			if ((substr($mName, 0, 8) == 'onBefore') || (substr($mName, 0, 7) == 'onAfter') || substr($mName, 0, 1) == '_')
 | ||
| 			{
 | ||
| 				continue;
 | ||
| 			}
 | ||
| 
 | ||
| 			// Add default display method if not explicitly declared.
 | ||
| 			if (!in_array($mName, $xMethods) || $mName == 'display' || $mName == 'main')
 | ||
| 			{
 | ||
| 				$this->methods[] = $mName;
 | ||
| 
 | ||
| 				// Auto register the methods as tasks.
 | ||
| 				$this->taskMap[$mName] = $mName;
 | ||
| 			}
 | ||
| 		}
 | ||
| 
 | ||
| 		if (isset($config['name']))
 | ||
| 		{
 | ||
| 			$this->name = $config['name'];
 | ||
| 		}
 | ||
| 
 | ||
| 		// Get the default values for the component and view names
 | ||
| 		$this->view   = $this->getName();
 | ||
| 		$this->layout = $this->input->getCmd('layout', null);
 | ||
| 
 | ||
| 		// If the default task is set, register it as such
 | ||
| 		if (array_key_exists('default_task', $config) && !empty($config['default_task']))
 | ||
| 		{
 | ||
| 			$this->registerDefaultTask($config['default_task']);
 | ||
| 		}
 | ||
| 		else
 | ||
| 		{
 | ||
| 			$this->registerDefaultTask('main');
 | ||
| 		}
 | ||
| 
 | ||
| 		// Cache the config
 | ||
| 		$this->config = $config;
 | ||
| 
 | ||
| 		// Set any model/view name overrides
 | ||
| 		if (array_key_exists('viewName', $config) && !empty($config['viewName']))
 | ||
| 		{
 | ||
| 			$this->setViewName($config['viewName']);
 | ||
| 		}
 | ||
| 
 | ||
| 		if (array_key_exists('modelName', $config) && !empty($config['modelName']))
 | ||
| 		{
 | ||
| 			$this->setModelName($config['modelName']);
 | ||
| 		}
 | ||
| 
 | ||
| 		// Apply the autoRouting preference
 | ||
| 		if (array_key_exists('autoRouting', $config))
 | ||
| 		{
 | ||
| 			$this->autoRouting = (int) $config['autoRouting'];
 | ||
| 		}
 | ||
| 
 | ||
| 		// Apply the csrfProtection preference
 | ||
| 		if (array_key_exists('csrfProtection', $config))
 | ||
| 		{
 | ||
| 			$this->csrfProtection = (int) $config['csrfProtection'];
 | ||
| 		}
 | ||
| 
 | ||
| 		// Apply the preventStateBleedover preference
 | ||
| 		if (array_key_exists('preventStateBleedover', $config))
 | ||
| 		{
 | ||
| 			$this->preventStateBleedover = (bool) ((int) $config['preventStateBleedover']);
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * 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;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Executes a given controller task. The onBefore<task> and onAfter<task>
 | ||
| 	 * methods are called automatically if they exist.
 | ||
| 	 *
 | ||
| 	 * @param   string  $task  The task to execute, e.g. "browse"
 | ||
| 	 *
 | ||
| 	 * @return  mixed|bool  False on execution failure
 | ||
| 	 *
 | ||
| 	 * @throws  TaskNotFound  When the task is not found
 | ||
| 	 */
 | ||
| 	public function execute(string $task)
 | ||
| 	{
 | ||
| 		$this->task = $task;
 | ||
| 
 | ||
| 		if (!isset($this->taskMap[$task]) && !isset($this->taskMap['__default']))
 | ||
| 		{
 | ||
| 			throw new TaskNotFound(Text::sprintf('JLIB_APPLICATION_ERROR_TASK_NOT_FOUND', $task), 404);
 | ||
| 		}
 | ||
| 
 | ||
| 		$result = $this->triggerEvent('onBeforeExecute', [&$task]);
 | ||
| 
 | ||
| 		if ($result === false)
 | ||
| 		{
 | ||
| 			return false;
 | ||
| 		}
 | ||
| 
 | ||
| 		$eventName = 'onBefore' . ucfirst($task);
 | ||
| 		$result    = $this->triggerEvent($eventName);
 | ||
| 
 | ||
| 		if ($result === false)
 | ||
| 		{
 | ||
| 			return false;
 | ||
| 		}
 | ||
| 
 | ||
| 		// Do not allow the display task to be directly called
 | ||
| 		if (isset($this->taskMap[$task]))
 | ||
| 		{
 | ||
| 			$doTask = $this->taskMap[$task];
 | ||
| 		}
 | ||
| 		elseif (isset($this->taskMap['__default']))
 | ||
| 		{
 | ||
| 			$doTask = $this->taskMap['__default'];
 | ||
| 		}
 | ||
| 		else
 | ||
| 		{
 | ||
| 			$doTask = null;
 | ||
| 		}
 | ||
| 
 | ||
| 		// Record the actual task being fired
 | ||
| 		$this->doTask = $doTask;
 | ||
| 
 | ||
| 		$ret = $this->$doTask();
 | ||
| 
 | ||
| 		$eventName = 'onAfter' . ucfirst($task);
 | ||
| 		$result    = $this->triggerEvent($eventName);
 | ||
| 
 | ||
| 		if ($result === false)
 | ||
| 		{
 | ||
| 			return false;
 | ||
| 		}
 | ||
| 
 | ||
| 		$result = $this->triggerEvent('onAfterExecute', [$task]);
 | ||
| 
 | ||
| 		if ($result === false)
 | ||
| 		{
 | ||
| 			return false;
 | ||
| 		}
 | ||
| 
 | ||
| 		return $ret;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Default task. Assigns a model to the view and asks the view to render
 | ||
| 	 * itself.
 | ||
| 	 *
 | ||
| 	 * YOU MUST NOT USE THIS TASK DIRECTLY IN A URL. It is supposed to be
 | ||
| 	 * used ONLY inside your code. In the URL, use task=browse instead.
 | ||
| 	 *
 | ||
| 	 * @param   bool        $cachable   Is this view cacheable?
 | ||
| 	 * @param   array|null  $urlparams  Add your safe URL parameters (see further down in the code)
 | ||
| 	 * @param   string      $tpl        The name of the template file to parse
 | ||
| 	 *
 | ||
| 	 * @return  void
 | ||
| 	 *
 | ||
| 	 * @throws Exception
 | ||
| 	 */
 | ||
| 	public function display(bool $cachable = false, ?array $urlparams = null, ?string $tpl = null): void
 | ||
| 	{
 | ||
| 		$document = $this->container->platform->getDocument();
 | ||
| 
 | ||
| 		if ($document instanceof Document)
 | ||
| 		{
 | ||
| 			$viewType = $document->getType();
 | ||
| 		}
 | ||
| 		else
 | ||
| 		{
 | ||
| 			$viewType = $this->input->getCmd('format', 'html');
 | ||
| 		}
 | ||
| 
 | ||
| 		$view = $this->getView();
 | ||
| 		$view->setTask($this->task);
 | ||
| 		$view->setDoTask($this->doTask);
 | ||
| 
 | ||
| 		// Get/Create the model
 | ||
| 		if ($model = $this->getModel())
 | ||
| 		{
 | ||
| 			// Push the model into the view (as default)
 | ||
| 			$view->setDefaultModel($model);
 | ||
| 		}
 | ||
| 
 | ||
| 		// Set the layout
 | ||
| 		if (!is_null($this->layout))
 | ||
| 		{
 | ||
| 			$view->setLayout($this->layout);
 | ||
| 		}
 | ||
| 
 | ||
| 		$conf = $this->container->platform->getConfig();
 | ||
| 
 | ||
| 		if ($cachable && ($viewType != 'feed') && ($conf->get('caching') >= 1))
 | ||
| 		{
 | ||
| 			// Get a Cache object
 | ||
| 			$option = $this->input->get('option', 'com_foobar', 'cmd');
 | ||
| 
 | ||
| 			// Set up a cache ID based on component, view, task and user group assignment
 | ||
| 			$user = $this->container->platform->getUser();
 | ||
| 
 | ||
| 			$groups = $user->guest ? [] : $user->groups;
 | ||
| 
 | ||
| 			$userId = $user->guest ? 0 : $user->id;
 | ||
| 
 | ||
| 			switch ($this->userCaching)
 | ||
| 			{
 | ||
| 				case 0:
 | ||
| 					// Developer chose to apply the same caching to everyone
 | ||
| 					$groups = [];
 | ||
| 					$userId = 0;
 | ||
| 					break;
 | ||
| 
 | ||
| 				case 1:
 | ||
| 					// Developer chose to apply caching per user group membership only
 | ||
| 					$userId = 0;
 | ||
| 					break;
 | ||
| 			}
 | ||
| 
 | ||
| 			$importantParameters = [];
 | ||
| 
 | ||
| 			// Set up safe URL parameters
 | ||
| 			if (!is_array($urlparams))
 | ||
| 			{
 | ||
| 				$urlparams = [
 | ||
| 					'option' => 'CMD',
 | ||
| 					'view'   => 'CMD',
 | ||
| 					'task'   => 'CMD',
 | ||
| 					'format' => 'CMD',
 | ||
| 					'layout' => 'CMD',
 | ||
| 					'id'     => 'INT',
 | ||
| 				];
 | ||
| 			}
 | ||
| 
 | ||
| 			if (is_array($urlparams))
 | ||
| 			{
 | ||
| 				/** @var CMSApplication $app */
 | ||
| 				$app = JoomlaFactory::getApplication();
 | ||
| 
 | ||
| 				$registeredurlparams = null;
 | ||
| 
 | ||
| 				$registeredurlparams = !empty($app->registeredurlparams) ? $app->registeredurlparams : new stdClass;
 | ||
| 
 | ||
| 				foreach ($urlparams as $key => $value)
 | ||
| 				{
 | ||
| 					// Add your safe url parameters with variable type as value {@see InputFilter::clean()}.
 | ||
| 					$registeredurlparams->$key = $value;
 | ||
| 
 | ||
| 					// Add the URL-important parameters into the array
 | ||
| 					$importantParameters[$key] = $this->input->get($key, null, $value);
 | ||
| 				}
 | ||
| 
 | ||
| 				$app->registeredurlparams = $registeredurlparams;
 | ||
| 			}
 | ||
| 
 | ||
| 			// Create the cache ID after setting the registered URL params, as they are used to generate the ID
 | ||
| 			$cacheId = md5(serialize([
 | ||
| 				Cache::makeId(), $view->getName(), $this->doTask, $groups, $userId, $importantParameters,
 | ||
| 			]));
 | ||
| 
 | ||
| 			// Get the cached view or cache the current view
 | ||
| 			try
 | ||
| 			{
 | ||
| 				/** @var \Joomla\CMS\Cache\Controller\ViewController $cache */
 | ||
| 				$cache = JoomlaFactory::getCache($option, 'view');
 | ||
| 				$cache->get($view, 'display', $cacheId);
 | ||
| 			}
 | ||
| 			catch (CacheExceptionInterface $e)
 | ||
| 			{
 | ||
| 				// Display without caching
 | ||
| 				$view->display($tpl);
 | ||
| 			}
 | ||
| 		}
 | ||
| 		else
 | ||
| 		{
 | ||
| 			// Display without caching
 | ||
| 			$view->display($tpl);
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Alias to the display() task
 | ||
| 	 *
 | ||
| 	 * @codeCoverageIgnore
 | ||
| 	 *
 | ||
| 	 * @throws Exception
 | ||
| 	 */
 | ||
| 	public function main()
 | ||
| 	{
 | ||
| 		$this->display();
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Returns a named Model object
 | ||
| 	 *
 | ||
| 	 * @param   string  $name    The Model name. If null we'll use the modelName
 | ||
| 	 *                           variable or, if it's empty, the same name as
 | ||
| 	 *                           the Controller
 | ||
| 	 * @param   array   $config  Configuration parameters to the Model. If skipped
 | ||
| 	 *                           we will use $this->config
 | ||
| 	 *
 | ||
| 	 * @return  Model  The instance of the Model known to this Controller
 | ||
| 	 */
 | ||
| 	public function getModel(?string $name = null, array $config = [])
 | ||
| 	{
 | ||
| 		if (!empty($name))
 | ||
| 		{
 | ||
| 			$modelName = $name;
 | ||
| 		}
 | ||
| 		elseif (!empty($this->modelName))
 | ||
| 		{
 | ||
| 			$modelName = $this->modelName;
 | ||
| 		}
 | ||
| 		else
 | ||
| 		{
 | ||
| 			$modelName = $this->view;
 | ||
| 		}
 | ||
| 
 | ||
| 		// Do we already have an instance of that model cached?
 | ||
| 		if (array_key_exists($modelName, $this->modelInstances))
 | ||
| 		{
 | ||
| 			return $this->modelInstances[$modelName];
 | ||
| 		}
 | ||
| 
 | ||
| 		if (empty($config) && isset($this->config['modelConfig']))
 | ||
| 		{
 | ||
| 			$config = $this->config['modelConfig'];
 | ||
| 		}
 | ||
| 
 | ||
| 		if (empty($name))
 | ||
| 		{
 | ||
| 			$config['modelTemporaryInstance'] = true;
 | ||
| 			$controllerName                   = $this->getName();
 | ||
| 
 | ||
| 			if ($controllerName !== $modelName)
 | ||
| 			{
 | ||
| 				$config['hash_view'] = $controllerName;
 | ||
| 			}
 | ||
| 		}
 | ||
| 		else
 | ||
| 		{
 | ||
| 			// Other classes are loaded with persistent state disabled and their state/input blanked out
 | ||
| 			$config['modelTemporaryInstance'] = false;
 | ||
| 			$config['modelClearState']        = true;
 | ||
| 			$config['modelClearInput']        = true;
 | ||
| 		}
 | ||
| 
 | ||
| 		$this->modelInstances[$modelName] = $this->container->factory->model(ucfirst($modelName), $config);
 | ||
| 
 | ||
| 		return $this->modelInstances[$modelName];
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Returns a named View object
 | ||
| 	 *
 | ||
| 	 * @param   string  $name    The Model name. If null we'll use the modelName
 | ||
| 	 *                           variable or, if it's empty, the same name as
 | ||
| 	 *                           the Controller
 | ||
| 	 * @param   array   $config  Configuration parameters to the Model. If skipped
 | ||
| 	 *                           we will use $this->config
 | ||
| 	 *
 | ||
| 	 * @return  View  The instance of the Model known to this Controller
 | ||
| 	 */
 | ||
| 	public function getView(?string $name = null, array $config = [])
 | ||
| 	{
 | ||
| 		if (!empty($name))
 | ||
| 		{
 | ||
| 			$viewName = $name;
 | ||
| 		}
 | ||
| 		elseif (!empty($this->viewName))
 | ||
| 		{
 | ||
| 			$viewName = $this->viewName;
 | ||
| 		}
 | ||
| 		else
 | ||
| 		{
 | ||
| 			$viewName = $this->view;
 | ||
| 		}
 | ||
| 
 | ||
| 		if (!array_key_exists($viewName, $this->viewInstances))
 | ||
| 		{
 | ||
| 			if (empty($config) && isset($this->config['viewConfig']))
 | ||
| 			{
 | ||
| 				$config = $this->config['viewConfig'];
 | ||
| 			}
 | ||
| 
 | ||
| 			$viewType = $this->input->getCmd('format', 'html');
 | ||
| 
 | ||
| 			// Get the model's class name
 | ||
| 			$this->viewInstances[$viewName] = $this->container->factory->view($viewName, $viewType, $config);
 | ||
| 		}
 | ||
| 
 | ||
| 		return $this->viewInstances[$viewName];
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Pushes a named view to the Controller
 | ||
| 	 *
 | ||
| 	 * @param   string  $viewName  The name of the View
 | ||
| 	 * @param   View    $view      The actual View object to push
 | ||
| 	 *
 | ||
| 	 * @return  void
 | ||
| 	 */
 | ||
| 	public function setView(string $viewName, View &$view): void
 | ||
| 	{
 | ||
| 		$this->viewInstances[$viewName] = $view;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Set the name of the view to be used by this Controller
 | ||
| 	 *
 | ||
| 	 * @param   string  $viewName  The name of the view
 | ||
| 	 *
 | ||
| 	 * @return  void
 | ||
| 	 */
 | ||
| 	public function setViewName(string $viewName): void
 | ||
| 	{
 | ||
| 		$this->viewName = $viewName;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Set the name of the model to be used by this Controller
 | ||
| 	 *
 | ||
| 	 * @param   string  $modelName  The name of the model
 | ||
| 	 *
 | ||
| 	 * @return  void
 | ||
| 	 */
 | ||
| 	public function setModelName(string $modelName): void
 | ||
| 	{
 | ||
| 		$this->modelName = $modelName;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Pushes a named model to the Controller
 | ||
| 	 *
 | ||
| 	 * @param   string  $modelName  The name of the Model
 | ||
| 	 * @param   Model   $model      The actual Model object to push
 | ||
| 	 *
 | ||
| 	 * @return  void
 | ||
| 	 */
 | ||
| 	public function setModel(string $modelName, Model &$model): void
 | ||
| 	{
 | ||
| 		$this->modelInstances[$modelName] = $model;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Method to get the controller name
 | ||
| 	 *
 | ||
| 	 * The controller name is set by default parsed using the classname, or it can be set
 | ||
| 	 * by passing a $config['name'] in the class constructor
 | ||
| 	 *
 | ||
| 	 * @return  string  The name of the controller
 | ||
| 	 *
 | ||
| 	 * @throws  CannotGetName  If it's impossible to determine the name and it's not set
 | ||
| 	 */
 | ||
| 	public function getName(): string
 | ||
| 	{
 | ||
| 		if (empty($this->name))
 | ||
| 		{
 | ||
| 			$r = null;
 | ||
| 
 | ||
| 			if (!preg_match('/(.*)\\\\Controller\\\\(.*)/i', get_class($this), $r))
 | ||
| 			{
 | ||
| 				throw new CannotGetName(Text::_('LIB_FOF40_CONTROLLER_ERR_GET_NAME'), 500);
 | ||
| 			}
 | ||
| 
 | ||
| 			$this->name = $r[2];
 | ||
| 		}
 | ||
| 
 | ||
| 		return $this->name;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Get the last task that is being performed or was most recently performed.
 | ||
| 	 *
 | ||
| 	 * @return  string  The task that is being performed or was most recently performed.
 | ||
| 	 */
 | ||
| 	public function getTask(): string
 | ||
| 	{
 | ||
| 		return $this->task;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Gets the available tasks in the controller.
 | ||
| 	 *
 | ||
| 	 * @return  string[]  Array of task names.
 | ||
| 	 */
 | ||
| 	public function getTasks(): array
 | ||
| 	{
 | ||
| 		return $this->methods;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Redirects the browser or returns false if no redirect is set.
 | ||
| 	 *
 | ||
| 	 * @return  boolean  False if no redirect exists.
 | ||
| 	 */
 | ||
| 	public function redirect(): bool
 | ||
| 	{
 | ||
| 		if (!empty($this->redirect))
 | ||
| 		{
 | ||
| 			$this->container->platform->redirect($this->redirect, 301, $this->message, $this->messageType);
 | ||
| 
 | ||
| 			return true;
 | ||
| 		}
 | ||
| 
 | ||
| 		return false;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Register the default task to perform if a mapping is not found.
 | ||
| 	 *
 | ||
| 	 * @param   string  $method  The name of the method in the derived class to perform if a named task is not found.
 | ||
| 	 *
 | ||
| 	 * @return  Controller  This object to support chaining.
 | ||
| 	 */
 | ||
| 	public function registerDefaultTask(string $method): Controller
 | ||
| 	{
 | ||
| 		$this->registerTask('__default', $method);
 | ||
| 
 | ||
| 		return $this;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Register (map) a task to a method in the class.
 | ||
| 	 *
 | ||
| 	 * @param   string  $task    The task.
 | ||
| 	 * @param   string  $method  The name of the method in the derived class to perform for this task.
 | ||
| 	 *
 | ||
| 	 * @return  Controller  This object to support chaining.
 | ||
| 	 */
 | ||
| 	public function registerTask(string $task, string $method): Controller
 | ||
| 	{
 | ||
| 		if (in_array($method, $this->methods))
 | ||
| 		{
 | ||
| 			$this->taskMap[$task] = $method;
 | ||
| 		}
 | ||
| 
 | ||
| 		return $this;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Unregister (unmap) a task in the class.
 | ||
| 	 *
 | ||
| 	 * @param   string  $task  The task.
 | ||
| 	 *
 | ||
| 	 * @return  Controller  This object to support chaining.
 | ||
| 	 */
 | ||
| 	public function unregisterTask(string $task): Controller
 | ||
| 	{
 | ||
| 		unset($this->taskMap[$task]);
 | ||
| 
 | ||
| 		return $this;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Sets the internal message that is passed with a redirect
 | ||
| 	 *
 | ||
| 	 * @param   string  $text  Message to display on redirect.
 | ||
| 	 * @param   string  $type  Message type. Optional, defaults to 'message'.
 | ||
| 	 *
 | ||
| 	 * @return  string|null  Previous message
 | ||
| 	 */
 | ||
| 	public function setMessage(string $text, string $type = 'message'): ?string
 | ||
| 	{
 | ||
| 		$previous          = $this->message;
 | ||
| 		$this->message     = $text;
 | ||
| 		$this->messageType = $type;
 | ||
| 
 | ||
| 		return $previous;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Set a URL for browser redirection.
 | ||
| 	 *
 | ||
| 	 * @param   string  $url   URL to redirect to.
 | ||
| 	 * @param   string  $msg   Message to display on redirect. Optional, defaults to value set internally by
 | ||
| 	 *                         controller, if any.
 | ||
| 	 * @param   string  $type  Message type. Optional, defaults to 'message' or the type set by a previous call to
 | ||
| 	 *                         setMessage.
 | ||
| 	 *
 | ||
| 	 * @return  Controller   This object to support chaining.
 | ||
| 	 */
 | ||
| 	public function setRedirect(string $url, ?string $msg = null, ?string $type = null): Controller
 | ||
| 	{
 | ||
| 		// If we're parsing a non-SEF URL decide whether to use Route or not
 | ||
| 		if (strpos($url, 'index.php') === 0)
 | ||
| 		{
 | ||
| 			$isAdmin = $this->container->platform->isBackend();
 | ||
| 			$auto    = false;
 | ||
| 
 | ||
| 			if (($this->autoRouting == 2 || $this->autoRouting == 3) && $isAdmin)
 | ||
| 			{
 | ||
| 				$auto = true;
 | ||
| 			}
 | ||
| 
 | ||
| 			if (($this->autoRouting == 1 || $this->autoRouting == 3) && !$isAdmin)
 | ||
| 			{
 | ||
| 				$auto = true;
 | ||
| 			}
 | ||
| 
 | ||
| 			if ($auto)
 | ||
| 			{
 | ||
| 				$url = Route::_($url, false);
 | ||
| 			}
 | ||
| 
 | ||
| 			/**
 | ||
| 			 * Joomla 4 does not add the base URI to redirections.
 | ||
| 			 *
 | ||
| 			 * This means that all bare redirects, e.g. to 'index.php?option=com_example', no longer work correctly.
 | ||
| 			 *
 | ||
| 			 * In the frontend, if your site is located in a subdirectory e.g. /foobar you get redirected to
 | ||
| 			 * /index.php?option=com_example instead of /foobar/index.php?option=com_example
 | ||
| 			 *
 | ||
| 			 * In the backend, you're redirected to /index.php?option=com_example instead of the expected
 | ||
| 			 * /administrator/index.php?option=com_example which breaks your application since the backend redirects to
 | ||
| 			 * the frontend.
 | ||
| 			 *
 | ||
| 			 * This is an undocumented b/c break in Joomla 4. It even breaks some of the core components...
 | ||
| 			 *
 | ||
| 			 * The following code detects bare redirect URLs and adds the base URI path if auto-routing has been
 | ||
| 			 * disabled, automatically fixing the observed issue. It only does that on Joomla 4 since adding the base
 | ||
| 			 * URI on Joomla 3 can cause redirection problems.
 | ||
| 			 */
 | ||
| 			if (!$auto && version_compare(JVERSION, '3.999.999', 'gt'))
 | ||
| 			{
 | ||
| 				$url = Uri::base() . $url;
 | ||
| 			}
 | ||
| 		}
 | ||
| 
 | ||
| 		// Set the redirection
 | ||
| 		$this->redirect = $url;
 | ||
| 
 | ||
| 		if ($msg !== null)
 | ||
| 		{
 | ||
| 			// Controller may have set this directly
 | ||
| 			$this->message = $msg;
 | ||
| 		}
 | ||
| 
 | ||
| 		// Ensure the type is not overwritten by a previous call to setMessage.
 | ||
| 		if (empty($this->messageType))
 | ||
| 		{
 | ||
| 			$this->messageType = 'message';
 | ||
| 		}
 | ||
| 
 | ||
| 		// If the type is explicitly set, set it.
 | ||
| 		if (!empty($type))
 | ||
| 		{
 | ||
| 			$this->messageType = $type;
 | ||
| 		}
 | ||
| 
 | ||
| 		return $this;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Returns true if there is a redirect set in the controller
 | ||
| 	 *
 | ||
| 	 * @return  boolean
 | ||
| 	 */
 | ||
| 	public function hasRedirect(): bool
 | ||
| 	{
 | ||
| 		return !empty($this->redirect);
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Provides CSRF protection through the forced use of a secure token. If the token doesn't match the one in the
 | ||
| 	 * session we return false.
 | ||
| 	 *
 | ||
| 	 * @return  bool
 | ||
| 	 *
 | ||
| 	 * @throws  Exception
 | ||
| 	 */
 | ||
| 	protected function csrfProtection(): bool
 | ||
| 	{
 | ||
| 		static $isCli = null, $isAdmin = null;
 | ||
| 
 | ||
| 		$platform = $this->container->platform;
 | ||
| 
 | ||
| 		if (is_null($isCli))
 | ||
| 		{
 | ||
| 			$isCli   = $platform->isCli();
 | ||
| 			$isAdmin = $platform->isBackend();
 | ||
| 		}
 | ||
| 
 | ||
| 		switch ($this->csrfProtection)
 | ||
| 		{
 | ||
| 			// Never
 | ||
| 			case 0:
 | ||
| 				return true;
 | ||
| 
 | ||
| 			// Always
 | ||
| 			case 1:
 | ||
| 				break;
 | ||
| 
 | ||
| 			// Only back-end and HTML format
 | ||
| 			case 2:
 | ||
| 				if ($isCli)
 | ||
| 				{
 | ||
| 					return true;
 | ||
| 				}
 | ||
| 				elseif (!$isAdmin && ($this->input->get('format', 'html', 'cmd') != 'html'))
 | ||
| 				{
 | ||
| 					return true;
 | ||
| 				}
 | ||
| 				break;
 | ||
| 
 | ||
| 			// Only back-end
 | ||
| 			case 3:
 | ||
| 				if (!$isAdmin)
 | ||
| 				{
 | ||
| 					return true;
 | ||
| 				}
 | ||
| 				break;
 | ||
| 		}
 | ||
| 
 | ||
| 		// Check for a session token
 | ||
| 		$token    = $this->container->platform->getToken(false);
 | ||
| 		$hasToken = $this->input->get($token, false, 'none') == 1;
 | ||
| 
 | ||
| 		if (!$hasToken)
 | ||
| 		{
 | ||
| 			$hasToken = $this->input->get('_token', null, 'none') == $token;
 | ||
| 		}
 | ||
| 
 | ||
| 		if ($hasToken)
 | ||
| 		{
 | ||
| 			$view = $this->input->getCmd('view');
 | ||
| 			$task = $this->input->getCmd('task');
 | ||
| 			Log::add(
 | ||
| 				"FOF: You are using a legacy session token in (view, task)=($view, $task). Support for legacy tokens will go away. Use form tokens instead.",
 | ||
| 				Log::WARNING,
 | ||
| 				'deprecated'
 | ||
| 			);
 | ||
| 		}
 | ||
| 
 | ||
| 		// Check for a form token
 | ||
| 		if (!$hasToken)
 | ||
| 		{
 | ||
| 			$token    = $this->container->platform->getToken(true);
 | ||
| 			$hasToken = $this->input->get($token, false, 'none') == 1;
 | ||
| 
 | ||
| 			if (!$hasToken)
 | ||
| 			{
 | ||
| 				$view = $this->input->getCmd('view');
 | ||
| 				$task = $this->input->getCmd('task');
 | ||
| 				Log::add(
 | ||
| 					"FOF: You are using the insecure _token form variable in (view, task)=($view, $task). Support for it will go away. Submit a variable with the token as the name and a value of 1 instead.",
 | ||
| 					Log::WARNING,
 | ||
| 					'deprecated'
 | ||
| 				);
 | ||
| 
 | ||
| 
 | ||
| 				$hasToken = $this->input->get('_token', null, 'none') == $token;
 | ||
| 			}
 | ||
| 		}
 | ||
| 
 | ||
| 		if (!$hasToken)
 | ||
| 		{
 | ||
| 			$platform->showErrorPage(new AccessForbidden(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')));
 | ||
| 
 | ||
| 			return false;
 | ||
| 		}
 | ||
| 
 | ||
| 		return true;
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * 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: onBeforeSomething, Arguments: array(123, 456)
 | ||
| 	 * The event calls:
 | ||
| 	 * 1. $this->onBeforeSomething(123, 456)
 | ||
| 	 * 2. $this->checkACL('@something') if there is no onBeforeSomething and the event starts with onBefore
 | ||
| 	 * 3. Joomla! plugin event onComFoobarControllerItemBeforeSomething($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 there is no handler method perform a simple ACL check
 | ||
| 		elseif (substr($event, 0, 8) == 'onBefore')
 | ||
| 		{
 | ||
| 			$task   = substr($event, 8);
 | ||
| 
 | ||
| 			if (array_key_exists($task, $this->taskMap))
 | ||
| 			{
 | ||
| 				$result = $this->checkACL('@' . $task);
 | ||
| 			}
 | ||
| 		}
 | ||
| 
 | ||
| 		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) . 'Controller';
 | ||
| 		$prefix .= ucfirst($this->getName());
 | ||
| 
 | ||
| 		// 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);
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Checks if the current user has enough privileges for the requested ACL area.
 | ||
| 	 *
 | ||
| 	 * @param   string  $area  The ACL area, e.g. core.manage.
 | ||
| 	 *
 | ||
| 	 * @return  boolean  True if the user has the ACL privilege specified
 | ||
| 	 */
 | ||
| 	protected function checkACL(string $area): bool
 | ||
| 	{
 | ||
| 		$area = $this->getACLRuleFor($area);
 | ||
| 
 | ||
| 		if (is_bool($area))
 | ||
| 		{
 | ||
| 			return $area;
 | ||
| 		}
 | ||
| 
 | ||
| 		if (in_array(strtolower($area), ['false', '0', 'no', '403']))
 | ||
| 		{
 | ||
| 			return false;
 | ||
| 		}
 | ||
| 
 | ||
| 		if (in_array(strtolower($area), ['true', '1', 'yes']))
 | ||
| 		{
 | ||
| 			return true;
 | ||
| 		}
 | ||
| 
 | ||
| 		if (strtolower($area) == 'guest')
 | ||
| 		{
 | ||
| 			return $this->container->platform->getUser()->guest;
 | ||
| 		}
 | ||
| 
 | ||
| 		if (strtolower($area) == 'user')
 | ||
| 		{
 | ||
| 			return !$this->container->platform->getUser()->guest;
 | ||
| 		}
 | ||
| 
 | ||
| 		if (empty($area))
 | ||
| 		{
 | ||
| 			return true;
 | ||
| 		}
 | ||
| 
 | ||
| 		return $this->container->platform->authorise($area, $this->container->componentName);
 | ||
| 	}
 | ||
| 
 | ||
| 	/**
 | ||
| 	 * Resolves @task and &callback notations for ACL privileges
 | ||
| 	 *
 | ||
| 	 * @param   string  $area      The task notation to resolve
 | ||
| 	 * @param   array   $oldAreas  Areas we've already been redirected from, used to detect circular references
 | ||
| 	 *
 | ||
| 	 * @return  string|bool  The resolved ACL privilege
 | ||
| 	 */
 | ||
| 	protected function getACLRuleFor(string $area, array $oldAreas = [])
 | ||
| 	{
 | ||
| 		// If it's a ¬ation return the callback result
 | ||
| 		if (substr($area, 0, 1) == '&')
 | ||
| 		{
 | ||
| 			$oldAreas[] = $area;
 | ||
| 			$method     = substr($area, 1);
 | ||
| 
 | ||
| 			// Method not found? Assume true.
 | ||
| 			if (!method_exists($this, $method))
 | ||
| 			{
 | ||
| 				return true;
 | ||
| 			}
 | ||
| 
 | ||
| 			$area = $this->$method();
 | ||
| 
 | ||
| 			return $this->getACLRuleFor($area, $oldAreas);
 | ||
| 		}
 | ||
| 
 | ||
| 		// If it's not an @notation return the raw string
 | ||
| 		if (substr($area, 0, 1) != '@')
 | ||
| 		{
 | ||
| 			return $area;
 | ||
| 		}
 | ||
| 
 | ||
| 		// Get the array index (other task)
 | ||
| 		$index = substr($area, 1);
 | ||
| 
 | ||
| 		// If the referenced task has no ACL map, return true
 | ||
| 		if (!isset($this->taskPrivileges[$index]))
 | ||
| 		{
 | ||
| 			$index = strtolower($index);
 | ||
| 
 | ||
| 			if (!isset($this->taskPrivileges[$index]))
 | ||
| 			{
 | ||
| 				return true;
 | ||
| 			}
 | ||
| 		}
 | ||
| 
 | ||
| 		// Get the new ACL area
 | ||
| 		$newArea = $this->taskPrivileges[$index];
 | ||
| 
 | ||
| 		$oldAreas[] = $area;
 | ||
| 
 | ||
| 		// Circular reference found
 | ||
| 		if (in_array($newArea, $oldAreas))
 | ||
| 		{
 | ||
| 			return true;
 | ||
| 		}
 | ||
| 
 | ||
| 		// We've found an ACL privilege. Return it.
 | ||
| 		if (substr($area, 0, 1) != '@')
 | ||
| 		{
 | ||
| 			return $newArea;
 | ||
| 		}
 | ||
| 
 | ||
| 		// We have another reference. Resolve it.
 | ||
| 		return $this->getACLRuleFor($newArea, $oldAreas);
 | ||
| 	}
 | ||
| 
 | ||
| 
 | ||
| }
 |