Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/ElggPluginManifest.php (10 issues)

1
<?php
2
/**
3
 * Parses Elgg manifest.xml files.
4
 *
5
 * Normalizes the values from the \ElggManifestParser object.
6
 *
7
 * This requires an \ElggPluginManifestParser class implementation
8
 * as $this->parser.
9
 *
10
 * To add new parser versions, name them \ElggPluginManifestParserXX
11
 * where XX is the version specified in the top-level <plugin_manifest>
12
 * tag's XML namespace.
13
 *
14
 * @package    Elgg.Core
15
 * @subpackage Plugins
16
 * @since      1.8
17
 */
18
class ElggPluginManifest {
19
20
	/**
21
	 * The parser object
22
	 *
23
	 * @var \ElggPluginManifestParser18
24
	 */
25
	protected $parser;
26
27
	/**
28
	 * The root for plugin manifest namespaces.
29
	 * This is in the format http://www.elgg.org/plugin_manifest/<version>
30
	 */
31
	protected $namespace_root = 'http://www.elgg.org/plugin_manifest/';
32
33
	/**
34
	 * The expected structure of a plugins requires element
35
	 */
36
	private $depsStructPlugin = [
37
		'type' => '',
38
		'name' => '',
39
		'version' => '',
40
		'comparison' => 'ge'
41
	];
42
43
	/**
44
	 * The expected structure of a priority element
45
	 */
46
	private $depsStructPriority = [
47
		'type' => '',
48
		'priority' => '',
49
		'plugin' => ''
50
	];
51
52
	/*
53
	 * The expected structure of elgg_release requires element
54
	 */
55
	private $depsStructElgg = [
56
		'type' => '',
57
		'version' => '',
58
		'comparison' => 'ge'
59
	];
60
61
	/**
62
	 * The expected structure of a requires php_version dependency element
63
	 */
64
	private $depsStructPhpVersion = [
65
		'type' => '',
66
		'version' => '',
67
		'comparison' => 'ge'
68
	];
69
70
	/**
71
	 * The expected structure of a requires php_ini dependency element
72
	 */
73
	private $depsStructPhpIni = [
74
		'type' => '',
75
		'name' => '',
76
		'value' => '',
77
		'comparison' => '='
78
	];
79
80
	/**
81
	 * The expected structure of a requires php_extension dependency element
82
	 */
83
	private $depsStructPhpExtension = [
84
		'type' => '',
85
		'name' => '',
86
		'version' => '',
87
		'comparison' => '='
88
	];
89
90
	/**
91
	 * The expected structure of a conflicts depedency element
92
	 */
93
	private $depsConflictsStruct = [
94
		'type' => '',
95
		'name' => '',
96
		'version' => '',
97
		'comparison' => '='
98
	];
99
100
	/**
101
	 * The expected structure of a provides dependency element.
102
	 */
103
	private $depsProvidesStruct = [
104
		'type' => '',
105
		'name' => '',
106
		'version' => ''
107
	];
108
109
	/**
110
	 * The expected structure of a screenshot element
111
	 */
112
	private $screenshotStruct = [
113
		'description' => '',
114
		'path' => ''
115
	];
116
117
	/**
118
	 * The expected structure of a contributor element
119
	 */
120
	private $contributorStruct = [
121
		'name' => '',
122
		'email' => '',
123
		'website' => '',
124
		'username' => '',
125
		'description' => '',
126
	];
127
128
	/**
129
	 * The API version of the manifest.
130
	 *
131
	 * @var int
132
	 */
133
	protected $apiVersion;
134
135
	/**
136
	 * The optional plugin id this manifest belongs to.
137
	 *
138
	 * @var string
139
	 */
140
	protected $pluginID;
141
142
	/**
143
	 * Load a manifest file, XmlElement or path to manifest.xml file
144
	 *
145
	 * @param mixed  $manifest  A string, XmlElement, or path of a manifest file.
146
	 * @param string $plugin_id Optional ID of the owning plugin. Used to
147
	 *                          fill in some values automatically.
148
	 *
149
	 * @throws PluginException
150
	 */
151 241
	public function __construct($manifest, $plugin_id = null) {
152 241
		if ($plugin_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $plugin_id of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
153 241
			$this->pluginID = $plugin_id;
154
		}
155
156
		// see if we need to construct the xml object.
157 241
		if ($manifest instanceof \ElggXMLElement) {
158 1
			$manifest_obj = $manifest;
159
		} else {
160 241
			$raw_xml = '';
161 241
			if (substr(trim($manifest), 0, 1) == '<') {
162
				// this is a string
163 1
				$raw_xml = $manifest;
164 241
			} elseif (is_file($manifest)) {
165
				// this is a file
166 241
				$raw_xml = file_get_contents($manifest);
167
			}
168 241
			if ($raw_xml) {
169 241
				$manifest_obj = new \ElggXMLElement($raw_xml);
170
			} else {
171
				$manifest_obj = null;
172
			}
173
		}
174
175 241
		if (!$manifest_obj) {
176
			throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidManifest',
177
						[$this->getPluginID()]));
178
		}
