Completed
Push — actionrefactor ( 6e4bf0 )
by Andreas
04:36
created

ActionRouter::setupAction()   C

Complexity

Conditions 7
Paths 25

Size

Total Lines 41
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 7
eloc 24
nc 25
nop 1
dl 0
loc 41
rs 6.7272
c 3
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
    /**
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) {
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...
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