Completed
Push — actionRouterImprovements ( b384ea )
by Michael
07:52 queued 04:04
created

ActionRouter::setupACT()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
rs 9.4285
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 = null;
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
    protected function __construct() {
37
        global $conf;
38
39
        $this->disabled = explode(',', $conf['disableactions']);
40
        $this->disabled = array_map('trim', $this->disabled);
41
        $this->transitions = 0;
42
    }
43
44
    /**
45
     * Get the singleton instance
46
     *
47
     * @param bool $reinit
48
     * @return ActionRouter
49
     */
50
    public static function getInstance($reinit = false) {
51
        if((self::$instance === null) || $reinit) {
52
            self::$instance = new ActionRouter();
53
        }
54
        return self::$instance;
55
    }
56
57
    /**
58
     * Sets up the correct action based on the $ACT global. Writes back the selected action to $ACT
59
     */
60
    public function setupACT() {
61
        global $ACT;
62
63
        $ACT = act_clean($ACT);
64
        $this->setupAction($ACT);
65
        $ACT = $this->action->getActionName();
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
        $presetup = $actionname;
79
80
        try {
81
            // give plugins an opportunity to process the actionname
82
            $evt = new \Doku_Event('ACTION_ACT_PREPROCESS', $actionname);
83
            if ($evt->advise_before()) {
84
                $this->action = $this->loadAction($actionname);
85
                $this->checkAction($this->action);
86
                $this->action->preProcess();
87
            } else {
88
                // event said the action should be kept, assume action plugin will handle it later
89
                $this->action = new Plugin($actionname);
90
            }
91
            $evt->advise_after();
92
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(is_a($e, ActionDisabledException::class)) {
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
110
        } catch(NoActionException $e) {
111
            msg('Action unknown: ' . hsc($actionname), -1);
112
            $actionname = 'show';
113
            $this->transitionAction($presetup, $actionname);
114
        } catch(\Exception $e) {
115
            $this->handleFatalException($e);
116
        }
117
    }
118
119
    /**
120
     * Transitions from one action to another
121
     *
122
     * Basically just calls setupAction() again but does some checks before.
123
     *
124
     * @param string $from current action name
125
     * @param string $to new action name
126
     * @param null|ActionException $e any previous exception that caused the transition
127
     */
128
    protected function transitionAction($from, $to, $e = null) {
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(\Exception $e) {
154
        if(is_a($e, FatalException::class)) {
155
            http_status($e->getCode());
156
        } else {
157
            http_status(500);
158
        }
159
        if(defined('DOKU_UNITTEST')) {
160
            throw $e;
161
        }
162
        $msg = 'Something unforseen has happened: ' . $e->getMessage();
163
        nice_die(hsc($msg));
164
    }
165
166
    /**
167
     * Load the given action
168
     *
169
     * This translates the given name to a class name by uppercasing the first letter.
170
     * Underscores translate to camelcase names. For actions with underscores, the different
171
     * parts are removed beginning from the end until a matching class is found. The instatiated
172
     * Action will always have the full original action set as Name
173
     *
174
     * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
175
     *
176
     * @param $actionname
177
     * @return AbstractAction
178
     * @throws NoActionException
179
     */
180
    public function loadAction($actionname) {
181
        $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
182
        $parts = explode('_', $actionname);
183
        while(!empty($parts)) {
184
            $load = join('_', $parts);
185
            $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
186
            if(class_exists($class)) {
187
                return new $class($actionname);
188
            }
189
            array_pop($parts);
190
        }
191
192
        throw new NoActionException();
193
    }
194
195
    /**
196
     * Execute all the checks to see if this action can be executed
197
     *
198
     * @param AbstractAction $action
199
     * @throws ActionDisabledException
200
     * @throws ActionException
201
     */
202
    public function checkAction(AbstractAction $action) {
203
        global $INFO;
204
        global $ID;
205
206
        if(in_array($action->getActionName(), $this->disabled)) {
207
            throw new ActionDisabledException();
208
        }
209
210
        $action->checkPermissions();
211
212
        if(isset($INFO)) {
213
            $perm = $INFO['perm'];
214
        } else {
215
            $perm = auth_quickaclcheck($ID);
216
        }
217
218
        if($perm < $action->minimumPermission()) {
219
            throw new ActionException('denied');
220
        }
221
    }
222
223
    /**
224
     * Returns the action handling the current request
225
     *
226
     * @return AbstractAction
227
     */
228
    public function getAction() {
229
        return $this->action;
230
    }
231
}
232