179
180
		// set manifest api version
181 241
		if (isset($manifest_obj->attributes['xmlns'])) {
182 241
			$namespace = $manifest_obj->attributes['xmlns'];
183 241
			$version = str_replace($this->namespace_root, '', $namespace);
184
		} else {
185
			$version = 1.7;
186
		}
187
188 241
		$this->apiVersion = $version;
189
190 241
		$parser_class_name = '\ElggPluginManifestParser' . str_replace('.', '', $this->apiVersion);
191
192
		// @todo currently the autoloader freaks out if a class doesn't exist.
193
		try {
194 241
			$class_exists = class_exists($parser_class_name);
195
		} catch (Exception $e) {
196
			$class_exists = false;
197
		}
198
199 241
		if ($class_exists) {
200 241
			$this->parser = new $parser_class_name($manifest_obj, $this);
201
		} else {
202
			throw new \PluginException(_elgg_services()->translator->translate('PluginException:NoAvailableParser',
203
							[$this->apiVersion, $this->getPluginID()]));
204
		}
205
206 241
		if (!$this->parser->parse()) {
207
			throw new \PluginException(_elgg_services()->translator->translate('PluginException:ParserError',
208
						[$this->apiVersion, $this->getPluginID()]));
209
		}
210 241
	}
211
212
	/**
213
	 * Returns the API version in use.
214
	 *
215
	 * @return int
216
	 */
217 33
	public function getApiVersion() {
218 33
		return $this->apiVersion;
219
	}
220
221
	/**
222
	 * Returns the plugin ID.
223
	 *
224
	 * @return string
225
	 */
226 63
	public function getPluginID() {
227 63
		if ($this->pluginID) {
228 63
			return $this->pluginID;
229
		} else {
230
			return _elgg_services()->translator->translate('unknown');
231
		}
232
	}
233
234
	/**
235
	 * Returns the manifest array.
236
	 *
237
	 * Used for backward compatibility.  Specific
238
	 * methods should be called instead.
239
	 *
240
	 * @return array
241
	 */
242 1
	public function getManifest() {
243 1
		return $this->parser->getManifest();
244
	}
245
246
	/***************************************
247
	 * Parsed and Normalized Manifest Data *
248
	 ***************************************/
249
250
	/**
251
	 * Returns the plugin name
252
	 *
253
	 * @return string
254
	 */
255 33
	public function getName() {
256 33
		$name = $this->parser->getAttribute('name');
257
258 33
		if (!$name && $this->pluginID) {
259
			$name = ucwords(str_replace('_', ' ', $this->pluginID));
260
		}
261
262 33
		return $name;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $name 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...
263
	}
264
265
	/**
266
	 * Return the plugin ID required by the author. If getPluginID() does
267
	 * not match this, the plugin should not be started.
268
	 *
269
	 * @return string empty string if not empty/not defined
270
	 */
271 64
	public function getID() {
272 64
		return trim((string) $this->parser->getAttribute('id'));
273
	}
274
275
276
	/**
277
	 * Return the description
278
	 *
279
	 * @return string
280
	 */
281 32
	public function getDescription() {
282 32
		return $this->parser->getAttribute('description');
283
	}
