1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace miBadger\Router; |
4
|
|
|
|
5
|
|
|
use miBadger\Http\ServerRequest; |
6
|
|
|
use miBadger\Http\ServerResponse; |
7
|
|
|
use miBadger\Http\ServerResponseException; |
8
|
|
|
|
9
|
|
|
class AccessRouter |
10
|
|
|
{ |
11
|
|
|
private $routes; // $routes[method] = (route, pattern, callable, permission) |
12
|
|
|
|
13
|
|
|
private $basePath; |
14
|
|
|
|
15
|
|
|
private $user; |
16
|
|
|
|
17
|
|
|
public function __construct(PermissionCheckable $user = null, $basePath = '') |
18
|
|
|
{ |
19
|
|
|
$this->user = $user; |
20
|
|
|
$this->routes = []; |
21
|
|
|
$this->basePath = $basePath; |
22
|
|
|
} |
23
|
|
|
/** |
24
|
|
|
* Returns the base path. |
25
|
|
|
* |
26
|
|
|
* @return string the base path. |
27
|
|
|
*/ |
28
|
|
|
public function getBasePath() |
29
|
|
|
{ |
30
|
|
|
return $this->basePath; |
31
|
|
|
} |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Set the base path. |
35
|
|
|
* |
36
|
|
|
* @param string $basePath |
37
|
|
|
* @return $this |
38
|
|
|
*/ |
39
|
|
|
public function setBasePath($basePath) |
40
|
|
|
{ |
41
|
|
|
$this->basePath = $basePath; |
42
|
|
|
|
43
|
|
|
return $this; |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* {@inheritdoc} |
48
|
|
|
*/ |
49
|
|
|
public function getIterator() |
50
|
|
|
{ |
51
|
|
|
return new \RecursiveArrayIterator($this->routes); |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Returns the number of key-value mappings in the router map. |
56
|
|
|
* |
57
|
|
|
* @return int the number of key-value mappings in the router map. |
58
|
|
|
*/ |
59
|
|
|
public function count() |
60
|
|
|
{ |
61
|
|
|
$result = 0; |
62
|
|
|
|
63
|
|
|
foreach ($this->routes as $routes) { |
64
|
|
|
$result += count($routes); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
return $result; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
View Code Duplication |
public function add(string $method, string $route, $permission, callable $callable) |
|
|
|
|
71
|
|
|
{ |
72
|
|
|
$entry = (object)[ |
73
|
|
|
'route' => $route, |
74
|
|
|
'pattern' => $this->createPattern($route), |
75
|
|
|
'permission' => $permission, |
76
|
|
|
'callable' => $callable |
77
|
|
|
]; |
78
|
|
|
|
79
|
|
|
if (!array_key_exists($method, $this->routes)) { |
80
|
|
|
$this->routes[$method] = []; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
$this->routes[$method][] = $entry; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Creates a regex-enabled pattern from the route |
88
|
|
|
* |
89
|
|
|
* @param string $route |
90
|
|
|
* @return string the pattern string. |
91
|
|
|
*/ |
92
|
|
|
private function createPattern(string $route) |
93
|
|
|
{ |
94
|
|
|
return '|^' . preg_replace('|\{[^\}]+\}|', '([^\/]+)', $route) . '$|'; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Removes the mapping for the specified route from the router map if present. |
99
|
|
|
* |
100
|
|
|
* @param string $method |
101
|
|
|
* @param string $route |
102
|
|
|
* @return null |
103
|
|
|
*/ |
104
|
|
|
public function remove(string $method, string $route, $permission) |
105
|
|
|
{ |
106
|
|
|
if (!array_key_exists($method, $this->routes)) { |
107
|
|
|
return; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
foreach ($this->routes[$method] as $key => $entry) { |
111
|
|
|
if ($entry->route == $route && $entry->permission == $permission) { |
112
|
|
|
unset($this->routes[$method][$key]); |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
if (count($this->routes[$method]) == 0) { |
117
|
|
|
unset($this->routes[$method]); |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Removes all of the mappings from the router map. |
123
|
|
|
* |
124
|
|
|
* @return null |
125
|
|
|
*/ |
126
|
|
|
public function clear() |
127
|
|
|
{ |
128
|
|
|
$this->routes = []; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Returns the result of the given route's callable. |
134
|
|
|
* |
135
|
|
|
* @param string|null $method = new ServerRequest()->getMethod() |
136
|
|
|
* @param string|null $route = new ServerRequest()->getUri()->getPath() |
137
|
|
|
* @return mixed the result of the given route's callable. |
138
|
|
|
* @throws ServerResponseException |
139
|
|
|
*/ |
140
|
|
View Code Duplication |
public function resolve($method = null, $route = null) |
|
|
|
|
141
|
|
|
{ |
142
|
|
|
$serverRequest = new ServerRequest(); |
143
|
|
|
|
144
|
|
|
if ($method === null) { |
145
|
|
|
$method = $serverRequest->getMethod(); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
if ($route === null) { |
149
|
|
|
$route = $serverRequest->getUri()->getPath(); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
if ($this->basePath !== '' && strpos($route, $this->basePath, 0) === 0) { |
153
|
|
|
$route = substr($route, strlen($this->basePath)); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
return $this->callCallable($this->getCallable($method, $route)); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Returns the result of the callable. |
161
|
|
|
* |
162
|
|
|
* @param array the callable and the route matches. |
163
|
|
|
* @return mixed the result the callable. |
164
|
|
|
*/ |
165
|
|
|
protected function callCallable($callable) |
166
|
|
|
{ |
167
|
|
|
return call_user_func_array($callable[0], $callable[1]); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
public function getMatchingRoutes($method, $route) |
171
|
|
|
{ |
172
|
|
|
if (!array_key_exists($method, $this->routes)) { |
173
|
|
|
throw new ServerResponseException(new ServerResponse(404)); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
$matchedRoutes = []; |
177
|
|
|
|
178
|
|
View Code Duplication |
foreach ($this->routes[$method] as $entry) { |
|
|
|
|
179
|
|
|
if (preg_match($entry->pattern, $route, $matches) > 0) { |
180
|
|
|
// At this point the route is matched |
181
|
|
|
array_shift($matches); |
182
|
|
|
$matchedRoutes[] = [$route, $entry, $matches]; |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
if (count($matchedRoutes) == 0) { |
187
|
|
|
throw new ServerResponseException(new ServerResponse(404)); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
return $matchedRoutes; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
protected function getCallable($method, $route) |
194
|
|
|
{ |
195
|
|
|
// Check if permissions are alright |
196
|
|
|
$matchedRoutes = $this->getMatchingRoutes($method, $route); |
197
|
|
|
|
198
|
|
|
foreach ($matchedRoutes as $match) { |
199
|
|
|
$route = $match[0]; |
|
|
|
|
200
|
|
|
$entry = $match[1]; |
201
|
|
|
$matches = $match[2]; |
202
|
|
|
|
203
|
|
|
if ($entry->permission === null |
204
|
|
|
|| ($entry->permission !== null && $this->user !== null && $this->user->hasPermission($entry->permission))) |
205
|
|
|
{ |
206
|
|
|
return [$entry->callable, $matches]; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
throw new ServerResponseException(new ServerResponse(403)); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* Returns the cleaned ({token} => %s) permission map for the router |
216
|
|
|
* (associative map from route to the set of permissions that have access to that route), |
217
|
|
|
* null if a route has wildcard permissions |
218
|
|
|
*/ |
219
|
|
|
public function makeGETRoutePermissionsMap() |
220
|
|
|
{ |
221
|
|
|
$outputMap = []; |
222
|
|
|
|
223
|
|
|
foreach ($this->routes["GET"] as $entry) { |
224
|
|
|
|
225
|
|
|
// Extract clean route by replacing every slug occurrence with %s |
226
|
|
|
$slugFreeRoute = preg_replace('/({.*?})/', '%s', $entry->route); |
227
|
|
|
|
228
|
|
|
if ($entry->permission === null) { |
229
|
|
|
$outputMap[$slugFreeRoute] = null; |
230
|
|
|
continue; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
if (!isset($outputMap[$slugFreeRoute])) { |
234
|
|
|
$outputMap[$slugFreeRoute] = []; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
$outputMap[$slugFreeRoute][] = $entry->permission; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
return $outputMap; |
241
|
|
|
} |
242
|
|
|
} |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.