PluginManager::getServerFiles()   B
last analyzed

Complexity

Conditions 9
Paths 10

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 21
nc 10
nop 1
dl 0
loc 32
rs 8.0555
c 0
b 0
f 0
1
<?php
2
3
define('TYPE_PLUGIN', 1);
4
define('TYPE_MODULE', 2);
5
define('TYPE_CONFIG', 3);
6
define('TYPE_NOTIFIER', 4);
7
8
define('DEPEND_DEPENDS', 1);
9
define('DEPEND_REQUIRES', 2);
10
define('DEPEND_RECOMMENDS', 3);
11
define('DEPEND_SUGGESTS', 4);
12
13
/**
14
 * Managing component for all plugins.
15
 *
16
 * This class handles all the plugin interaction with the webaccess on the server side.
17
 */
18
class PluginManager {
19
	// True if the Plugin framework is enabled
20
	public $enabled;
21
22
	// The path to the folder which contains the plugins
23
	public $pluginpath;
24
25
	// The path to the folder which holds the configuration for the plugins
26
	// This folder has same structure as $this->pluginpath
27
	public $pluginconfigpath;
28
29
	// List of all plugins and their data
30
	public $plugindata;
31
32
	// List of the plugins in the order in which
33
	// they should be loaded
34
	public $pluginorder;
35
36
	/**
37
	 * List of all hooks registered by plugins.
38
	 * [eventID][] = plugin.
39
	 */
40
	public $hooks;
41
42
	/**
43
	 * List of all plugin objects
44
	 * [pluginname] = pluginObj.
45
	 */
46
	public $plugins;
47
48
	/**
49
	 * List of all provided modules
50
	 * [modulename] = moduleFile.
51
	 */
52
	public $modules;
53
54
	/**
55
	 * List of all provided notifiers
56
	 * [notifiername] = notifierFile.
57
	 */
58
	public $notifiers;
59
60
	/**
61
	 * Mapping of legacy plugin identifiers to their canonical replacements.
62
	 *
63
	 * @var array<string, string>
64
	 */
65
	private $pluginAliases = [
66
		'filesbackendOwncloud' => 'filesbackendDefault',
67
	];
68
69
	/**
70
	 * List of sessiondata from plugins.
71
	 * [pluginname] = sessiondata.
72
	 */
73
	public $sessionData;
74
75
	/**
76
	 * Mapping for the XML 'load' attribute values
77
	 * on the <serverfile>, <clientfile> or <resourcefile> element
78
	 * to the corresponding define.
79
	 */
80
	public $loadMap = [
81
		'release' => LOAD_RELEASE,
82
		'debug' => LOAD_DEBUG,
83
		'source' => LOAD_SOURCE,
84
	];
85
86
	/**
87
	 * Mapping for the XML 'type' attribute values
88
	 * on the <serverfile> element to the corresponding define.
89
	 */
90
	public $typeMap = [
91
		'plugin' => TYPE_PLUGIN,
92
		'module' => TYPE_MODULE,
93
		'notifier' => TYPE_NOTIFIER,
94
	];
95
96
	/**
97
	 * Mapping for the XML 'type' attribute values
98
	 * on the <depends> element to the corresponding define.
99
	 */
100
	public $dependMap = [
101
		'depends' => DEPEND_DEPENDS,
102
		'requires' => DEPEND_REQUIRES,
103
		'recommends' => DEPEND_RECOMMENDS,
104
		'suggests' => DEPEND_SUGGESTS,
105
	];
106
107
	/**
108
	 * Constructor.
109
	 *
110
	 * @param mixed $enable
111
	 */
112
	public function __construct($enable = ENABLE_PLUGINS) {
113
		$this->enabled = $enable && defined('PATH_PLUGIN_DIR');
114
		$this->plugindata = [];
115
		$this->pluginorder = [];
116
		$this->hooks = [];
117
		$this->plugins = [];
118
		$this->modules = [];
119
		$this->notifiers = [];
120
		$this->sessionData = false;
121
		if ($this->enabled) {
122
			$this->pluginpath = PATH_PLUGIN_DIR;
123
			$this->pluginconfigpath = PATH_PLUGIN_CONFIG_DIR;
124
		}
125
	}
126
127
	/**
128
	 * pluginsEnabled.
129
	 *
130
	 * Checks whether the plugins have been enabled by checking if the proper
131
	 * configuration keys are set.
132
	 *
133
	 * @return bool returns true when plugins enabled, false when not
134
	 */
135
	public function pluginsEnabled() {
136
		return $this->enabled;
137
	}
138
139
	/**
140
	 * detectPlugins.
141
	 *
142
	 * Detecting the installed plugins either by using the already ready data
143
	 * from the state object or otherwise read in all the data and write it into
144
	 * the state.
145
	 *
146
	 * @param string $disabled the list of plugins to disable, this list is separated
147
	 *                         by the ';' character
148
	 */
149
	public function detectPlugins($disabled = '') {
150
		if (!$this->pluginsEnabled()) {
151
			return false;
152
		}
153
154
		// Get the plugindata from the state.
155
		$pluginState = new State('plugin');
156
		$pluginState->open();
157
158
		if (!DEBUG_PLUGINS_DISABLE_CACHE) {
159
			$this->plugindata = $pluginState->read("plugindata");
160
			$pluginOrder = $pluginState->read("pluginorder");
161
			$this->plugindata = $this->normalizePluginData($this->plugindata ?? []);
0 ignored issues
show
Bug introduced by
It seems like $this->plugindata ?? array() can also be of type string; however, parameter $plugindata of PluginManager::normalizePluginData() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

161
			$this->plugindata = $this->normalizePluginData(/** @scrutinizer ignore-type */ $this->plugindata ?? []);
Loading history...
162
			$this->pluginorder = $this->normalizePluginOrder(empty($pluginOrder) ? [] : $pluginOrder);
0 ignored issues
show
Bug introduced by
It seems like empty($pluginOrder) ? array() : $pluginOrder can also be of type string; however, parameter $pluginorder of PluginManager::normalizePluginOrder() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

162
			$this->pluginorder = $this->normalizePluginOrder(/** @scrutinizer ignore-type */ empty($pluginOrder) ? [] : $pluginOrder);
Loading history...
163
		}
164
165
		// If no plugindata has been stored yet, get it from the plugins dir.
166
		if (!$this->plugindata || !$this->pluginorder) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->plugindata of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->pluginorder of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
167
			$disabledPlugins = [];
168
			if (!empty($disabled)) {
169
				$disabledPlugins = array_map([$this, 'normalizePluginName'], explode(';', $disabled));
170
			}
171
172
			// Read all plugins from the plugins folders.
173
			$this->plugindata = $this->readPluginFolder($disabledPlugins);
174
			$this->plugindata = $this->normalizePluginData($this->plugindata);
175
176
			// Check if any plugin directories found or not
177
			if (!empty($this->plugindata)) {
178
				// Not we update plugindata and pluginorder based on the configured dependencies.
179
				// Note that each change to plugindata requires the requirements and dependencies
180
				// to be recalculated.
181
				while (!$this->pluginorder || !$this->validatePluginRequirements()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->pluginorder of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
182
					// Generate the order in which the plugins should be loaded,
183
					// this uses the $this->plugindata as base.
184
					$pluginOrder = $this->buildPluginDependencyOrder();
185
					$this->pluginorder = $this->normalizePluginOrder(empty($pluginOrder) ? [] : $pluginOrder);
186
				}
187
			}
188
		}
189
190
		// Decide whether to show password plugin in settings:
191
		// - show if the users are in a db
192
		// - don't show if the users are in ldap
193
		if (isset($this->plugindata['passwd'], $GLOBALS['usersinldap']) && $GLOBALS['usersinldap']) {
194
			unset($this->plugindata['passwd']);
195
			if (($passwdKey = array_search('passwd', $this->pluginorder)) !== false) {
196
				unset($this->pluginorder[$passwdKey]);
197
			}
198
		}
199
200
		// Write the plugindata back to the state
201
		if (!DEBUG_PLUGINS_DISABLE_CACHE) {
202
			$pluginState->write("plugindata", $this->plugindata);
203
			$pluginState->write("pluginorder", $this->pluginorder);
204
		}
205
206
		// Free the state again.
207
		$pluginState->close();
208
	}
