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