diff options
Diffstat (limited to 'engine/classes/ElggPluginPackage.php')
-rw-r--r-- | engine/classes/ElggPluginPackage.php | 636 |
1 files changed, 636 insertions, 0 deletions
diff --git a/engine/classes/ElggPluginPackage.php b/engine/classes/ElggPluginPackage.php new file mode 100644 index 000000000..2dc4bdb3d --- /dev/null +++ b/engine/classes/ElggPluginPackage.php @@ -0,0 +1,636 @@ +<?php +/** + * Manages plugin packages under mod. + * + * @todo This should eventually be merged into ElggPlugin. + * Currently ElggPlugin objects are only used to get and save + * plugin settings and user settings, so not every plugin + * has an ElggPlugin object. It's not implemented in ElggPlugin + * right now because of conflicts with at least the constructor, + * enable(), disable(), and private settings. + * + * Around 1.9 or so we should each plugin over to using + * ElggPlugin and merge ElggPluginPackage and ElggPlugin. + * + * @package Elgg.Core + * @subpackage Plugins + * @since 1.8 + */ +class ElggPluginPackage { + + /** + * The required files in the package + * + * @var array + */ + private $requiredFiles = array( + 'start.php', 'manifest.xml' + ); + + /** + * The optional files that can be read and served through the markdown page handler + * @var array + */ + private $textFiles = array( + 'README.txt', 'CHANGES.txt', + 'INSTALL.txt', 'COPYRIGHT.txt', 'LICENSE.txt', + + 'README', 'README.md', 'README.markdown' + ); + + /** + * Valid types for provides. + * + * @var array + */ + private $providesSupportedTypes = array( + 'plugin', 'php_extension' + ); + + /** + * The type of requires/conflicts supported + * + * @var array + */ + private $depsSupportedTypes = array( + 'elgg_version', 'elgg_release', 'php_extension', 'php_ini', 'plugin', 'priority', + ); + + /** + * An invalid plugin error. + */ + private $errorMsg = ''; + + /** + * Any dependencies messages + */ + private $depsMsgs = array(); + + /** + * The plugin's manifest object + * + * @var ElggPluginManifest + */ + protected $manifest; + + /** + * The plugin's full path + * + * @var string + */ + protected $path; + + /** + * Is the plugin valid? + * + * @var mixed Bool after validation check, null before. + */ + protected $valid = null; + + /** + * The plugin ID (dir name) + * + * @var string + */ + protected $id; + + /** + * Load a plugin package from mod/$id or by full path. + * + * @param string $plugin The ID (directory name) or full path of the plugin. + * @param bool $validate Automatically run isValid()? + * + * @return true + * @throws PluginException + */ + public function __construct($plugin, $validate = true) { + $plugin_path = elgg_get_plugins_path(); + // @todo wanted to avoid another is_dir() call here. + // should do some profiling to see how much it affects + if (strpos($plugin, $plugin_path) === 0 || is_dir($plugin)) { + // this is a path + $path = sanitise_filepath($plugin); + + // the id is the last element of the array + $path_array = explode('/', trim($path, '/')); + $id = array_pop($path_array); + } else { + // this is a plugin id + // strict plugin names + if (preg_match('/[^a-z0-9\.\-_]/i', $plugin)) { + throw new PluginException(elgg_echo('PluginException:InvalidID', array($plugin))); + } + + $path = "{$plugin_path}$plugin/"; + $id = $plugin; + } + + if (!is_dir($path)) { + throw new PluginException(elgg_echo('PluginException:InvalidPath', array($path))); + } + + $this->path = $path; + $this->id = $id; + + if ($validate && !$this->isValid()) { + if ($this->errorMsg) { + throw new PluginException(elgg_echo('PluginException:InvalidPlugin:Details', + array($plugin, $this->errorMsg))); + } else { + throw new PluginException(elgg_echo('PluginException:InvalidPlugin', array($plugin))); + } + } + + return true; + } + + /******************************** + * Validation and sanity checks * + ********************************/ + + /** + * Checks if this is a valid Elgg plugin. + * + * Checks for requires files as defined at the start of this + * class. Will check require manifest fields via ElggPluginManifest + * for Elgg 1.8 plugins. + * + * @note This doesn't check dependencies or conflicts. + * Use {@link ElggPluginPackage::canActivate()} or + * {@link ElggPluginPackage::checkDependencies()} for that. + * + * @return bool + */ + public function isValid() { + if (isset($this->valid)) { + return $this->valid; + } + + // check required files. + $have_req_files = true; + foreach ($this->requiredFiles as $file) { + if (!is_readable($this->path . $file)) { + $have_req_files = false; + $this->errorMsg = + elgg_echo('ElggPluginPackage:InvalidPlugin:MissingFile', array($file)); + break; + } + } + + // check required files + if (!$have_req_files) { + return $this->valid = false; + } + + // check for valid manifest. + if (!$this->loadManifest()) { + return $this->valid = false; + } + + // can't require or conflict with yourself or something you provide. + // make sure provides are all valid. + if (!$this->isSaneDeps()) { + return $this->valid = false; + } + + return $this->valid = true; + } + + /** + * Check the plugin doesn't require or conflict with itself + * or something provides. Also check that it only list + * valid provides. Deps are checked in checkDependencies() + * + * @note Plugins always provide themselves. + * + * @todo Don't let them require and conflict the same thing + * + * @return bool + */ + private function isSaneDeps() { + // protection against plugins with no manifest file + if (!$this->getManifest()) { + return false; + } + + $conflicts = $this->getManifest()->getConflicts(); + $requires = $this->getManifest()->getRequires(); + $provides = $this->getManifest()->getProvides(); + + foreach ($provides as $provide) { + // only valid provide types + if (!in_array($provide['type'], $this->providesSupportedTypes)) { + $this->errorMsg = + elgg_echo('ElggPluginPackage:InvalidPlugin:InvalidProvides', array($provide['type'])); + return false; + } + + // doesn't conflict or require any of its provides + $name = $provide['name']; + foreach (array('conflicts', 'requires') as $dep_type) { + foreach (${$dep_type} as $dep) { + if (!in_array($dep['type'], $this->depsSupportedTypes)) { + $this->errorMsg = + elgg_echo('ElggPluginPackage:InvalidPlugin:InvalidDependency', array($dep['type'])); + return false; + } + + // make sure nothing is providing something it conflicts or requires. + if (isset($dep['name']) && $dep['name'] == $name) { + $version_compare = version_compare($provide['version'], $dep['version'], $dep['comparison']); + + if ($version_compare) { + $this->errorMsg = + elgg_echo('ElggPluginPackage:InvalidPlugin:CircularDep', + array($dep['type'], $dep['name'], $this->id)); + + return false; + } + } + } + } + } + + return true; + } + + + /************ + * Manifest * + ************/ + + /** + * Returns a parsed manifest file. + * + * @return ElggPluginManifest + */ + public function getManifest() { + if (!$this->manifest) { + if (!$this->loadManifest()) { + return false; + } + } + + return $this->manifest; + } + + /** + * Loads the manifest into this->manifest as an + * ElggPluginManifest object. + * + * @return bool + */ + private function loadManifest() { + $file = $this->path . 'manifest.xml'; + + try { + $this->manifest = new ElggPluginManifest($file, $this->id); + } catch (Exception $e) { + $this->errorMsg = $e->getMessage(); + return false; + } + + if ($this->manifest instanceof ElggPluginManifest) { + return true; + } + + return false; + } + + /**************** + * Readme Files * + ***************/ + + /** + * Returns an array of present and readable text files + * + * @return array + */ + public function getTextFilenames() { + return $this->textFiles; + } + + /*********************** + * Dependencies system * + ***********************/ + + /** + * Returns if the Elgg system meets the plugin's dependency + * requirements. This includes both requires and conflicts. + * + * Full reports can be requested. The results are returned + * as an array of arrays in the form array( + * 'type' => requires|conflicts, + * 'dep' => array( dependency array ), + * 'status' => bool if depedency is met, + * 'comment' => optional comment to display to the user. + * ) + * + * @param bool $full_report Return a full report. + * @return bool|array + */ + public function checkDependencies($full_report = false) { + $requires = $this->getManifest()->getRequires(); + $conflicts = $this->getManifest()->getConflicts(); + $enabled_plugins = elgg_get_plugins('active'); + $this_id = $this->getID(); + $report = array(); + + // first, check if any active plugin conflicts with us. + foreach ($enabled_plugins as $plugin) { + $temp_conflicts = array(); + $temp_manifest = $plugin->getManifest(); + if ($temp_manifest instanceof ElggPluginManifest) { + $temp_conflicts = $plugin->getManifest()->getConflicts(); + } + foreach ($temp_conflicts as $conflict) { + if ($conflict['type'] == 'plugin' && $conflict['name'] == $this_id) { + $result = $this->checkDepPlugin($conflict, $enabled_plugins, false); + + // rewrite the conflict to show the originating plugin + $conflict['name'] = $plugin->getManifest()->getName(); + + if (!$full_report && !$result['status']) { + $this->errorMsg = "Conflicts with plugin \"{$plugin->getManifest()->getName()}\"."; + return $result['status']; + } else { + $report[] = array( + 'type' => 'conflicted', + 'dep' => $conflict, + 'status' => $result['status'], + 'value' => $this->getManifest()->getVersion() + ); + } + } + } + } + + $check_types = array('requires', 'conflicts'); + + if ($full_report) { + $suggests = $this->getManifest()->getSuggests(); + $check_types[] = 'suggests'; + } + + foreach ($check_types as $dep_type) { + $inverse = ($dep_type == 'conflicts') ? true : false; + + foreach (${$dep_type} as $dep) { + switch ($dep['type']) { + case 'elgg_version': + $result = $this->checkDepElgg($dep, get_version(), $inverse); + break; + + case 'elgg_release': + $result = $this->checkDepElgg($dep, get_version(true), $inverse); + break; + + case 'plugin': + $result = $this->checkDepPlugin($dep, $enabled_plugins, $inverse); + break; + + case 'priority': + $result = $this->checkDepPriority($dep, $enabled_plugins, $inverse); + break; + + case 'php_extension': + $result = $this->checkDepPhpExtension($dep, $inverse); + break; + + case 'php_ini': + $result = $this->checkDepPhpIni($dep, $inverse); + break; + } + + // unless we're doing a full report, break as soon as we fail. + if (!$full_report && !$result['status']) { + $this->errorMsg = "Missing dependencies."; + return $result['status']; + } else { + // build report element and comment + $report[] = array( + 'type' => $dep_type, + 'dep' => $dep, + 'status' => $result['status'], + 'value' => $result['value'] + ); + } + } + } + + if ($full_report) { + // add provides to full report + $provides = $this->getManifest()->getProvides(); + + foreach ($provides as $provide) { + $report[] = array( + 'type' => 'provides', + 'dep' => $provide, + 'status' => true, + 'value' => '' + ); + } + + return $report; + } + + return true; + } + + /** + * Checks if $plugins meets the requirement by $dep. + * + * @param array $dep An Elgg manifest.xml deps array + * @param array $plugins A list of plugins as returned by elgg_get_plugins(); + * @param bool $inverse Inverse the results to use as a conflicts. + * @return bool + */ + private function checkDepPlugin(array $dep, array $plugins, $inverse = false) { + $r = elgg_check_plugins_provides('plugin', $dep['name'], $dep['version'], $dep['comparison']); + + if ($inverse) { + $r['status'] = !$r['status']; + } + + return $r; + } + + /** + * Checks if $plugins meets the requirement by $dep. + * + * @param array $dep An Elgg manifest.xml deps array + * @param array $plugins A list of plugins as returned by elgg_get_plugins(); + * @param bool $inverse Inverse the results to use as a conflicts. + * @return bool + */ + private function checkDepPriority(array $dep, array $plugins, $inverse = false) { + // grab the ElggPlugin using this package. + $plugin_package = elgg_get_plugin_from_id($this->getID()); + $plugin_priority = $plugin_package->getPriority(); + $test_plugin = elgg_get_plugin_from_id($dep['plugin']); + + // If this isn't a plugin or the plugin isn't installed or active + // priority doesn't matter. Use requires to check if a plugin is active. + if (!$plugin_package || !$test_plugin || !$test_plugin->isActive()) { + return array( + 'status' => true, + 'value' => 'uninstalled' + ); + } + + $test_plugin_priority = $test_plugin->getPriority(); + + switch ($dep['priority']) { + case 'before': + $status = $plugin_priority < $test_plugin_priority; + break; + + case 'after': + $status = $plugin_priority > $test_plugin_priority; + break; + + default; + $status = false; + } + + // get the current value + if ($plugin_priority < $test_plugin_priority) { + $value = 'before'; + } else { + $value = 'after'; + } + + if ($inverse) { + $status = !$status; + } + + return array( + 'status' => $status, + 'value' => $value + ); + } + + /** + * Checks if $elgg_version meets the requirement by $dep. + * + * @param array $dep An Elgg manifest.xml deps array + * @param array $elgg_version An Elgg version (either YYYYMMDDXX or X.Y.Z) + * @param bool $inverse Inverse the result to use as a conflicts. + * @return bool + */ + private function checkDepElgg(array $dep, $elgg_version, $inverse = false) { + $status = version_compare($elgg_version, $dep['version'], $dep['comparison']); + + if ($inverse) { + $status = !$status; + } + + return array( + 'status' => $status, + 'value' => $elgg_version + ); + } + + /** + * Checks if the PHP extension in $dep is loaded. + * + * @todo Can this be merged with the plugin checker? + * + * @param array $dep An Elgg manifest.xml deps array + * @param bool $inverse Inverse the result to use as a conflicts. + * @return array An array in the form array( + * 'status' => bool + * 'value' => string The version provided + * ) + */ + private function checkDepPhpExtension(array $dep, $inverse = false) { + $name = $dep['name']; + $version = $dep['version']; + $comparison = $dep['comparison']; + + // not enabled. + $status = extension_loaded($name); + + // enabled. check version. + $ext_version = phpversion($name); + + if ($status) { + // some extensions (like gd) don't provide versions. neat. + // don't check version info and return a lie. + if ($ext_version && $version) { + $status = version_compare($ext_version, $version, $comparison); + } + + if (!$ext_version) { + $ext_version = '???'; + } + } + + // some php extensions can be emulated, so check provides. + if ($status == false) { + $provides = elgg_check_plugins_provides('php_extension', $name, $version, $comparison); + $status = $provides['status']; + $ext_version = $provides['value']; + } + + if ($inverse) { + $status = !$status; + } + + return array( + 'status' => $status, + 'value' => $ext_version + ); + } + + /** + * Check if the PHP ini setting satisfies $dep. + * + * @param array $dep An Elgg manifest.xml deps array + * @param bool $inverse Inverse the result to use as a conflicts. + * @return bool + */ + private function checkDepPhpIni($dep, $inverse = false) { + $name = $dep['name']; + $value = $dep['value']; + $comparison = $dep['comparison']; + + // ini_get() normalizes truthy values to 1 but falsey values to 0 or ''. + // version_compare() considers '' < 0, so normalize '' to 0. + // ElggPluginManifest normalizes all bool values and '' to 1 or 0. + $setting = ini_get($name); + + if ($setting === '') { + $setting = 0; + } + + $status = version_compare($setting, $value, $comparison); + + if ($inverse) { + $status = !$status; + } + + return array( + 'status' => $status, + 'value' => $setting + ); + } + + /** + * Returns the Plugin ID + * + * @return string + */ + public function getID() { + return $this->id; + } + + /** + * Returns the last error message. + * + * @return string + */ + public function getError() { + return $this->errorMsg; + } +} |