209
210
	/**
211
	 * Convert legacy plugin identifiers to their canonical replacements.
212
	 *
213
	 * @param string $pluginname
214
	 *
215
	 * @return string
216
	 */
217
	private function normalizePluginName($pluginname) {
218
		return $this->pluginAliases[$pluginname] ?? $pluginname;
219
	}
220
221
	/**
222
	 * Normalize the plugindata array by applying legacy aliases.
223
	 *
224
	 * @return array
225
	 */
226
	private function normalizePluginData(array $plugindata) {
227
		foreach ($this->pluginAliases as $legacy => $canonical) {
228
			$legacyData = $plugindata[$legacy] ?? null;
229
			$canonicalData = $plugindata[$canonical] ?? null;
230
			$freshData = $this->processPlugin($canonical);
231
			if ($freshData !== null) {
232
				$canonicalData = $freshData;
233
			}
234
			elseif ($canonicalData === null && $legacyData !== null) {
235
				$canonicalData = $legacyData;
236
			}
237
238
			if ($canonicalData !== null) {
239
				$canonicalData['pluginname'] = $canonical;
240
				$canonicalData = $this->migrateLegacyFileReferences($canonicalData, $legacy, $canonical);
241
				$plugindata[$canonical] = $canonicalData;
242
			}
243
244
			if ($legacyData !== null) {
245
				unset($plugindata[$legacy]);
246
			}
247
		}
248
249
		return $plugindata;
250
	}
251
252
	/**
253
	 * Replace legacy filenames inside plugin metadata so cached state stays in sync with the new canonical plugin.
254
	 *
255
	 * @param string $legacy
256
	 * @param string $canonical
257
	 *
258
	 * @return array
259
	 */
260
	private function migrateLegacyFileReferences(array $pluginData, $legacy, $canonical) {
261
		$search = $legacy;
262
		$replace = $canonical;
263
		if (!isset($pluginData['components'])) {
264
			return $pluginData;
265
		}
266
267
		foreach ($pluginData['components'] as $componentIndex => $component) {
268
			foreach (['clientfiles', 'resourcefiles', 'serverfiles'] as $group) {
269
				if (empty($component[$group])) {
270
					continue;
271
				}
272
				foreach ($component[$group] as $load => $files) {
273
					if (empty($files) || !is_array($files)) {
274
						continue;
275
					}
276
					foreach ($files as $fileIndex => $file) {
277
						if (!is_array($file) || !isset($file['file'])) {
278
							continue;
279
						}
280
						$pluginData['components'][$componentIndex][$group][$load][$fileIndex]['file'] = str_replace($search, $replace, $file['file']);
281
					}
282
				}
283
			}
284
		}
285
286
		return $pluginData;
287
	}
288
289
	/**
290
	 * Normalize a list of plugin names by applying legacy aliases.
291
	 *
292
	 * @return array
293
	 */
294
	private function normalizePluginOrder(array $pluginorder) {
295
		$normalized = [];
296
		foreach ($pluginorder as $pluginname) {
297
			$canonical = $this->normalizePluginName($pluginname);
298
			if (!in_array($canonical, $normalized, true)) {
299
				$normalized[] = $canonical;
300
			}
301
		}
302
303
		return $normalized;
304
	}
305
306
	/**
307
	 * readPluginFolder.
308
	 *
309
	 * Read all subfolders of the directory referenced to by $this->pluginpath,
310
	 * for each subdir, we $this->processPlugin it as a plugin.
311
	 *
312
	 * @param $disabledPlugins Array The list of disabled plugins, the subfolders
313
	 *                         named as any of the strings inside this list will not be processed
314
	 *
315
	 * @returns Array The object containing all the processed plugins. The object is a key-value'
316
	 * object where the key is the unique name of the plugin, and the value the parsed data.
317
	 */