284
285
	/**
286
	 * Return the short description
287
	 *
288
	 * @return string
289
	 */
290 1
	public function getBlurb() {
291 1
		$blurb = $this->parser->getAttribute('blurb');
292
293 1
		if (!$blurb) {
294
			$blurb = elgg_get_excerpt($this->getDescription());
295
		}
296
297 1
		return $blurb;
298
	}
299
300
	/**
301
	 * Returns the license
302
	 *
303
	 * @return string
304
	 */
305 32
	public function getLicense() {
306
		// license vs licence.  Use license.
307 32
		$en_us = $this->parser->getAttribute('license');
308 32
		if ($en_us) {
309 32
			return $en_us;
310
		} else {
311
			return $this->parser->getAttribute('licence');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parser->getAttribute('licence') 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...
312
		}
313
	}
314
315
	/**
316
	 * Returns the repository url
317
	 *
318
	 * @return string
319
	 */
320 1
	public function getRepositoryURL() {
321 1
		return $this->parser->getAttribute('repository');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parser->getAttribute('repository') 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...
322
	}
323
324
	/**
325
	 * Returns the bug tracker page
326
	 *
327
	 * @return string
328
	 */
329 1
	public function getBugTrackerURL() {
330 1
		return $this->parser->getAttribute('bugtracker');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parser->getAttribute('bugtracker') 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...
331
	}
332
333
	/**
334
	 * Returns the donations page
335
	 *
336
	 * @return string
337
	 */
338 1
	public function getDonationsPageURL() {
339 1
		return $this->parser->getAttribute('donations');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parser->getAttribute('donations') 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...
340
	}
341
342
	/**
343
	 * Returns the version of the plugin.
344
	 *
345
	 * @return float
346
	 */
347 33
	public function getVersion() {
348 33
		return $this->parser->getAttribute('version');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parser->getAttribute('version') could also return false which is incompatible with the documented return type double. 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...
349
	}
350
351
	/**
352
	 * Returns the plugin author.
353
	 *
354
	 * @return string
355
	 */
356 32
	public function getAuthor() {
357 32
		return $this->parser->getAttribute('author');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parser->getAttribute('author') 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...
358
	}
359
360
	/**
361
	 * Return the copyright
362
	 *
363
	 * @return string
364
	 */
365 1
	public function getCopyright() {
366 1
		return $this->parser->getAttribute('copyright');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parser->getAttribute('copyright') 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...
367
	}
368
369
	/**
370
	 * Return the website
371
	 *
372
	 * @return string
373
	 */
374 1
	public function getWebsite() {
375 1
		return $this->parser->getAttribute('website');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parser->getAttribute('website') 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...
376
	}
377
378
	/**
379
	 * Return the categories listed for this plugin
380
	 *
381
	 * @return array
382
	 */
383 32
	public function getCategories() {
384
		$bundled_plugins = [
385 32
			'activity',
386
			'blog',
387
			'bookmarks',
388
			'ckeditor',
389
			'custom_index',
390
			'dashboard',
391
			'developers',
392
			'diagnostics',
393
			'discussions',
394
			'embed',
395
			'externalpages',
396
			'file',
397
			'friends',
398
			'friends_collections',
399
			'garbagecollector',
400
			'groups',
401
			'invitefriends',
402
			'legacy_urls',
403
			'likes',
404
			'login_as',
405
			'members',
406
			'messageboard',
407
			'messages',
408
			'notifications',
409
			'pages',
410
			'profile',
411
			'reportedcontent',
412
			'search',
413
			'site_notifications',
414
			'system_log',
415
			'tagcloud',
416
			'thewire',
417
			'uservalidationbyemail',
418
			'web_services',
419
		];
420
421 32
		$cats = $this->parser->getAttribute('category');
422
423 32
		if (!$cats) {
424
			$cats = [];
425
		}
426
427 32
		if (in_array('bundled', $cats) && !in_array($this->getPluginID(), $bundled_plugins)) {
428
			unset($cats[array_search('bundled', $cats)]);
429
		}
430
431 32
		return $cats;
432
	}
