Elgg  Version 3.0
ElggPluginPackage.php
Go to the documentation of this file.
1 <?php
2 
21 
22  const STATIC_CONFIG_FILENAME = 'elgg-plugin.php';
23 
29  private $requiredFiles = [
30  'manifest.xml'
31  ];
32 
37  private $textFiles = [
38  'README.txt',
39  'CHANGES.txt',
40  'INSTALL.txt',
41  'COPYRIGHT.txt',
42  'LICENSE.txt',
43  'README',
44  'README.md',
45  'README.markdown'
46  ];
47 
53  private $providesSupportedTypes = [
54  'plugin',
55  'php_extension'
56  ];
57 
63  private $depsSupportedTypes = [
64  'elgg_release',
65  'php_version',
66  'php_extension',
67  'php_ini',
68  'plugin',
69  'priority',
70  ];
71 
75  private $errorMsg = '';
76 
82  protected $manifest;
83 
89  protected $path;
90 
96  protected $valid = null;
97 
103  protected $id;
104 
113  public function __construct($plugin, $validate = true) {
114  $plugin_path = _elgg_config()->plugins_path;
115  // @todo wanted to avoid another is_dir() call here.
116  // should do some profiling to see how much it affects
117  if (strpos($plugin, $plugin_path) === 0 || is_dir($plugin)) {
118  // this is a path
120 
121  // the id is the last element of the array
122  $path_array = explode('/', trim($path, '/'));
123  $id = array_pop($path_array);
124  } else {
125  // this is a plugin id
126  // strict plugin names
127  if (preg_match('/[^a-z0-9\.\-_]/i', $plugin)) {
128  $msg = elgg_echo('PluginException:InvalidID', [$plugin]);
129  throw PluginException::factory('InvalidID', null, $msg);
130  }
131 
132  $path = "{$plugin_path}$plugin/";
133  $id = $plugin;
134  }
135 
136  if (!is_dir($path)) {
137  $msg = elgg_echo('PluginException:InvalidPath', [$path]);
138  throw PluginException::factory('InvalidPath', null, $msg);
139  }
140 
141  $this->path = $path;
142  $this->id = $id;
143 
144  if ($validate && !$this->isValid()) {
145  if ($this->errorMsg) {
146  $msg = elgg_echo('PluginException:InvalidPlugin:Details', [$plugin, $this->errorMsg]);
147  throw PluginException::factory('InvalidPluginDetails', null, $msg);
148  } else {
149  $msg = elgg_echo('PluginException:InvalidPlugin', [$plugin]);
150  throw PluginException::factory('InvalidPlugin', null, $msg);
151  }
152  }
153  }
154 
155  /********************************
156  * Validation and sanity checks *
157  ********************************/
158 
172  public function isValid() {
173  if (!isset($this->valid)) {
174  $this->valid = $this->validate();
175  }
176 
177  return $this->valid;
178  }
179 
183  private function validate() {
184  // check required files.
185  foreach ($this->requiredFiles as $file) {
186  if (!is_readable($this->path . $file)) {
187  $this->errorMsg = elgg_echo('ElggPluginPackage:InvalidPlugin:MissingFile', [$file]);
188 
189  return false;
190  }
191  }
192 
193  // check for valid manifest.
194  if (!$this->loadManifest()) {
195  return false;
196  }
197 
198  if (!$this->isNamedCorrectly()) {
199  return false;
200  }
201 
202  // can't require or conflict with yourself or something you provide.
203  // make sure provides are all valid.
204  if (!$this->hasSaneDependencies()) {
205  return false;
206  }
207 
208  if (!$this->hasReadableConfigFile()) {
209  return false;
210  }
211 
212  return true;
213  }
214 
221  private function hasReadableConfigFile() {
222  $file = "{$this->path}/" . self::STATIC_CONFIG_FILENAME;
223  if (!is_file($file)) {
224  return true;
225  }
226 
227  if (is_readable($file)) {
228  return true;
229  }
230 
231  $this->errorMsg = elgg_echo('ElggPluginPackage:InvalidPlugin:UnreadableConfig');
232 
233  return false;
234  }
235 
242  private function isNamedCorrectly() {
243  $manifest = $this->getManifest();
244  if ($manifest) {
245  $required_id = $manifest->getID();
246  if (!empty($required_id) && ($required_id !== $this->id)) {
247  $this->errorMsg = elgg_echo('ElggPluginPackage:InvalidPlugin:InvalidId', [$required_id]);
248 
249  return false;
250  }
251  }
252 
253  return true;
254  }
255 
267  private function hasSaneDependencies() {
268  // protection against plugins with no manifest file
269  if (!$this->getManifest()) {
270  return false;
271  }
272 
273  // Note: $conflicts and $requires are not unused. They're called dynamically
274  $conflicts = $this->getManifest()->getConflicts();
275  $requires = $this->getManifest()->getRequires();
276  $provides = $this->getManifest()->getProvides();
277 
278  foreach ($provides as $provide) {
279  // only valid provide types
280  if (!in_array($provide['type'], $this->providesSupportedTypes)) {
281  $this->errorMsg = elgg_echo('ElggPluginPackage:InvalidPlugin:InvalidProvides', [
282  $provide['type'],
283  ]);
284 
285  return false;
286  }
287 
288  // doesn't conflict or require any of its provides
289  $name = $provide['name'];
290  foreach (['conflicts', 'requires'] as $dep_type) {
291  foreach (${$dep_type} as $dep) {
292  if (!in_array($dep['type'], $this->depsSupportedTypes)) {
293  $this->errorMsg = elgg_echo('ElggPluginPackage:InvalidPlugin:InvalidDependency', [
294  $dep['type'],
295  ]);
296 
297  return false;
298  }
299 
300  // make sure nothing is providing something it conflicts or requires.
301  if (isset($dep['name']) && $dep['name'] == $name) {
302  $version_compare = version_compare($provide['version'], $dep['version'], $dep['comparison']);
303 
304  if ($version_compare) {
305  $this->errorMsg = elgg_echo('ElggPluginPackage:InvalidPlugin:CircularDep', [
306  $dep['type'],
307  $dep['name'],
308  $this->id,
309  ]);
310 
311  return false;
312  }
313  }
314  }
315  }
316  }
317 
318  return true;
319  }
320 
321 
322  /************
323  * Manifest *
324  ************/
325 
331  public function getManifest() {
332  if (!$this->manifest) {
333  if (!$this->loadManifest()) {
334  return false;
335  }
336  }
337 
338  return $this->manifest;
339  }
340 
347  private function loadManifest() {
348  $file = $this->path . 'manifest.xml';
349 
350  try {
351  $this->manifest = new \ElggPluginManifest($file, $this->id);
352  } catch (Exception $e) {
353  elgg_log($e, \Psr\Log\LogLevel::ERROR);
354 
355  $this->errorMsg = $e->getMessage();
356 
357  return false;
358  }
359 
360  if ($this->manifest instanceof \ElggPluginManifest) {
361  return true;
362  }
363 
364  $this->errorMsg = elgg_echo('unknown_error');
365 
366  return false;
367  }
368 
369  /****************
370  * Readme Files *
371  ***************/
372 
378  public function getTextFilenames() {
379  return $this->textFiles;
380  }
381 
382  /***********************
383  * Dependencies system *
384  ***********************/
385 
402  public function checkDependencies($full_report = false) {
403  // Note: $conflicts and $requires are not unused. They're called dynamically
404  $requires = $this->getManifest()->getRequires();
405  $conflicts = $this->getManifest()->getConflicts();
406 
407  $enabled_plugins = elgg_get_plugins('active');
408  $this_id = $this->getID();
409  $report = [];
410 
411  // first, check if any active plugin conflicts with us.
412  foreach ($enabled_plugins as $plugin) {
413  $temp_conflicts = [];
414  $temp_manifest = $plugin->getManifest();
415  if ($temp_manifest instanceof \ElggPluginManifest) {
416  $temp_conflicts = $plugin->getManifest()->getConflicts();
417  }
418  foreach ($temp_conflicts as $conflict) {
419  if ($conflict['type'] == 'plugin' && $conflict['name'] == $this_id) {
420  $result = $this->checkDepPlugin($conflict, $enabled_plugins, false);
421 
422  // rewrite the conflict to show the originating plugin
423  $conflict['name'] = $plugin->getDisplayName();
424 
425  if (!$full_report && !$result['status']) {
426  $css_id = preg_replace('/[^a-z0-9-]/i', '-', $plugin->getManifest()->getID());
427  $link = elgg_view('output/url', [
428  'text' => $plugin->getDisplayName(),
429  'href' => "#$css_id",
430  ]);
431 
432  $key = 'ElggPluginPackage:InvalidPlugin:ConflictsWithPlugin';
433  $this->errorMsg = elgg_echo($key, [$link]);
434 
435  return $result['status'];
436  } else {
437  $report[] = [
438  'type' => 'conflicted',
439  'dep' => $conflict,
440  'status' => $result['status'],
441  'value' => $this->getManifest()->getVersion()
442  ];
443  }
444  }
445  }
446  }
447 
448  $check_types = ['requires', 'conflicts'];
449 
450  if ($full_report) {
451  // Note: $suggests is not unused. It's called dynamically
452  $suggests = $this->getManifest()->getSuggests();
453  $check_types[] = 'suggests';
454  }
455 
456  foreach ($check_types as $dep_type) {
457  $inverse = ($dep_type == 'conflicts') ? true : false;
458 
459  foreach (${$dep_type} as $dep) {
460  switch ($dep['type']) {
461  case 'elgg_release':
462  $result = $this->checkDepElgg($dep, elgg_get_version(true), $inverse);
463  break;
464 
465  case 'plugin':
466  $result = $this->checkDepPlugin($dep, $enabled_plugins, $inverse);
467  break;
468 
469  case 'priority':
470  $result = $this->checkDepPriority($dep, $enabled_plugins, $inverse);
471  break;
472 
473  case 'php_version':
474  $result = $this->checkDepPhpVersion($dep, $inverse);
475  break;
476 
477  case 'php_extension':
478  $result = $this->checkDepPhpExtension($dep, $inverse);
479  break;
480 
481  case 'php_ini':
482  $result = $this->checkDepPhpIni($dep, $inverse);
483  break;
484 
485  default:
486  $result = null;//skip further check
487  break;
488  }
489 
490  if ($result !== null) {
491  // unless we're doing a full report, break as soon as we fail.
492  if (!$full_report && !$result['status']) {
493  $type = $dep['type'];
494 
495  if ($type === 'priority') {
496  $text = "{$dep['priority']} {$dep['plugin']}";
497  } else {
498  $text = $dep['name'];
499  }
500 
501  $this->errorMsg = elgg_echo('admin:plugins:label:missing_dependency', ["{$type}: {$text}"]);
502 
503  return $result['status'];
504  } else {
505  // build report element and comment
506  $report[] = [
507  'type' => $dep_type,
508  'dep' => $dep,
509  'status' => $result['status'],
510  'value' => $result['value']
511  ];
512  }
513  }
514  }
515  }
516 
517  if ($full_report) {
518  // add provides to full report
519  $provides = $this->getManifest()->getProvides();
520 
521  foreach ($provides as $provide) {
522  $report[] = [
523  'type' => 'provides',
524  'dep' => $provide,
525  'status' => true,
526  'value' => ''
527  ];
528  }
529 
530  return $report;
531  }
532 
533  return true;
534  }
535 
545  private function checkDepPlugin(array $dep, array $plugins, $inverse = false) {
546  $r = _elgg_services()->plugins->checkProvides('plugin', $dep['name'], $dep['version'], $dep['comparison']);
547 
548  if ($inverse) {
549  $r['status'] = !$r['status'];
550  }
551 
552  return $r;
553  }
554 
564  private function checkDepPriority(array $dep, array $plugins, $inverse = false) {
565  // grab the \ElggPlugin using this package.
566  $plugin_package = elgg_get_plugin_from_id($this->getID());
567  if (!$plugin_package) {
568  return [
569  'status' => true,
570  'value' => 'uninstalled'
571  ];
572  }
573 
574  $test_plugin = elgg_get_plugin_from_id($dep['plugin']);
575 
576  // If this isn't a plugin or the plugin isn't installed or active
577  // priority doesn't matter. Use requires to check if a plugin is active.
578  if (!$test_plugin || !$test_plugin->isActive()) {
579  return [
580  'status' => true,
581  'value' => 'uninstalled'
582  ];
583  }
584 
585  $plugin_priority = $plugin_package->getPriority();
586  $test_plugin_priority = $test_plugin->getPriority();
587 
588  switch ($dep['priority']) {
589  case 'before':
590  $status = $plugin_priority < $test_plugin_priority;
591  break;
592 
593  case 'after':
594  $status = $plugin_priority > $test_plugin_priority;
595  break;
596 
597  default;
598  $status = false;
599  }
600 
601  // get the current value
602  if ($plugin_priority < $test_plugin_priority) {
603  $value = 'before';
604  } else {
605  $value = 'after';
606  }
607 
608  if ($inverse) {
609  $status = !$status;
610  }
611 
612  return [
613  'status' => $status,
614  'value' => $value
615  ];
616  }
617 
627  private function checkDepElgg(array $dep, $elgg_version, $inverse = false) {
628  $status = version_compare($elgg_version, $dep['version'], $dep['comparison']);
629 
630  if ($inverse) {
631  $status = !$status;
632  }
633 
634  return [
635  'status' => $status,
636  'value' => $elgg_version
637  ];
638  }
639 
648  private function checkDepPhpVersion(array $dep, $inverse = false) {
649  $php_version = phpversion();
650  $status = version_compare($php_version, $dep['version'], $dep['comparison']);
651 
652  if ($inverse) {
653  $status = !$status;
654  }
655 
656  return [
657  'status' => $status,
658  'value' => $php_version
659  ];
660  }
661 
675  private function checkDepPhpExtension(array $dep, $inverse = false) {
676  $name = $dep['name'];
677  $version = $dep['version'];
678  $comparison = $dep['comparison'];
679 
680  // not enabled.
681  $status = extension_loaded($name);
682 
683  // enabled. check version.
684  $ext_version = phpversion($name);
685 
686  if ($status) {
687  // some extensions (like gd) don't provide versions. neat.
688  // don't check version info and return a lie.
689  if ($ext_version && $version) {
690  $status = version_compare($ext_version, $version, $comparison);
691  }
692 
693  if (!$ext_version) {
694  $ext_version = '???';
695  }
696  }
697 
698  // some php extensions can be emulated, so check provides.
699  if ($status == false) {
700  $provides = _elgg_services()->plugins->checkProvides('php_extension', $name, $version, $comparison);
701  $status = $provides['status'];
702  $ext_version = $provides['value'];
703  }
704 
705  if ($inverse) {
706  $status = !$status;
707  }
708 
709  return [
710  'status' => $status,
711  'value' => $ext_version
712  ];
713  }
714 
723  private function checkDepPhpIni($dep, $inverse = false) {
724  $name = $dep['name'];
725  $value = $dep['value'];
726  $comparison = $dep['comparison'];
727 
728  // ini_get() normalizes truthy values to 1 but falsey values to 0 or ''.
729  // version_compare() considers '' < 0, so normalize '' to 0.
730  // \ElggPluginManifest normalizes all bool values and '' to 1 or 0.
731  $setting = ini_get($name);
732 
733  if ($setting === '') {
734  $setting = 0;
735  }
736 
737  $status = version_compare($setting, $value, $comparison);
738 
739  if ($inverse) {
740  $status = !$status;
741  }
742 
743  return [
744  'status' => $status,
745  'value' => $setting
746  ];
747  }
748 
754  public function getID() {
755  return $this->id;
756  }
757 
763  public function getError() {
764  return $this->errorMsg;
765  }
766 }
$plugin
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
if(!array_key_exists($filename, $text_files)) $file
getManifest()
Returns a parsed manifest file.
checkDependencies($full_report=false)
Returns if the Elgg system meets the plugin&#39;s dependency requirements.
$plugins
Definition: categories.php:3
$type
Definition: delete.php:21
$report
elgg_echo($message_key, array $args=[], $language="")
Given a message key, returns an appropriately translated full-text string.
Definition: languages.php:21
$CONFIG path
Legacy documentation for the old $CONFIG object.
Definition: config.php:17
if(!$item instanceof ElggEntity) $link
Definition: container.php:16
$text
Definition: default.php:28
getTextFilenames()
Returns an array of present and readable text files.
static factory($reason, ElggPlugin $plugin=null, $message=null, Throwable $previous=null)
Create a new plugin exception.
elgg_log($message, $level=\Psr\Log\LogLevel::NOTICE)
Log a message.
Definition: elgglib.php:786
elgg_get_plugins($status= 'active')
Returns an ordered list of plugins.
Definition: plugins.php:76
if($container instanceof ElggGroup &&$container->guid!=elgg_get_page_owner_guid()) $key
Definition: summary.php:55
_elgg_config()
Get the Elgg config service.
$value
Definition: debugging.php:7
__construct($plugin, $validate=true)
Load a plugin package from mod/$id or by full path.
elgg_get_version($human_readable=false)
Get the current Elgg version information.
Definition: elgglib.php:814
getError()
Returns the last error message.
static sanitize($path, $append_slash=true)
Sanitise file paths ensuring that they begin and end with slashes etc.
Definition: Paths.php:76
_elgg_services()
Get the global service provider.
Definition: elgglib.php:1292
elgg ajax ERROR
Definition: ajax.js:33
getID()
Returns the Plugin ID.
elgg_view($view, $vars=[], $viewtype= '')
Return a parsed view.
Definition: views.php:246
$version
Definition: version.php:14
isValid()
Checks if this is a valid Elgg plugin.
elgg_get_plugin_from_id($plugin_id)
Returns an object with the path $path.
Definition: plugins.php:28