318
	public function readPluginFolder($disabledPlugins) {
319
		$data = [];
320
321
		$pluginsdir = opendir($this->pluginpath);
322
		if ($pluginsdir) {
0 ignored issues
show
introduced by
$pluginsdir is of type resource, thus it always evaluated to false.
Loading history...
323
			while (($plugin = readdir($pluginsdir)) !== false) {
324
				if ($plugin != '.' && $plugin != '..' && !in_array($plugin, $disabledPlugins)) {
325
					if (is_dir($this->pluginpath . DIRECTORY_SEPARATOR . $plugin)) {
326
						if (is_file($this->pluginpath . DIRECTORY_SEPARATOR . $plugin . DIRECTORY_SEPARATOR . 'manifest.xml')) {
327
							$processed = $this->processPlugin($plugin);
328
							$data[$processed['pluginname']] = $processed;
329
						}
330
					}
331
				}
332
			}
333
334
			closedir($pluginsdir);
335
		}
336
337
		return $data;
338
	}
339
340
	/**
341
	 * validatePluginRequirements.
342
	 *
343
	 * Go over the parsed $this->plugindata and check if all requirements are met.
344
	 * This means that for each plugin which defined a "depends" or "requires" plugin
345
	 * we check if those plugins are present on the system. If some dependencies are
346
	 * not met, the plugin is removed from $this->plugindata.
347
	 *
348
	 * @return bool False if the $this->plugindata was modified by this function
349
	 */
350
	public function validatePluginRequirements() {
351
		$modified = false;
352
353
		do {
354
			$success = true;
355
356
			foreach ($this->plugindata as $pluginname => &$plugin) {
357
				// Check if the plugin had any dependencies
358
				// declared in the manifest. If not, they are obviously
359
				// met. Otherwise we have to check the type of dependencies
360
				// which were declared.
361
				if ($plugin['dependencies']) {
362
					// We only care about the 'depends' and 'requires'
363
					// dependency types. All others are not blocking.
364
					foreach ($plugin['dependencies'][DEPEND_DEPENDS] as &$depends) {
365
						if (!$this->pluginExists($depends['plugin'])) {
366
							if (DEBUG_PLUGINS) {
367
								dump('[PLUGIN ERROR] Plugin "' . $pluginname . '" requires "' . $depends['plugin'] . '" which could not be found');
368
							}
369
							unset($this->plugindata[$pluginname]);
370
							// Indicate failure, as we have removed a plugin, and the requirements
371
							// must be rechecked.
372
							$success = false;
373
							// Indicate that the plugindata was modified.
374
							$modified = true;
375
						}
376
					}
377
378
					foreach ($plugin['dependencies'][DEPEND_REQUIRES] as &$depends) {
379
						if (!$this->pluginExists($depends['plugin'])) {
380
							if (DEBUG_PLUGINS) {
381
								dump('[PLUGIN ERROR] Plugin "' . $pluginname . '" requires "' . $depends['plugin'] . '" which could not be found');
382
							}
383
							unset($this->plugindata[$pluginname]);
384
							// Indicate failure, as we have removed a plugin, and the requirements
385
							// must be rechecked.
386
							$success = false;
387
							// Indicate that the plugindata was modified.
388
							$modified = true;
389
						}
390
					}
391
				}
392
			}
393
394
			// If a plugin was removed because of a failed dependency or requirement,
395
			// then we have to redo the cycle, because another plugin might have depended
396
			// on the removed plugin.
397
		}
398
		while (!$success);
399
400
		return !$modified;
401
	}
402
403
	/**
404
	 * buildPluginDependencyOrder.
405
	 *
406
	 * Go over the parsed $this->plugindata and create a ordered list of the plugins, resembling
407
	 * the order in which those plugins should be loaded. This goes over all plugins to read
408
	 * the 'dependencies' data and ordering those plugins based on the DEPEND_DEPENDS dependency type.
409
	 *
410
	 * In case of circular dependencies, the $this->plugindata object might be altered to remove
411
	 * the plugin which the broken dependencies.
412
	 *
413
	 * @return array The array of plugins in the order of which they should be loaded
414
	 */
415
	public function buildPluginDependencyOrder() {
416
		$plugins = array_keys($this->plugindata);
417
		$ordered = [];
418
		$failedCount = 0;
419
420
		// We are going to keep it quite simple, we keep looping over the $plugins
421
		// array until it is empty. Each time we find a plugin for which all dependencies
422
		// are met, we can put it on the $ordered list. If we have looped over the list twice,
423
		// without updated the $ordered list in any way, then we have found a circular dependency
424
		// and we cannot resolve the plugins correctly.
425
		while (!empty($plugins)) {
426
			$pluginname = array_shift($plugins);
427
			$plugin = $this->plugindata[$pluginname];
428
			$accepted = true;
429
430
			// Go over all dependencies to see if they have been met.
431
			if ($plugin['dependencies']) {
432
				for ($i = 0, $len = count($plugin['dependencies'][DEPEND_DEPENDS]); $i < $len; ++$i) {
433
					$dependency = $plugin['dependencies'][DEPEND_DEPENDS][$i];
434
					if (array_search($dependency['plugin'], $ordered) === false) {
435
						$accepted = false;
436
						break;
437
					}
438
				}
439
			}
440
441
			if ($accepted) {
442
				// The dependencies for this plugin have been met, we can push
443
				// the plugin into the tree.
444
				$ordered[] = $pluginname;
445
446
				// Reset the $failedCount property, this ensures that we can keep
447
				// looping because other plugins with previously unresolved dependencies
448
				// could possible be resolved.
449
				$failedCount = 0;
450
			}
451
			else {
452
				// The dependencies for this plugin have not been met, we push
453
				// the plugin back to the list and we will retry later when the
454
				// $ordered list contains more items.
455
				$plugins[] = $pluginname;
456
457
				// Increase the $failedCount property, this prevents that we could go into
458
				// an infinite loop when a circular dependency was defined.
459
				++$failedCount;
460
			}
461
462
			// If the $failedCount matches the the number of items in the $plugins array,
463
			// it means that all unordered plugins have unmet dependencies. This could only
464
			// happen for circular dependencies. In that case we will refuse to load those plugins.
465
			if ($failedCount === count($plugins)) {
466
				foreach ($plugins as $plugin) {
467
					if (DEBUG_PLUGINS) {
468
						dump('[PLUGIN ERROR] Circular dependency detected for plugin "' . $plugin . '"');
469
					}
470
					unset($this->plugindata[$plugin]);
471
				}
472
				break;
473
			}
474
		}
475
476
		return $ordered;
477
	}
478
479
	/**
480
	 * initPlugins.
481
	 *
482
	 * This function includes the server plugin classes, instantiate and
483
	 * initialize them.
484
	 *
485
	 * @param number $load One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
486
	 *                     the files based on the 'load' attribute.
487
	 */
488
	public function initPlugins($load = LOAD_RELEASE) {
489
		if (!$this->pluginsEnabled()) {
490
			return false;
491
		}
492
493
		$files = $this->getServerFiles($load);
494
		foreach ($files['server'] as $file) {
495
			include_once $file;
496
		}
497
498
		// Include the root files of all the plugins and instantiate the plugin
499
		foreach ($this->pluginorder as $plugName) {
500
			$pluginClassName = 'Plugin' . $plugName;
501
			if (class_exists($pluginClassName)) {
502
				$this->plugins[$plugName] = new $pluginClassName();
503
				$this->plugins[$plugName]->setPluginName($plugName);
504
				$this->plugins[$plugName]->init();
505
			}
506
		}
507
508
		$this->modules = $files['modules'];
509
		$this->notifiers = $files['notifiers'];
510
	}
511
512
	/**
513
	 * processPlugin.
514
	 *
515
	 * Read in the manifest and get the files that need to be included
516
	 * for placing hooks, defining modules, etc.
517
	 *
518
	 * @param $dirname string name of the directory of the plugin
519
	 *
520
	 * @return array The plugin data read from the given directory
521
	 */
522
	public function processPlugin($dirname) {
523
		// Read XML manifest file of plugin
524
		$handle = fopen($this->pluginpath . DIRECTORY_SEPARATOR . $dirname . DIRECTORY_SEPARATOR . 'manifest.xml', 'rb');
525
		$xml = '';
526
		if ($handle) {
0 ignored issues
show
introduced by
$handle is of type resource, thus it always evaluated to false.
Loading history...
527
			while (!feof($handle)) {
528
				$xml .= fread($handle, 4096);
529
			}
530
			fclose($handle);
531
		}
532
533
		$plugindata = $this->extractPluginDataFromXML($xml, $dirname);
534
		if ($plugindata) {
535
			// Apply the name to the object
536
			$plugindata['pluginname'] = $dirname;
537
		}
538
		else {
539
			if (DEBUG_PLUGINS) {
540
				dump('[PLUGIN ERROR] Plugin "' . $dirname . '" has an invalid manifest.');
541
			}
542
		}
543
544
		return $plugindata;
545
	}
546
547
	/**
548
	 * loadSessionData.
549
	 *
550
	 * Loads sessiondata of the plugins from disk.
551
	 * To improve performance the data is only loaded if a
552
	 * plugin requests (reads or saves) the data.
553
	 *
554
	 * @param $pluginname string Identifier of the plugin
555
	 */
556
	public function loadSessionData($pluginname) {
557
		$canonicalName = $this->normalizePluginName($pluginname);
558
559
		// lazy reading of sessionData
560
		if (!$this->sessionData) {
561
			$sessState = new State('plugin_sessiondata');
562
			$sessState->open();
563
			$this->sessionData = $sessState->read("sessionData");
564
			if (!isset($this->sessionData) || $this->sessionData == "") {
565
				$this->sessionData = [];
566
			}
567
			$sessState->close();
568
		}
569
570
		if ($pluginname !== $canonicalName && isset($this->sessionData[$pluginname])) {
571
			// migrate legacy session data key to canonical name
572
			$this->sessionData[$canonicalName] = $this->sessionData[$pluginname];
573
			unset($this->sessionData[$pluginname]);
574
		}
575
576
		if ($this->pluginExists($canonicalName)) {
577
			if (!isset($this->sessionData[$canonicalName])) {
578
				$this->sessionData[$canonicalName] = [];
579
			}
580
			$this->plugins[$canonicalName]->setSessionData($this->sessionData[$canonicalName]);
581
		}
582
	}
583
584
	/**
585
	 * saveSessionData.
586
	 *
587
	 * Saves sessiondata of the plugins to the disk.
588
	 *
589
	 * @param $pluginname string Identifier of the plugin
590
	 */
591
	public function saveSessionData($pluginname) {
592
		$canonicalName = $this->normalizePluginName($pluginname);
593
		if ($this->pluginExists($canonicalName)) {
594
			$this->sessionData[$canonicalName] = $this->plugins[$canonicalName]->getSessionData();
595
		}
596
		if ($this->sessionData) {
597
			$sessState = new State('plugin_sessiondata');
598
			$sessState->open();
599
			$sessState->write("sessionData", $this->sessionData);
600
			$sessState->close();
601
		}
602
	}
603
604
	/**
605
	 * pluginExists.
606
	 *
607
	 * Checks if plugin exists.
608
	 *
609
	 * @param $pluginname string Identifier of the plugin
610
	 *
611
	 * @return bool true when plugin exists, false when it does not
612
	 */
613
	public function pluginExists($pluginname) {
614
		$canonicalName = $this->normalizePluginName($pluginname);
615
		if (isset($this->plugindata[$canonicalName])) {
616
			return true;
617
		}
618
619
		return false;
620
	}
621
622
	/**
623
	 * getModuleFilePath.
624
	 *
625
	 * Obtain the filepath of the given modulename
626
	 *
627
	 * @param $modulename string Identifier of the modulename
628
	 *
629
	 * @return string The path to the file for the module
630
	 */
631
	public function getModuleFilePath($modulename) {
632
		return $this->modules[$modulename] ?? false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->modules[$modulename] ?? false could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
633
	}
634
635
	/**
636
	 * getNotifierFilePath.
637
	 *
638
	 * Obtain the filepath of the given notifiername
639
	 *
640
	 * @param $notifiername string Identifier of the notifiername
641
	 *
642
	 * @return string The path to the file for the notifier
643
	 */
644
	public function getNotifierFilePath($notifiername) {
645
		return $this->notifiers[$notifiername] ?? false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->notifiers[$notifiername] ?? false could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
646
	}
