Completed
Push — master ( f4a3b3...4452de )
by Jeroen
67:28 queued 11:29
created

engine/classes/ElggPluginManifest.php (8 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 235
	public function __construct($manifest, $plugin_id = null) {
152 235
		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 235
			$this->pluginID = $plugin_id;
154
		}
155
156
		// see if we need to construct the xml object.
157 235
		if ($manifest instanceof \ElggXMLElement) {
158 1
			$manifest_obj = $manifest;
159
		} else {
160 235
			$raw_xml = '';
161 235
			if (substr(trim($manifest), 0, 1) == '<') {
162
				// this is a string
163 1
				$raw_xml = $manifest;
164 235
			} elseif (is_file($manifest)) {
165
				// this is a file
166 235
				$raw_xml = file_get_contents($manifest);
167
			}
168 235
			if ($raw_xml) {
169 235
				$manifest_obj = new \ElggXMLElement($raw_xml);
170
			} else {
171
				$manifest_obj = null;
172
			}
173
		}
174
175 235
		if (!$manifest_obj) {
176
			throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidManifest',
177
						[$this->getPluginID()]));
178
		}
179
180
		// set manifest api version
181 235
		if (isset($manifest_obj->attributes['xmlns'])) {
182 235
			$namespace = $manifest_obj->attributes['xmlns'];
183 235
			$version = str_replace($this->namespace_root, '', $namespace);
184
		} else {
185
			$version = 1.7;
186
		}
187
188 235
		$this->apiVersion = $version;
0 ignored issues
show
Documentation Bug introduced by
It seems like $version can also be of type string. However, the property $apiVersion is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
189
190 235
		$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 235
			$class_exists = class_exists($parser_class_name);
195
		} catch (Exception $e) {
196
			$class_exists = false;
197
		}
