PluginManager   F
last analyzed

Complexity

Total Complexity 162

Size/Duplication

Total Lines 1120
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 439
c 0
b 0
f 0
dl 0
loc 1120
rs 2
wmc 162

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 3
C detectPlugins() 0 57 15
A pluginsEnabled() 0 2 1
A triggerHook() 0 8 4
A getNotifierFilePath() 0 2 1
A getClientFilesForComponent() 0 9 2
A getTranslationFilePaths() 0 15 4
B getServerFiles() 0 32 9
A initPlugins() 0 22 5
B getClientFiles() 0 26 9
A getModuleFilePath() 0 2 1
B getServerFilesForComponent() 0 29 6
B getResourceFiles() 0 26 9
A pluginExists() 0 6 2
A processPlugin() 0 23 5
F extractPluginDataFromXML() 0 222 35
B validatePluginRequirements() 0 51 10
A getResourceFilesForComponent() 0 9 2
B buildPluginDependencyOrder() 0 62 9
A loadSessionData() 0 16 6
B readPluginFolder() 0 20 8
A registerHook() 0 2 1
A getPluginsVersion() 0 7 2
A saveSessionData() 0 9 3
B expandPluginList() 0 28 7
A _expandPluginNameWithWildcard() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like PluginManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PluginManager, and based on these observations, apply Extract Interface, too.

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
	 * List of sessiondata from plugins.
62
	 * [pluginname] = sessiondata.
63
	 */
64
	public $sessionData;
65
66
	/**
67
	 * Mapping for the XML 'load' attribute values
68
	 * on the <serverfile>, <clientfile> or <resourcefile> element
69
	 * to the corresponding define.
70
	 */
71
	public $loadMap = [
72
		'release' => LOAD_RELEASE,
73
		'debug' => LOAD_DEBUG,
74
		'source' => LOAD_SOURCE,
75
	];
76
77
	/**
78
	 * Mapping for the XML 'type' attribute values
79
	 * on the <serverfile> element to the corresponding define.
80
	 */
81
	public $typeMap = [
82
		'plugin' => TYPE_PLUGIN,
83
		'module' => TYPE_MODULE,
84
		'notifier' => TYPE_NOTIFIER,
85
	];
86
87
	/**
88
	 * Mapping for the XML 'type' attribute values
89
	 * on the <depends> element to the corresponding define.
90
	 */
91
	public $dependMap = [
92
		'depends' => DEPEND_DEPENDS,
93
		'requires' => DEPEND_REQUIRES,
94
		'recommends' => DEPEND_RECOMMENDS,
95
		'suggests' => DEPEND_SUGGESTS,
96
	];
97
98
	/**
99
	 * Constructor.
100
	 *
101
	 * @param mixed $enable
102
	 */
103
	public function __construct($enable = ENABLE_PLUGINS) {
104
		$this->enabled = $enable && defined('PATH_PLUGIN_DIR');
105
		$this->plugindata = [];
106
		$this->pluginorder = [];
107
		$this->hooks = [];
108
		$this->plugins = [];
109
		$this->modules = [];
110
		$this->notifiers = [];
111
		$this->sessionData = false;
112
		if ($this->enabled) {
113
			$this->pluginpath = PATH_PLUGIN_DIR;
114
			$this->pluginconfigpath = PATH_PLUGIN_CONFIG_DIR;
115
		}
116
	}
117
118
	/**
119
	 * pluginsEnabled.
120
	 *
121
	 * Checks whether the plugins have been enabled by checking if the proper
122
	 * configuration keys are set.
123
	 *
124
	 * @return bool returns true when plugins enabled, false when not
125
	 */
126
	public function pluginsEnabled() {
127
		return $this->enabled;
128
	}
129
130
	/**
131
	 * detectPlugins.
132
	 *
133
	 * Detecting the installed plugins either by using the already ready data
134
	 * from the state object or otherwise read in all the data and write it into
135
	 * the state.
136
	 *
137
	 * @param string $disabled the list of plugins to disable, this list is separated
138
	 *                         by the ';' character
139
	 */
140
	public function detectPlugins($disabled = '') {
141
		if (!$this->pluginsEnabled()) {
142
			return false;
143
		}
144
145
		// Get the plugindata from the state.
146
		$pluginState = new State('plugin');
147
		$pluginState->open();
148
149
		if (!DEBUG_PLUGINS_DISABLE_CACHE) {
150
			$this->plugindata = $pluginState->read("plugindata");
151
			$pluginOrder = $pluginState->read("pluginorder");
152
			$this->pluginorder = empty($pluginOrder) ? [] : $pluginOrder;
153
		}
154
155
		// If no plugindata has been stored yet, get it from the plugins dir.
156
		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...
157
			$disabledPlugins = [];
158
			if (!empty($disabled)) {
159
				$disabledPlugins = explode(';', $disabled);
160
			}
161
162
			// Read all plugins from the plugins folders.
163
			$this->plugindata = $this->readPluginFolder($disabledPlugins);
164
165
			// Check if any plugin directories found or not
166
			if (!empty($this->plugindata)) {
167
				// Not we update plugindata and pluginorder based on the configured dependencies.
168
				// Note that each change to plugindata requires the requirements and dependencies
169
				// to be recalculated.
170
				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...
171
					// Generate the order in which the plugins should be loaded,
172
					// this uses the $this->plugindata as base.
173
					$pluginOrder = $this->buildPluginDependencyOrder();
174
					$this->pluginorder = empty($pluginOrder) ? [] : $pluginOrder;
175
				}
176
			}