647
648
	/**
649
	 * registerHook.
650
	 *
651
	 * This function allows the plugin to register their hooks.
652
	 *
653
	 * @param $eventID    string Identifier of the event where this hook must be triggered
654
	 * @param $pluginName string Name of the plugin that is registering this hook
655
	 */
656
	public function registerHook($eventID, $pluginName) {
657
		$canonicalName = $this->normalizePluginName($pluginName);
658
		$this->hooks[$eventID][$canonicalName] = $canonicalName;
659
	}
660
661
	/**
662
	 * triggerHook.
663
	 *
664
	 * This function will call all the registered hooks when their event is triggered.
665
	 *
666
	 * @param $eventID string Identifier of the event that has just been triggered
667
	 * @param $data    mixed (Optional) Usually an array of data that the callback function can modify
668
	 *
669
	 * @return mixed data that has been changed by plugins
670
	 */
671
	public function triggerHook($eventID, $data = []) {
672
		if (isset($this->hooks[$eventID]) && is_array($this->hooks[$eventID])) {
673
			foreach ($this->hooks[$eventID] as $key => $pluginname) {
674
				$this->plugins[$pluginname]->execute($eventID, $data);
675
			}
676
		}
677
678
		return $data;
679
	}
680
681
	/**
682
	 * getPluginVersion.
683
	 *
684
	 * Function is used to prepare version information array from plugindata.
685
	 *
686
	 * @return array the array of plugins version information
687
	 */
688
	public function getPluginsVersion() {
689
		$versionInfo = [];
690
		foreach ($this->plugindata as $pluginName => $data) {
691
			$versionInfo[$pluginName] = $data["version"];
692
		}
693
694
		return $versionInfo;
695
	}
696
697
	/**
698
	 * getServerFilesForComponent.
699
	 *
700
	 * Called by getServerFiles() to return the list of files which are provided
701
	 * for the given component in a particular plugin.
702
	 * The paths which are returned start at the root of the webapp.
703
	 *
704
	 * This function might call itself recursively if it couldn't find any files for
705
	 * the given $load type. If no 'source' files are found, it will obtain the 'debug'
706
	 * files, if that too files it will fallback to 'release' files. If the latter is
707
	 * not found either, no files are returned.
708
	 *
709
	 * @param string $pluginname The name of the plugin (this is used in the pathname)
710
	 * @param array  $component  The component to read the serverfiles from
711
	 * @param number $load       One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
712
	 *                           the files based on the 'load' attribute.
713
	 *
714
	 * @return array list of paths to the files in this component
715
	 */
716
	public function getServerFilesForComponent($pluginname, $component, $load) {
717
		$pluginname = $this->normalizePluginName($pluginname);
718
		$componentfiles = [
719
			'server' => [],
720
			'modules' => [],
721
			'notifiers' => [],
722
		];
723
724
		foreach ($component['serverfiles'][$load] as &$file) {
725
			switch ($file['type']) {
726
				case TYPE_CONFIG:
727
					$componentfiles['server'][] = $this->pluginconfigpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
728
					break;
729
730
				case TYPE_PLUGIN:
731
					$componentfiles['server'][] = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
732
					break;
733
734
				case TYPE_MODULE:
735
					$componentfiles['modules'][$file['module']] = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
736
					break;
737
738
				case TYPE_NOTIFIER:
739
					$componentfiles['notifiers'][$file['notifier']] = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
740
					break;
741
			}
742
		}
743
		unset($file);
744
745
		return $componentfiles;
746
	}
747
748
	/**
749
	 * getServerFiles.
750
	 *
751
	 * Returning an array of paths to files that need to be included.
752
	 * The paths which are returned start at the root of the webapp.
753
	 *
754
	 * This calls getServerFilesForComponent() to obtain the files
755
	 * for each component inside the requested plugin
756
	 *
757
	 * @param number $load One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
758
	 *                     the files based on the 'load' attribute.
759
	 *
760
	 * @return array list of paths to files
761
	 */
762
	public function getServerFiles($load = LOAD_RELEASE) {
763
		$files = [
764
			'server' => [],
765
			'modules' => [],
766
			'notifiers' => [],
767
		];
768
769
		foreach ($this->pluginorder as $pluginname) {
770
			$plugin = &$this->plugindata[$pluginname];
771
			foreach ($plugin['components'] as &$component) {
772
				if (!empty($component['serverfiles'][$load])) {
773
					$componentfiles = $this->getServerFilesForComponent($pluginname, $component, $load);
774
				}
775
				elseif ($load === LOAD_SOURCE && !empty($component['serverfiles'][LOAD_DEBUG])) {
776
					$componentfiles = $this->getServerFilesForComponent($pluginname, $component, LOAD_DEBUG);
777
				}
778
				elseif ($load !== LOAD_RELEASE && !empty($component['serverfiles'][LOAD_RELEASE])) {
779
					$componentfiles = $this->getServerFilesForComponent($pluginname, $component, LOAD_RELEASE);
780
				} // else tough luck, at least release should be present
781
782
				if (isset($componentfiles)) {
783
					$files['server'] = array_merge($files['server'], $componentfiles['server']);
784
					$files['modules'] = array_merge($files['modules'], $componentfiles['modules']);
785
					$files['notifiers'] = array_merge($files['notifiers'], $componentfiles['notifiers']);
786
					unset($componentfiles);
787
				}
788
			}
789
			unset($component);
790
		}
791
		unset($plugin);
792
793
		return $files;
794
	}
795
796
	/**
797
	 * getClientFilesForComponent.
798
	 *
799
	 * Called by getClientFiles() to return the list of files which are provided
800
	 * for the given component in a particular plugin.
801
	 * The paths which are returned start at the root of the webapp.
802
	 *
803
	 * This function might call itself recursively if it couldn't find any files for
804
	 * the given $load type. If no 'source' files are found, it will obtain the 'debug'
805
	 * files, if that too files it will fallback to 'release' files. If the latter is
806
	 * not found either, no files are returned.
807
	 *
808
	 * @param string $pluginname The name of the plugin (this is used in the pathname)
809
	 * @param array  $component  The component to read the clientfiles from
810
	 * @param number $load       One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
811
	 *                           the files based on the 'load' attribute.
812
	 *
813
	 * @return array list of paths to the files in this component
814
	 */
