1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 2.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Core;
16:
17: use Cake\Core\Exception\MissingPluginException;
18: use DirectoryIterator;
19:
20: /**
21: * Plugin is used to load and locate plugins.
22: *
23: * It also can retrieve plugin paths and load their bootstrap and routes files.
24: *
25: * @link https://book.cakephp.org/3.0/en/plugins.html
26: */
27: class Plugin
28: {
29:
30: /**
31: * Holds a list of all loaded plugins and their configuration
32: *
33: * @var \Cake\Core\PluginCollection|null
34: */
35: protected static $plugins;
36:
37: /**
38: * Class loader instance
39: *
40: * @var \Cake\Core\ClassLoader
41: */
42: protected static $_loader;
43:
44: /**
45: * Loads a plugin and optionally loads bootstrapping,
46: * routing files or runs an initialization function.
47: *
48: * Plugins only need to be loaded if you want bootstrapping/routes/cli commands to
49: * be exposed. If your plugin does not expose any of these features you do not need
50: * to load them.
51: *
52: * This method does not configure any autoloaders. That must be done separately either
53: * through composer, or your own code during config/bootstrap.php.
54: *
55: * ### Examples:
56: *
57: * `Plugin::load('DebugKit')`
58: *
59: * Will load the DebugKit plugin and will not load any bootstrap nor route files.
60: * However, the plugin will be part of the framework default routes, and have its
61: * CLI tools (if any) available for use.
62: *
63: * `Plugin::load('DebugKit', ['bootstrap' => true, 'routes' => true])`
64: *
65: * Will load the bootstrap.php and routes.php files.
66: *
67: * `Plugin::load('DebugKit', ['bootstrap' => false, 'routes' => true])`
68: *
69: * Will load routes.php file but not bootstrap.php
70: *
71: * `Plugin::load('FOC/Authenticate')`
72: *
73: * Will load plugin from `plugins/FOC/Authenticate`.
74: *
75: * It is also possible to load multiple plugins at once. Examples:
76: *
77: * `Plugin::load(['DebugKit', 'ApiGenerator'])`
78: *
79: * Will load the DebugKit and ApiGenerator plugins.
80: *
81: * `Plugin::load(['DebugKit', 'ApiGenerator'], ['bootstrap' => true])`
82: *
83: * Will load bootstrap file for both plugins
84: *
85: * ```
86: * Plugin::load([
87: * 'DebugKit' => ['routes' => true],
88: * 'ApiGenerator'
89: * ],
90: * ['bootstrap' => true])
91: * ```
92: *
93: * Will only load the bootstrap for ApiGenerator and only the routes for DebugKit
94: *
95: * ### Configuration options
96: *
97: * - `bootstrap` - array - Whether or not you want the $plugin/config/bootstrap.php file loaded.
98: * - `routes` - boolean - Whether or not you want to load the $plugin/config/routes.php file.
99: * - `ignoreMissing` - boolean - Set to true to ignore missing bootstrap/routes files.
100: * - `path` - string - The path the plugin can be found on. If empty the default plugin path (App.pluginPaths) will be used.
101: * - `classBase` - The path relative to `path` which contains the folders with class files.
102: * Defaults to "src".
103: * - `autoload` - boolean - Whether or not you want an autoloader registered. This defaults to false. The framework
104: * assumes you have configured autoloaders using composer. However, if your application source tree is made up of
105: * plugins, this can be a useful option.
106: *
107: * @param string|array $plugin name of the plugin to be loaded in CamelCase format or array or plugins to load
108: * @param array $config configuration options for the plugin
109: * @throws \Cake\Core\Exception\MissingPluginException if the folder for the plugin to be loaded is not found
110: * @return void
111: * @deprecated 3.7.0 This method will be removed in 4.0.0. Use Application::addPlugin() instead.
112: */
113: public static function load($plugin, array $config = [])
114: {
115: deprecationWarning(
116: 'Plugin::load() is deprecated. ' .
117: 'Use Application::addPlugin() instead. ' .
118: 'This method will be removed in 4.0.0.'
119: );
120:
121: if (is_array($plugin)) {
122: foreach ($plugin as $name => $conf) {
123: list($name, $conf) = is_numeric($name) ? [$conf, $config] : [$name, $conf];
124: static::load($name, $conf);
125: }
126:
127: return;
128: }
129:
130: $config += [
131: 'autoload' => false,
132: 'bootstrap' => false,
133: 'routes' => false,
134: 'console' => true,
135: 'classBase' => 'src',
136: 'ignoreMissing' => false,
137: 'name' => $plugin
138: ];
139:
140: if (!isset($config['path'])) {
141: $config['path'] = static::getCollection()->findPath($plugin);
142: }
143:
144: $config['classPath'] = $config['path'] . $config['classBase'] . DIRECTORY_SEPARATOR;
145: if (!isset($config['configPath'])) {
146: $config['configPath'] = $config['path'] . 'config' . DIRECTORY_SEPARATOR;
147: }
148: $pluginClass = str_replace('/', '\\', $plugin) . '\\Plugin';
149: if (class_exists($pluginClass)) {
150: $instance = new $pluginClass($config);
151: } else {
152: // Use stub plugin as this method will be removed long term.
153: $instance = new BasePlugin($config);
154: }
155: static::getCollection()->add($instance);
156:
157: if ($config['autoload'] === true) {
158: if (empty(static::$_loader)) {
159: static::$_loader = new ClassLoader();
160: static::$_loader->register();
161: }
162: static::$_loader->addNamespace(
163: str_replace('/', '\\', $plugin),
164: $config['path'] . $config['classBase'] . DIRECTORY_SEPARATOR
165: );
166: static::$_loader->addNamespace(
167: str_replace('/', '\\', $plugin) . '\Test',
168: $config['path'] . 'tests' . DIRECTORY_SEPARATOR
169: );
170: }
171:
172: if ($config['bootstrap'] === true) {
173: static::bootstrap($plugin);
174: }
175: }
176:
177: /**
178: * Will load all the plugins located in the default plugin folder.
179: *
180: * If passed an options array, it will be used as a common default for all plugins to be loaded
181: * It is possible to set specific defaults for each plugins in the options array. Examples:
182: *
183: * ```
184: * Plugin::loadAll([
185: * ['bootstrap' => true],
186: * 'DebugKit' => ['routes' => true],
187: * ]);
188: * ```
189: *
190: * The above example will load the bootstrap file for all plugins, but for DebugKit it will only load the routes file
191: * and will not look for any bootstrap script.
192: *
193: * If a plugin has been loaded already, it will not be reloaded by loadAll().
194: *
195: * @param array $options Options.
196: * @return void
197: * @throws \Cake\Core\Exception\MissingPluginException
198: * @deprecated 3.7.0 This method will be removed in 4.0.0.
199: */
200: public static function loadAll(array $options = [])
201: {
202: $plugins = [];
203: foreach (App::path('Plugin') as $path) {
204: if (!is_dir($path)) {
205: continue;
206: }
207: $dir = new DirectoryIterator($path);
208: foreach ($dir as $dirPath) {
209: if ($dirPath->isDir() && !$dirPath->isDot()) {
210: $plugins[] = $dirPath->getBasename();
211: }
212: }
213: }
214: if (Configure::check('plugins')) {
215: $plugins = array_merge($plugins, array_keys(Configure::read('plugins')));
216: $plugins = array_unique($plugins);
217: }
218:
219: $collection = static::getCollection();
220: foreach ($plugins as $p) {
221: $opts = isset($options[$p]) ? $options[$p] : null;
222: if ($opts === null && isset($options[0])) {
223: $opts = $options[0];
224: }
225: if ($collection->has($p)) {
226: continue;
227: }
228: static::load($p, (array)$opts);
229: }
230: }
231:
232: /**
233: * Returns the filesystem path for a plugin
234: *
235: * @param string $name name of the plugin in CamelCase format
236: * @return string path to the plugin folder
237: * @throws \Cake\Core\Exception\MissingPluginException if the folder for plugin was not found or plugin has not been loaded
238: */
239: public static function path($name)
240: {
241: $plugin = static::getCollection()->get($name);
242:
243: return $plugin->getPath();
244: }
245:
246: /**
247: * Returns the filesystem path for plugin's folder containing class folders.
248: *
249: * @param string $name name of the plugin in CamelCase format.
250: * @return string Path to the plugin folder container class folders.
251: * @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
252: */
253: public static function classPath($name)
254: {
255: $plugin = static::getCollection()->get($name);
256:
257: return $plugin->getClassPath();
258: }
259:
260: /**
261: * Returns the filesystem path for plugin's folder containing config files.
262: *
263: * @param string $name name of the plugin in CamelCase format.
264: * @return string Path to the plugin folder container config files.
265: * @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded.
266: */
267: public static function configPath($name)
268: {
269: $plugin = static::getCollection()->get($name);
270:
271: return $plugin->getConfigPath();
272: }
273:
274: /**
275: * Loads the bootstrapping files for a plugin, or calls the initialization setup in the configuration
276: *
277: * @param string $name name of the plugin
278: * @return mixed
279: * @see \Cake\Core\Plugin::load() for examples of bootstrap configuration
280: * @deprecated 3.7.0 This method will be removed in 4.0.0.
281: */
282: public static function bootstrap($name)
283: {
284: deprecationWarning(
285: 'Plugin::bootstrap() is deprecated. ' .
286: 'This method will be removed in 4.0.0.'
287: );
288: $plugin = static::getCollection()->get($name);
289: if (!$plugin->isEnabled('bootstrap')) {
290: return false;
291: }
292: // Disable bootstrapping for this plugin as it will have
293: // been bootstrapped.
294: $plugin->disable('bootstrap');
295:
296: return static::_includeFile(
297: $plugin->getConfigPath() . 'bootstrap.php',
298: true
299: );
300: }
301:
302: /**
303: * Loads the routes file for a plugin, or all plugins configured to load their respective routes file.
304: *
305: * If you need fine grained control over how routes are loaded for plugins, you
306: * can use {@see Cake\Routing\RouteBuilder::loadPlugin()}
307: *
308: * @param string|null $name name of the plugin, if null will operate on all
309: * plugins having enabled the loading of routes files.
310: * @return bool
311: * @deprecated 3.6.5 This method is no longer needed when using HttpApplicationInterface based applications.
312: * This method will be removed in 4.0.0
313: */
314: public static function routes($name = null)
315: {
316: deprecationWarning(
317: 'You no longer need to call `Plugin::routes()` after upgrading to use Http\Server. ' .
318: 'See https://book.cakephp.org/3.0/en/development/application.html#adding-the-new-http-stack-to-an-existing-application ' .
319: 'for upgrade information.'
320: );
321: if ($name === null) {
322: foreach (static::loaded() as $p) {
323: static::routes($p);
324: }
325:
326: return true;
327: }
328: $plugin = static::getCollection()->get($name);
329: if (!$plugin->isEnabled('routes')) {
330: return false;
331: }
332:
333: return (bool)static::_includeFile(
334: $plugin->getConfigPath() . 'routes.php',
335: true
336: );
337: }
338:
339: /**
340: * Check whether or not a plugin is loaded.
341: *
342: * @param string $plugin The name of the plugin to check.
343: * @return bool
344: * @since 3.7.0
345: */
346: public static function isLoaded($plugin)
347: {
348: return static::getCollection()->has($plugin);
349: }
350:
351: /**
352: * Return a list of loaded plugins.
353: *
354: * If a plugin name is provided, the return value will be a bool
355: * indicating whether or not the named plugin is loaded. This usage
356: * is deprecated. Instead you should use Plugin::isLoaded($name)
357: *
358: * @param string|null $plugin Plugin name.
359: * @return bool|array Boolean true if $plugin is already loaded.
360: * If $plugin is null, returns a list of plugins that have been loaded
361: */
362: public static function loaded($plugin = null)
363: {
364: if ($plugin !== null) {
365: deprecationWarning(
366: 'Checking a single plugin with Plugin::loaded() is deprecated. ' .
367: 'Use Plugin::isLoaded() instead.'
368: );
369:
370: return static::getCollection()->has($plugin);
371: }
372: $names = [];
373: foreach (static::getCollection() as $plugin) {
374: $names[] = $plugin->getName();
375: }
376: sort($names);
377:
378: return $names;
379: }
380:
381: /**
382: * Forgets a loaded plugin or all of them if first parameter is null
383: *
384: * @param string|null $plugin name of the plugin to forget
385: * @deprecated 3.7.0 This method will be removed in 4.0.0. Use PluginCollection::remove() or clear() instead.
386: * @return void
387: */
388: public static function unload($plugin = null)
389: {
390: deprecationWarning('Plugin::unload() will be removed in 4.0. Use PluginCollection::remove() or clear()');
391: if ($plugin === null) {
392: static::getCollection()->clear();
393: } else {
394: static::getCollection()->remove($plugin);
395: }
396: }
397:
398: /**
399: * Include file, ignoring include error if needed if file is missing
400: *
401: * @param string $file File to include
402: * @param bool $ignoreMissing Whether to ignore include error for missing files
403: * @return mixed
404: */
405: protected static function _includeFile($file, $ignoreMissing = false)
406: {
407: if ($ignoreMissing && !is_file($file)) {
408: return false;
409: }
410:
411: return include $file;
412: }
413:
414: /**
415: * Get the shared plugin collection.
416: *
417: * This method should generally not be used during application
418: * runtime as plugins should be set during Application startup.
419: *
420: * @return \Cake\Core\PluginCollection
421: */
422: public static function getCollection()
423: {
424: if (!isset(static::$plugins)) {
425: static::$plugins = new PluginCollection();
426: }
427:
428: return static::$plugins;
429: }
430: }
431: