Elgg  Version 2.3
ElggPluginPackage.php
Go to the documentation of this file.
1 <?php
20 
21  const STATIC_CONFIG_FILENAME = 'elgg-plugin.php';
22 
28  private $requiredFiles = array(
29  'start.php', 'manifest.xml'
30  );
31 
36  private $textFiles = array(
37  'README.txt', 'CHANGES.txt',
38  'INSTALL.txt', 'COPYRIGHT.txt', 'LICENSE.txt',
39 
40  'README', 'README.md', 'README.markdown'
41  );
42 
48  private $providesSupportedTypes = array(
49  'plugin', 'php_extension'
50  );
51 
57  private $depsSupportedTypes = array(
58  'elgg_release', 'php_version', 'php_extension', 'php_ini', 'plugin', 'priority',
59  );
60 
64  private $errorMsg = '';
65 
71  protected $manifest;
72 
78  protected $path;
79 
85  protected $valid = null;
86 
92  protected $id;
93 
102  public function __construct($plugin, $validate = true) {
103  $plugin_path = _elgg_services()->config->getPluginsPath();
104  // @todo wanted to avoid another is_dir() call here.
105  // should do some profiling to see how much it affects
106  if (strpos($plugin, $plugin_path) === 0 || is_dir($plugin)) {
107  // this is a path
109 
110  // the id is the last element of the array
111  $path_array = explode('/', trim($path, '/'));
112  $id = array_pop($path_array);
113  } else {
114  // this is a plugin id
115  // strict plugin names
116  if (preg_match('/[^a-z0-9\.\-_]/i', $plugin)) {
117  throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidID', array($plugin)));
118  }
119 
120  $path = "{$plugin_path}$plugin/";
121  $id = $plugin;
122  }
123 
124  if (!is_dir($path)) {
125  throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidPath', array($path)));
126  }
127 
128  $this->path = $path;
129  $this->id = $id;
130 
131  if ($validate && !$this->isValid()) {
132  if ($this->errorMsg) {
133  throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidPlugin:Details',
134  array($plugin, $this->errorMsg)));
135  } else {
136  throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidPlugin', array($plugin)));
137  }
138  }
139  }
140 
141  /********************************
142  * Validation and sanity checks *
143  ********************************/
144 
158  public function isValid() {
159  if (!isset($this->valid)) {
160  $this->valid = $this->validate();
161  }
162  return $this->valid;
163  }
164 
168  private function validate() {
169  // check required files.
170  $have_req_files = true;
171  foreach ($this->requiredFiles as $file) {
172  if (!is_readable($this->path . $file)) {
173  $have_req_files = false;
174  $this->errorMsg =
175  _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:MissingFile', array($file));
176  return false;
177  }
178  }
179 
180  // check required files
181  if (!$have_req_files) {
182  return $this->valid = false;
183  }
184 
185  // check for valid manifest.
186  if (!$this->loadManifest()) {
187  return false;
188  }
189 
190  if (!$this->isNamedCorrectly()) {
191  return false;
192  }
193 
194  // can't require or conflict with yourself or something you provide.
195  // make sure provides are all valid.
196  if (!$this->hasSaneDependencies()) {
197  return false;
198  }
199 
200  if (!$this->hasReadableConfigFile()) {
201  return false;
202  }
203 
204  return true;
205  }
206 
213  private function hasReadableConfigFile() {
214  $file = "{$this->path}/" . self::STATIC_CONFIG_FILENAME;
215  if (!is_file($file)) {
216  return true;
217  }
218 
219  if (is_readable($file)) {
220  return true;
221  }
222 
223  $this->errorMsg =
224  _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:UnreadableConfig');
225  return false;
226  }
227 
234  private function isNamedCorrectly() {
235  $manifest = $this->getManifest();
236  if ($manifest) {
237  $required_id = $manifest->getID();
238  if (!empty($required_id) && ($required_id !== $this->id)) {
239  $this->errorMsg =
240  _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:InvalidId', array($required_id));
241  return false;
242  }
243  }
244  return true;
245  }
246 
258  private function hasSaneDependencies() {
259  // protection against plugins with no manifest file
260  if (!$this->getManifest()) {
261  return false;
262  }
263 
264  // Note: $conflicts and $requires are not unused. They're called dynamically
265  $conflicts = $this->getManifest()->getConflicts();
266  $requires = $this->getManifest()->getRequires();
267  $provides = $this->getManifest()->getProvides();
268 
269  foreach ($provides as $provide) {
270  // only valid provide types
271  if (!in_array($provide['type'], $this->providesSupportedTypes)) {
272  $this->errorMsg =
273  _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:InvalidProvides', array($provide['type']));
274  return false;
275  }
276 
277  // doesn't conflict or require any of its provides
278  $name = $provide['name'];
279  foreach (array('conflicts', 'requires') as $dep_type) {
280  foreach (${$dep_type} as $dep) {
281  if (!in_array($dep['type'], $this->depsSupportedTypes)) {
282  $this->errorMsg =
283  _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:InvalidDependency', array($dep['type']));
284  return false;
285  }
286 
287  // make sure nothing is providing something it conflicts or requires.
288  if (isset($dep['name']) && $dep['name'] == $name) {
289  $version_compare = version_compare($provide['version'], $dep['version'], $dep['comparison']);
290 
291  if ($version_compare) {
292  $this->errorMsg =
293  _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:CircularDep',
294  array($dep['type'], $dep['name'], $this->id));
295 
296  return false;
297  }
298  }
299  }
300  }
301  }
302 
303  return true;
304  }
305 
306 
307  /************
308  * Manifest *
309  ************/
310 
316  public function getManifest() {
317  if (!$this->manifest) {
318  if (!$this->loadManifest()) {
319  return false;
320  }
321  }
322 
323  return $this->manifest;
324  }
325 
332  private function loadManifest() {
333  $file = $this->path . 'manifest.xml';
334 
335  try {
336  $this->manifest = new \ElggPluginManifest($file, $this->id);
337  } catch (Exception $e) {
338  $this->errorMsg = $e->getMessage();
339  return false;
340  }
341 
342  if ($this->manifest instanceof \ElggPluginManifest) {
343  return true;
344  }
345 
346  $this->errorMsg = _elgg_services()->translator->translate('unknown_error');
347  return false;
348  }
349 
350  /****************
351  * Readme Files *
352  ***************/
353 
359  public function getTextFilenames() {
360  return $this->textFiles;
361  }
362 
363  /***********************
364  * Dependencies system *
365  ***********************/
366 
382  public function checkDependencies($full_report = false) {
383  // Note: $conflicts and $requires are not unused. They're called dynamically
384  $requires = $this->getManifest()->getRequires();
385  $conflicts = $this->getManifest()->getConflicts();
386 
387  $enabled_plugins = elgg_get_plugins('active');
388  $this_id = $this->getID();
389  $report = array();
390 
391  // first, check if any active plugin conflicts with us.
392  foreach ($enabled_plugins as $plugin) {
393  $temp_conflicts = array();
394  $temp_manifest = $plugin->getManifest();
395  if ($temp_manifest instanceof \ElggPluginManifest) {
396  $temp_conflicts = $plugin->getManifest()->getConflicts();
397  }
398  foreach ($temp_conflicts as $conflict) {
399  if ($conflict['type'] == 'plugin' && $conflict['name'] == $this_id) {
400  $result = $this->checkDepPlugin($conflict, $enabled_plugins, false);
401 
402  // rewrite the conflict to show the originating plugin
403  $conflict['name'] = $plugin->getManifest()->getName();
404 
405  if (!$full_report && !$result['status']) {
406  $css_id = preg_replace('/[^a-z0-9-]/i', '-', $plugin->getManifest()->getID());
407  $link = elgg_view('output/url', [
408  'text' => $plugin->getManifest()->getName(),
409  'href' => "#$css_id",
410  ]);
411 
412  $key = 'ElggPluginPackage:InvalidPlugin:ConflictsWithPlugin';
413  $this->errorMsg = _elgg_services()->translator->translate($key, [$link]);
414  return $result['status'];
415  } else {
416  $report[] = array(
417  'type' => 'conflicted',
418  'dep' => $conflict,
419  'status' => $result['status'],
420  'value' => $this->getManifest()->getVersion()
421  );
422  }
423  }
424  }
425  }
426 
427  $check_types = array('requires', 'conflicts');
428 
429  if ($full_report) {
430  // Note: $suggests is not unused. It's called dynamically
431  $suggests = $this->getManifest()->getSuggests();
432  $check_types[] = 'suggests';
433  }
434 
435  foreach ($check_types as $dep_type) {
436  $inverse = ($dep_type == 'conflicts') ? true : false;
437 
438  foreach (${$dep_type} as $dep) {
439  switch ($dep['type']) {
440  case 'elgg_release':
441  $result = $this->checkDepElgg($dep, elgg_get_version(true), $inverse);
442  break;
443 
444  case 'plugin':
445  $result = $this->checkDepPlugin($dep, $enabled_plugins, $inverse);
446  break;
447 
448  case 'priority':
449  $result = $this->checkDepPriority($dep, $enabled_plugins, $inverse);
450  break;
451 
452  case 'php_version':
453  $result = $this->checkDepPhpVersion($dep, $inverse);
454  break;
455 
456  case 'php_extension':
457  $result = $this->checkDepPhpExtension($dep, $inverse);
458  break;
459 
460  case 'php_ini':
461  $result = $this->checkDepPhpIni($dep, $inverse);
462  break;
463 
464  default:
465  $result = null;//skip further check
466  break;
467  }
468 
469  if ($result !== null) {
470  // unless we're doing a full report, break as soon as we fail.
471  if (!$full_report && !$result['status']) {
472  $this->errorMsg = "Missing dependencies.";
473  return $result['status'];
474  } else {
475  // build report element and comment
476  $report[] = array(
477  'type' => $dep_type,
478  'dep' => $dep,
479  'status' => $result['status'],
480  'value' => $result['value']
481  );
482  }
483  }
484  }
485  }
486 
487  if ($full_report) {
488  // add provides to full report
489  $provides = $this->getManifest()->getProvides();
490 
491  foreach ($provides as $provide) {
492  $report[] = array(
493  'type' => 'provides',
494  'dep' => $provide,
495  'status' => true,
496  'value' => ''
497  );
498  }
499 
500  return $report;
501  }
502 
503  return true;
504  }
505 
514  private function checkDepPlugin(array $dep, array $plugins, $inverse = false) {
515  $r = _elgg_check_plugins_provides('plugin', $dep['name'], $dep['version'], $dep['comparison']);
516 
517  if ($inverse) {
518  $r['status'] = !$r['status'];
519  }
520 
521  return $r;
522  }
523 
532  private function checkDepPriority(array $dep, array $plugins, $inverse = false) {
533  // grab the \ElggPlugin using this package.
534  $plugin_package = elgg_get_plugin_from_id($this->getID());
535  if (!$plugin_package) {
536  return array(
537  'status' => true,
538  'value' => 'uninstalled'
539  );
540  }
541 
542  $test_plugin = elgg_get_plugin_from_id($dep['plugin']);
543 
544  // If this isn't a plugin or the plugin isn't installed or active
545  // priority doesn't matter. Use requires to check if a plugin is active.
546  if (!$test_plugin || !$test_plugin->isActive()) {
547  return array(
548  'status' => true,
549  'value' => 'uninstalled'
550  );
551  }
552 
553  $plugin_priority = $plugin_package->getPriority();
554  $test_plugin_priority = $test_plugin->getPriority();
555 
556  switch ($dep['priority']) {
557  case 'before':
558  $status = $plugin_priority < $test_plugin_priority;
559  break;
560 
561  case 'after':
562  $status = $plugin_priority > $test_plugin_priority;
563  break;
564 
565  default;
566  $status = false;
567  }
568 
569  // get the current value
570  if ($plugin_priority < $test_plugin_priority) {
571  $value = 'before';
572  } else {
573  $value = 'after';
574  }
575 
576  if ($inverse) {
577  $status = !$status;
578  }
579 
580  return array(
581  'status' => $status,
582  'value' => $value
583  );
584  }
585 
594  private function checkDepElgg(array $dep, $elgg_version, $inverse = false) {
595  $status = version_compare($elgg_version, $dep['version'], $dep['comparison']);
596 
597  if ($inverse) {
598  $status = !$status;
599  }
600 
601  return array(
602  'status' => $status,
603  'value' => $elgg_version
604  );
605  }
606 
614  private function checkDepPhpVersion(array $dep, $inverse = false) {
615  $php_version = phpversion();
616  $status = version_compare($php_version, $dep['version'], $dep['comparison']);
617 
618  if ($inverse) {
619  $status = !$status;
620  }
621 
622  return array(
623  'status' => $status,
624  'value' => $php_version
625  );
626  }
627 
640  private function checkDepPhpExtension(array $dep, $inverse = false) {
641  $name = $dep['name'];
642  $version = $dep['version'];
643  $comparison = $dep['comparison'];
644 
645  // not enabled.
646  $status = extension_loaded($name);
647 
648  // enabled. check version.
649  $ext_version = phpversion($name);
650 
651  if ($status) {
652  // some extensions (like gd) don't provide versions. neat.
653  // don't check version info and return a lie.
654  if ($ext_version && $version) {
655  $status = version_compare($ext_version, $version, $comparison);
656  }
657 
658  if (!$ext_version) {
659  $ext_version = '???';
660  }
661  }
662 
663  // some php extensions can be emulated, so check provides.
664  if ($status == false) {
665  $provides = _elgg_check_plugins_provides('php_extension', $name, $version, $comparison);
666  $status = $provides['status'];
667  $ext_version = $provides['value'];
668  }
669 
670  if ($inverse) {
671  $status = !$status;
672  }
673 
674  return array(
675  'status' => $status,
676  'value' => $ext_version
677  );
678  }
679 
687  private function checkDepPhpIni($dep, $inverse = false) {
688  $name = $dep['name'];
689  $value = $dep['value'];
690  $comparison = $dep['comparison'];
691 
692  // ini_get() normalizes truthy values to 1 but falsey values to 0 or ''.
693  // version_compare() considers '' < 0, so normalize '' to 0.
694  // \ElggPluginManifest normalizes all bool values and '' to 1 or 0.
695  $setting = ini_get($name);
696 
697  if ($setting === '') {
698  $setting = 0;
699  }
700 
701  $status = version_compare($setting, $value, $comparison);
702 
703  if ($inverse) {
704  $status = !$status;
705  }
706 
707  return array(
708  'status' => $status,
709  'value' => $setting
710  );
711  }
712 
718  public function getID() {
719  return $this->id;
720  }
721 
727  public function getError() {
728  return $this->errorMsg;
729  }
730 }
$r
$plugin
if(!array_key_exists($filename, $text_files)) $file
$version
if($guid==elgg_get_logged_in_user_guid()) $name
Definition: delete.php:21
_elgg_check_plugins_provides($type, $name, $version=null, $comparison= 'ge')
Checks if a plugin is currently providing $type and $name, and optionally checking a version...
Definition: plugins.php:182
$e
Definition: metadata.php:12
getManifest()
Returns a parsed manifest file.
checkDependencies($full_report=false)
Returns if the Elgg system meets the plugin&#39;s dependency requirements.
sanitise_filepath($path, $append_slash=true)
Sanitise file paths ensuring that they begin and end with slashes etc.
Definition: elgglib.php:411
$value
Definition: longtext.php:42
$link
Definition: container.php:14
$report
$CONFIG path
The full path where Elgg is installed.
Definition: config.php:16
getTextFilenames()
Returns an array of present and readable text files.
$key
Definition: summary.php:34
elgg_view($view, $vars=array(), $ignore1=false, $ignore2=false, $viewtype= '')
Return a parsed view.
Definition: views.php:336
$css_id
Definition: full.php:30
_elgg_services(\Elgg\Di\ServiceProvider $services=null)
Get the global service provider.
Definition: autoloader.php:17
elgg_get_plugins($status= 'active', $site_guid=null)
Returns an ordered list of plugins.
Definition: plugins.php:132
__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:1071
getError()
Returns the last error message.
getID()
Returns the Plugin ID.
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:82