177
		}
178
179
		// Decide whether to show password plugin in settings:
180
		// - show if the users are in a db
181
		// - don't show if the users are in ldap
182
		if (isset($this->plugindata['passwd'], $GLOBALS['usersinldap']) && $GLOBALS['usersinldap']) {
183
			unset($this->plugindata['passwd']);
184
			if (($passwdKey = array_search('passwd', $this->pluginorder)) !== false) {
185
				unset($this->pluginorder[$passwdKey]);
186
			}
187
		}
188
189
		// Write the plugindata back to the state
190
		if (!DEBUG_PLUGINS_DISABLE_CACHE) {
191
			$pluginState->write("plugindata", $this->plugindata);
192
			$pluginState->write("pluginorder", $this->pluginorder);
193
		}
194
195
		// Free the state again.
196
		$pluginState->close();
197
	}
198
199
	/**
200
	 * readPluginFolder.
201
	 *
202
	 * Read all subfolders of the directory referenced to by $this->pluginpath,
203
	 * for each subdir, we $this->processPlugin it as a plugin.
204
	 *
205
	 * @param $disabledPlugins Array The list of disabled plugins, the subfolders
206
	 *                         named as any of the strings inside this list will not be processed
207
	 *
208
	 * @returns Array The object containing all the processed plugins. The object is a key-value'
209
	 * object where the key is the unique name of the plugin, and the value the parsed data.
210
	 */
211
	public function readPluginFolder($disabledPlugins) {
212
		$data = [];
213
214
		$pluginsdir = opendir($this->pluginpath);
215
		if ($pluginsdir) {
0 ignored issues
show
introduced by
$pluginsdir is of type resource, thus it always evaluated to false.
Loading history...
216
			while (($plugin = readdir($pluginsdir)) !== false) {
217
				if ($plugin != '.' && $plugin != '..' && !in_array($plugin, $disabledPlugins)) {
218
					if (is_dir($this->pluginpath . DIRECTORY_SEPARATOR . $plugin)) {
219
						if (is_file($this->pluginpath . DIRECTORY_SEPARATOR . $plugin . DIRECTORY_SEPARATOR . 'manifest.xml')) {
220
							$processed = $this->processPlugin($plugin);
221
							$data[$processed['pluginname']] = $processed;
222
						}
223
					}
224
				}
225
			}
226
227
			closedir($pluginsdir);
228
		}
229
230
		return $data;
231
	}
232
233
	/**
234
	 * validatePluginRequirements.
235
	 *
236
	 * Go over the parsed $this->plugindata and check if all requirements are met.
237
	 * This means that for each plugin which defined a "depends" or "requires" plugin
238
	 * we check if those plugins are present on the system. If some dependencies are
239
	 * not met, the plugin is removed from $this->plugindata.
240
	 *
241
	 * @return bool False if the $this->plugindata was modified by this function
242
	 */
243
	public function validatePluginRequirements() {
244
		$modified = false;
245
246
		do {
247
			$success = true;
248
249
			foreach ($this->plugindata as $pluginname => &$plugin) {
250
				// Check if the plugin had any dependencies
251
				// declared in the manifest. If not, they are obviously
252
				// met. Otherwise we have to check the type of dependencies
253
				// which were declared.
254
				if ($plugin['dependencies']) {
255
					// We only care about the 'depends' and 'requires'
256
					// dependency types. All others are not blocking.
257
					foreach ($plugin['dependencies'][DEPEND_DEPENDS] as &$depends) {
258
						if (!$this->pluginExists($depends['plugin'])) {
259
							if (DEBUG_PLUGINS) {
260
								dump('[PLUGIN ERROR] Plugin "' . $pluginname . '" requires "' . $depends['plugin'] . '" which could not be found');
261
							}
262
							unset($this->plugindata[$pluginname]);
263
							// Indicate failure, as we have removed a plugin, and the requirements
264
							// must be rechecked.
265
							$success = false;
266
							// Indicate that the plugindata was modified.
267
							$modified = true;
268
						}
269
					}
270
271
					foreach ($plugin['dependencies'][DEPEND_REQUIRES] as &$depends) {
272
						if (!$this->pluginExists($depends['plugin'])) {
273
							if (DEBUG_PLUGINS) {
274
								dump('[PLUGIN ERROR] Plugin "' . $pluginname . '" requires "' . $depends['plugin'] . '" which could not be found');
275
							}
276
							unset($this->plugindata[$pluginname]);
277
							// Indicate failure, as we have removed a plugin, and the requirements
278
							// must be rechecked.
279
							$success = false;
280
							// Indicate that the plugindata was modified.
281
							$modified = true;
282
						}
283
					}
284
				}
285
			}
286
287
			// If a plugin was removed because of a failed dependency or requirement,
288
			// then we have to redo the cycle, because another plugin might have depended
289
			// on the removed plugin.
290
		}
291
		while (!$success);
292
293
		return !$modified;
294
	}
295
296
	/**
297
	 * buildPluginDependencyOrder.
298
	 *
299
	 * Go over the parsed $this->plugindata and create a ordered list of the plugins, resembling
300
	 * the order in which those plugins should be loaded. This goes over all plugins to read
301
	 * the 'dependencies' data and ordering those plugins based on the DEPEND_DEPENDS dependency type.
302
	 *
303
	 * In case of circular dependencies, the $this->plugindata object might be altered to remove
304
	 * the plugin which the broken dependencies.
305
	 *
306
	 * @return array The array of plugins in the order of which they should be loaded
307
	 */
