about summary refs log tree commit diff stats
path: root/wiki/inc/ActionRouter.php
diff options
context:
space:
mode:
Diffstat (limited to 'wiki/inc/ActionRouter.php')
-rw-r--r--wiki/inc/ActionRouter.php228
1 files changed, 228 insertions, 0 deletions
diff --git a/wiki/inc/ActionRouter.php b/wiki/inc/ActionRouter.php
new file mode 100644
index 0000000..edc45cf
--- /dev/null
+++ b/wiki/inc/ActionRouter.php
@@ -0,0 +1,228 @@
+<?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 \Doku_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 unforseen 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;
+    }
+}