1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Licensed under The GPL-3.0 License |
4
|
|
|
* For full copyright and license information, please see the LICENSE.txt |
5
|
|
|
* Redistributions of files must retain the above copyright notice. |
6
|
|
|
* |
7
|
|
|
* @since 2.0.0 |
8
|
|
|
* @author Christopher Castro <[email protected]> |
9
|
|
|
* @link http://www.quickappscms.org |
10
|
|
|
* @license http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License |
11
|
|
|
*/ |
12
|
|
|
namespace User\Utility; |
13
|
|
|
|
14
|
|
|
use Cake\Datasource\ModelAwareTrait; |
15
|
|
|
use Cake\Error\FatalErrorException; |
16
|
|
|
use Cake\Filesystem\Folder; |
17
|
|
|
use Cake\Routing\Router; |
18
|
|
|
use Cake\Utility\Inflector; |
19
|
|
|
use CMS\Core\Plugin; |
20
|
|
|
use CMS\Core\StaticCacheTrait; |
21
|
|
|
use ReflectionException; |
22
|
|
|
use ReflectionMethod; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* A simple class for handling plugin's ACOs. |
26
|
|
|
* |
27
|
|
|
* ### Usage: |
28
|
|
|
* |
29
|
|
|
* ```php |
30
|
|
|
* $manager = new AcoManager('PluginName'); |
31
|
|
|
* ``` |
32
|
|
|
* |
33
|
|
|
* You must indicate the plugin to manage. |
34
|
|
|
*/ |
35
|
|
|
class AcoManager |
36
|
|
|
{ |
37
|
|
|
|
38
|
|
|
use ModelAwareTrait; |
39
|
|
|
use StaticCacheTrait; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Name of the plugin being managed. |
43
|
|
|
* |
44
|
|
|
* @var string |
45
|
|
|
*/ |
46
|
|
|
protected $_pluginName; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Constructor. |
50
|
|
|
* |
51
|
|
|
* @param string $pluginName The plugin being managed |
52
|
|
|
* @throws \Cake\Error\FatalErrorException When no plugin name is given. |
53
|
|
|
*/ |
54
|
|
|
public function __construct($pluginName = null) |
55
|
|
|
{ |
56
|
|
|
$this->_pluginName = $pluginName; |
57
|
|
|
|
58
|
|
|
if ($this->_pluginName === null) { |
59
|
|
|
throw new FatalErrorException(__d('user', 'You must provide a Plugin name to manage.')); |
60
|
|
|
} else { |
61
|
|
|
$this->_pluginName = (string)Inflector::camelize($this->_pluginName); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
$this->modelFactory('Table', ['Cake\ORM\TableRegistry', 'get']); |
65
|
|
|
$this->loadModel('User.Acos'); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Grants permissions to all users within $roles over the given $aco. |
70
|
|
|
* |
71
|
|
|
* ### ACO path format: |
72
|
|
|
* |
73
|
|
|
* - `ControllerName/`: Maps to \<PluginName>\Controller\ControllerName::index() |
74
|
|
|
* - `ControllerName`: Same. |
75
|
|
|
* - `ControllerName/action_name`: Maps to \<PluginName>\Controller\ControllerName::action_name() |
76
|
|
|
* - `Prefix/ControllerName/action_name`: Maps to \<PluginName>\Controller\Prefix\ControllerName::action_name() |
77
|
|
|
* |
78
|
|
|
* @param string $path ACO path as described above |
79
|
|
|
* @param array $roles List of user roles to grant access to. If not given, |
80
|
|
|
* $path cannot be used by anyone but "administrators" |
81
|
|
|
* @return bool True on success |
82
|
|
|
*/ |
83
|
|
|
public function add($path, $roles = []) |
84
|
|
|
{ |
85
|
|
|
$path = $this->_parseAco($path); |
86
|
|
|
if (!$path) { |
87
|
|
|
return false; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
// path already exists |
91
|
|
|
$contents = $this->Acos->node($path); |
92
|
|
|
if (is_object($contents)) { |
93
|
|
|
$contents = $contents->extract('alias')->toArray(); |
94
|
|
|
} |
95
|
|
|
if (!empty($contents) && implode('/', $contents) === $path) { |
96
|
|
|
return true; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
$parent = null; |
100
|
|
|
$current = null; |
101
|
|
|
$parts = explode('/', $path); |
102
|
|
|
$this->Acos->connection()->transactional(function () use ($parts, $current, &$parent, $path) { |
103
|
|
|
foreach ($parts as $alias) { |
104
|
|
|
$current[] = $alias; |
105
|
|
|
$content = $this->Acos->node(implode('/', $current)); |
106
|
|
|
|
107
|
|
|
if ($content) { |
108
|
|
|
$parent = $content->first(); |
109
|
|
|
} else { |
110
|
|
|
$acoEntity = $this->Acos->newEntity([ |
111
|
|
|
'parent_id' => (isset($parent->id) ? $parent->id : null), |
112
|
|
|
'plugin' => $this->_pluginName, |
113
|
|
|
'alias' => $alias, |
114
|
|
|
'alias_hash' => md5($alias), |
115
|
|
|
]); |
116
|
|
|
$parent = $this->Acos->save($acoEntity); |
117
|
|
|
} |
118
|
|
|
} |
119
|
|
|
}); |
120
|
|
|
|
121
|
|
|
if ($parent) { |
122
|
|
|
// register roles |
123
|
|
|
if (!empty($roles)) { |
124
|
|
|
$this->loadModel('User.Permissions'); |
125
|
|
|
$roles = $this->Acos->Roles |
126
|
|
|
->find() |
127
|
|
|
->select(['id']) |
128
|
|
|
->where(['Roles.slug IN' => $roles]) |
129
|
|
|
->all(); |
130
|
|
|
|
131
|
|
|
foreach ($roles as $role) { |
132
|
|
|
$permissionEntity = $this->Permissions->newEntity([ |
133
|
|
|
'aco_id' => $parent->id, // action |
134
|
|
|
'role_id' => $role->id, |
135
|
|
|
]); |
136
|
|
|
$this->Permissions->save($permissionEntity); |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
return true; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
return false; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Removes the given ACO and its permissions. |
148
|
|
|
* |
149
|
|
|
* @param string $path ACO path e.g. `ControllerName/action_name` |
150
|
|
|
* @return bool True on success, false if path was not found |
151
|
|
|
*/ |
152
|
|
|
public function remove($path) |
153
|
|
|
{ |
154
|
|
|
$contents = $this->Acos->node($path); |
155
|
|
|
if (!$contents) { |
156
|
|
|
return false; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
$content = $contents->first(); |
160
|
|
|
$this->Acos->removeFromTree($content); |
161
|
|
|
$this->Acos->delete($content); |
162
|
|
|
|
163
|
|
|
return true; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* This method should never be used unless you know what are you doing. |
168
|
|
|
* |
169
|
|
|
* Populates the "acos" DB with information of every installed plugin, or |
170
|
|
|
* for the given plugin. It will automatically extracts plugin's controllers |
171
|
|
|
* and actions for creating a tree structure as follow: |
172
|
|
|
* |
173
|
|
|
* - PluginName |
174
|
|
|
* - Admin |
175
|
|
|
* - PrivateController |
176
|
|
|
* - index |
177
|
|
|
* - some_action |
178
|
|
|
* - ControllerName |
179
|
|
|
* - index |
180
|
|
|
* - another_action |
181
|
|
|
* |
182
|
|
|
* After tree is created you should be able to change permissions using |
183
|
|
|
* User's permissions section in backend. |
184
|
|
|
* |
185
|
|
|
* @param string $for Optional, build ACOs for the given plugin, or all plugins |
186
|
|
|
* if not given |
187
|
|
|
* @param bool $sync Whether to sync the tree or not. When syncing all invalid |
188
|
|
|
* ACO entries will be removed from the tree, also new ones will be added. When |
189
|
|
|
* syn is set to false only new ACO entries will be added, any invalid entry |
190
|
|
|
* will remain in the tree. Defaults to false |
191
|
|
|
* @return bool True on success, false otherwise |
192
|
|
|
*/ |
193
|
|
|
public static function buildAcos($for = null, $sync = false) |
194
|
|
|
{ |
195
|
|
|
if (function_exists('ini_set')) { |
196
|
|
|
ini_set('max_execution_time', 300); |
197
|
|
|
} elseif (function_exists('set_time_limit')) { |
198
|
|
|
set_time_limit(300); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
if ($for === null) { |
202
|
|
|
$plugins = plugin()->toArray(); |
203
|
|
|
} else { |
204
|
|
|
try { |
205
|
|
|
$plugins = [plugin($for)]; |
206
|
|
|
} catch (\Exception $e) { |
207
|
|
|
return false; |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
$added = []; |
212
|
|
|
foreach ($plugins as $plugin) { |
213
|
|
|
if (!Plugin::exists($plugin->name)) { |
214
|
|
|
continue; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
$aco = new AcoManager($plugin->name); |
218
|
|
|
$controllerDir = normalizePath("{$plugin->path}/src/Controller/"); |
219
|
|
|
$folder = new Folder($controllerDir); |
220
|
|
|
$controllers = $folder->findRecursive('.*Controller\.php'); |
221
|
|
|
|
222
|
|
|
foreach ($controllers as $controller) { |
223
|
|
|
$controller = str_replace([$controllerDir, '.php'], '', $controller); |
224
|
|
|
$className = $plugin->name . '\\' . 'Controller\\' . str_replace(DS, '\\', $controller); |
225
|
|
|
$methods = static::_controllerMethods($className); |
226
|
|
|
|
227
|
|
|
if (!empty($methods)) { |
228
|
|
|
$path = explode('Controller\\', $className)[1]; |
229
|
|
|
$path = str_replace_last('Controller', '', $path); |
230
|
|
|
$path = str_replace('\\', '/', $path); |
231
|
|
|
|
232
|
|
|
foreach ($methods as $method) { |
233
|
|
|
if ($aco->add("{$path}/{$method}")) { |
234
|
|
|
$added[] = "{$plugin->name}/{$path}/{$method}"; |
235
|
|
|
} |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
if ($sync && isset($aco)) { |
242
|
|
|
$aco->Acos->recover(); |
243
|
|
|
$existingPaths = static::paths($for); |
244
|
|
|
foreach ($existingPaths as $exists) { |
245
|
|
|
if (!in_array($exists, $added)) { |
246
|
|
|
$aco->remove($exists); |
247
|
|
|
} |
248
|
|
|
} |
249
|
|
|
$validLeafs = $aco->Acos |
250
|
|
|
->find() |
251
|
|
|
->select(['id']) |
252
|
|
|
->where([ |
253
|
|
|
'id NOT IN' => $aco->Acos->find() |
254
|
|
|
->select(['parent_id']) |
255
|
|
|
->where(['parent_id IS NOT' => null]) |
256
|
|
|
]); |
257
|
|
|
|
258
|
|
|
$aco->Acos->Permissions->deleteAll([ |
259
|
|
|
'aco_id NOT IN' => $validLeafs |
260
|
|
|
]); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return true; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Gets a list of existing ACO paths for the given plugin, or the entire list |
268
|
|
|
* if no plugin is given. |
269
|
|
|
* |
270
|
|
|
* @param string $for Optional plugin name. e.g. `Taxonomy` |
271
|
|
|
* @return array All registered ACO paths |
272
|
|
|
*/ |
273
|
|
|
public static function paths($for = null) |
274
|
|
|
{ |
275
|
|
|
if ($for !== null) { |
276
|
|
|
try { |
277
|
|
|
$for = plugin($for)->name; |
278
|
|
|
} catch (\Exception $e) { |
279
|
|
|
return []; |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
$cacheKey = "paths({$for})"; |
284
|
|
|
$paths = static::cache($cacheKey); |
285
|
|
|
|
286
|
|
|
if ($paths === null) { |
287
|
|
|
$paths = []; |
288
|
|
|
$aco = new AcoManager('__dummy__'); |
289
|
|
|
$aco->loadModel('User.Acos'); |
290
|
|
|
$leafs = $aco->Acos |
291
|
|
|
->find('all') |
292
|
|
|
->select(['id']) |
293
|
|
|
->where([ |
294
|
|
|
'Acos.id NOT IN' => $aco->Acos |
295
|
|
|
->find() |
296
|
|
|
->select(['parent_id']) |
297
|
|
|
->where(['parent_id IS NOT' => null]) |
298
|
|
|
]); |
299
|
|
|
|
300
|
|
|
foreach ($leafs as $leaf) { |
301
|
|
|
$path = $aco->Acos |
302
|
|
|
->find('path', ['for' => $leaf->id]) |
303
|
|
|
->extract('alias') |
304
|
|
|
->toArray(); |
305
|
|
|
$path = implode('/', $path); |
306
|
|
|
|
307
|
|
|
if ($for === null || |
308
|
|
|
($for !== null && str_starts_with($path, "{$for}/")) |
309
|
|
|
) { |
310
|
|
|
$paths[] = $path; |
311
|
|
|
} |
312
|
|
|
} |
313
|
|
|
static::cache($cacheKey, $paths); |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
return $paths; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* Extracts method names of the given controller class. |
321
|
|
|
* |
322
|
|
|
* @param string $className Fully qualified name |
323
|
|
|
* @return array List of method names |
324
|
|
|
*/ |
325
|
|
|
protected static function _controllerMethods($className) |
326
|
|
|
{ |
327
|
|
|
if (!class_exists($className)) { |
328
|
|
|
return []; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
$methods = (array)get_this_class_methods($className); |
332
|
|
|
$actions = []; |
333
|
|
|
foreach ($methods as $methodName) { |
334
|
|
|
try { |
335
|
|
|
$method = new ReflectionMethod($className, $methodName); |
336
|
|
|
if ($method->isPublic()) { |
337
|
|
|
$actions[] = $methodName; |
338
|
|
|
} |
339
|
|
|
} catch (\ReflectionException $e) { |
340
|
|
|
// error |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
return $actions; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* Sanitizes the given ACO path. |
349
|
|
|
* |
350
|
|
|
* This methods can return an array with the following keys if `$string` option |
351
|
|
|
* is set to false: |
352
|
|
|
* |
353
|
|
|
* - `plugin`: The name of the plugin being managed by this class. |
354
|
|
|
* - `prefix`: ACO prefix, for example `Admin` for controller within /Controller/Admin/ |
355
|
|
|
* it may be empty, if not prefix is found. |
356
|
|
|
* - `controller`: Controller name. e.g.: `MySuperController` |
357
|
|
|
* - `action`: Controller's action. e.g.: `mini_action`, `index` by default |
358
|
|
|
* |
359
|
|
|
* For example: |
360
|
|
|
* |
361
|
|
|
* `Admin/Users/` |
362
|
|
|
* |
363
|
|
|
* Returns: |
364
|
|
|
* |
365
|
|
|
* - plugin: YourPlugin |
366
|
|
|
* - prefix: Admin |
367
|
|
|
* - controller: Users |
368
|
|
|
* - action: index |
369
|
|
|
* |
370
|
|
|
* Where "YourPlugin" is the plugin name passed to this class's constructor. |
371
|
|
|
* |
372
|
|
|
* @param string $aco An ACO path to parse |
373
|
|
|
* @param bool $string Indicates if it should return a string format path (/Controller/action) |
374
|
|
|
* @return bool|array|string An array as described above or false if an invalid $aco was given |
375
|
|
|
*/ |
376
|
|
|
protected function _parseAco($aco, $string = true) |
377
|
|
|
{ |
378
|
|
|
$aco = preg_replace('/\/{2,}/', '/', trim($aco, '/')); |
379
|
|
|
$parts = explode('/', $aco); |
380
|
|
|
|
381
|
|
|
if (empty($parts)) { |
382
|
|
|
return false; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
if (count($parts) === 1) { |
386
|
|
|
$controller = Inflector::camelize($parts[0]); |
387
|
|
|
|
388
|
|
|
return [ |
389
|
|
|
'prefix' => '', |
390
|
|
|
'controller' => $controller, |
391
|
|
|
'action' => 'index', |
392
|
|
|
]; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
$prefixes = $this->_routerPrefixes(); |
396
|
|
|
$prefix = Inflector::camelize($parts[0]); |
397
|
|
|
if (!in_array($prefix, $prefixes)) { |
398
|
|
|
$prefix = ''; |
399
|
|
|
} else { |
400
|
|
|
array_shift($parts); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
if (count($parts) == 2) { |
404
|
|
|
list($controller, $action) = $parts; |
405
|
|
|
} else { |
406
|
|
|
$controller = array_shift($parts); |
407
|
|
|
$action = 'index'; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
$plugin = $this->_pluginName; |
411
|
|
|
$result = compact('plugin', 'prefix', 'controller', 'action'); |
412
|
|
|
|
413
|
|
|
if ($string) { |
414
|
|
|
$result = implode('/', array_values($result)); |
415
|
|
|
$result = str_replace('//', '/', $result); |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
return $result; |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* Gets a CamelizedList of all existing router prefixes. |
423
|
|
|
* |
424
|
|
|
* @return array |
425
|
|
|
*/ |
426
|
|
|
protected function _routerPrefixes() |
427
|
|
|
{ |
428
|
|
|
$cache = static::cache('_routerPrefixes'); |
429
|
|
|
if (!$cache) { |
430
|
|
|
$prefixes = []; |
431
|
|
|
foreach (Router::routes() as $route) { |
432
|
|
|
if (empty($route->defaults['prefix'])) { |
433
|
|
|
continue; |
434
|
|
|
} else { |
435
|
|
|
$prefix = Inflector::camelize($route->defaults['prefix']); |
436
|
|
|
if (!in_array($prefix, $prefixes)) { |
437
|
|
|
$prefixes[] = $prefix; |
438
|
|
|
} |
439
|
|
|
} |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
$cache = static::cache('_routerPrefixes', $prefixes); |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
return $cache; |
446
|
|
|
} |
447
|
|
|
} |
448
|
|
|
|