xref: /dokuwiki/inc/ActionRouter.php (revision 90fb952c4c30c09c8446076ba05991c89a3f0b01)
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        } catch (ActionException $e) {
94            // we should have gotten a new action
95            $actionname = $e->getNewAction();
96
97            // this one should trigger a user message
98            if ($e instanceof ActionDisabledException) {
99                msg('Action disabled: ' . hsc($presetup), -1);
100            }
101
102            // some actions may request the display of a message
103            if ($e->displayToUser()) {
104                msg(hsc($e->getMessage()), -1);
105            }
106
107            // do setup for new action
108            $this->transitionAction($presetup, $actionname);
109        } catch (NoActionException $e) {
110            msg('Action unknown: ' . hsc($actionname), -1);
111            $actionname = 'show';
112            $this->transitionAction($presetup, $actionname);
113        } catch (\Exception $e) {
114            $this->handleFatalException($e);
115        }
116    }
117
118    /**
119     * Transitions from one action to another
120     *
121     * Basically just calls setupAction() again but does some checks before.
122     *
123     * @param string $from current action name
124     * @param string $to new action name
125     * @param null|ActionException $e any previous exception that caused the transition
126     */
127    protected function transitionAction($from, $to, $e = null)
128    {
129        $this->transitions++;
130
131        // no infinite recursion
132        if ($from == $to) {
133            $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
134        }
135
136        // larger loops will be caught here
137        if ($this->transitions >= self::MAX_TRANSITIONS) {
138            $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
139        }
140
141        // do the recursion
142        $this->setupAction($to);
143    }
144
145    /**
146     * Aborts all processing with a message
147     *
148     * When a FataException instanc is passed, the code is treated as Status code
149     *
150     * @param \Exception|FatalException $e
151     * @throws FatalException during unit testing
152     */
153    protected function handleFatalException(\Throwable $e)
154    {
155        if ($e instanceof FatalException) {
156            http_status($e->getCode());
157        } else {
158            http_status(500);
159        }
160        if (defined('DOKU_UNITTEST')) {
161            throw $e;
162        }
163        ErrorHandler::logException($e);
164        $msg = 'Something unforeseen 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    {
184        $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
185        $parts = explode('_', $actionname);
186        while ($parts !== []) {
187            $load = implode('_', $parts);
188            $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
189            if (class_exists($class)) {
190                return new $class($actionname);
191            }
192            array_pop($parts);
193        }
194
195        throw new NoActionException();
196    }
197
198    /**
199     * Execute all the checks to see if this action can be executed
200     *
201     * @param AbstractAction $action
202     * @throws ActionDisabledException
203     * @throws ActionException
204     */
205    public function checkAction(AbstractAction $action)
206    {
207        global $INFO;
208        global $ID;
209
210        if (in_array($action->getActionName(), $this->disabled)) {
211            throw new ActionDisabledException();
212        }
213
214        $action->checkPreconditions();
215
216        if (isset($INFO)) {
217            $perm = $INFO['perm'];
218        } else {
219            $perm = auth_quickaclcheck($ID);
220        }
221
222        if ($perm < $action->minimumPermission()) {
223            throw new ActionException('denied');
224        }
225    }
226
227    /**
228     * Returns the action handling the current request
229     *
230     * @return AbstractAction
231     */
232    public function getAction()
233    {
234        return $this->action;
235    }
236}
237