xref: /dokuwiki/inc/ActionRouter.php (revision 81f9e22b2e291e7d7b0156600c1f0dd4591fd4f8)
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 */
22ae7bcdc7SAndreas Gohr    protected static $instance;
2364ab5140SAndreas Gohr
2450701b66SAndreas Gohr    /** @var int transition counter */
2550701b66SAndreas Gohr    protected $transitions = 0;
2650701b66SAndreas Gohr
2750701b66SAndreas Gohr    /** maximum loop */
2850701b66SAndreas Gohr    const MAX_TRANSITIONS = 5;
2950701b66SAndreas Gohr
30480336a3SAndreas Gohr    /** @var string[] the actions disabled in the configuration */
31480336a3SAndreas Gohr    protected $disabled;
32480336a3SAndreas Gohr
3364ab5140SAndreas Gohr    /**
3464ab5140SAndreas Gohr     * ActionRouter constructor. Singleton, thus protected!
35a3f6fae6SAndreas Gohr     *
36a3f6fae6SAndreas Gohr     * Sets up the correct action based on the $ACT global. Writes back
37a3f6fae6SAndreas Gohr     * the selected action to $ACT
3864ab5140SAndreas Gohr     */
3964ab5140SAndreas Gohr    protected function __construct() {
40a3f6fae6SAndreas Gohr        global $ACT;
41480336a3SAndreas Gohr        global $conf;
42480336a3SAndreas Gohr
43480336a3SAndreas Gohr        $this->disabled = explode(',', $conf['disableactions']);
44480336a3SAndreas Gohr        $this->disabled = array_map('trim', $this->disabled);
45480336a3SAndreas Gohr        $this->transitions = 0;
46480336a3SAndreas Gohr
47a3f6fae6SAndreas Gohr        $ACT = act_clean($ACT);
48a3f6fae6SAndreas Gohr        $this->setupAction($ACT);
49a3f6fae6SAndreas Gohr        $ACT = $this->action->getActionName();
5064ab5140SAndreas Gohr    }
5164ab5140SAndreas Gohr
5264ab5140SAndreas Gohr    /**
5364ab5140SAndreas Gohr     * Get the singleton instance
5464ab5140SAndreas Gohr     *
5564ab5140SAndreas Gohr     * @param bool $reinit
5664ab5140SAndreas Gohr     * @return ActionRouter
5764ab5140SAndreas Gohr     */
58ae7bcdc7SAndreas Gohr    public static function getInstance($reinit = false) {
59ae7bcdc7SAndreas Gohr        if((self::$instance === null) || $reinit) {
60ae7bcdc7SAndreas Gohr            self::$instance = new ActionRouter();
6164ab5140SAndreas Gohr        }
62ae7bcdc7SAndreas Gohr        return self::$instance;
6364ab5140SAndreas Gohr    }
6464ab5140SAndreas Gohr
6564ab5140SAndreas Gohr    /**
6664ab5140SAndreas Gohr     * Setup the given action
6764ab5140SAndreas Gohr     *
6864ab5140SAndreas Gohr     * Instantiates the right class, runs permission checks and pre-processing and
69a3f6fae6SAndreas Gohr     * sets $action
7064ab5140SAndreas Gohr     *
7164ab5140SAndreas Gohr     * @param string $actionname
72a3f6fae6SAndreas Gohr     * @triggers ACTION_ACT_PREPROCESS
7364ab5140SAndreas Gohr     */
7464ab5140SAndreas Gohr    protected function setupAction($actionname) {
75a3f6fae6SAndreas Gohr        $presetup = $actionname;
76a3f6fae6SAndreas Gohr
7764ab5140SAndreas Gohr        try {
7864ab5140SAndreas Gohr            $this->action = $this->loadAction($actionname);
79480336a3SAndreas Gohr            $this->checkAction($this->action);
8064ab5140SAndreas Gohr            $this->action->preProcess();
8164ab5140SAndreas Gohr
8264ab5140SAndreas Gohr        } catch(ActionException $e) {
8364ab5140SAndreas Gohr            // we should have gotten a new action
84a3f6fae6SAndreas Gohr            $actionname = $e->getNewAction();
8564ab5140SAndreas Gohr
8664ab5140SAndreas Gohr            // this one should trigger a user message
8764ab5140SAndreas Gohr            if(is_a($e, ActionDisabledException::class)) {
88a3f6fae6SAndreas Gohr                msg('Action disabled: ' . hsc($presetup), -1);
8964ab5140SAndreas Gohr            }
9064ab5140SAndreas Gohr
91*81f9e22bSAndreas Gohr            // some actions may request the display of a message
92*81f9e22bSAndreas Gohr            if($e->displayToUser()) {
93*81f9e22bSAndreas Gohr                msg(hsc($e->getMessage()), -1);
94*81f9e22bSAndreas Gohr            }
95*81f9e22bSAndreas Gohr
9664ab5140SAndreas Gohr            // do setup for new action
9750701b66SAndreas Gohr            $this->transitionAction($presetup, $actionname);
9864ab5140SAndreas Gohr
9964ab5140SAndreas Gohr        } catch(NoActionException $e) {
100a3f6fae6SAndreas Gohr            // give plugins an opportunity to process the actionname
101a3f6fae6SAndreas Gohr            $evt = new \Doku_Event('ACTION_ACT_PREPROCESS', $actionname);
102a3f6fae6SAndreas Gohr            if($evt->advise_before()) {
103a3f6fae6SAndreas Gohr                if($actionname == $presetup) {
104a3f6fae6SAndreas Gohr                    // no plugin changed the action, complain and switch to show
105a3f6fae6SAndreas Gohr                    msg('Action unknown: ' . hsc($actionname), -1);
106a3f6fae6SAndreas Gohr                    $actionname = 'show';
107a3f6fae6SAndreas Gohr                }
10850701b66SAndreas Gohr                $this->transitionAction($presetup, $actionname);
109a3f6fae6SAndreas Gohr            } else {
110a3f6fae6SAndreas Gohr                // event said the action should be kept, assume action plugin will handle it later
11173522543SAndreas Gohr                $this->action = new Plugin($actionname);
112a3f6fae6SAndreas Gohr            }
113a3f6fae6SAndreas Gohr            $evt->advise_after();
11464ab5140SAndreas Gohr
11564ab5140SAndreas Gohr        } catch(\Exception $e) {
11664ab5140SAndreas Gohr            $this->handleFatalException($e);
11764ab5140SAndreas Gohr        }
11864ab5140SAndreas Gohr    }
11964ab5140SAndreas Gohr
12064ab5140SAndreas Gohr    /**
12150701b66SAndreas Gohr     * Transitions from one action to another
12250701b66SAndreas Gohr     *
12350701b66SAndreas Gohr     * Basically just calls setupAction() again but does some checks before. Also triggers
12450701b66SAndreas Gohr     * redirects for POST to show transitions
12550701b66SAndreas Gohr     *
12650701b66SAndreas Gohr     * @param string $from current action name
12750701b66SAndreas Gohr     * @param string $to new action name
12850701b66SAndreas Gohr     * @param null|ActionException $e any previous exception that caused the transition
12950701b66SAndreas Gohr     */
13050701b66SAndreas Gohr    protected function transitionAction($from, $to, $e = null) {
13150701b66SAndreas Gohr        global $INPUT;
13250701b66SAndreas Gohr        global $ID;
13350701b66SAndreas Gohr
13450701b66SAndreas Gohr        $this->transitions++;
13550701b66SAndreas Gohr
13650701b66SAndreas Gohr        // no infinite recursion
13750701b66SAndreas Gohr        if($from == $to) {
13850701b66SAndreas Gohr            $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
13950701b66SAndreas Gohr        }
14050701b66SAndreas Gohr
14150701b66SAndreas Gohr        // larger loops will be caught here
14250701b66SAndreas Gohr        if($this->transitions >= self::MAX_TRANSITIONS) {
14350701b66SAndreas Gohr            $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
14450701b66SAndreas Gohr        }
14550701b66SAndreas Gohr
14650701b66SAndreas Gohr        // POST transitions to show should be a redirect
14716d428e9SAndreas Gohr        if($to == 'show' && strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post') {
14850701b66SAndreas Gohr            act_redirect($ID, $from); // FIXME we may want to move this function to the class
14950701b66SAndreas Gohr        }
15050701b66SAndreas Gohr
15150701b66SAndreas Gohr        // do the recursion
15250701b66SAndreas Gohr        $this->setupAction($to);
15350701b66SAndreas Gohr    }
15450701b66SAndreas Gohr
15550701b66SAndreas Gohr    /**
15664ab5140SAndreas Gohr     * Aborts all processing with a message
15764ab5140SAndreas Gohr     *
15864ab5140SAndreas Gohr     * When a FataException instanc is passed, the code is treated as Status code
15964ab5140SAndreas Gohr     *
16064ab5140SAndreas Gohr     * @param \Exception|FatalException $e
16164ab5140SAndreas Gohr     */
16264ab5140SAndreas Gohr    protected function handleFatalException(\Exception $e) {
16364ab5140SAndreas Gohr        if(is_a($e, FatalException::class)) {
16464ab5140SAndreas Gohr            http_status($e->getCode());
16564ab5140SAndreas Gohr        } else {
16664ab5140SAndreas Gohr            http_status(500);
16764ab5140SAndreas Gohr        }
16864ab5140SAndreas Gohr        $msg = 'Something unforseen has happened: ' . $e->getMessage();
16964ab5140SAndreas Gohr        nice_die(hsc($msg));
17064ab5140SAndreas Gohr    }
17164ab5140SAndreas Gohr
17264ab5140SAndreas Gohr    /**
17364ab5140SAndreas Gohr     * Load the given action
17464ab5140SAndreas Gohr     *
17573522543SAndreas Gohr     * This translates the given name to a class name by uppercasing the first letter.
17673522543SAndreas Gohr     * Underscores translate to camelcase names. For actions with underscores, the different
17773522543SAndreas Gohr     * parts are removed beginning from the end until a matching class is found. The instatiated
17873522543SAndreas Gohr     * Action will always have the full original action set as Name
17973522543SAndreas Gohr     *
18073522543SAndreas Gohr     * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
18173522543SAndreas Gohr     *
18264ab5140SAndreas Gohr     * @param $actionname
18364ab5140SAndreas Gohr     * @return AbstractAction
18464ab5140SAndreas Gohr     * @throws NoActionException
18564ab5140SAndreas Gohr     */
186480336a3SAndreas Gohr    public function loadAction($actionname) {
18773522543SAndreas Gohr        $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
18873522543SAndreas Gohr        $parts = explode('_', $actionname);
18973522543SAndreas Gohr        while($parts) {
19073522543SAndreas Gohr            $load = join('_', $parts);
19173522543SAndreas Gohr            $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
19264ab5140SAndreas Gohr            if(class_exists($class)) {
19373522543SAndreas Gohr                return new $class($actionname);
19464ab5140SAndreas Gohr            }
19573522543SAndreas Gohr            array_pop($parts);
19673522543SAndreas Gohr        }
19773522543SAndreas Gohr
19864ab5140SAndreas Gohr        throw new NoActionException();
19964ab5140SAndreas Gohr    }
20064ab5140SAndreas Gohr
20164ab5140SAndreas Gohr    /**
202480336a3SAndreas Gohr     * Execute all the checks to see if this action can be executed
203480336a3SAndreas Gohr     *
204480336a3SAndreas Gohr     * @param AbstractAction $action
205480336a3SAndreas Gohr     * @throws ActionDisabledException
206480336a3SAndreas Gohr     * @throws ActionException
207480336a3SAndreas Gohr     */
208480336a3SAndreas Gohr    public function checkAction(AbstractAction $action) {
209480336a3SAndreas Gohr        global $INFO;
210480336a3SAndreas Gohr        global $ID;
211480336a3SAndreas Gohr
212480336a3SAndreas Gohr        if(in_array($action->getActionName(), $this->disabled)) {
213480336a3SAndreas Gohr            throw new ActionDisabledException();
214480336a3SAndreas Gohr        }
215480336a3SAndreas Gohr
216480336a3SAndreas Gohr        $action->checkPermissions();
217480336a3SAndreas Gohr
218480336a3SAndreas Gohr        if(isset($INFO)) {
219480336a3SAndreas Gohr            $perm = $INFO['perm'];
220480336a3SAndreas Gohr        } else {
221480336a3SAndreas Gohr            $perm = auth_quickaclcheck($ID);
222480336a3SAndreas Gohr        }
223480336a3SAndreas Gohr
224480336a3SAndreas Gohr        if($perm < $action->minimumPermission()) {
225480336a3SAndreas Gohr            throw new ActionException('denied');
226480336a3SAndreas Gohr        }
227480336a3SAndreas Gohr    }
228480336a3SAndreas Gohr
229480336a3SAndreas Gohr    /**
23064ab5140SAndreas Gohr     * Returns the action handling the current request
23164ab5140SAndreas Gohr     *
23264ab5140SAndreas Gohr     * @return AbstractAction
23364ab5140SAndreas Gohr     */
23464ab5140SAndreas Gohr    public function getAction() {
23564ab5140SAndreas Gohr        return $this->action;
23664ab5140SAndreas Gohr    }
23764ab5140SAndreas Gohr}
238