Completed
Push — actionrefactor ( 480336...d77352 )
by Andreas
03:58
created

ActionRouter::checkAction()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 5
nop 1
dl 0
loc 20
rs 9.2
c 0
b 0
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
            // do setup for new action
92
            $this->transitionAction($presetup, $actionname);
93
94
        } catch(NoActionException $e) {
95
            // give plugins an opportunity to process the actionname
96
            $evt = new \Doku_Event('ACTION_ACT_PREPROCESS', $actionname);
97
            if($evt->advise_before()) {
98
                if($actionname == $presetup) {
99
                    // no plugin changed the action, complain and switch to show
100
                    msg('Action unknown: ' . hsc($actionname), -1);
101
                    $actionname = 'show';
102
                }
103
                $this->transitionAction($presetup, $actionname);
104
            } else {
105
                // event said the action should be kept, assume action plugin will handle it later
106
                $this->action = new Plugin($actionname);
107
            }
108
            $evt->advise_after();
109
110
        } catch(\Exception $e) {
111
            $this->handleFatalException($e);
112
        }
113
    }
114
115
    /**
116
     * Transitions from one action to another
117
     *
118
     * Basically just calls setupAction() again but does some checks before. Also triggers
119
     * redirects for POST to show transitions
120
     *
121
     * @param string $from current action name
122
     * @param string $to new action name
123
     * @param null|ActionException $e any previous exception that caused the transition
124
     */
125
    protected function transitionAction($from, $to, $e = null) {
126
        global $INPUT;
127
        global $ID;
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
        // POST transitions to show should be a redirect
142
        if($to == 'show' && $from != $to && strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post') {
143
            act_redirect($ID, $from); // FIXME we may want to move this function to the class
144
        }
145
146
        // do the recursion
147
        $this->setupAction($to);
148
    }
149
150
    /**
151
     * Aborts all processing with a message
152
     *
153
     * When a FataException instanc is passed, the code is treated as Status code
154
     *
155
     * @param \Exception|FatalException $e
156
     */
157
    protected function handleFatalException(\Exception $e) {
158
        if(is_a($e, FatalException::class)) {
159
            http_status($e->getCode());
160
        } else {
161
            http_status(500);
162
        }
163
        $msg = 'Something unforseen has happened: ' . $e->getMessage();
164
        nice_die(hsc($msg));
165
    }
166
167
    /**
168
     * Load the given action
169
     *
170
     * This translates the given name to a class name by uppercasing the first letter.
171
     * Underscores translate to camelcase names. For actions with underscores, the different
172
     * parts are removed beginning from the end until a matching class is found. The instatiated
173
     * Action will always have the full original action set as Name
174
     *
175
     * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
176
     *
177
     * @param $actionname
178
     * @return AbstractAction
179
     * @throws NoActionException
180
     */
181
    public function loadAction($actionname) {
182
        $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
183
        $parts = explode('_', $actionname);
184
        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...
185
            $load = join('_', $parts);
186
            $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
187
            if(class_exists($class)) {
188
                return new $class($actionname);
189
            }
190
            array_pop($parts);
191
        }
192
193
        throw new NoActionException();
194
    }
195
196
    /**
197
     * Execute all the checks to see if this action can be executed
198
     *
199
     * @param AbstractAction $action
200
     * @throws ActionDisabledException
201
     * @throws ActionException
202
     */
203
    public function checkAction(AbstractAction $action) {
204
        global $INFO;
205
        global $ID;
206
207
        if(in_array($action->getActionName(), $this->disabled)) {
208
            throw new ActionDisabledException();
209
        }
210
211
        $action->checkPermissions();
212
213
        if(isset($INFO)) {
214
            $perm = $INFO['perm'];
215
        } else {
216
            $perm = auth_quickaclcheck($ID);
217
        }
218
219
        if($perm < $action->minimumPermission()) {
220
            throw new ActionException('denied');
221
        }
222
    }
223
224
    /**
225
     * Returns the action handling the current request
226
     *
227
     * @return AbstractAction
228
     */
229
    public function getAction() {
230
        return $this->action;
231
    }
232
}
233