dokuwiki/inc/ActionRouter.php

236 lines
6.9 KiB
PHP

<?php
namespace dokuwiki;
use dokuwiki\Extension\Event;
use dokuwiki\Action\AbstractAction;
use dokuwiki\Action\Exception\ActionDisabledException;
use dokuwiki\Action\Exception\ActionException;
use dokuwiki\Action\Exception\FatalException;
use dokuwiki\Action\Exception\NoActionException;
use dokuwiki\Action\Plugin;
/**
* Class ActionRouter
* @package dokuwiki
*/
class ActionRouter
{
/** @var AbstractAction */
protected $action;
/** @var ActionRouter */
protected static $instance;
/** @var int transition counter */
protected $transitions = 0;
/** maximum loop */
protected const MAX_TRANSITIONS = 5;
/** @var string[] the actions disabled in the configuration */
protected $disabled;
/**
* ActionRouter constructor. Singleton, thus protected!
*
* Sets up the correct action based on the $ACT global. Writes back
* the selected action to $ACT
*/
protected function __construct()
{
global $ACT;
global $conf;
$this->disabled = explode(',', $conf['disableactions']);
$this->disabled = array_map('trim', $this->disabled);
$ACT = act_clean($ACT);
$this->setupAction($ACT);
$ACT = $this->action->getActionName();
}
/**
* Get the singleton instance
*
* @param bool $reinit
* @return ActionRouter
*/
public static function getInstance($reinit = false)
{
if ((!self::$instance instanceof \dokuwiki\ActionRouter) || $reinit) {
self::$instance = new ActionRouter();
}
return self::$instance;
}
/**
* Setup the given action
*
* Instantiates the right class, runs permission checks and pre-processing and
* sets $action
*
* @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility
* @triggers ACTION_ACT_PREPROCESS
*/
protected function setupAction(&$actionname)
{
$presetup = $actionname;
try {
// give plugins an opportunity to process the actionname
$evt = new Event('ACTION_ACT_PREPROCESS', $actionname);
if ($evt->advise_before()) {
$this->action = $this->loadAction($actionname);
$this->checkAction($this->action);
$this->action->preProcess();
} else {
// event said the action should be kept, assume action plugin will handle it later
$this->action = new Plugin($actionname);
}
$evt->advise_after();
} catch (ActionException $e) {
// we should have gotten a new action
$actionname = $e->getNewAction();
// this one should trigger a user message
if ($e instanceof ActionDisabledException) {
msg('Action disabled: ' . hsc($presetup), -1);
}
// some actions may request the display of a message
if ($e->displayToUser()) {
msg(hsc($e->getMessage()), -1);
}
// do setup for new action
$this->transitionAction($presetup, $actionname);
} catch (NoActionException $e) {
msg('Action unknown: ' . hsc($actionname), -1);
$actionname = 'show';
$this->transitionAction($presetup, $actionname);
} catch (\Exception $e) {
$this->handleFatalException($e);
}
}
/**
* Transitions from one action to another
*
* Basically just calls setupAction() again but does some checks before.
*
* @param string $from current action name
* @param string $to new action name
* @param null|ActionException $e any previous exception that caused the transition
*/
protected function transitionAction($from, $to, $e = null)
{
$this->transitions++;
// no infinite recursion
if ($from == $to) {
$this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
}
// larger loops will be caught here
if ($this->transitions >= self::MAX_TRANSITIONS) {
$this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
}
// do the recursion
$this->setupAction($to);
}
/**
* Aborts all processing with a message
*
* When a FataException instanc is passed, the code is treated as Status code
*
* @param \Exception|FatalException $e
* @throws FatalException during unit testing
*/
protected function handleFatalException(\Throwable $e)
{
if ($e instanceof FatalException) {
http_status($e->getCode());
} else {
http_status(500);
}
if (defined('DOKU_UNITTEST')) {
throw $e;
}
ErrorHandler::logException($e);
$msg = 'Something unforeseen has happened: ' . $e->getMessage();
nice_die(hsc($msg));
}
/**
* Load the given action
*
* This translates the given name to a class name by uppercasing the first letter.
* Underscores translate to camelcase names. For actions with underscores, the different
* parts are removed beginning from the end until a matching class is found. The instatiated
* Action will always have the full original action set as Name
*
* Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
*
* @param $actionname
* @return AbstractAction
* @throws NoActionException
*/
public function loadAction($actionname)
{
$actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
$parts = explode('_', $actionname);
while ($parts !== []) {
$load = implode('_', $parts);
$class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
if (class_exists($class)) {
return new $class($actionname);
}
array_pop($parts);
}
throw new NoActionException();
}
/**
* Execute all the checks to see if this action can be executed
*
* @param AbstractAction $action
* @throws ActionDisabledException
* @throws ActionException
*/
public function checkAction(AbstractAction $action)
{
global $INFO;
global $ID;
if (in_array($action->getActionName(), $this->disabled)) {
throw new ActionDisabledException();
}
$action->checkPreconditions();
if (isset($INFO)) {
$perm = $INFO['perm'];
} else {
$perm = auth_quickaclcheck($ID);
}
if ($perm < $action->minimumPermission()) {
throw new ActionException('denied');
}
}
/**
* Returns the action handling the current request
*
* @return AbstractAction
*/
public function getAction()
{
return $this->action;
}
}