xref: /dokuwiki/inc/ActionRouter.php (revision 54cc7aa41e0f453bd6887b0e79242a139d84a47a)
1<?php
2
3namespace dokuwiki;
4
5use dokuwiki\Extension\Event;
6use dokuwiki\Action\AbstractAction;
7use dokuwiki\Action\Exception\ActionDisabledException;
8use dokuwiki\Action\Exception\ActionException;
9use dokuwiki\Action\Exception\FatalException;
10use dokuwiki\Action\Exception\NoActionException;
11use dokuwiki\Action\Plugin;
12
13/**
14 * Class ActionRouter
15 * @package dokuwiki
16 */
17class ActionRouter {
18
19    /** @var  AbstractAction */
20    protected $action;
21
22    /** @var  ActionRouter */
23    protected static $instance;
24
25    /** @var int transition counter */
26    protected $transitions = 0;
27
28    /** maximum loop */
29    const MAX_TRANSITIONS = 5;
30
31    /** @var string[] the actions disabled in the configuration */
32    protected $disabled;
33
34    /**
35     * ActionRouter constructor. Singleton, thus protected!
36     *
37     * Sets up the correct action based on the $ACT global. Writes back
38     * the selected action to $ACT
39     */
40    protected function __construct() {
41        global $ACT;
42        global $conf;
43
44        $this->disabled = explode(',', $conf['disableactions']);
45        $this->disabled = array_map('trim', $this->disabled);
46
47        $ACT = act_clean($ACT);
48        $this->setupAction($ACT);
49        $ACT = $this->action->getActionName();
50    }
51
52    /**
53     * Get the singleton instance
54     *
55     * @param bool $reinit
56     * @return ActionRouter
57     */
58    public static function getInstance($reinit = false) {
59        if((!self::$instance instanceof \dokuwiki\ActionRouter) || $reinit) {
60            self::$instance = new ActionRouter();
61        }
62        return self::$instance;
63    }
64
65    /**
66     * Setup the given action
67     *
68     * Instantiates the right class, runs permission checks and pre-processing and
69     * sets $action
70     *
71     * @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility
72     * @triggers ACTION_ACT_PREPROCESS
73     */
74    protected function setupAction(&$actionname) {
75        $presetup = $actionname;
76
77        try {
78            // give plugins an opportunity to process the actionname
79            $evt = new Event('ACTION_ACT_PREPROCESS', $actionname);
80            if ($evt->advise_before()) {
81                $this->action = $this->loadAction($actionname);
82                $this->checkAction($this->action);
83                $this->action->preProcess();
84            } else {
85                // event said the action should be kept, assume action plugin will handle it later
86                $this->action = new Plugin($actionname);
87            }
88            $evt->advise_after();
89
90        } catch(ActionException $e) {
91            // we should have gotten a new action
92            $actionname = $e->getNewAction();
93
94            // this one should trigger a user message
95            if($e instanceof ActionDisabledException) {
96                msg('Action disabled: ' . hsc($presetup), -1);
97            }
98
99            // some actions may request the display of a message
100            if($e->displayToUser()) {
101                msg(hsc($e->getMessage()), -1);
102            }
103
104            // do setup for new action
105            $this->transitionAction($presetup, $actionname);
106
107        } catch(NoActionException $e) {
108            msg('Action unknown: ' . hsc($actionname), -1);
109            $actionname = 'show';
110            $this->transitionAction($presetup, $actionname);
111        } catch(\Exception $e) {
112            $this->handleFatalException($e);
113        }
114    }
115
116    /**
117     * Transitions from one action to another
118     *
119     * Basically just calls setupAction() again but does some checks before.
120     *
121     * @param string $from current action name
122     * @param string $to new action name
123     * @param null|ActionException $e any previous exception that caused the transition
124     */
125    protected function transitionAction($from, $to, $e = null) {
126        $this->transitions++;
127
128        // no infinite recursion
129        if($from == $to) {
130            $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
131        }
132
133        // larger loops will be caught here
134        if($this->transitions >= self::MAX_TRANSITIONS) {
135            $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
136        }
137
138        // do the recursion
139        $this->setupAction($to);
140    }
141
142    /**
143     * Aborts all processing with a message
144     *
145     * When a FataException instanc is passed, the code is treated as Status code
146     *
147     * @param \Exception|FatalException $e
148     * @throws FatalException during unit testing
149     */
150    protected function handleFatalException(\Throwable $e) {
151        if($e instanceof FatalException) {
152            http_status($e->getCode());
153        } else {
154            http_status(500);
155        }
156        if(defined('DOKU_UNITTEST')) {
157            throw $e;
158        }
159        ErrorHandler::logException($e);
160        $msg = 'Something unforeseen has happened: ' . $e->getMessage();
161        nice_die(hsc($msg));
162    }
163
164    /**
165     * Load the given action
166     *
167     * This translates the given name to a class name by uppercasing the first letter.
168     * Underscores translate to camelcase names. For actions with underscores, the different
169     * parts are removed beginning from the end until a matching class is found. The instatiated
170     * Action will always have the full original action set as Name
171     *
172     * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
173     *
174     * @param $actionname
175     * @return AbstractAction
176     * @throws NoActionException
177     */
178    public function loadAction($actionname) {
179        $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
180        $parts = explode('_', $actionname);
181        while($parts !== []) {
182            $load = implode('_', $parts);
183            $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
184            if(class_exists($class)) {
185                return new $class($actionname);
186            }
187            array_pop($parts);
188        }
189
190        throw new NoActionException();
191    }
192
193    /**
194     * Execute all the checks to see if this action can be executed
195     *
196     * @param AbstractAction $action
197     * @throws ActionDisabledException
198     * @throws ActionException
199     */
200    public function checkAction(AbstractAction $action) {
201        global $INFO;
202        global $ID;
203
204        if(in_array($action->getActionName(), $this->disabled)) {
205            throw new ActionDisabledException();
206        }
207
208        $action->checkPreconditions();
209
210        if(isset($INFO)) {
211            $perm = $INFO['perm'];
212        } else {
213            $perm = auth_quickaclcheck($ID);
214        }
215
216        if($perm < $action->minimumPermission()) {
217            throw new ActionException('denied');
218        }
219    }
220
221    /**
222     * Returns the action handling the current request
223     *
224     * @return AbstractAction
225     */
226    public function getAction() {
227        return $this->action;
228    }
229}
230