xref: /dokuwiki/inc/ActionRouter.php (revision 73522543bc6b95590563b07feeb1b3d0113a2018)
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    /**
31     * ActionRouter constructor. Singleton, thus protected!
32     *
33     * Sets up the correct action based on the $ACT global. Writes back
34     * the selected action to $ACT
35     */
36    protected function __construct() {
37        global $ACT;
38        $ACT = act_clean($ACT);
39        $this->setupAction($ACT);
40        $ACT = $this->action->getActionName();
41    }
42
43    /**
44     * Get the singleton instance
45     *
46     * @param bool $reinit
47     * @return ActionRouter
48     */
49    public static function getInstance($reinit = false) {
50        if((self::$instance === null) || $reinit) {
51            self::$instance = new ActionRouter();
52        }
53        return self::$instance;
54    }
55
56    /**
57     * Setup the given action
58     *
59     * Instantiates the right class, runs permission checks and pre-processing and
60     * sets $action
61     *
62     * @param string $actionname
63     * @triggers ACTION_ACT_PREPROCESS
64     */
65    protected function setupAction($actionname) {
66        $presetup = $actionname;
67
68        try {
69            $this->action = $this->loadAction($actionname);
70            $this->action->checkPermissions();
71            $this->ensureMinimumPermission($this->action->minimumPermission());
72            $this->action->preProcess();
73
74        } catch(ActionException $e) {
75            // we should have gotten a new action
76            $actionname = $e->getNewAction();
77
78            // this one should trigger a user message
79            if(is_a($e, ActionDisabledException::class)) {
80                msg('Action disabled: ' . hsc($presetup), -1);
81            }
82
83            // do setup for new action
84            $this->transitionAction($presetup, $actionname);
85
86        } catch(NoActionException $e) {
87            // give plugins an opportunity to process the actionname
88            $evt = new \Doku_Event('ACTION_ACT_PREPROCESS', $actionname);
89            if($evt->advise_before()) {
90                if($actionname == $presetup) {
91                    // no plugin changed the action, complain and switch to show
92                    msg('Action unknown: ' . hsc($actionname), -1);
93                    $actionname = 'show';
94                }
95                $this->transitionAction($presetup, $actionname);
96            } else {
97                // event said the action should be kept, assume action plugin will handle it later
98                $this->action = new Plugin($actionname);
99            }
100            $evt->advise_after();
101
102        } catch(\Exception $e) {
103            $this->handleFatalException($e);
104        }
105    }
106
107    /**
108     * Transitions from one action to another
109     *
110     * Basically just calls setupAction() again but does some checks before. Also triggers
111     * redirects for POST to show transitions
112     *
113     * @param string $from current action name
114     * @param string $to new action name
115     * @param null|ActionException $e any previous exception that caused the transition
116     */
117    protected function transitionAction($from, $to, $e = null) {
118        global $INPUT;
119        global $ID;
120
121        $this->transitions++;
122
123        // no infinite recursion
124        if($from == $to) {
125            $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
126        }
127
128        // larger loops will be caught here
129        if($this->transitions >= self::MAX_TRANSITIONS) {
130            $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
131        }
132
133        // POST transitions to show should be a redirect
134        if($to == 'show' && $from != $to && strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post') {
135            act_redirect($ID, $from); // FIXME we may want to move this function to the class
136        }
137
138        // do the recursion
139        $this->setupAction($to);
140    }
141
142    /**
143     * Check that the given minimum permissions are reached
144     *
145     * @param int $permneed
146     * @throws ActionException
147     */
148    protected function ensureMinimumPermission($permneed) {
149        global $INFO;
150        if($INFO['perm'] < $permneed) {
151            throw new ActionException('denied');
152        }
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    protected 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     * Returns the action handling the current request
203     *
204     * @return AbstractAction
205     */
206    public function getAction() {
207        return $this->action;
208    }
209}
210