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