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