diff options
Diffstat (limited to 'engine/classes/ElggPluginPackage.php')
| -rw-r--r-- | engine/classes/ElggPluginPackage.php | 640 | 
1 files changed, 640 insertions, 0 deletions
| diff --git a/engine/classes/ElggPluginPackage.php b/engine/classes/ElggPluginPackage.php new file mode 100644 index 000000000..37eb4bf4d --- /dev/null +++ b/engine/classes/ElggPluginPackage.php @@ -0,0 +1,640 @@ +<?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()? +	 * +	 * @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; +		} + +		// 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_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; +		} + +		$this->errorMsg = elgg_echo('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': +						$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; +	} +} | 