308
	public function buildPluginDependencyOrder() {
309
		$plugins = array_keys($this->plugindata);
310
		$ordered = [];
311
		$failedCount = 0;
312
313
		// We are going to keep it quite simple, we keep looping over the $plugins
314
		// array until it is empty. Each time we find a plugin for which all dependencies
315
		// are met, we can put it on the $ordered list. If we have looped over the list twice,
316
		// without updated the $ordered list in any way, then we have found a circular dependency
317
		// and we cannot resolve the plugins correctly.
318
		while (!empty($plugins)) {
319
			$pluginname = array_shift($plugins);
320
			$plugin = $this->plugindata[$pluginname];
321
			$accepted = true;
322
323
			// Go over all dependencies to see if they have been met.
324
			if ($plugin['dependencies']) {
325
				for ($i = 0, $len = count($plugin['dependencies'][DEPEND_DEPENDS]); $i < $len; ++$i) {
326
					$dependency = $plugin['dependencies'][DEPEND_DEPENDS][$i];
327
					if (array_search($dependency['plugin'], $ordered) === false) {
328
						$accepted = false;
329
						break;
330
					}
331
				}
332
			}
333
334
			if ($accepted) {
335
				// The dependencies for this plugin have been met, we can push
336
				// the plugin into the tree.
337
				$ordered[] = $pluginname;
338
339
				// Reset the $failedCount property, this ensures that we can keep
340
				// looping because other plugins with previously unresolved dependencies
341
				// could possible be resolved.
342
				$failedCount = 0;
343
			}
344
			else {
345
				// The dependencies for this plugin have not been met, we push
346
				// the plugin back to the list and we will retry later when the
347
				// $ordered list contains more items.
348
				$plugins[] = $pluginname;
349
350
				// Increase the $failedCount property, this prevents that we could go into
351
				// an infinite loop when a circular dependency was defined.
352
				++$failedCount;
353
			}
354
355
			// If the $failedCount matches the the number of items in the $plugins array,
356
			// it means that all unordered plugins have unmet dependencies. This could only
357
			// happen for circular dependencies. In that case we will refuse to load those plugins.
358
			if ($failedCount === count($plugins)) {
359
				foreach ($plugins as $plugin) {
360
					if (DEBUG_PLUGINS) {
361
						dump('[PLUGIN ERROR] Circular dependency detected for plugin "' . $plugin . '"');
362
					}
363
					unset($this->plugindata[$plugin]);
364
				}
365
				break;
366
			}
367
		}
368
369
		return $ordered;
370
	}
371
372
	/**
373
	 * initPlugins.
374
	 *
375
	 * This function includes the server plugin classes, instantiate and
376
	 * initialize them.
377
	 *
378
	 * @param number $load One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
379
	 *                     the files based on the 'load' attribute.
380
	 */
381
	public function initPlugins($load = LOAD_RELEASE) {
382
		if (!$this->pluginsEnabled()) {
383
			return false;
384
		}
385
386
		$files = $this->getServerFiles($load);
387
		foreach ($files['server'] as $file) {
388
			include_once $file;
389
		}
390
391
		// Include the root files of all the plugins and instantiate the plugin
392
		foreach ($this->pluginorder as $plugName) {
393
			$pluginClassName = 'Plugin' . $plugName;
394
			if (class_exists($pluginClassName)) {
395
				$this->plugins[$plugName] = new $pluginClassName();
396
				$this->plugins[$plugName]->setPluginName($plugName);
397
				$this->plugins[$plugName]->init();
398
			}
399
		}
400
401
		$this->modules = $files['modules'];
402
		$this->notifiers = $files['notifiers'];
403
	}
404
405
	/**
406
	 * processPlugin.
407
	 *
408
	 * Read in the manifest and get the files that need to be included
409
	 * for placing hooks, defining modules, etc.
410
	 *
411
	 * @param $dirname string name of the directory of the plugin
412
	 *
413
	 * @return array The plugin data read from the given directory
414
	 */
415
	public function processPlugin($dirname) {
416
		// Read XML manifest file of plugin
417
		$handle = fopen($this->pluginpath . DIRECTORY_SEPARATOR . $dirname . DIRECTORY_SEPARATOR . 'manifest.xml', 'rb');
418
		$xml = '';
419
		if ($handle) {
0 ignored issues
show
introduced by
$handle is of type resource, thus it always evaluated to false.
Loading history...
420
			while (!feof($handle)) {
421
				$xml .= fread($handle, 4096);
422
			}
423
			fclose($handle);
424
		}
425
426
		$plugindata = $this->extractPluginDataFromXML($xml, $dirname);
427
		if ($plugindata) {
428
			// Apply the name to the object
429
			$plugindata['pluginname'] = $dirname;
430
		}
431
		else {
432
			if (DEBUG_PLUGINS) {
433
				dump('[PLUGIN ERROR] Plugin "' . $dirname . '" has an invalid manifest.');
434
			}
435
		}
436
437
		return $plugindata;
438
	}
439
440
	/**
441
	 * loadSessionData.
442
	 *
443
	 * Loads sessiondata of the plugins from disk.
444
	 * To improve performance the data is only loaded if a
445
	 * plugin requests (reads or saves) the data.
446
	 *
447
	 * @param $pluginname string Identifier of the plugin
448
	 */
