123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695 |
- <?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_version', 'php_extension', 'php_ini', 'plugin', 'priority',
- );
- /**
- * An invalid plugin error.
- */
- private $errorMsg = '';
- /**
- * 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()?
- *
- * @throws PluginException
- */
- public function __construct($plugin, $validate = true) {
- $plugin_path = _elgg_services()->config->getPluginsPath();
- // @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_services()->translator->translate('PluginException:InvalidID', array($plugin)));
- }
- $path = "{$plugin_path}$plugin/";
- $id = $plugin;
- }
- if (!is_dir($path)) {
- throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidPath', array($path)));
- }
- $this->path = $path;
- $this->id = $id;
- if ($validate && !$this->isValid()) {
- if ($this->errorMsg) {
- throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidPlugin:Details',
- array($plugin, $this->errorMsg)));
- } else {
- throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidPlugin', array($plugin)));
- }
- }
- }
- /********************************
- * 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)) {
- $this->valid = $this->validate();
- }
- return $this->valid;
- }
- /**
- * @return bool
- */
- private function validate() {
- // 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_services()->translator->translate('ElggPluginPackage:InvalidPlugin:MissingFile', array($file));
- return false;
- }
- }
- // check required files
- if (!$have_req_files) {
- return $this->valid = false;
- }
- // check for valid manifest.
- if (!$this->loadManifest()) {
- return false;
- }
- if (!$this->isNamedCorrectly()) {
- return false;
- }
- // can't require or conflict with yourself or something you provide.
- // make sure provides are all valid.
- if (!$this->hasSaneDependencies()) {
- return false;
- }
- return true;
- }
- /**
- * Check that the plugin is installed in the directory with name specified
- * in the manifest's "id" element.
- *
- * @return bool
- */
- private function isNamedCorrectly() {
- $manifest = $this->getManifest();
- if ($manifest) {
- $required_id = $manifest->getID();
- if (!empty($required_id) && ($required_id !== $this->id)) {
- $this->errorMsg =
- _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:InvalidId', array($required_id));
- return false;
- }
- }
- return 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 hasSaneDependencies() {
- // protection against plugins with no manifest file
- if (!$this->getManifest()) {
- return false;
- }
- // Note: $conflicts and $requires are not unused. They're called dynamically
- $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_services()->translator->translate('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_services()->translator->translate('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_services()->translator->translate('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;
- }
- $this->errorMsg = _elgg_services()->translator->translate('unknown_error');
- 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) {
- // Note: $conflicts and $requires are not unused. They're called dynamically
- $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) {
- // Note: $suggests is not unused. It's called dynamically
- $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':
- elgg_deprecated_notice("elgg_version in manifest.xml files is deprecated. Use elgg_release", 1.9);
- $result = $this->checkDepElgg($dep, elgg_get_version(), $inverse);
- break;
- case 'elgg_release':
- $result = $this->checkDepElgg($dep, elgg_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_version':
- $result = $this->checkDepPhpVersion($dep, $inverse);
- break;
-
- case 'php_extension':
- $result = $this->checkDepPhpExtension($dep, $inverse);
- break;
- case 'php_ini':
- $result = $this->checkDepPhpIni($dep, $inverse);
- break;
-
- default:
- $result = null;//skip further check
- break;
- }
- if ($result !== null) {
- // 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 $php_version meets the requirement by $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 checkDepPhpVersion(array $dep, $inverse = false) {
- $php_version = phpversion();
- $status = version_compare($php_version, $dep['version'], $dep['comparison']);
- if ($inverse) {
- $status = !$status;
- }
- return array(
- 'status' => $status,
- 'value' => $php_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;
- }
- }
|