815
	public function getClientFilesForComponent($pluginname, $component, $load) {
816
		$pluginname = $this->normalizePluginName($pluginname);
817
		$componentfiles = [];
818
819
		foreach ($component['clientfiles'][$load] as &$file) {
820
			$componentfiles[] = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
821
		}
822
		unset($file);
823
824
		return $componentfiles;
825
	}
826
827
	/**
828
	 * getClientFiles.
829
	 *
830
	 * Returning an array of paths to files that need to be included.
831
	 * The paths which are returned start at the root of the webapp.
832
	 *
833
	 * This calls getClientFilesForComponent() to obtain the files
834
	 * for each component inside each plugin.
835
	 *
836
	 * @param number $load One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
837
	 *                     the files based on the 'load' attribute.
838
	 *
839
	 * @return array list of paths to files
840
	 */
841
	public function getClientFiles($load = LOAD_RELEASE) {
842
		$files = [];
843
844
		foreach ($this->pluginorder as $pluginname) {
845
			$plugin = &$this->plugindata[$pluginname];
846
			foreach ($plugin['components'] as &$component) {
847
				if (!empty($component['clientfiles'][$load])) {
848
					$componentfiles = $this->getClientFilesForComponent($pluginname, $component, $load);
849
				}
850
				elseif ($load === LOAD_SOURCE && !empty($component['clientfiles'][LOAD_DEBUG])) {
851
					$componentfiles = $this->getClientFilesForComponent($pluginname, $component, LOAD_DEBUG);
852
				}
853
				elseif ($load !== LOAD_RELEASE && !empty($component['clientfiles'][LOAD_RELEASE])) {
854
					$componentfiles = $this->getClientFilesForComponent($pluginname, $component, LOAD_RELEASE);
855
				} // else tough luck, at least release should be present
856
857
				if (isset($componentfiles)) {
858
					$files = array_merge($files, $componentfiles);
859
					unset($componentfiles);
860
				}
861
			}
862
			unset($component);
863
		}
864
		unset($plugin);
865
866
		return $files;
867
	}
868
869
	/**
870
	 * getResourceFilesForComponent.
871
	 *
872
	 * Called by getResourceFiles() to return the list of files which are provided
873
	 * for the given component in a particular plugin.
874
	 * The paths which are returned start at the root of the webapp.
875
	 *
876
	 * This function might call itself recursively if it couldn't find any files for
877
	 * the given $load type. If no 'source' files are found, it will obtain the 'debug'
878
	 * files, if that too files it will fallback to 'release' files. If the latter is
879
	 * not found either, no files are returned.
880
	 *
881
	 * @param string $pluginname The name of the plugin (this is used in the pathname)
882
	 * @param array  $component  The component to read the resourcefiles from
883
	 * @param number $load       One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
884
	 *                           the files based on the 'load' attribute.
885
	 *
886
	 * @return array list of paths to the files in this component
887
	 */
888
	public function getResourceFilesForComponent($pluginname, $component, $load) {
889
		$pluginname = $this->normalizePluginName($pluginname);
890
		$componentfiles = [];
891
892
		foreach ($component['resourcefiles'][$load] as &$file) {
893
			$componentfiles[] = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
894
		}
895
		unset($file);
896
897
		return $componentfiles;
898
	}
899
900
	/**
901
	 * getResourceFiles.
902
	 *
903
	 * Returning an array of paths to files that need to be included.
904
	 * The paths which are returned start at the root of the webapp.
905
	 *
906
	 * This calls getResourceFilesForComponent() to obtain the files
907
	 * for each component inside each plugin.
908
	 *
909
	 * @param number $load One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
910
	 *                     the files based on the 'load' attribute.
911
	 *
912
	 * @return array list of paths to files
913
	 */
914
	public function getResourceFiles($load = LOAD_RELEASE) {
915
		$files = [];
916
917
		foreach ($this->pluginorder as $pluginname) {
918
			$plugin = &$this->plugindata[$pluginname];
919
			foreach ($plugin['components'] as &$component) {
920
				if (!empty($component['resourcefiles'][$load])) {
921
					$componentfiles = $this->getResourceFilesForComponent($pluginname, $component, $load);
922
				}
923
				elseif ($load === LOAD_SOURCE && !empty($component['resourcefiles'][LOAD_DEBUG])) {
924
					$componentfiles = $this->getResourceFilesForComponent($pluginname, $component, LOAD_DEBUG);
925
				}
926
				elseif ($load !== LOAD_RELEASE && !empty($component['resourcefiles'][LOAD_RELEASE])) {
927
					$componentfiles = $this->getResourceFilesForComponent($pluginname, $component, LOAD_RELEASE);
928
				} // else tough luck, at least release should be present
929
930
				if (isset($componentfiles)) {
931
					$files = array_merge($files, $componentfiles);
932
					unset($componentfiles);
933
				}
934
			}
935
			unset($component);
936
		}
937
		unset($plugin);
938
939
		return $files;
940
	}
941
942
	/**
943
	 * getTranslationFilePaths.
944
	 *
945
	 * Returning an array of paths to to the translations files. This will be
946
	 * used by the gettext functionality.
947
	 *
948
	 * @return array list of paths to translations
949
	 */
950
	public function getTranslationFilePaths() {
951
		$paths = [];
952
953
		foreach ($this->pluginorder as $pluginname) {
954
			$canonicalName = $this->normalizePluginName($pluginname);
955
			$plugin = &$this->plugindata[$pluginname];
956
			if ($plugin['translationsdir']) {
957
				$translationPath = $this->pluginpath . DIRECTORY_SEPARATOR . $canonicalName . DIRECTORY_SEPARATOR . $plugin['translationsdir']['dir'];
958
				if (is_dir($translationPath)) {
959
					$paths[$canonicalName] = $translationPath;
960
				}
961
			}
962
		}
963
		unset($plugin);
964
965
		return $paths;
966
	}
967
968
	/**
969
	 * extractPluginDataFromXML.
970
	 *
971
	 * Extracts all the data from the Plugin XML manifest.
972
	 *
973
	 * @param $xml     string XML manifest of plugin
974
	 * @param $dirname string name of the directory of the plugin
975
	 *
976
	 * @return array data from XML converted into array that the PluginManager can use
977
	 */
