xref: /dokuwiki/inc/ActionRouter.php (revision 50701b661ac16d144bf9c99dce7abc68848c5021)
164ab5140SAndreas Gohr<?php
264ab5140SAndreas Gohr
364ab5140SAndreas Gohrnamespace dokuwiki;
464ab5140SAndreas Gohr
564ab5140SAndreas Gohruse dokuwiki\Action\AbstractAction;
664ab5140SAndreas Gohruse dokuwiki\Action\Exception\ActionDisabledException;
764ab5140SAndreas Gohruse dokuwiki\Action\Exception\ActionException;
864ab5140SAndreas Gohruse dokuwiki\Action\Exception\FatalException;
964ab5140SAndreas Gohruse dokuwiki\Action\Exception\NoActionException;
10a3f6fae6SAndreas Gohruse dokuwiki\Action\Plugin;
1164ab5140SAndreas Gohr
12a3f6fae6SAndreas Gohr/**
13a3f6fae6SAndreas Gohr * Class ActionRouter
14a3f6fae6SAndreas Gohr * @package dokuwiki
15a3f6fae6SAndreas Gohr */
1664ab5140SAndreas Gohrclass ActionRouter {
1764ab5140SAndreas Gohr
1864ab5140SAndreas Gohr    /** @var  AbstractAction */
1964ab5140SAndreas Gohr    protected $action;
2064ab5140SAndreas Gohr
2164ab5140SAndreas Gohr    /** @var  ActionRouter */
2264ab5140SAndreas Gohr    protected $instance;
2364ab5140SAndreas Gohr
24*50701b66SAndreas Gohr    /** @var int transition counter */
25*50701b66SAndreas Gohr    protected $transitions = 0;
26*50701b66SAndreas Gohr
27*50701b66SAndreas Gohr    /** maximum loop */
28*50701b66SAndreas Gohr    const MAX_TRANSITIONS = 5;
29*50701b66SAndreas Gohr
3064ab5140SAndreas Gohr    /**
3164ab5140SAndreas Gohr     * ActionRouter constructor. Singleton, thus protected!
32a3f6fae6SAndreas Gohr     *
33a3f6fae6SAndreas Gohr     * Sets up the correct action based on the $ACT global. Writes back
34a3f6fae6SAndreas Gohr     * the selected action to $ACT
3564ab5140SAndreas Gohr     */
3664ab5140SAndreas Gohr    protected function __construct() {
37a3f6fae6SAndreas Gohr        global $ACT;
38a3f6fae6SAndreas Gohr        $ACT = act_clean($ACT);
39a3f6fae6SAndreas Gohr        $this->setupAction($ACT);
40a3f6fae6SAndreas Gohr        $ACT = $this->action->getActionName();
4164ab5140SAndreas Gohr    }
4264ab5140SAndreas Gohr
4364ab5140SAndreas Gohr    /**
4464ab5140SAndreas Gohr     * Get the singleton instance
4564ab5140SAndreas Gohr     *
4664ab5140SAndreas Gohr     * @param bool $reinit
4764ab5140SAndreas Gohr     * @return ActionRouter
4864ab5140SAndreas Gohr     */
4964ab5140SAndreas Gohr    public function getInstance($reinit = false) {
5064ab5140SAndreas Gohr        if(($this->instance === null) || $reinit) {
5164ab5140SAndreas Gohr            $this->instance = new ActionRouter();
5264ab5140SAndreas Gohr        }
5364ab5140SAndreas Gohr        return $this->instance;
5464ab5140SAndreas Gohr    }
5564ab5140SAndreas Gohr
5664ab5140SAndreas Gohr    /**
5764ab5140SAndreas Gohr     * Setup the given action
5864ab5140SAndreas Gohr     *
5964ab5140SAndreas Gohr     * Instantiates the right class, runs permission checks and pre-processing and
60a3f6fae6SAndreas Gohr     * sets $action
6164ab5140SAndreas Gohr     *
6264ab5140SAndreas Gohr     * @param string $actionname
63a3f6fae6SAndreas Gohr     * @triggers ACTION_ACT_PREPROCESS
6464ab5140SAndreas Gohr     */
6564ab5140SAndreas Gohr    protected function setupAction($actionname) {
66a3f6fae6SAndreas Gohr        $presetup = $actionname;
67a3f6fae6SAndreas Gohr
6864ab5140SAndreas Gohr        try {
6964ab5140SAndreas Gohr            $this->action = $this->loadAction($actionname);
7064ab5140SAndreas Gohr            $this->action->checkPermissions();
7164ab5140SAndreas Gohr            $this->ensureMinimumPermission($this->action->minimumPermission());
7264ab5140SAndreas Gohr            $this->action->preProcess();
7364ab5140SAndreas Gohr
7464ab5140SAndreas Gohr        } catch(ActionException $e) {
7564ab5140SAndreas Gohr            // we should have gotten a new action
76a3f6fae6SAndreas Gohr            $actionname = $e->getNewAction();
7764ab5140SAndreas Gohr
7864ab5140SAndreas Gohr            // this one should trigger a user message
7964ab5140SAndreas Gohr            if(is_a($e, ActionDisabledException::class)) {
80a3f6fae6SAndreas Gohr                msg('Action disabled: ' . hsc($presetup), -1);
8164ab5140SAndreas Gohr            }
8264ab5140SAndreas Gohr
8364ab5140SAndreas Gohr            // do setup for new action
84*50701b66SAndreas Gohr            $this->transitionAction($presetup, $actionname);
8564ab5140SAndreas Gohr
8664ab5140SAndreas Gohr        } catch(NoActionException $e) {
87a3f6fae6SAndreas Gohr            // give plugins an opportunity to process the actionname
88a3f6fae6SAndreas Gohr            $evt = new \Doku_Event('ACTION_ACT_PREPROCESS', $actionname);
89a3f6fae6SAndreas Gohr            if($evt->advise_before()) {
90a3f6fae6SAndreas Gohr                if($actionname == $presetup) {
91a3f6fae6SAndreas Gohr                    // no plugin changed the action, complain and switch to show
92a3f6fae6SAndreas Gohr                    msg('Action unknown: ' . hsc($actionname), -1);
93a3f6fae6SAndreas Gohr                    $actionname = 'show';
94a3f6fae6SAndreas Gohr                }
95*50701b66SAndreas Gohr                $this->transitionAction($presetup, $actionname);
96a3f6fae6SAndreas Gohr            } else {
97a3f6fae6SAndreas Gohr                // event said the action should be kept, assume action plugin will handle it later
98a3f6fae6SAndreas Gohr                $this->action = new Plugin();
99a3f6fae6SAndreas Gohr                $this->action->setActionName($actionname);
100a3f6fae6SAndreas Gohr            }
101a3f6fae6SAndreas Gohr            $evt->advise_after();
10264ab5140SAndreas Gohr
10364ab5140SAndreas Gohr        } catch(\Exception $e) {
10464ab5140SAndreas Gohr            $this->handleFatalException($e);
10564ab5140SAndreas Gohr        }
10664ab5140SAndreas Gohr    }
10764ab5140SAndreas Gohr
10864ab5140SAndreas Gohr    /**
109*50701b66SAndreas Gohr     * Transitions from one action to another
110*50701b66SAndreas Gohr     *
111*50701b66SAndreas Gohr     * Basically just calls setupAction() again but does some checks before. Also triggers
112*50701b66SAndreas Gohr     * redirects for POST to show transitions
113*50701b66SAndreas Gohr     *
114*50701b66SAndreas Gohr     * @param string $from current action name
115*50701b66SAndreas Gohr     * @param string $to new action name
116*50701b66SAndreas Gohr     * @param null|ActionException $e any previous exception that caused the transition
117*50701b66SAndreas Gohr     */
118*50701b66SAndreas Gohr    protected function transitionAction($from, $to, $e = null) {
119*50701b66SAndreas Gohr        global $INPUT;
120*50701b66SAndreas Gohr        global $ID;
121*50701b66SAndreas Gohr
122*50701b66SAndreas Gohr        $this->transitions++;
123*50701b66SAndreas Gohr
124*50701b66SAndreas Gohr        // no infinite recursion
125*50701b66SAndreas Gohr        if($from == $to) {
126*50701b66SAndreas Gohr            $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
127*50701b66SAndreas Gohr        }
128*50701b66SAndreas Gohr
129*50701b66SAndreas Gohr        // larger loops will be caught here
130*50701b66SAndreas Gohr        if($this->transitions >= self::MAX_TRANSITIONS) {
131*50701b66SAndreas Gohr            $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
132*50701b66SAndreas Gohr        }
133*50701b66SAndreas Gohr
134*50701b66SAndreas Gohr        // POST transitions to show should be a redirect
135*50701b66SAndreas Gohr        if($to == 'show' && $from != $to && strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post') {
136*50701b66SAndreas Gohr            act_redirect($ID, $from); // FIXME we may want to move this function to the class
137*50701b66SAndreas Gohr        }
138*50701b66SAndreas Gohr
139*50701b66SAndreas Gohr        // do the recursion
140*50701b66SAndreas Gohr        $this->setupAction($to);
141*50701b66SAndreas Gohr    }
142*50701b66SAndreas Gohr
143*50701b66SAndreas Gohr    /**
14464ab5140SAndreas Gohr     * Check that the given minimum permissions are reached
14564ab5140SAndreas Gohr     *
14664ab5140SAndreas Gohr     * @param int $permneed
14764ab5140SAndreas Gohr     * @throws ActionException
14864ab5140SAndreas Gohr     */
14964ab5140SAndreas Gohr    protected function ensureMinimumPermission($permneed) {
15064ab5140SAndreas Gohr        global $INFO;
15164ab5140SAndreas Gohr        if($INFO['perm'] < $permneed) {
15264ab5140SAndreas Gohr            throw new ActionException('denied');
15364ab5140SAndreas Gohr        }
15464ab5140SAndreas Gohr    }
15564ab5140SAndreas Gohr
15664ab5140SAndreas Gohr    /**
15764ab5140SAndreas Gohr     * Aborts all processing with a message
15864ab5140SAndreas Gohr     *
15964ab5140SAndreas Gohr     * When a FataException instanc is passed, the code is treated as Status code
16064ab5140SAndreas Gohr     *
16164ab5140SAndreas Gohr     * @param \Exception|FatalException $e
16264ab5140SAndreas Gohr     */
16364ab5140SAndreas Gohr    protected function handleFatalException(\Exception $e) {
16464ab5140SAndreas Gohr        if(is_a($e, FatalException::class)) {
16564ab5140SAndreas Gohr            http_status($e->getCode());
16664ab5140SAndreas Gohr        } else {
16764ab5140SAndreas Gohr            http_status(500);
16864ab5140SAndreas Gohr        }
16964ab5140SAndreas Gohr        $msg = 'Something unforseen has happened: ' . $e->getMessage();
17064ab5140SAndreas Gohr        nice_die(hsc($msg));
17164ab5140SAndreas Gohr    }
17264ab5140SAndreas Gohr
17364ab5140SAndreas Gohr    /**
17464ab5140SAndreas Gohr     * Load the given action
17564ab5140SAndreas Gohr     *
17664ab5140SAndreas Gohr     * @param $actionname
17764ab5140SAndreas Gohr     * @return AbstractAction
17864ab5140SAndreas Gohr     * @throws NoActionException
17964ab5140SAndreas Gohr     */
18064ab5140SAndreas Gohr    protected function loadAction($actionname) {
18164ab5140SAndreas Gohr        $class = 'dokuwiki\\Action\\' . ucfirst(strtolower($actionname));
18264ab5140SAndreas Gohr        if(class_exists($class)) {
18364ab5140SAndreas Gohr            return new $class;
18464ab5140SAndreas Gohr        }
18564ab5140SAndreas Gohr        throw new NoActionException();
18664ab5140SAndreas Gohr    }
18764ab5140SAndreas Gohr
18864ab5140SAndreas Gohr    /**
18964ab5140SAndreas Gohr     * Returns the action handling the current request
19064ab5140SAndreas Gohr     *
19164ab5140SAndreas Gohr     * @return AbstractAction
19264ab5140SAndreas Gohr     */
19364ab5140SAndreas Gohr    public function getAction() {
19464ab5140SAndreas Gohr        return $this->action;
19564ab5140SAndreas Gohr    }
19664ab5140SAndreas Gohr}
197