AcoManager::add()   C
last analyzed

Complexity

Conditions 11
Paths 9

Size

Total Lines 62
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 40
nc 9
nop 2
dl 0
loc 62
rs 6.1722
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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