978
	public function extractPluginDataFromXML($xml, $dirname) {
979
		$plugindata = [
980
			'components' => [],
981
			'dependencies' => null,
982
			'translationsdir' => null,
983
			'version' => null,
984
		];
985
986
		// Parse all XML data
987
		$data = new SimpleXMLElement($xml);
988
989
		// Parse the <plugin> attributes
990
		if (isset($data['version']) && (int) $data['version'] !== 2) {
991
			if (DEBUG_PLUGINS) {
992
				dump("[PLUGIN ERROR] Plugin {$dirname} manifest uses version " . $data['version'] . " while only version 2 is supported");
993
			}
994
995
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
996
		}
997
998
		// Parse the <info> element
999
		if (isset($data->info->version)) {
1000
			$plugindata['version'] = (string) $data->info->version;
1001
		}
1002
		else {
1003
			dump("[PLUGIN WARNING] Plugin {$dirname} has not specified version information in manifest.xml");
1004
		}
1005
1006
		// Parse the <config> element
1007
		if (isset($data->config)) {
1008
			if (isset($data->config->configfile)) {
1009
				if (empty($data->config->configfile)) {
1010
					dump("[PLUGIN ERROR] Plugin {$dirname} manifest contains empty configfile declaration");
1011
				}
1012
				if (!file_exists($data->config->configfile)) {
1013
					dump("[PLUGIN ERROR] Plugin {$dirname} manifest config file does not exists");
1014
				}
1015
			}
1016
			else {
1017
				dump("[PLUGIN ERROR] Plugin {$dirname} manifest configfile entry is missing");
1018
			}
1019
1020
			$files = [
1021
				LOAD_SOURCE => [],
1022
				LOAD_DEBUG => [],
1023
				LOAD_RELEASE => [],
1024
			];
1025
			foreach ($data->config->configfile as $filename) {
1026
				$files[LOAD_RELEASE][] = [
1027
					'file' => (string) $filename,
1028
					'type' => TYPE_CONFIG,
1029
					'load' => LOAD_RELEASE,
1030
					'module' => null,
1031
					'notifier' => null,
1032
				];
1033
			}
1034
			$plugindata['components'][] = [
1035
				'serverfiles' => $files,
1036
				'clientfiles' => [],
1037
				'resourcefiles' => [],
1038
			];
1039
		}
1040
1041
		// Parse the <dependencies> element
1042
		if (isset($data->dependencies, $data->dependencies->depends)) {
1043
			$dependencies = [
1044
				DEPEND_DEPENDS => [],
1045
				DEPEND_REQUIRES => [],
1046
				DEPEND_RECOMMENDS => [],
1047
				DEPEND_SUGGESTS => [],
1048
			];
1049
			foreach ($data->dependencies->depends as $depends) {
1050
				$type = $this->dependMap[(string) $depends->attributes()->type];
1051
				$plugin = (string) $depends->dependsname;
1052
				$dependencies[$type][] = [
1053
					'plugin' => $plugin,
1054
				];
1055
			}
1056
			$plugindata['dependencies'] = $dependencies;
1057
		}
1058
1059
		// Parse the <translations> element
1060
		if (isset($data->translations, $data->translations->translationsdir)) {
1061
			$plugindata['translationsdir'] = [
1062
				'dir' => (string) $data->translations->translationsdir,
1063
			];
1064
		}
1065
1066
		// Parse the <components> element
1067
		if (isset($data->components, $data->components->component)) {
1068
			foreach ($data->components->component as $component) {
1069
				$componentdata = [
1070
					'serverfiles' => [
1071
						LOAD_SOURCE => [],
1072
						LOAD_DEBUG => [],
1073
						LOAD_RELEASE => [],
1074
					],
1075
					'clientfiles' => [
1076
						LOAD_SOURCE => [],
1077
						LOAD_DEBUG => [],
1078
						LOAD_RELEASE => [],
1079
					],
1080
					'resourcefiles' => [
1081
						LOAD_SOURCE => [],
1082
						LOAD_DEBUG => [],
1083
						LOAD_RELEASE => [],
1084
					],
1085
				];
1086
				if (isset($component->files)) {
1087
					if (isset($component->files->server, $component->files->server->serverfile)) {
1088
						$files = [
1089
							LOAD_SOURCE => [],
1090
							LOAD_DEBUG => [],
1091
							LOAD_RELEASE => [],
1092
						];
1093
						foreach ($component->files->server->serverfile as $serverfile) {
1094
							$load = LOAD_RELEASE;
1095
							$type = TYPE_PLUGIN;
1096
							$module = null;
1097
							$notifier = null;
1098
1099
							$filename = (string) $serverfile;
1100
							if (empty($filename)) {
1101
								dump("[PLUGIN ERROR] Plugin {$dirname} manifest contains empty serverfile declaration");
1102
							}
1103
							if (isset($serverfile['type'])) {
1104
								$type = $this->typeMap[(string) $serverfile['type']];
1105
							}
1106
							if (isset($serverfile['load'])) {
1107
								$load = $this->loadMap[(string) $serverfile['load']];
1108
							}
1109
							if (isset($serverfile['module'])) {
1110
								$module = (string) $serverfile['module'];
1111
							}
1112
							if (isset($serverfile['notifier'])) {
1113
								$notifier = (string) $serverfile['notifier'];
1114
							}
1115
							if ($filename) {
1116
								$files[$load][] = [
1117
									'file' => $filename,
1118
									'type' => $type,
1119
									'load' => $load,
1120
									'module' => $module,
1121
									'notifier' => $notifier,
1122
								];
1123
							}
1124
						}
1125
						$componentdata['serverfiles'][LOAD_SOURCE] = array_merge($componentdata['serverfiles'][LOAD_SOURCE], $files[LOAD_SOURCE]);
1126
						$componentdata['serverfiles'][LOAD_DEBUG] = array_merge($componentdata['serverfiles'][LOAD_DEBUG], $files[LOAD_DEBUG]);
1127
						$componentdata['serverfiles'][LOAD_RELEASE] = array_merge($componentdata['serverfiles'][LOAD_RELEASE], $files[LOAD_RELEASE]);
1128
					}
1129
					if (isset($component->files->client, $component->files->client->clientfile)) {
1130
						$files = [
1131
							LOAD_SOURCE => [],
1132
							LOAD_DEBUG => [],
1133
							LOAD_RELEASE => [],
1134
						];
1135
						foreach ($component->files->client->clientfile as $clientfile) {
1136
							$filename = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $filename is dead and can be removed.
Loading history...
1137
							$load = LOAD_RELEASE;
1138
							$filename = (string) $clientfile;
1139
							if (isset($clientfile['load'])) {
1140
								$load = $this->loadMap[(string) $clientfile['load']];
1141
							}
1142
							if (empty($filename)) {
1143
								if (DEBUG_PLUGINS) {
1144
									dump("[PLUGIN ERROR] Plugin {$dirname} manifest contains empty resourcefile declaration");
1145
								}
1146
							}
1147
							else {
1148
								$files[$load][] = [
1149
									'file' => $filename,
1150
									'load' => $load,
1151
								];
1152
							}
1153
						}
1154
						$componentdata['clientfiles'][LOAD_SOURCE] = array_merge($componentdata['clientfiles'][LOAD_SOURCE], $files[LOAD_SOURCE]);
1155
						$componentdata['clientfiles'][LOAD_DEBUG] = array_merge($componentdata['clientfiles'][LOAD_DEBUG], $files[LOAD_DEBUG]);
1156
						$componentdata['clientfiles'][LOAD_RELEASE] = array_merge($componentdata['clientfiles'][LOAD_RELEASE], $files[LOAD_RELEASE]);
1157
					}
1158
					if (isset($component->files->resources, $component->files->resources->resourcefile)) {
1159
						$files = [
1160
							LOAD_SOURCE => [],
1161
							LOAD_DEBUG => [],
1162
							LOAD_RELEASE => [],
1163
						];
1164
						foreach ($component->files->resources->resourcefile as $resourcefile) {
1165
							$filename = false;
1166
							$load = LOAD_RELEASE;
1167
							$filename = (string) $resourcefile;
1168
							if (isset($resourcefile['load'])) {
1169
								$load = $this->loadMap[(string) $resourcefile['load']];
1170
							}
1171
							if (empty($filename)) {
1172
								if (DEBUG_PLUGINS) {
1173
									dump("[PLUGIN ERROR] Plugin {$dirname} manifest contains empty resourcefile declaration");
1174
								}
1175
							}
1176
							else {
1177
								$files[$load][] = [
1178
									'file' => $filename,
1179
									'load' => $load,
1180
								];
1181
							}
1182
						}
1183
						$componentdata['resourcefiles'][LOAD_SOURCE] = array_merge($componentdata['resourcefiles'][LOAD_SOURCE], $files[LOAD_SOURCE]);
1184
						$componentdata['resourcefiles'][LOAD_DEBUG] = array_merge($componentdata['resourcefiles'][LOAD_DEBUG], $files[LOAD_DEBUG]);
1185
						$componentdata['resourcefiles'][LOAD_RELEASE] = array_merge($componentdata['resourcefiles'][LOAD_RELEASE], $files[LOAD_RELEASE]);
1186
					}
1187
					$plugindata['components'][] = $componentdata;
1188
				}
1189
			}
1190
		}
1191
		else {
1192
			if (DEBUG_PLUGINS) {
1193
				dump("[PLUGIN ERROR] Plugin {$dirname} manifest didn't provide any components");
1194
			}
1195
1196
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
1197
		}
1198
1199
		return $plugindata;
1200
	}
