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