xref: /dokuwiki/inc/ActionRouter.php (revision d4f83172d9533c4d84f450fe22ef630816b21d75)
164ab5140SAndreas Gohr<?php
264ab5140SAndreas Gohr
364ab5140SAndreas Gohrnamespace dokuwiki;
464ab5140SAndreas Gohr
524870174SAndreas Gohruse dokuwiki\Extension\Event;
664ab5140SAndreas Gohruse dokuwiki\Action\AbstractAction;
764ab5140SAndreas Gohruse dokuwiki\Action\Exception\ActionDisabledException;
864ab5140SAndreas Gohruse dokuwiki\Action\Exception\ActionException;
964ab5140SAndreas Gohruse dokuwiki\Action\Exception\FatalException;
1064ab5140SAndreas Gohruse dokuwiki\Action\Exception\NoActionException;
11a3f6fae6SAndreas Gohruse dokuwiki\Action\Plugin;
1264ab5140SAndreas Gohr
13a3f6fae6SAndreas Gohr/**
14a3f6fae6SAndreas Gohr * Class ActionRouter
15a3f6fae6SAndreas Gohr * @package dokuwiki
16a3f6fae6SAndreas Gohr */
178c7c53b0SAndreas Gohrclass ActionRouter
188c7c53b0SAndreas Gohr{
1964ab5140SAndreas Gohr    /** @var  AbstractAction */
2064ab5140SAndreas Gohr    protected $action;
2164ab5140SAndreas Gohr
2264ab5140SAndreas Gohr    /** @var  ActionRouter */
2324870174SAndreas Gohr    protected static $instance;
2464ab5140SAndreas Gohr
2550701b66SAndreas Gohr    /** @var int transition counter */
2650701b66SAndreas Gohr    protected $transitions = 0;
2750701b66SAndreas Gohr
2850701b66SAndreas Gohr    /** maximum loop */
2974981a4eSAndreas Gohr    protected const MAX_TRANSITIONS = 5;
3050701b66SAndreas Gohr
31480336a3SAndreas Gohr    /** @var string[] the actions disabled in the configuration */
32480336a3SAndreas Gohr    protected $disabled;
33480336a3SAndreas Gohr
3464ab5140SAndreas Gohr    /**
3564ab5140SAndreas Gohr     * ActionRouter constructor. Singleton, thus protected!
36a3f6fae6SAndreas Gohr     *
37a3f6fae6SAndreas Gohr     * Sets up the correct action based on the $ACT global. Writes back
38a3f6fae6SAndreas Gohr     * the selected action to $ACT
3964ab5140SAndreas Gohr     */
40*d868eb89SAndreas Gohr    protected function __construct()
41*d868eb89SAndreas Gohr    {
42a3f6fae6SAndreas Gohr        global $ACT;
43480336a3SAndreas Gohr        global $conf;
44480336a3SAndreas Gohr
45480336a3SAndreas Gohr        $this->disabled = explode(',', $conf['disableactions']);
46480336a3SAndreas Gohr        $this->disabled = array_map('trim', $this->disabled);
47480336a3SAndreas Gohr
48a3f6fae6SAndreas Gohr        $ACT = act_clean($ACT);
49a3f6fae6SAndreas Gohr        $this->setupAction($ACT);
50a3f6fae6SAndreas Gohr        $ACT = $this->action->getActionName();
5164ab5140SAndreas Gohr    }
5264ab5140SAndreas Gohr
5364ab5140SAndreas Gohr    /**
5464ab5140SAndreas Gohr     * Get the singleton instance
5564ab5140SAndreas Gohr     *
5664ab5140SAndreas Gohr     * @param bool $reinit
5764ab5140SAndreas Gohr     * @return ActionRouter
5864ab5140SAndreas Gohr     */
59*d868eb89SAndreas Gohr    public static function getInstance($reinit = false)
60*d868eb89SAndreas Gohr    {
6124870174SAndreas Gohr        if ((!self::$instance instanceof \dokuwiki\ActionRouter) || $reinit) {
62ae7bcdc7SAndreas Gohr            self::$instance = new ActionRouter();
6364ab5140SAndreas Gohr        }
64ae7bcdc7SAndreas Gohr        return self::$instance;
6564ab5140SAndreas Gohr    }
6664ab5140SAndreas Gohr
6764ab5140SAndreas Gohr    /**
6864ab5140SAndreas Gohr     * Setup the given action
6964ab5140SAndreas Gohr     *
7064ab5140SAndreas Gohr     * Instantiates the right class, runs permission checks and pre-processing and
71a3f6fae6SAndreas Gohr     * sets $action
7264ab5140SAndreas Gohr     *
73c7d61a4eSAndreas Gohr     * @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility
74a3f6fae6SAndreas Gohr     * @triggers ACTION_ACT_PREPROCESS
7564ab5140SAndreas Gohr     */
76*d868eb89SAndreas Gohr    protected function setupAction(&$actionname)
77*d868eb89SAndreas Gohr    {
78a3f6fae6SAndreas Gohr        $presetup = $actionname;
79a3f6fae6SAndreas Gohr
8064ab5140SAndreas Gohr        try {
8168667f4aSMichael Große            // give plugins an opportunity to process the actionname
8224870174SAndreas Gohr            $evt = new Event('ACTION_ACT_PREPROCESS', $actionname);
8368667f4aSMichael Große            if ($evt->advise_before()) {
8464ab5140SAndreas Gohr                $this->action = $this->loadAction($actionname);
85480336a3SAndreas Gohr                $this->checkAction($this->action);
8664ab5140SAndreas Gohr                $this->action->preProcess();
8768667f4aSMichael Große            } else {
8868667f4aSMichael Große                // event said the action should be kept, assume action plugin will handle it later
8968667f4aSMichael Große                $this->action = new Plugin($actionname);
9068667f4aSMichael Große            }
9168667f4aSMichael Große            $evt->advise_after();
9264ab5140SAndreas Gohr        } catch (ActionException $e) {
9364ab5140SAndreas Gohr            // we should have gotten a new action
94a3f6fae6SAndreas Gohr            $actionname = $e->getNewAction();
9564ab5140SAndreas Gohr
9664ab5140SAndreas Gohr            // this one should trigger a user message
9724870174SAndreas Gohr            if ($e instanceof ActionDisabledException) {
98a3f6fae6SAndreas Gohr                msg('Action disabled: ' . hsc($presetup), -1);
9964ab5140SAndreas Gohr            }
10064ab5140SAndreas Gohr
10181f9e22bSAndreas Gohr            // some actions may request the display of a message
10281f9e22bSAndreas Gohr            if ($e->displayToUser()) {
10381f9e22bSAndreas Gohr                msg(hsc($e->getMessage()), -1);
10481f9e22bSAndreas Gohr            }
10581f9e22bSAndreas Gohr
10664ab5140SAndreas Gohr            // do setup for new action
10750701b66SAndreas Gohr            $this->transitionAction($presetup, $actionname);
10864ab5140SAndreas Gohr        } catch (NoActionException $e) {
109a3f6fae6SAndreas Gohr            msg('Action unknown: ' . hsc($actionname), -1);
110a3f6fae6SAndreas Gohr            $actionname = 'show';
11150701b66SAndreas Gohr            $this->transitionAction($presetup, $actionname);
11264ab5140SAndreas Gohr        } catch (\Exception $e) {
11364ab5140SAndreas Gohr            $this->handleFatalException($e);
11464ab5140SAndreas Gohr        }
11564ab5140SAndreas Gohr    }
11664ab5140SAndreas Gohr
11764ab5140SAndreas Gohr    /**
11850701b66SAndreas Gohr     * Transitions from one action to another
11950701b66SAndreas Gohr     *
12058528803SAndreas Gohr     * Basically just calls setupAction() again but does some checks before.
12150701b66SAndreas Gohr     *
12250701b66SAndreas Gohr     * @param string $from current action name
12350701b66SAndreas Gohr     * @param string $to new action name
12450701b66SAndreas Gohr     * @param null|ActionException $e any previous exception that caused the transition
12550701b66SAndreas Gohr     */
126*d868eb89SAndreas Gohr    protected function transitionAction($from, $to, $e = null)
127*d868eb89SAndreas Gohr    {
12850701b66SAndreas Gohr        $this->transitions++;
12950701b66SAndreas Gohr
13050701b66SAndreas Gohr        // no infinite recursion
13150701b66SAndreas Gohr        if ($from == $to) {
13250701b66SAndreas Gohr            $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
13350701b66SAndreas Gohr        }
13450701b66SAndreas Gohr
13550701b66SAndreas Gohr        // larger loops will be caught here
13650701b66SAndreas Gohr        if ($this->transitions >= self::MAX_TRANSITIONS) {
13750701b66SAndreas Gohr            $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
13850701b66SAndreas Gohr        }
13950701b66SAndreas Gohr
14050701b66SAndreas Gohr        // do the recursion
14150701b66SAndreas Gohr        $this->setupAction($to);
14250701b66SAndreas Gohr    }
14350701b66SAndreas Gohr
14450701b66SAndreas Gohr    /**
14564ab5140SAndreas Gohr     * Aborts all processing with a message
14664ab5140SAndreas Gohr     *
14764ab5140SAndreas Gohr     * When a FataException instanc is passed, the code is treated as Status code
14864ab5140SAndreas Gohr     *
14964ab5140SAndreas Gohr     * @param \Exception|FatalException $e
1507675e707SAndreas Gohr     * @throws FatalException during unit testing
15164ab5140SAndreas Gohr     */
152*d868eb89SAndreas Gohr    protected function handleFatalException(\Throwable $e)
153*d868eb89SAndreas Gohr    {
15424870174SAndreas Gohr        if ($e instanceof FatalException) {
15564ab5140SAndreas Gohr            http_status($e->getCode());
15664ab5140SAndreas Gohr        } else {
15764ab5140SAndreas Gohr            http_status(500);
15864ab5140SAndreas Gohr        }
1597675e707SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
1607675e707SAndreas Gohr            throw $e;
1617675e707SAndreas Gohr        }
162ecad51ddSAndreas Gohr        ErrorHandler::logException($e);
163985f440fSDamien Regad        $msg = 'Something unforeseen has happened: ' . $e->getMessage();
16464ab5140SAndreas Gohr        nice_die(hsc($msg));
16564ab5140SAndreas Gohr    }
16664ab5140SAndreas Gohr
16764ab5140SAndreas Gohr    /**
16864ab5140SAndreas Gohr     * Load the given action
16964ab5140SAndreas Gohr     *
17073522543SAndreas Gohr     * This translates the given name to a class name by uppercasing the first letter.
17173522543SAndreas Gohr     * Underscores translate to camelcase names. For actions with underscores, the different
17273522543SAndreas Gohr     * parts are removed beginning from the end until a matching class is found. The instatiated
17373522543SAndreas Gohr     * Action will always have the full original action set as Name
17473522543SAndreas Gohr     *
17573522543SAndreas Gohr     * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
17673522543SAndreas Gohr     *
17764ab5140SAndreas Gohr     * @param $actionname
17864ab5140SAndreas Gohr     * @return AbstractAction
17964ab5140SAndreas Gohr     * @throws NoActionException
18064ab5140SAndreas Gohr     */
181*d868eb89SAndreas Gohr    public function loadAction($actionname)
182*d868eb89SAndreas Gohr    {
18373522543SAndreas Gohr        $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
18473522543SAndreas Gohr        $parts = explode('_', $actionname);
18524870174SAndreas Gohr        while ($parts !== []) {
18624870174SAndreas Gohr            $load = implode('_', $parts);
18773522543SAndreas Gohr            $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
18864ab5140SAndreas Gohr            if (class_exists($class)) {
18973522543SAndreas Gohr                return new $class($actionname);
19064ab5140SAndreas Gohr            }
19173522543SAndreas Gohr            array_pop($parts);
19273522543SAndreas Gohr        }
19373522543SAndreas Gohr
19464ab5140SAndreas Gohr        throw new NoActionException();
19564ab5140SAndreas Gohr    }
19664ab5140SAndreas Gohr
19764ab5140SAndreas Gohr    /**
198480336a3SAndreas Gohr     * Execute all the checks to see if this action can be executed
199480336a3SAndreas Gohr     *
200480336a3SAndreas Gohr     * @param AbstractAction $action
201480336a3SAndreas Gohr     * @throws ActionDisabledException
202480336a3SAndreas Gohr     * @throws ActionException
203480336a3SAndreas Gohr     */
204*d868eb89SAndreas Gohr    public function checkAction(AbstractAction $action)
205*d868eb89SAndreas Gohr    {
206480336a3SAndreas Gohr        global $INFO;
207480336a3SAndreas Gohr        global $ID;
208480336a3SAndreas Gohr
209480336a3SAndreas Gohr        if (in_array($action->getActionName(), $this->disabled)) {
210480336a3SAndreas Gohr            throw new ActionDisabledException();
211480336a3SAndreas Gohr        }
212480336a3SAndreas Gohr
213b2c9cd19SAndreas Gohr        $action->checkPreconditions();
214480336a3SAndreas Gohr
215480336a3SAndreas Gohr        if (isset($INFO)) {
216480336a3SAndreas Gohr            $perm = $INFO['perm'];
217480336a3SAndreas Gohr        } else {
218480336a3SAndreas Gohr            $perm = auth_quickaclcheck($ID);
219480336a3SAndreas Gohr        }
220480336a3SAndreas Gohr
221480336a3SAndreas Gohr        if ($perm < $action->minimumPermission()) {
222480336a3SAndreas Gohr            throw new ActionException('denied');
223480336a3SAndreas Gohr        }
224480336a3SAndreas Gohr    }
225480336a3SAndreas Gohr
226480336a3SAndreas Gohr    /**
22764ab5140SAndreas Gohr     * Returns the action handling the current request
22864ab5140SAndreas Gohr     *
22964ab5140SAndreas Gohr     * @return AbstractAction
23064ab5140SAndreas Gohr     */
231*d868eb89SAndreas Gohr    public function getAction()
232*d868eb89SAndreas Gohr    {
23364ab5140SAndreas Gohr        return $this->action;
23464ab5140SAndreas Gohr    }
23564ab5140SAndreas Gohr}
236