1201
1202
	/**
1203
	 * Expands a string that contains a semicolon separated list of plugins.
1204
	 * All wildcards (*) will be resolved.
1205
	 *
1206
	 * @param mixed $pluginList
1207
	 */
1208
	public function expandPluginList($pluginList) {
1209
		$pluginNames = explode(';', (string) $pluginList);
1210
		$pluginList = [];
1211
		foreach ($pluginNames as $pluginName) {
1212
			$pluginName = trim($pluginName);
1213
			if ($pluginName === '') {
1214
				continue;
1215
			}
1216
			$canonicalName = $this->normalizePluginName($pluginName);
1217
			if (array_key_exists($canonicalName, $this->plugindata)) {
1218
				$pluginList[] = $canonicalName;
1219
			}
1220
			else {
1221
				// Check if it contains a wildcard
1222
				if (str_contains($pluginName, '*')) {
1223
					$expandedPluginList = $this->_expandPluginNameWithWildcard($pluginName);
1224
					$pluginList = array_merge($pluginList, $expandedPluginList);
1225
				}
1226
			}
1227
		}
1228
1229
		// Remove duplicates
1230
		$pluginList = array_unique($pluginList);
1231
1232
		// Decide whether to show password plugin in settings:
1233
		// - show if the users are in a db
1234
		// - don't show if the users are in ldap
1235
		if (($key = array_search('passwd', $pluginList)) !== false && isset($GLOBALS['usersinldap']) && $GLOBALS['usersinldap']) {
1236
			unset($pluginList[$key]);
1237
		}
1238
1239
		return implode(';', $pluginList);
1240
	}
1241
1242
	/**
1243
	 * Finds all plugins that match the given string name (that contains one or
1244
	 * more wildcards).
1245
	 *
1246
	 * @param string $pluginNameWithWildcard A plugin identifying string that
1247
	 *                                       contains a wildcard character (*)
1248
	 *
1249
	 * @return array An array with the names of the plugins that are identified by
1250
	 *               $pluginNameWithWildcard
1251
	 */
1252
	private function _expandPluginNameWithWildcard($pluginNameWithWildcard) {
1253
		$retVal = [];
1254
		$pluginNames = array_keys($this->plugindata);
1255
		$regExp = '/^' . str_replace('*', '.*?', $pluginNameWithWildcard) . '$/';
1256
		dump('rexexp = ' . $regExp);
1257
		foreach ($pluginNames as $pluginName) {
1258
			dump('checking plugin: ' . $pluginName);
1259
			if (preg_match($regExp, $pluginName)) {
1260
				$retVal[] = $pluginName;
1261
			}
1262
		}
1263
1264
		return $retVal;
1265
	}
1266
}
1267