Completed
Push — 2.0 ( 614370...70c382 )
by Christopher
02:51
created

Plugin::checkReverseDependency()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 0
loc 9
rs 9.6666
c 2
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
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 CMS\Core;
13
14
use Cake\Core\App;
15
use Cake\Core\Plugin as CakePlugin;
16
use Cake\Error\FatalErrorException;
17
use Cake\Filesystem\File;
18
use Cake\Filesystem\Folder;
19
use Cake\Utility\Hash;
20
use Cake\Utility\Inflector;
21
use CMS\Core\Package\PackageFactory;
22
use CMS\Core\Package\PluginPackage;
23
use CMS\Core\StaticCacheTrait;
24
25
/**
26
 * Plugin is used to load and locate plugins.
27
 *
28
 * Wrapper for `Cake\Core\Plugin`, it adds some QuickAppsCMS specifics methods.
29
 */
30
class Plugin extends CakePlugin
31
{
32
33
    use StaticCacheTrait;
34
35
    /**
36
     * Get the given plugin as an object, or a collection of objects if not
37
     * specified.
38
     *
39
     * @param string $plugin Plugin name to get, or null to get a collection of
40
     *  all plugin objects
41
     * @return \CMS\Core\Package\PluginPackage|\Cake\Collection\Collection
42
     * @throws \Cake\Error\FatalErrorException When requested plugin was not found
43
     */
44
    public static function get($plugin = null)
45
    {
46
        $cacheKey = "get({$plugin})";
47
        $cache = static::cache($cacheKey);
48
49
        if ($cache !== null) {
50
            return $cache;
51
        }
52
53
        if ($plugin === null) {
54
            $collection = [];
55
            foreach ((array)quickapps('plugins') as $plugin) {
56
                $plugin = PackageFactory::create($plugin['name']);
57
                if ($plugin instanceof PluginPackage) {
58
                    $collection[] = $plugin;
59
                }
60
            }
61
            return static::cache($cacheKey, collection($collection));
62
        }
63
64
        $package = PackageFactory::create($plugin);
65
        if ($package instanceof PluginPackage) {
66
            return static::cache($cacheKey, $package);
67
        }
68
69
        throw new FatalErrorException(__d('cms', 'Plugin "{0}" was not found', $plugin));
70
    }
71
72
    /**
73
     * Scan plugin directories and returns plugin names and their paths within file
74
     * system. We consider "plugin name" as the name of the container directory.
75
     *
76
     * Example output:
77
     *
78
     * ```php
79
     * [
80
     *     'Users' => '/full/path/plugins/Users/',
81
     *     'ThemeManager' => '/full/path/plugins/ThemeManager/',
82
     *     ...
83
     *     'MySuperPlugin' => '/full/path/plugins/MySuperPlugin/',
84
     *     'DarkGreenTheme' => '/full/path/plugins/DarkGreenTheme/',
85
     * ]
86
     * ```
87
     *
88
     * If $ignoreThemes is set to true `DarkGreenTheme` will not be part of the
89
     * result.
90
     *
91
     * NOTE: All paths includes trailing slash.
92
     *
93
     * @param bool $ignoreThemes Whether include themes as well or not
94
     * @return array Associative array as `PluginName` => `/full/path/to/PluginName`
95
     */
96
    public static function scan($ignoreThemes = false)
97
    {
98
        $cacheKey = "scan({$ignoreThemes})";
99
        $cache = static::cache($cacheKey);
100
101
        if (!$cache) {
102
            $cache = [];
103
            $paths = App::path('Plugin');
104
            $Folder = new Folder();
105
            $Folder->sort = true;
106
107
            foreach ($paths as $path) {
108
                $Folder->cd($path);
109
                foreach ($Folder->read(true, true, true)[0] as $dir) {
110
                    $name = basename($dir);
111
                    $cache[$name] = normalizePath("{$dir}/");
112
                }
113
            }
114
115
            // look for Cake plugins installed using Composer
116
            if (file_exists(VENDOR_INCLUDE_PATH . 'cakephp-plugins.php')) {
117
                $cakePlugins = (array)include VENDOR_INCLUDE_PATH . 'cakephp-plugins.php';
118
                if (!empty($cakePlugins['plugins'])) {
119
                    $cache = Hash::merge($cakePlugins['plugins'], $cache);
120
                }
121
            }
122
123
            // filter, remove hidden folders and others
124
            foreach ($cache as $name => $path) {
125
                if (strpos($name, '.') === 0) {
126
                    unset($cache[$name]);
127
                } elseif ($name == 'CMS') {
128
                    unset($cache[$name]);
129
                } elseif ($ignoreThemes && str_ends_with($name, 'Theme')) {
130
                    unset($cache[$name]);
131
                }
132
            }
133
134
            $cache = static::cache($cacheKey, $cache);
135
        }
136
137
        return $cache;
138
    }
139
140
    /**
141
     * Checks whether a plugins is installed on the system regardless of its status.
142
     *
143
     * @param string $plugin Plugin to check
144
     * @return bool True if exists, false otherwise
145
     */
146
    public static function exists($plugin)
147
    {
148
        $check = quickapps("plugins.{$plugin}");
149
        return !empty($check);
150
    }
151
152
    /**
153
     * Validates a composer.json file.
154
     *
155
     * Below a list of validation rules that are applied:
156
     *
157
     * - must be a valid JSON file.
158
     * - key `name` must be present and follow the pattern `author/package`
159
     * - key `type` must be present.
160
     * - key `extra.regions` must be present if it's a theme (its name ends with
161
     *   `-theme`, e.g. `quickapps/blue-sky-theme`)
162
     *
163
     * ### Usage:
164
     *
165
     * ```php
166
     * $json = json_decode(file_gets_content('/path/to/composer.json'), true);
167
     * Plugin::validateJson($json);
168
     *
169
     * // OR:
170
     *
171
     * Plugin::validateJson('/path/to/composer.json');
172
     * ```
173
     *
174
     * @param array|string $json JSON given as an array result of
175
     *  `json_decode(..., true)`, or a string as path to where .json file can be found
176
     * @param bool $errorMessages If set to true an array of error messages
177
     *  will be returned, if set to false boolean result will be returned; true on
178
     *  success, false on validation failure. Defaults to false (boolean result)
179
     * @return array|bool
180
     */
181
    public static function validateJson($json, $errorMessages = false)
182
    {
183
        if (is_string($json) && is_readable($json) && !is_dir($json)) {
184
            $json = json_decode((new File($json))->read(), true);
185
        }
186
187
        $errors = [];
188
        if (!is_array($json) || empty($json)) {
189
            $errors[] = __d('cms', 'Corrupt JSON information.');
190
        } else {
191
            if (!isset($json['type'])) {
192
                $errors[] = __d('cms', 'Missing field: "{0}"', 'type');
193
            }
194
195
            if (!isset($json['name'])) {
196
                $errors[] = __d('cms', 'Missing field: "{0}"', 'name');
197
            } elseif (!preg_match('/^(.+)\/(.+)+$/', $json['name'])) {
198
                $errors[] = __d('cms', 'Invalid field: "{0}" ({1}). It should be: {2}', 'name', $json['name'], '{author-name}/{package-name}');
199
            } elseif (str_ends_with(strtolower($json['name']), 'theme')) {
200
                if (!isset($json['extra']['regions'])) {
201
                    $errors[] = __d('cms', 'Missing field: "{0}"', 'extra.regions');
202
                }
203
            }
204
        }
205
206
        if ($errorMessages) {
207
            return $errors;
208
        }
209
210
        return empty($errors);
211
    }
212
213
    /**
214
     * Checks if there is any active plugin that depends of $plugin.
215
     *
216
     * @param string|CMS\Package\PluginPackage $plugin Plugin name, package
217
     *  name (as `vendor/package`) or plugin package object result of
218
     *  `static::get()`
219
     * @return array A list of all plugin names that depends on $plugin, an empty
220
     *  array means that no other plugins depends on $pluginName, so $plugin can be
221
     *  safely deleted or turned off.
222
     * @throws \Cake\Error\FatalErrorException When requested plugin was not found
223
     * @see \CMS\Core\Plugin::get()
224
     */
225
    public static function checkReverseDependency($plugin)
226
    {
227
        if (!($plugin instanceof PluginPackage)) {
228
            list(, $pluginName) = packageSplit($plugin, true);
229
            $plugin = static::get($pluginName);
230
        }
231
232
        return $plugin->requiredBy()->toArray();
233
    }
234
}
235