449
	public function loadSessionData($pluginname) {
450
		// lazy reading of sessionData
451
		if (!$this->sessionData) {
452
			$sessState = new State('plugin_sessiondata');
453
			$sessState->open();
454
			$this->sessionData = $sessState->read("sessionData");
455
			if (!isset($this->sessionData) || $this->sessionData == "") {
456
				$this->sessionData = [];
457
			}
458
			$sessState->close();
459
		}
460
		if ($this->pluginExists($pluginname)) {
461
			if (!isset($this->sessionData[$pluginname])) {
462
				$this->sessionData[$pluginname] = [];
463
			}
464
			$this->plugins[$pluginname]->setSessionData($this->sessionData[$pluginname]);
465
		}
466
	}
467
468
	/**
469
	 * saveSessionData.
470
	 *
471
	 * Saves sessiondata of the plugins to the disk.
472
	 *
473
	 * @param $pluginname string Identifier of the plugin
474
	 */
475
	public function saveSessionData($pluginname) {
476
		if ($this->pluginExists($pluginname)) {
477
			$this->sessionData[$pluginname] = $this->plugins[$pluginname]->getSessionData();
478
		}
479
		if ($this->sessionData) {
480
			$sessState = new State('plugin_sessiondata');
481
			$sessState->open();
482
			$sessState->write("sessionData", $this->sessionData);
483
			$sessState->close();
484
		}
485
	}
486
487
	/**
488
	 * pluginExists.
489
	 *
490
	 * Checks if plugin exists.
491
	 *
492
	 * @param $pluginname string Identifier of the plugin
493
	 *
494
	 * @return bool true when plugin exists, false when it does not
495
	 */
496
	public function pluginExists($pluginname) {
497
		if (isset($this->plugindata[$pluginname])) {
498
			return true;
499
		}
500
501
		return false;
502
	}
503
504
	/**
505
	 * getModuleFilePath.
506
	 *
507
	 * Obtain the filepath of the given modulename
508
	 *
509
	 * @param $modulename string Identifier of the modulename
510
	 *
511
	 * @return string The path to the file for the module
512
	 */
513
	public function getModuleFilePath($modulename) {
514
		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...
515
	}
516
517
	/**
518
	 * getNotifierFilePath.
519
	 *
520
	 * Obtain the filepath of the given notifiername
521
	 *
522
	 * @param $notifiername string Identifier of the notifiername
523
	 *
524
	 * @return string The path to the file for the notifier
525
	 */
526
	public function getNotifierFilePath($notifiername) {
527
		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...
528
	}
529
530
	/**
531
	 * registerHook.
532
	 *
533
	 * This function allows the plugin to register their hooks.
534
	 *
535
	 * @param $eventID    string Identifier of the event where this hook must be triggered
536
	 * @param $pluginName string Name of the plugin that is registering this hook
537
	 */
538
	public function registerHook($eventID, $pluginName) {
539
		$this->hooks[$eventID][$pluginName] = $pluginName;
540
	}
541
542
	/**
543
	 * triggerHook.
544
	 *
545
	 * This function will call all the registered hooks when their event is triggered.
546
	 *
547
	 * @param $eventID string Identifier of the event that has just been triggered
548
	 * @param $data    mixed (Optional) Usually an array of data that the callback function can modify
549
	 *
550
	 * @return mixed data that has been changed by plugins
551
	 */
552
	public function triggerHook($eventID, $data = []) {
553
		if (isset($this->hooks[$eventID]) && is_array($this->hooks[$eventID])) {
554
			foreach ($this->hooks[$eventID] as $key => $pluginname) {
555
				$this->plugins[$pluginname]->execute($eventID, $data);
556
			}
557
		}
558
559
		return $data;
560
	}
561
562
	/**
563
	 * getPluginVersion.
564
	 *
565
	 * Function is used to prepare version information array from plugindata.
566
	 *
567
	 * @return array the array of plugins version information
568
	 */
569
	public function getPluginsVersion() {
570
		$versionInfo = [];
571
		foreach ($this->plugindata as $pluginName => $data) {
572
			$versionInfo[$pluginName] = $data["version"];
573
		}
574
575
		return $versionInfo;
576
	}
577
578
	/**
579
	 * getServerFilesForComponent.
580
	 *
581
	 * Called by getServerFiles() to return the list of files which are provided
582
	 * for the given component in a particular plugin.
583
	 * The paths which are returned start at the root of the webapp.
584
	 *
585
	 * This function might call itself recursively if it couldn't find any files for
586
	 * the given $load type. If no 'source' files are found, it will obtain the 'debug'
587
	 * files, if that too files it will fallback to 'release' files. If the latter is
588
	 * not found either, no files are returned.
589
	 *
590
	 * @param string $pluginname The name of the plugin (this is used in the pathname)
591
	 * @param array  $component  The component to read the serverfiles from
592
	 * @param number $load       One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
593
	 *                           the files based on the 'load' attribute.
594
	 *
595
	 * @return array list of paths to the files in this component
596
	 */