433
434
	/**
435
	 * Return the screenshots listed.
436
	 *
437
	 * @return array
438
	 */
439 1
	public function getScreenshots() {
440 1
		$ss = $this->parser->getAttribute('screenshot');
441
442 1
		if (!$ss) {
443
			$ss = [];
444
		}
445
446 1
		$normalized = [];
447 1
		foreach ($ss as $s) {
448 1
			$normalized[] = $this->buildStruct($this->screenshotStruct, $s);
449
		}
450
451 1
		return $normalized;
452
	}
453
454
	/**
455
	 * Return the contributors listed.
456
	 *
457
	 * @return array
458
	 */
459 1
	public function getContributors() {
460 1
		$ss = $this->parser->getAttribute('contributor');
461
462 1
		if (!$ss) {
463
			$ss = [];
464
		}
465
466 1
		$normalized = [];
467 1
		foreach ($ss as $s) {
468 1
			$normalized[] = $this->buildStruct($this->contributorStruct, $s);
469
		}
470
471 1
		return $normalized;
472
	}
473
474
	/**
475
	 * Return the list of provides by this plugin.
476
	 *
477
	 * @return array
478
	 */
479 33
	public function getProvides() {
480
		// normalize for 1.7
481 33
		if ($this->getApiVersion() < 1.8) {
482
			$provides = [];
483
		} else {
484 33
			$provides = $this->parser->getAttribute('provides');
485
		}
486
487 33
		if (!$provides) {
488 5
			$provides = [];
489
		}
490
491
		// always provide ourself if we can
492 33
		if ($this->pluginID) {
493 33
			$provides[] = [
494 33
				'type' => 'plugin',
495 33
				'name' => $this->getPluginID(),
496 33
				'version' => $this->getVersion()
497
			];
498
		}
499
500 33
		$normalized = [];
501 33
		foreach ($provides as $provide) {
502 33
			$normalized[] = $this->buildStruct($this->depsProvidesStruct, $provide);
503
		}
504
505 33
		return $normalized;
506
	}
507
508
	/**
509
	 * Returns the dependencies listed.
510
	 *
511
	 * @return array
512
	 */
513 33
	public function getRequires() {
514 33
		$reqs = $this->parser->getAttribute('requires');
515
516 33
		if (!$reqs) {
517
			$reqs = [];
518
		}
519
520 33
		$normalized = [];
521 33
		foreach ($reqs as $req) {
522 33
			$normalized[] = $this->normalizeDep($req);
523
		}
524
525 33
		return $normalized;
526
	}
527
528
	/**
529
	 * Returns the suggests elements.
530
	 *
531
	 * @return array
532
	 */
533 1
	public function getSuggests() {
534 1
		$suggests = $this->parser->getAttribute('suggests');
535
536 1
		if (!$suggests) {
537
			$suggests = [];
538
		}
539
540 1
		$normalized = [];
541 1
		foreach ($suggests as $suggest) {
542 1
			$normalized[] = $this->normalizeDep($suggest);
543
		}
544
545 1
		return $normalized;
546
	}
547
548
	/**
549
	 * Normalizes a dependency array using the defined structs.
550
	 * Can be used with either requires or suggests.
551
	 *
552
	 * @param array $dep A dependency array.
553
	 * @return array The normalized deps array.
554
	 */