198
199 235
		if ($class_exists) {
200 235
			$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 235
		if (!$this->parser->parse()) {
207
			throw new \PluginException(_elgg_services()->translator->translate('PluginException:ParserError',
208
						[$this->apiVersion, $this->getPluginID()]));
209
		}
210 235
	}
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 62
	public function getPluginID() {
227 62
		if ($this->pluginID) {
228 62
			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 32
	public function getName() {
256 32
		$name = $this->parser->getAttribute('name');
257
258 32
		if (!$name && $this->pluginID) {
259
			$name = ucwords(str_replace('_', ' ', $this->pluginID));
260
		}
261
262 32
		return $name;
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 63
	public function getID() {
272 63
		return trim((string) $this->parser->getAttribute('id'));
273
	}
274
275
276
	/**
277
	 * Return the description
278
	 *
279
	 * @return string
280
	 */
281 31
	public function getDescription() {
282 31
		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 31
	public function getLicense() {
306
		// license vs licence.  Use license.
307 31
		$en_us = $this->parser->getAttribute('license');
308 31
		if ($en_us) {
309 31
			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');
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');
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');
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 31
	public function getAuthor() {
357 31
		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 31
	public function getCategories() {
384
		$bundled_plugins = [
385 31
			'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
			'likes',
403
			'login_as',
404
			'members',
405
			'messageboard',
406
			'messages',
407
			'notifications',
408
			'pages',
409
			'profile',
410
			'reportedcontent',
411
			'search',
412
			'site_notifications',
413
			'system_log',
414
			'tagcloud',
415
			'thewire',
416
			'uservalidationbyemail',
417
			'web_services',
418
		];
419
420 31
		$cats = $this->parser->getAttribute('category');
421
422 31
		if (!$cats) {
423
			$cats = [];
424
		}
425
426 31
		if (in_array('bundled', $cats) && !in_array($this->getPluginID(), $bundled_plugins)) {
427
			unset($cats[array_search('bundled', $cats)]);
428
		}
429
430 31
		return $cats;
431
	}
432
433
	/**
434
	 * Return the screenshots listed.
435
	 *
436
	 * @return array
437
	 */
438 1
	public function getScreenshots() {
439 1
		$ss = $this->parser->getAttribute('screenshot');
440
441 1
		if (!$ss) {
442
			$ss = [];
443
		}
444
445 1
		$normalized = [];
446 1
		foreach ($ss as $s) {
447 1
			$normalized[] = $this->buildStruct($this->screenshotStruct, $s);
448
		}
449
450 1
		return $normalized;
451
	}
452
453
	/**
454
	 * Return the contributors listed.
455
	 *
456
	 * @return array
457
	 */
458 1
	public function getContributors() {
459 1
		$ss = $this->parser->getAttribute('contributor');
460
461 1
		if (!$ss) {
462
			$ss = [];
463
		}
464
465 1
		$normalized = [];
466 1
		foreach ($ss as $s) {
467 1
			$normalized[] = $this->buildStruct($this->contributorStruct, $s);
468
		}
469
470 1
		return $normalized;
471
	}
472
473
	/**
474
	 * Return the list of provides by this plugin.
475
	 *
476
	 * @return array
477
	 */
478 33
	public function getProvides() {
479
		// normalize for 1.7
480 33
		if ($this->getApiVersion() < 1.8) {
481
			$provides = [];
482
		} else {
483 33
			$provides = $this->parser->getAttribute('provides');
484
		}
485
486 33
		if (!$provides) {
487 5
			$provides = [];
488
		}
489
490
		// always provide ourself if we can
491 33
		if ($this->pluginID) {
492 33
			$provides[] = [
493 33
				'type' => 'plugin',
494 33
				'name' => $this->getPluginID(),
495 33
				'version' => $this->getVersion()
496
			];
497
		}
498
499 33
		$normalized = [];
500 33
		foreach ($provides as $provide) {
501 33
			$normalized[] = $this->buildStruct($this->depsProvidesStruct, $provide);
502
		}
503
504 33
		return $normalized;
505
	}
506
507
	/**
508
	 * Returns the dependencies listed.
509
	 *
510
	 * @return array
511
	 */
512 33
	public function getRequires() {
513 33
		$reqs = $this->parser->getAttribute('requires');
514
515 33
		if (!$reqs) {
516
			$reqs = [];
517
		}
518
519 33
		$normalized = [];
520 33
		foreach ($reqs as $req) {
521 33
			$normalized[] = $this->normalizeDep($req);
522
		}
523
524 33
		return $normalized;
525
	}
526
527
	/**
528
	 * Returns the suggests elements.
529
	 *
530
	 * @return array
531
	 */
532 1
	public function getSuggests() {
533 1
		$suggests = $this->parser->getAttribute('suggests');
534
535 1
		if (!$suggests) {
536
			$suggests = [];
537
		}
538
539 1
		$normalized = [];
540 1
		foreach ($suggests as $suggest) {
541 1
			$normalized[] = $this->normalizeDep($suggest);
542
		}
543
544 1
		return $normalized;
545
	}
546
547
	/**
548
	 * Normalizes a dependency array using the defined structs.
549
	 * Can be used with either requires or suggests.
550
	 *
551
	 * @param array $dep A dependency array.
552
	 * @return array The normalized deps array.
553
	 */
554 33
	private function normalizeDep($dep) {
555
		
556 33
		$struct = [];
557
		
558 33
		switch ($dep['type']) {
559
			case 'elgg_release':
560 33
				$struct = $this->depsStructElgg;
561 33
				break;
562
563
			case 'plugin':
564 31
				$struct = $this->depsStructPlugin;
565 31
				break;
566
567
			case 'priority':
568 31
				$struct = $this->depsStructPriority;
569 31
				break;
570
571
			case 'php_version':
572 30
				$struct = $this->depsStructPhpVersion;
573 30
				break;
574
575
			case 'php_extension':
576 30
				$struct = $this->depsStructPhpExtension;
577 30
				break;
578
579
			case 'php_ini':
580 30
				$struct = $this->depsStructPhpIni;
581
582
				// also normalize boolean values
583 30
				if (isset($dep['value'])) {
584 30
					switch (strtolower($dep['value'])) {
585 30
						case 'yes':
586 30
						case 'true':
587 30
						case 'on':
588 30
						case 1:
589
							$dep['value'] = 1;
590
							break;
591
592 30
						case 'no':
593 30
						case 'false':
594 30
						case 'off':
595
						case 0:
596
						case '':
597 30
							$dep['value'] = 0;
598 30
							break;
599
					}
600
				}
601 30
				break;
602
			default:
603
				// unrecognized so we just return the raw dependency
604
				return $dep;
605
		}
606
		
607 33
		$normalized_dep = $this->buildStruct($struct, $dep);
608
609
		// normalize comparison operators
610 33
		if (isset($normalized_dep['comparison'])) {
611 33
			switch ($normalized_dep['comparison']) {
612
				case '<':
613
					$normalized_dep['comparison'] = 'lt';
614
					break;
615
616
				case '<=':
617
					$normalized_dep['comparison'] = 'le';
618
					break;
619
620
				case '>':
621
					$normalized_dep['comparison'] = 'gt';
622
					break;
623
624
				case '>=':
625
					$normalized_dep['comparison'] = 'ge';
626
					break;
627
628
				case '==':
629
				case 'eq':
630
					$normalized_dep['comparison'] = '=';
631
					break;
632
633
				case '<>':
634
				case 'ne':
635
					$normalized_dep['comparison'] = '!=';
636
					break;
637
			}
638
		}
639
640 33
		return $normalized_dep;
641
	}
642
643
	/**
644
	 * Returns the conflicts listed
645
	 *
646
	 * @return array
647
	 */
648 33
	public function getConflicts() {
649
		// normalize for 1.7
650 33
		if ($this->getApiVersion() < 1.8) {
651
			$conflicts = [];
652
		} else {
653 33
			$conflicts = $this->parser->getAttribute('conflicts');
654
		}
655
656 33
		if (!$conflicts) {
657 5
			$conflicts = [];
658
		}
659
660 33
		$normalized = [];
661
662 33
		foreach ($conflicts as $conflict) {
663 33
			$normalized[] = $this->buildStruct($this->depsConflictsStruct, $conflict);
664
		}
665
666 33
		return $normalized;
667
	}
668
669
	/**
670
	 * Should this plugin be activated when Elgg is installed
671
	 *
672
	 *  @return bool
673
	 */
674 1
	public function getActivateOnInstall() {
675 1
		$activate = $this->parser->getAttribute('activate_on_install');
676 1
		switch (strtolower($activate)) {
0 ignored issues
show
It seems like $activate can also be of type false; however, parameter $str of strtolower() does only seem to accept string, 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

676
		switch (strtolower(/** @scrutinizer ignore-type */ $activate)) {
Loading history...
677 1
			case 'yes':
678 1
			case 'true':
679
			case 'on':
680
			case 1:
681 1
				return true;
682
683
			case 'no':
684
			case 'false':
685
			case 'off':
686
			case 0:
687
			case '':
688
				return false;
689
		}
690
	}
691
692
	/**
693
	 * Normalizes an array into the structure specified
694
	 *
695
	 * @param array $struct The struct to normalize $element to.
696
	 * @param array $array  The array
697
	 *
698
	 * @return array
699
	 */
700 33
	protected function buildStruct(array $struct, array $array) {
701 33
		$return = [];
702
703 33
		foreach ($struct as $index => $default) {
704 33
			$return[$index] = elgg_extract($index, $array, $default);
705
		}
706
707 33
		return $return;
708
	}
709
710
	/**
711
	 * Returns a category's friendly name. This can be localized by
712
	 * defining the string 'admin:plugins:category:<category>'. If no
713
	 * localization is found, returns the category with _ and - converted to ' '
714
	 * and then ucwords()'d.
715
	 *
716
	 * @param string $category The category as defined in the manifest.
717
	 * @return string A human-readable category
718
	 */
719
	static public function getFriendlyCategory($category) {
720
		$cat_raw_string = "admin:plugins:category:$category";
721
		if (_elgg_services()->translator->languageKeyExists($cat_raw_string)) {
722
			return _elgg_services()->translator->translate($cat_raw_string);
723
		}
724
		
725
		$category = str_replace(['-', '_'], ' ', $category);
726
		return ucwords($category);
727
	}
728
}
729