597
	public function getServerFilesForComponent($pluginname, $component, $load) {
598
		$componentfiles = [
599
			'server' => [],
600
			'modules' => [],
601
			'notifiers' => [],
602
		];
603
604
		foreach ($component['serverfiles'][$load] as &$file) {
605
			switch ($file['type']) {
606
				case TYPE_CONFIG:
607
					$componentfiles['server'][] = $this->pluginconfigpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
608
					break;
609
610
				case TYPE_PLUGIN:
611
					$componentfiles['server'][] = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
612
					break;
613
614
				case TYPE_MODULE:
615
					$componentfiles['modules'][$file['module']] = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
616
					break;
617
618
				case TYPE_NOTIFIER:
619
					$componentfiles['notifiers'][$file['notifier']] = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
620
					break;
621
			}
622
		}
623
		unset($file);
624
625
		return $componentfiles;
626
	}
627
628
	/**
629
	 * getServerFiles.
630
	 *
631
	 * Returning an array of paths to files that need to be included.
632
	 * The paths which are returned start at the root of the webapp.
633
	 *
634
	 * This calls getServerFilesForComponent() to obtain the files
635
	 * for each component inside the requested plugin
636
	 *
637
	 * @param number $load One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
638
	 *                     the files based on the 'load' attribute.
639
	 *
640
	 * @return array list of paths to files
641
	 */
642
	public function getServerFiles($load = LOAD_RELEASE) {
643
		$files = [
644
			'server' => [],
645
			'modules' => [],
646
			'notifiers' => [],
647
		];
648
649
		foreach ($this->pluginorder as $pluginname) {
650
			$plugin = &$this->plugindata[$pluginname];
651
			foreach ($plugin['components'] as &$component) {
652
				if (!empty($component['serverfiles'][$load])) {
653
					$componentfiles = $this->getServerFilesForComponent($pluginname, $component, $load);
654
				}
655
				elseif ($load === LOAD_SOURCE && !empty($component['serverfiles'][LOAD_DEBUG])) {
656
					$componentfiles = $this->getServerFilesForComponent($pluginname, $component, LOAD_DEBUG);
657
				}
658
				elseif ($load !== LOAD_RELEASE && !empty($component['serverfiles'][LOAD_RELEASE])) {
659
					$componentfiles = $this->getServerFilesForComponent($pluginname, $component, LOAD_RELEASE);
660
				} // else tough luck, at least release should be present
661
662
				if (isset($componentfiles)) {
663
					$files['server'] = array_merge($files['server'], $componentfiles['server']);
664
					$files['modules'] = array_merge($files['modules'], $componentfiles['modules']);
665
					$files['notifiers'] = array_merge($files['notifiers'], $componentfiles['notifiers']);
666
					unset($componentfiles);
667
				}
668
			}
669
			unset($component);
670
		}
671
		unset($plugin);
672
673
		return $files;
674
	}
675
676
	/**
677
	 * getClientFilesForComponent.
678
	 *
679
	 * Called by getClientFiles() to return the list of files which are provided
680
	 * for the given component in a particular plugin.
681
	 * The paths which are returned start at the root of the webapp.
682
	 *
683
	 * This function might call itself recursively if it couldn't find any files for
684
	 * the given $load type. If no 'source' files are found, it will obtain the 'debug'
685
	 * files, if that too files it will fallback to 'release' files. If the latter is
686
	 * not found either, no files are returned.
687
	 *
688
	 * @param string $pluginname The name of the plugin (this is used in the pathname)
689
	 * @param array  $component  The component to read the clientfiles from
690
	 * @param number $load       One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
691
	 *                           the files based on the 'load' attribute.
692
	 *
693
	 * @return array list of paths to the files in this component
694
	 */
695
	public function getClientFilesForComponent($pluginname, $component, $load) {
696
		$componentfiles = [];
697
698
		foreach ($component['clientfiles'][$load] as &$file) {
699
			$componentfiles[] = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
700
		}
701
		unset($file);
702
703
		return $componentfiles;
704
	}
705
706
	/**
707
	 * getClientFiles.
708
	 *
709
	 * Returning an array of paths to files that need to be included.
710
	 * The paths which are returned start at the root of the webapp.
711
	 *
712
	 * This calls getClientFilesForComponent() to obtain the files
713
	 * for each component inside each plugin.
714
	 *
715
	 * @param number $load One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
716
	 *                     the files based on the 'load' attribute.
717
	 *
718
	 * @return array list of paths to files
719
	 */
720
	public function getClientFiles($load = LOAD_RELEASE) {
721
		$files = [];
722
723
		foreach ($this->pluginorder as $pluginname) {
724
			$plugin = &$this->plugindata[$pluginname];
725
			foreach ($plugin['components'] as &$component) {
726
				if (!empty($component['clientfiles'][$load])) {
727
					$componentfiles = $this->getClientFilesForComponent($pluginname, $component, $load);
728
				}
729
				elseif ($load === LOAD_SOURCE && !empty($component['clientfiles'][LOAD_DEBUG])) {
730
					$componentfiles = $this->getClientFilesForComponent($pluginname, $component, LOAD_DEBUG);
731
				}
732
				elseif ($load !== LOAD_RELEASE && !empty($component['clientfiles'][LOAD_RELEASE])) {
733
					$componentfiles = $this->getClientFilesForComponent($pluginname, $component, LOAD_RELEASE);
734
				} // else tough luck, at least release should be present
735
736
				if (isset($componentfiles)) {
737
					$files = array_merge($files, $componentfiles);
738
					unset($componentfiles);
739
				}
740
			}
741
			unset($component);
742
		}
743
		unset($plugin);
744
745
		return $files;
746
	}
