Completed
Push — master ( bf6f54...20dc95 )
by Andreas
03:16
created

ActionRouter::setupAction()   C

Complexity

Conditions 8
Paths 25

Size

Total Lines 45
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 8
eloc 25
nc 25
nop 1
dl 0
loc 45
rs 5.3846
c 5
b 1
f 0
1
<?php
2
3
namespace dokuwiki;
4
5
use dokuwiki\Action\AbstractAction;
6
use dokuwiki\Action\Exception\ActionDisabledException;
7
use dokuwiki\Action\Exception\ActionException;
8
use dokuwiki\Action\Exception\FatalException;
9
use dokuwiki\Action\Exception\NoActionException;
10
use dokuwiki\Action\Plugin;
11
12
/**
13
 * Class ActionRouter
14
 * @package dokuwiki
15
 */
16
class 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.
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
        $this->transitions++;
131
132
        // no infinite recursion
133
        if($from == $to) {
134
            $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
135
        }
136
137
        // larger loops will be caught here
138
        if($this->transitions >= self::MAX_TRANSITIONS) {
139
            $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
140
        }
141
142
        // do the recursion
143
        $this->setupAction($to);
144
    }
145
146
    /**
147
     * Aborts all processing with a message
148
     *
149
     * When a FataException instanc is passed, the code is treated as Status code
150
     *
151
     * @param \Exception|FatalException $e
152
     */
153
    protected function handleFatalException(\Exception $e) {
154
        if(is_a($e, FatalException::class)) {
155
            http_status($e->getCode());
156
        } else {
157
            http_status(500);
158
        }
159
        $msg = 'Something unforseen has happened: ' . $e->getMessage();
160
        nice_die(hsc($msg));
161
    }
162
163
    /**
164
     * Load the given action
165
     *
166
     * This translates the given name to a class name by uppercasing the first letter.
167
     * Underscores translate to camelcase names. For actions with underscores, the different
168
     * parts are removed beginning from the end until a matching class is found. The instatiated
169
     * Action will always have the full original action set as Name
170
     *
171
     * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
172
     *
173
     * @param $actionname
174
     * @return AbstractAction
175
     * @throws NoActionException
176
     */
177
    public function loadAction($actionname) {
178
        $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
179
        $parts = explode('_', $actionname);
180
        while($parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
181
            $load = join('_', $parts);
182
            $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
183
            if(class_exists($class)) {
184
                return new $class($actionname);
185
            }
186
            array_pop($parts);
187
        }
188
189
        throw new NoActionException();
190
    }
191
192
    /**
193
     * Execute all the checks to see if this action can be executed
194
     *
195
     * @param AbstractAction $action
196
     * @throws ActionDisabledException
197
     * @throws ActionException
198
     */
199
    public function checkAction(AbstractAction $action) {
200
        global $INFO;
201
        global $ID;
202
203
        if(in_array($action->getActionName(), $this->disabled)) {
204
            throw new ActionDisabledException();
205
        }
206
207
        $action->checkPermissions();
208
209
        if(isset($INFO)) {
210
            $perm = $INFO['perm'];
211
        } else {
212
            $perm = auth_quickaclcheck($ID);
213
        }
214
215
        if($perm < $action->minimumPermission()) {
216
            throw new ActionException('denied');
217
        }
218
    }
219
220
    /**
221
     * Returns the action handling the current request
222
     *
223
     * @return AbstractAction
224
     */
225
    public function getAction() {
226
        return $this->action;
227
    }
228
}
229