229 lines
6.8 KiB
PHP
229 lines
6.8 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace dokuwiki;
|
||
|
|
||
|
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 = null;
|
||
|
|
||
|
/** @var int transition counter */
|
||
|
protected $transitions = 0;
|
||
|
|
||
|
/** maximum loop */
|
||
|
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);
|
||
|
$this->transitions = 0;
|
||
|
|
||
|
$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 === null) || $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 Extension\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(is_a($e, ActionDisabledException::class)) {
|
||
|
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(\Exception $e) {
|
||
|
if(is_a($e, FatalException::class)) {
|
||
|
http_status($e->getCode());
|
||
|
} else {
|
||
|
http_status(500);
|
||
|
}
|
||
|
if(defined('DOKU_UNITTEST')) {
|
||
|
throw $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(!empty($parts)) {
|
||
|
$load = join('_', $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;
|
||
|
}
|
||
|
}
|