747
748
	/**
749
	 * getResourceFilesForComponent.
750
	 *
751
	 * Called by getResourceFiles() to return the list of files which are provided
752
	 * for the given component in a particular plugin.
753
	 * The paths which are returned start at the root of the webapp.
754
	 *
755
	 * This function might call itself recursively if it couldn't find any files for
756
	 * the given $load type. If no 'source' files are found, it will obtain the 'debug'
757
	 * files, if that too files it will fallback to 'release' files. If the latter is
758
	 * not found either, no files are returned.
759
	 *
760
	 * @param string $pluginname The name of the plugin (this is used in the pathname)
761
	 * @param array  $component  The component to read the resourcefiles from
762
	 * @param number $load       One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
763
	 *                           the files based on the 'load' attribute.
764
	 *
765
	 * @return array list of paths to the files in this component
766
	 */
767
	public function getResourceFilesForComponent($pluginname, $component, $load) {
768
		$componentfiles = [];
769
770
		foreach ($component['resourcefiles'][$load] as &$file) {
771
			$componentfiles[] = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $file['file'];
772
		}
773
		unset($file);
774
775
		return $componentfiles;
776
	}
777
778
	/**
779
	 * getResourceFiles.
780
	 *
781
	 * Returning an array of paths to files that need to be included.
782
	 * The paths which are returned start at the root of the webapp.
783
	 *
784
	 * This calls getResourceFilesForComponent() to obtain the files
785
	 * for each component inside each plugin.
786
	 *
787
	 * @param number $load One of LOAD_RELEASE, LOAD_DEBUG, LOAD_SOURCE. This will filter
788
	 *                     the files based on the 'load' attribute.
789
	 *
790
	 * @return array list of paths to files
791
	 */
792
	public function getResourceFiles($load = LOAD_RELEASE) {
793
		$files = [];
794
795
		foreach ($this->pluginorder as $pluginname) {
796
			$plugin = &$this->plugindata[$pluginname];
797
			foreach ($plugin['components'] as &$component) {
798
				if (!empty($component['resourcefiles'][$load])) {
799
					$componentfiles = $this->getResourceFilesForComponent($pluginname, $component, $load);
800
				}
801
				elseif ($load === LOAD_SOURCE && !empty($component['resourcefiles'][LOAD_DEBUG])) {
802
					$componentfiles = $this->getResourceFilesForComponent($pluginname, $component, LOAD_DEBUG);
803
				}
804
				elseif ($load !== LOAD_RELEASE && !empty($component['resourcefiles'][LOAD_RELEASE])) {
805
					$componentfiles = $this->getResourceFilesForComponent($pluginname, $component, LOAD_RELEASE);
806
				} // else tough luck, at least release should be present
807
808
				if (isset($componentfiles)) {
809
					$files = array_merge($files, $componentfiles);
810
					unset($componentfiles);
811
				}
812
			}
813
			unset($component);
814
		}
815
		unset($plugin);
816
817
		return $files;
818
	}
819
820
	/**
821
	 * getTranslationFilePaths.
822
	 *
823
	 * Returning an array of paths to to the translations files. This will be
824
	 * used by the gettext functionality.
825
	 *
826
	 * @return array list of paths to translations
827
	 */
828
	public function getTranslationFilePaths() {
829
		$paths = [];
830
831
		foreach ($this->pluginorder as $pluginname) {
832
			$plugin = &$this->plugindata[$pluginname];
833
			if ($plugin['translationsdir']) {
834
				$translationPath = $this->pluginpath . DIRECTORY_SEPARATOR . $pluginname . DIRECTORY_SEPARATOR . $plugin['translationsdir']['dir'];
835
				if (is_dir($translationPath)) {
836
					$paths[$pluginname] = $translationPath;
837
				}
838
			}
839
		}
840
		unset($plugin);
841
842
		return $paths;
843
	}
844
845
	/**
846
	 * extractPluginDataFromXML.
847
	 *
848
	 * Extracts all the data from the Plugin XML manifest.
849
	 *
850
	 * @param $xml     string XML manifest of plugin
851
	 * @param $dirname string name of the directory of the plugin
852
	 *
853
	 * @return array data from XML converted into array that the PluginManager can use
854
	 */