555 33
	private function normalizeDep($dep) {
556
		
557 33
		$struct = [];
558
		
559 33
		switch ($dep['type']) {
560
			case 'elgg_release':
561 33
				$struct = $this->depsStructElgg;
562 33
				break;
563
564
			case 'plugin':
565 31
				$struct = $this->depsStructPlugin;
566 31
				break;
567
568
			case 'priority':
569 31
				$struct = $this->depsStructPriority;
570 31
				break;
571
572
			case 'php_version':
573 30
				$struct = $this->depsStructPhpVersion;
574 30
				break;
575
576
			case 'php_extension':
577 30
				$struct = $this->depsStructPhpExtension;
578 30
				break;
579
580
			case 'php_ini':
581 30
				$struct = $this->depsStructPhpIni;
582
583
				// also normalize boolean values
584 30
				if (isset($dep['value'])) {
585 30
					switch (strtolower($dep['value'])) {
586 30
						case 'yes':
587 30
						case 'true':
588 30
						case 'on':
589 30
						case 1:
590
							$dep['value'] = 1;
591
							break;
592
593 30
						case 'no':
594 30
						case 'false':
595 30
						case 'off':
596
						case 0:
597
						case '':
598 30
							$dep['value'] = 0;
599 30
							break;
600
					}
601
				}
602 30
				break;
603
			default:
604
				// unrecognized so we just return the raw dependency
605
				return $dep;
606
		}
607
		
608 33
		$normalized_dep = $this->buildStruct($struct, $dep);
609
610
		// normalize comparison operators
611 33
		if (isset($normalized_dep['comparison'])) {
612 33
			switch ($normalized_dep['comparison']) {
613
				case '<':
614
					$normalized_dep['comparison'] = 'lt';
615
					break;
616
617
				case '<=':
618
					$normalized_dep['comparison'] = 'le';
619
					break;
620
621
				case '>':
622
					$normalized_dep['comparison'] = 'gt';
623
					break;
624
625
				case '>=':
626
					$normalized_dep['comparison'] = 'ge';
627
					break;
628
629
				case '==':
630
				case 'eq':
631
					$normalized_dep['comparison'] = '=';
632
					break;
633
634
				case '<>':
635
				case 'ne':
636
					$normalized_dep['comparison'] = '!=';
637
					break;
638
			}
639
		}
640
641 33
		return $normalized_dep;
642
	}
643
644
	/**
645
	 * Returns the conflicts listed
646
	 *
647
	 * @return array
648
	 */
649 33
	public function getConflicts() {
650
		// normalize for 1.7
651 33
		if ($this->getApiVersion() < 1.8) {
652
			$conflicts = [];
653
		} else {
654 33
			$conflicts = $this->parser->getAttribute('conflicts');
655
		}
656
657 33
		if (!$conflicts) {
658 5
			$conflicts = [];
659
		}
660
661 33
		$normalized = [];
662
663 33
		foreach ($conflicts as $conflict) {
664 33
			$normalized[] = $this->buildStruct($this->depsConflictsStruct, $conflict);
665
		}
666
667 33
		return $normalized;
668
	}
669
670
	/**
671
	 * Should this plugin be activated when Elgg is installed
672
	 *
673
	 *  @return bool
674
	 */
675 1
	public function getActivateOnInstall() {
676 1
		$activate = $this->parser->getAttribute('activate_on_install');
677 1
		switch (strtolower($activate)) {
678 1
			case 'yes':
679 1
			case 'true':
680
			case 'on':
681
			case 1:
682 1
				return true;
683
684
			case 'no':
685
			case 'false':
686
			case 'off':
687
			case 0:
688
			case '':
689
				return false;
690
		}
691
	}
692
693
	/**
694
	 * Normalizes an array into the structure specified
695
	 *
696
	 * @param array $struct The struct to normalize $element to.
697
	 * @param array $array  The array
698
	 *
699
	 * @return array
700
	 */
701 33
	protected function buildStruct(array $struct, array $array) {
702 33
		$return = [];
703
704 33
		foreach ($struct as $index => $default) {
705 33
			$return[$index] = elgg_extract($index, $array, $default);
706
		}
707
708 33
		return $return;
709
	}
710
711
	/**
712
	 * Returns a category's friendly name. This can be localized by
713
	 * defining the string 'admin:plugins:category:<category>'. If no
714
	 * localization is found, returns the category with _ and - converted to ' '
715
	 * and then ucwords()'d.
716
	 *
717
	 * @param string $category The category as defined in the manifest.
718
	 * @return string A human-readable category
719
	 */
720
	static public function getFriendlyCategory($category) {
721
		$cat_raw_string = "admin:plugins:category:$category";
722
		if (_elgg_services()->translator->languageKeyExists($cat_raw_string)) {
723
			return _elgg_services()->translator->translate($cat_raw_string);
724
		}
725
		
726
		$category = str_replace(['-', '_'], ' ', $category);
727
		return ucwords($category);
728
	}
729
}
730