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) { |
|
|
|
|
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
|
|
|
|
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.