855
	public function extractPluginDataFromXML($xml, $dirname) {
856
		$plugindata = [
857
			'components' => [],
858
			'dependencies' => null,
859
			'translationsdir' => null,
860
			'version' => null,
861
		];
862
863
		// Parse all XML data
864
		$data = new SimpleXMLElement($xml);
865
866
		// Parse the <plugin> attributes
867
		if (isset($data['version']) && (int) $data['version'] !== 2) {
868
			if (DEBUG_PLUGINS) {
869
				dump("[PLUGIN ERROR] Plugin {$dirname} manifest uses version " . $data['version'] . " while only version 2 is supported");
870
			}
871
872
			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...
873
		}
874
875
		// Parse the <info> element
876
		if (isset($data->info->version)) {
877
			$plugindata['version'] = (string) $data->info->version;
878
		}
879
		else {
880
			dump("[PLUGIN WARNING] Plugin {$dirname} has not specified version information in manifest.xml");
881
		}
882
883
		// Parse the <config> element
884
		if (isset($data->config)) {
885
			if (isset($data->config->configfile)) {
886
				if (empty($data->config->configfile)) {
887
					dump("[PLUGIN ERROR] Plugin {$dirname} manifest contains empty configfile declaration");
888
				}
889
				if (!file_exists($data->config->configfile)) {
890
					dump("[PLUGIN ERROR] Plugin {$dirname} manifest config file does not exists");
891
				}
892
			}
893
			else {
894
				dump("[PLUGIN ERROR] Plugin {$dirname} manifest configfile entry is missing");
895
			}
896
897
			$files = [
898
				LOAD_SOURCE => [],
899
				LOAD_DEBUG => [],
900
				LOAD_RELEASE => [],
901
			];
902
			foreach ($data->config->configfile as $filename) {
903
				$files[LOAD_RELEASE][] = [
904
					'file' => (string) $filename,
905
					'type' => TYPE_CONFIG,
906
					'load' => LOAD_RELEASE,
907
					'module' => null,
908
					'notifier' => null,
909
				];
910
			}
911
			$plugindata['components'][] = [
912
				'serverfiles' => $files,
913
				'clientfiles' => [],
914
				'resourcefiles' => [],
915
			];
916
		}
917
918
		// Parse the <dependencies> element
919
		if (isset($data->dependencies, $data->dependencies->depends)) {
920
			$dependencies = [
921
				DEPEND_DEPENDS => [],
922
				DEPEND_REQUIRES => [],
923
				DEPEND_RECOMMENDS => [],
924
				DEPEND_SUGGESTS => [],
925
			];
926
			foreach ($data->dependencies->depends as $depends) {
927
				$type = $this->dependMap[(string) $depends->attributes()->type];
928
				$plugin = (string) $depends->dependsname;
929
				$dependencies[$type][] = [
930
					'plugin' => $plugin,
931
				];
932
			}
933
			$plugindata['dependencies'] = $dependencies;
934
		}
935
936
		// Parse the <translations> element
937
		if (isset($data->translations, $data->translations->translationsdir)) {
938
			$plugindata['translationsdir'] = [
939
				'dir' => (string) $data->translations->translationsdir,
940
			];
941
		}
942
943
		// Parse the <components> element
944
		if (isset($data->components, $data->components->component)) {
945
			foreach ($data->components->component as $component) {
946
				$componentdata = [
947
					'serverfiles' => [
948
						LOAD_SOURCE => [],
949
						LOAD_DEBUG => [],
950
						LOAD_RELEASE => [],
951
					],
952
					'clientfiles' => [
953
						LOAD_SOURCE => [],
954
						LOAD_DEBUG => [],
955
						LOAD_RELEASE => [],
956
					],
957
					'resourcefiles' => [
958
						LOAD_SOURCE => [],
959
						LOAD_DEBUG => [],
960
						LOAD_RELEASE => [],
961
					],
962
				];
963
				if (isset($component->files)) {
964
					if (isset($component->files->server, $component->files->server->serverfile)) {
965
						$files = [
966
							LOAD_SOURCE => [],
967
							LOAD_DEBUG => [],
968
							LOAD_RELEASE => [],
969
						];
970
						foreach ($component->files->server->serverfile as $serverfile) {
971
							$load = LOAD_RELEASE;
972
							$type = TYPE_PLUGIN;
973
							$module = null;
974
							$notifier = null;
975
976
							$filename = (string) $serverfile;
977
							if (empty($filename)) {
978
								dump("[PLUGIN ERROR] Plugin {$dirname} manifest contains empty serverfile declaration");
979
							}
980
							if (isset($serverfile['type'])) {
981
								$type = $this->typeMap[(string) $serverfile['type']];
982
							}
983
							if (isset($serverfile['load'])) {
984
								$load = $this->loadMap[(string) $serverfile['load']];
985
							}
986
							if (isset($serverfile['module'])) {
987
								$module = (string) $serverfile['module'];
988
							}
989
							if (isset($serverfile['notifier'])) {
990
								$notifier = (string) $serverfile['notifier'];
991
							}
992
							if ($filename) {
993
								$files[$load][] = [
994
									'file' => $filename,
995
									'type' => $type,
996
									'load' => $load,
997
									'module' => $module,
998
									'notifier' => $notifier,
999
								];
1000
							}
1001
						}
1002
						$componentdata['serverfiles'][LOAD_SOURCE] = array_merge($componentdata['serverfiles'][LOAD_SOURCE], $files[LOAD_SOURCE]);
1003
						$componentdata['serverfiles'][LOAD_DEBUG] = array_merge($componentdata['serverfiles'][LOAD_DEBUG], $files[LOAD_DEBUG]);
1004
						$componentdata['serverfiles'][LOAD_RELEASE] = array_merge($componentdata['serverfiles'][LOAD_RELEASE], $files[LOAD_RELEASE]);
1005
					}
1006
					if (isset($component->files->client, $component->files->client->clientfile)) {
1007
						$files = [
1008
							LOAD_SOURCE => [],
1009
							LOAD_DEBUG => [],
1010
							LOAD_RELEASE => [],
1011
						];
1012
						foreach ($component->files->client->clientfile as $clientfile) {
1013
							$filename = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $filename is dead and can be removed.
Loading history...
1014
							$load = LOAD_RELEASE;
1015
							$filename = (string) $clientfile;
1016
							if (isset($clientfile['load'])) {
1017
								$load = $this->loadMap[(string) $clientfile['load']];
1018
							}
1019
							if (empty($filename)) {
1020
								if (DEBUG_PLUGINS) {
1021
									dump("[PLUGIN ERROR] Plugin {$dirname} manifest contains empty resourcefile declaration");
1022
								}
1023
							}
1024
							else {
1025
								$files[$load][] = [
1026
									'file' => $filename,
1027
									'load' => $load,
1028
								];
1029
							}
1030
						}
1031
						$componentdata['clientfiles'][LOAD_SOURCE] = array_merge($componentdata['clientfiles'][LOAD_SOURCE], $files[LOAD_SOURCE]);
1032
						$componentdata['clientfiles'][LOAD_DEBUG] = array_merge($componentdata['clientfiles'][LOAD_DEBUG], $files[LOAD_DEBUG]);
1033
						$componentdata['clientfiles'][LOAD_RELEASE] = array_merge($componentdata['clientfiles'][LOAD_RELEASE], $files[LOAD_RELEASE]);
1034
					}
1035
					if (isset($component->files->resources, $component->files->resources->resourcefile)) {
1036
						$files = [
1037
							LOAD_SOURCE => [],
1038
							LOAD_DEBUG => [],
1039
							LOAD_RELEASE => [],
1040
						];
1041
						foreach ($component->files->resources->resourcefile as $resourcefile) {
1042
							$filename = false;
1043
							$load = LOAD_RELEASE;
1044
							$filename = (string) $resourcefile;
1045
							if (isset($resourcefile['load'])) {
1046
								$load = $this->loadMap[(string) $resourcefile['load']];
1047
							}
1048
							if (empty($filename)) {
1049
								if (DEBUG_PLUGINS) {
1050
									dump("[PLUGIN ERROR] Plugin {$dirname} manifest contains empty resourcefile declaration");
1051
								}
1052
							}
1053
							else {
1054
								$files[$load][] = [
1055
									'file' => $filename,
1056
									'load' => $load,
1057
								];
1058
							}
1059
						}
1060
						$componentdata['resourcefiles'][LOAD_SOURCE] = array_merge($componentdata['resourcefiles'][LOAD_SOURCE], $files[LOAD_SOURCE]);
1061
						$componentdata['resourcefiles'][LOAD_DEBUG] = array_merge($componentdata['resourcefiles'][LOAD_DEBUG], $files[LOAD_DEBUG]);
1062
						$componentdata['resourcefiles'][LOAD_RELEASE] = array_merge($componentdata['resourcefiles'][LOAD_RELEASE], $files[LOAD_RELEASE]);
1063
					}
1064
					$plugindata['components'][] = $componentdata;
1065
				}
1066
			}
1067
		}
1068
		else {
1069
			if (DEBUG_PLUGINS) {
1070
				dump("[PLUGIN ERROR] Plugin {$dirname} manifest didn't provide any components");
1071
			}
1072
1073
			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...
1074
		}
1075
1076
		return $plugindata;
1077
	}
