%PDF- %PDF-
Direktori : /home/lightco1/upgrade.lightco.com.au/libraries/fof30/Controller/ |
Current File : /home/lightco1/upgrade.lightco.com.au/libraries/fof30/Controller/Controller.php |
<?php /** * @package FOF * @copyright 2010-2017 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU GPL version 2 or later */ namespace FOF30\Controller; use FOF30\Container\Container; use FOF30\Controller\Exception\CannotGetName; use FOF30\Controller\Exception\TaskNotFound; use FOF30\Model\DataModel; use FOF30\Model\Model; use FOF30\View\View; defined('_JEXEC') or die; /** * Class Controller * * A generic MVC controller implementation * * @property-read \FOF30\Input\Input $input The input object (magic __get returns the Input from the Container) */ class Controller { /** * The name of the controller * * @var array */ protected $name = null; /** * The mapped task that was performed. * * @var string */ protected $doTask; /** * Bit mask to enable routing through JRoute 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 */ 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; /** * Instance container. * * @var Controller */ protected static $instance; /** * 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 = null; /** * A cached copy of the class configuration parameter passed during initialisation * * @var array */ protected $config = array(); /** * Overrides the name of the view's default model * * @var string */ protected $modelName = null; /** * Overrides the name of the view's default view * * @var string */ protected $viewName = null; /** * An array of Model instances known to this Controller * * @var array[Model] */ protected $modelInstances = array(); /** * An array of View instances known to this Controller * * @var array[View] */ protected $viewInstances = array(); /** * The container attached to this Controller * * @var Container */ protected $container = null; /** * The tasks for which caching should be enabled by default * * @var array */ protected $cacheableTasks = array(); /** * 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 = array(); /** * 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 = array()) { // Initialise $this->methods = array(); $this->message = null; $this->messageType = 'message'; $this->paths = array(); $this->redirect = null; $this->taskMap = array(); // Get a local copy of the container $this->container = $container; // Determine the methods to exclude from the base class. $xMethods = get_class_methods('\\FOF30\\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($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 null|bool False on execution failure * * @throws TaskNotFound When the task is not found */ public function execute($task) { $this->task = $task; if (!isset($this->taskMap[$task]) && !isset($this->taskMap['__default'])) { throw new TaskNotFound(\JText::sprintf('JLIB_APPLICATION_ERROR_TASK_NOT_FOUND', $task), 404); } $result = $this->triggerEvent('onBeforeExecute', array(&$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', array($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 bool $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 */ public function display($cachable = false, $urlparams = false, $tpl = null) { $document = $this->container->platform->getDocument(); if ($document instanceof \JDocument) { $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 ($this->container->platform->isFrontend() && $cachable && ($viewType != 'feed') && ($conf->get('caching') >= 1)) { // Get a JCache object $option = $this->input->get('option', 'com_foobar', 'cmd'); /** @var \JCacheControllerView $cache */ $cache = \JFactory::getCache($option, 'view'); // Set up a cache ID based on component, view, task and user group assignment $user = $this->container->platform->getUser(); if ($user->guest) { $groups = array(); } else { $groups = $user->groups; } $importantParameters = array(); // Set up safe URL parameters if (!is_array($urlparams)) { $urlparams = array( 'option' => 'CMD', 'view' => 'CMD', 'task' => 'CMD', 'format' => 'CMD', 'layout' => 'CMD', 'id' => 'INT', ); } if (is_array($urlparams)) { /** @var \JApplicationCms $app */ $app = \JFactory::getApplication(); $registeredurlparams = null; if (!empty($app->registeredurlparams)) { $registeredurlparams = $app->registeredurlparams; } else { $registeredurlparams = new \stdClass; } foreach ($urlparams as $key => $value) { // Add your safe url parameters with variable type as value {@see JFilterInput::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(array(\JCache::makeId(), $view->getName(), $this->doTask, $groups, $importantParameters))); // Get the cached view or cache the current view $cache->get($view, 'display', $cacheId); } else { // Display without caching $view->display($tpl); } } /** * Alias to the display() task * * @codeCoverageIgnore */ 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($name = null, $config = array()) { if (!empty($name)) { $modelName = $name; } elseif (!empty($this->modelName)) { $modelName = $this->modelName; } else { $modelName = $this->view; } if (!array_key_exists($modelName, $this->modelInstances)) { 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($name = null, $config = array()) { 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]; } /** * 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($viewName) { $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($modelName) { $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($modelName, Model &$model) { $this->modelInstances[$modelName] = $model; } /** * 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($viewName, View &$view) { $this->viewInstances[$viewName] = $view; } /** * 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() { if (empty($this->name)) { $r = null; if (!preg_match('/(.*)\\\\Controller\\\\(.*)/i', get_class($this), $r)) { throw new CannotGetName(\JText::_('LIB_FOF_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() { return $this->task; } /** * Gets the available tasks in the controller. * * @return array Array[i] of task names. */ public function getTasks() { return $this->methods; } /** * Redirects the browser or returns false if no redirect is set. * * @return boolean False if no redirect exists. */ public function redirect() { if ($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($method) { $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($task, $method) { 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($task) { 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 Previous message */ public function setMessage($text, $type = 'message') { $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($url, $msg = null, $type = null) { // If we're parsing a non-SEF URL decide whether to use JRoute 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 = \JRoute::_($url, false); } } // 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; } /** * 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() { 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; break; // 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'); \JLog::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.", \JLog::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'); \JLog::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.", \JLog::WARNING, 'deprecated' ); $hasToken = $this->input->get('_token', null, 'none') == $token; } } if (!$hasToken) { $platform->raiseError(403, \JText::_('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($event, array $arguments = array()) { $result = true; // If there is an object method for this event, call it if (method_exists($this, $event)) { switch (count($arguments)) { case 0: $result = $this->{$event}(); break; case 1: $result = $this->{$event}($arguments[0]); break; case 2: $result = $this->{$event}($arguments[0], $arguments[1]); break; case 3: $result = $this->{$event}($arguments[0], $arguments[1], $arguments[2]); break; case 4: $result = $this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3]); break; case 5: $result = $this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]); break; default: $result = call_user_func_array(array($this, $event), $arguments); break; } } // If there is no handler method perform a simple ACL check elseif (substr($event, 0, 8) == 'onBefore') { $task = substr($event, 8); $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); if (!empty($results)) { foreach ($results as $result) { if ($result === false) { return false; } } } return 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($area) { $area = $this->getACLRuleFor($area); if (is_bool($area)) { return $area; } if (in_array(strtolower($area), array('false','0','no','403'))) { return false; } if (in_array(strtolower($area), array('true','1','yes'))) { return true; } if (in_array(strtolower($area), array('guest'))) { return $this->container->platform->getUser()->guest; } if (in_array(strtolower($area), array('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 mixed The resolved ACL privilege */ protected function getACLRuleFor($area, $oldAreas = array()) { // 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); } /** * Returns true if there is a redirect set in the controller * * @return boolean */ public function hasRedirect() { return !empty($this->redirect); } }