1078
1079
	/**
1080
	 * Expands a string that contains a semicolon separated list of plugins.
1081
	 * All wildcards (*) will be resolved.
1082
	 *
1083
	 * @param mixed $pluginList
1084
	 */
1085
	public function expandPluginList($pluginList) {
1086
		$pluginNames = explode(';', (string) $pluginList);
1087
		$pluginList = [];
1088
		foreach ($pluginNames as $pluginName) {
1089
			$pluginName = trim($pluginName);
1090
			if (array_key_exists($pluginName, $this->plugindata)) {
1091
				$pluginList[] = $pluginName;
1092
			}
1093
			else {
1094
				// Check if it contains a wildcard
1095
				if (str_contains($pluginName, '*')) {
1096
					$expandedPluginList = $this->_expandPluginNameWithWildcard($pluginName);
1097
					$pluginList = array_merge($pluginList, $expandedPluginList);
1098
				}
1099
			}
1100
		}
1101
1102
		// Remove duplicates
1103
		$pluginList = array_unique($pluginList);
1104
1105
		// Decide whether to show password plugin in settings:
1106
		// - show if the users are in a db
1107
		// - don't show if the users are in ldap
1108
		if (($key = array_search('passwd', $pluginList)) !== false && isset($GLOBALS['usersinldap']) && $GLOBALS['usersinldap']) {
1109
			unset($pluginList[$key]);
1110
		}
1111
1112
		return implode(';', $pluginList);
1113
	}
1114
1115
	/**
1116
	 * Finds all plugins that match the given string name (that contains one or
1117
	 * more wildcards).
1118
	 *
1119
	 * @param string $pluginNameWithWildcard A plugin identifying string that
1120
	 *                                       contains a wildcard character (*)
1121
	 *
1122
	 * @return array An array with the names of the plugins that are identified by
1123
	 *               $pluginNameWithWildcard
1124
	 */
1125
	private function _expandPluginNameWithWildcard($pluginNameWithWildcard) {
1126
		$retVal = [];
1127
		$pluginNames = array_keys($this->plugindata);
1128
		$regExp = '/^' . str_replace('*', '.*?', $pluginNameWithWildcard) . '$/';
1129
		dump('rexexp = ' . $regExp);
1130
		foreach ($pluginNames as $pluginName) {
1131
			dump('checking plugin: ' . $pluginName);
1132
			if (preg_match($regExp, $pluginName)) {
1133
				$retVal[] = $pluginName;
1134
			}
1135
		}
1136
1137
		return $retVal;
1138
	}
1139
}
1140