<?php
/**
 * Elgg plugins library
 * Contains functions for managing plugins
 *
 * @package Elgg.Core
 * @subpackage Plugins
 */

/// Cache enabled plugins per page
$ENABLED_PLUGINS_CACHE = NULL;

/**
 * Returns a list of plugins to load, in the order that they should be loaded.
 *
 * @return array List of plugins
 */
function get_plugin_list() {
	global $CONFIG;

	if (!empty($CONFIG->pluginlistcache)) {
		return $CONFIG->pluginlistcache;
	}

	if ($site = get_entity($CONFIG->site_guid)) {
		$pluginorder = $site->pluginorder;
		if (!empty($pluginorder)) {
			$plugins = unserialize($pluginorder);

			$CONFIG->pluginlistcache = $plugins;
			return $plugins;
		} else {
			// this only runs on install, otherwise uses serialized plugin order
			$plugins = array();

			if ($handle = opendir($CONFIG->pluginspath)) {
				while ($mod = readdir($handle)) {
					// must be directory and not begin with a .
					if (substr($mod, 0, 1) !== '.' && is_dir($CONFIG->pluginspath . "/" . $mod)) {
						$plugins[] = $mod;
					}
				}
			}

			sort($plugins);

			$CONFIG->pluginlistcache = $plugins;
			return $plugins;
		}
	}

	return false;
}

/**
 * Regenerates the list of known plugins and saves it to the current site
 *
 * Important: You should regenerate simplecache and the viewpath cache after executing this function
 * otherwise you may experience view display artifacts. Do this with the following code:
 *
 * 		elgg_view_regenerate_simplecache();
 *		elgg_filepath_cache_reset();
 *
 * @param array $pluginorder Optionally, a list of existing plugins and their orders
 *
 * @return array The new list of plugins and their orders
 */
function regenerate_plugin_list($pluginorder = FALSE) {
	global $CONFIG;

	$CONFIG->pluginlistcache = NULL;

	if ($site = get_entity($CONFIG->site_guid)) {
		if (empty($pluginorder)) {
			$pluginorder = $site->pluginorder;
			$pluginorder = unserialize($pluginorder);
		} else {
			ksort($pluginorder);
		}

		if (empty($pluginorder)) {
			$pluginorder = array();
		}

		$max = 0;
		if (sizeof($pluginorder)) {
			foreach ($pluginorder as $key => $plugin) {
				if (is_dir($CONFIG->pluginspath . "/" . $plugin)) {
					if ($key > $max) {
						$max = $key;
					}
				} else {
					unset($pluginorder[$key]);
				}
			}
		}
		// Add new plugins to the end
		if ($handle = opendir($CONFIG->pluginspath)) {
			while ($mod = readdir($handle)) {
				// must be directory and not begin with a .
				if (substr($mod, 0, 1) !== '.' && is_dir($CONFIG->pluginspath . "/" . $mod)) {
					if (!in_array($mod, $pluginorder)) {
						$max = $max + 10;
						$pluginorder[$max] = $mod;
					}
				}
			}
		}

		ksort($pluginorder);

		// Now reorder the keys ..
		$key = 10;
		$plugins = array();
		if (sizeof($pluginorder)) {
			foreach ($pluginorder as $plugin) {
				$plugins[$key] = $plugin;
				$key = $key + 10;
			}
		}

		$plugins = serialize($plugins);

		$site->pluginorder = $plugins;

		return $plugins;
	}

	return FALSE;
}


/**
 * For now, loads plugins directly
 *
 * @todo Add proper plugin handler that launches plugins in an
 * admin-defined order and activates them on admin request
 *
 * @return void
 */
function load_plugins() {
	global $CONFIG;

	if (!empty($CONFIG->pluginspath)) {
		// See if we have cached values for things
		$cached_view_paths = elgg_filepath_cache_load();
		if ($cached_view_paths) {
			$CONFIG->views = unserialize($cached_view_paths);
		}

		// temporary disable all plugins if there is a file called 'disabled' in the plugin dir
		if (file_exists($CONFIG->pluginspath . "disabled")) {
			return;
		}

		$plugins = get_plugin_list();

		if (sizeof($plugins)) {
			foreach ($plugins as $mod) {
				if (is_plugin_enabled($mod)) {
					if (file_exists($CONFIG->pluginspath . $mod)) {
						if (!include($CONFIG->pluginspath . $mod . "/start.php")) {
							// automatically disable the bad plugin
							disable_plugin($mod);

							// register error rather than rendering the site unusable with exception
							register_error(sprintf(elgg_echo('PluginException:MisconfiguredPlugin'), $mod));

							// continue loading remaining plugins
							continue;
						}

						if (!$cached_view_paths) {
							$view_dir = $CONFIG->pluginspath . $mod . '/views/';

							if (is_dir($view_dir) && ($handle = opendir($view_dir))) {
								while (FALSE !== ($view_type = readdir($handle))) {
									$view_type_dir = $view_dir . $view_type;

									if ('.' !== substr($view_type, 0, 1) && is_dir($view_type_dir)) {
										if (autoregister_views('', $view_type_dir, $view_dir, $view_type)) {
											// add the valid view type.
											if (!in_array($view_type, $CONFIG->view_types)) {
												$CONFIG->view_types[] = $view_type;
											}
										}
									}
								}
							}
						}

						if (is_dir($CONFIG->pluginspath . $mod . "/languages")) {
							register_translations($CONFIG->pluginspath . $mod . "/languages/");
						}

						if (is_dir($CONFIG->pluginspath . "$mod/classes")) {
							elgg_register_classes($CONFIG->pluginspath . "$mod/classes");
						}
					}
				}
			}
		}

		// Cache results
		if (!$cached_view_paths) {
			elgg_filepath_cache_save(serialize($CONFIG->views));
		}
	}
}

/**
 * Get the name of the most recent plugin to be called in the
 * call stack (or the plugin that owns the current page, if any).
 *
 * i.e., if the last plugin was in /mod/foobar/, get_plugin_name would return foo_bar.
 *
 * @param boolean $mainfilename If set to true, this will instead determine the
 *                              context from the main script filename called by
 *                              the browser. Default = false.
 *
 * @return string|false Plugin name, or false if no plugin name was called
 */
function get_plugin_name($mainfilename = false) {
	if (!$mainfilename) {
		if ($backtrace = debug_backtrace()) {
			foreach ($backtrace as $step) {
				$file = $step['file'];
				$file = str_replace("\\", "/", $file);
				$file = str_replace("//", "/", $file);
				if (preg_match("/mod\/([a-zA-Z0-9\-\_]*)\/start\.php$/", $file, $matches)) {
					return $matches[1];
				}
			}
		}
	} else {
		if (preg_match("/pg\/([a-zA-Z0-9\-\_]*)\//", $_SERVER['REQUEST_URI'], $matches)) {
			return $matches[1];
		} else {
			$file = $_SERVER["SCRIPT_NAME"];
			$file = str_replace("\\", "/", $file);
			$file = str_replace("//", "/", $file);
			if (preg_match("/mod\/([a-zA-Z0-9\-\_]*)\//", $file, $matches)) {
				return $matches[1];
			}
		}
	}
	return false;
}

/**
 * Load and parse a plugin manifest from a plugin XML file.
 *
 * Example file:
 *
 *	<plugin_manifest>
 *		<!-- Basic information -->
 *		<field key="name" value="My Plugin" />
 *		<field key="description" value="My Plugin's concise description" />
 *		<field key="version" value="1.0" />
 *		<field key="category" value="theme" />
 *		<field key="category" value="bundled" />
 *		<field key="screenshot" value="path/relative/to/my_plugin.jpg" />
 *		<field key="screenshot" value="path/relative/to/my_plugin_2.jpg" />
 *
 *		<field key="author" value="Curverider Ltd" />
 *		<field key="website" value="http://www.elgg.org/" />
 *		<field key="copyright" value="(C) Curverider 2008-2010" />
 *		<field key="licence" value="GNU Public License version 2" />
 *	</plugin_manifest>
 *
 * @param string $plugin Plugin name.
 *
 * @return array of values
 */
function load_plugin_manifest($plugin) {
	global $CONFIG;

	$xml = xml_to_object(file_get_contents($CONFIG->pluginspath . $plugin . "/manifest.xml"));

	if ($xml) {
		// set up some defaults to normalize expected values to arrays
		$elements = array(
			'screenshot' => array(),
			'category' => array()
		);

		foreach ($xml->children as $element) {
			$key = $element->attributes['key'];
			$value = $element->attributes['value'];

			// create arrays if multiple fields are set
			if (array_key_exists($key, $elements)) {
				if (!is_array($elements[$key])) {
					$orig = $elements[$key];
					$elements[$key] = array($orig);
				}

				$elements[$key][] = $value;
			} else {
				$elements[$key] = $value;
			}
		}

		// handle plugins that don't define a name
		if (!isset($elements['name'])) {
			$elements['name'] = ucwords($plugin);
		}

		return $elements;
	}

	return false;
}

/**
 * This function checks a plugin manifest 'elgg_version' value against the current install
 * returning TRUE if the elgg_version is >= the current install's version.
 *
 * @param string $manifest_elgg_version_string The build version (eg 2009010201).
 *
 * @return bool
 */
function check_plugin_compatibility($manifest_elgg_version_string) {
	$version = get_version();

	if (strpos($manifest_elgg_version_string, '.') === false) {
		// Using version
		$req_version = (int)$manifest_elgg_version_string;

		return ($version >= $req_version);
	}

	return false;
}

/**
 * Shorthand function for finding the plugin settings.
 *
 * @param string $plugin_name Optional plugin name, if not specified
 *                            then it is detected from where you are calling from.
 *
 * @return mixed
 */
function find_plugin_settings($plugin_name = "") {
	$options = array('type' => 'object', 'subtype' => 'plugin', 'limit' => 9999);
	$plugins = elgg_get_entities($options);
	$plugin_name = sanitise_string($plugin_name);
	if (!$plugin_name) {
		$plugin_name = get_plugin_name();
	}

	if ($plugins) {
		foreach ($plugins as $plugin) {
			if (strcmp($plugin->title, $plugin_name) == 0) {
				return $plugin;
			}
		}
	}

	return false;
}

/**
 * Find the plugin settings for a user.
 *
 * @param string $plugin_name Plugin name.
 * @param int    $user_guid   The guid who's settings to retrieve.
 *
 * @return array of settings in an associative array minus prefix.
 */
function find_plugin_usersettings($plugin_name = "", $user_guid = 0) {
	$plugin_name = sanitise_string($plugin_name);
	$user_guid = (int)$user_guid;

	if (!$plugin_name) {
		$plugin_name = get_plugin_name();
	}

	if ($user_guid == 0) {
		$user_guid = get_loggedin_userid();
	}

	// Get metadata for user
	$all_metadata = get_all_private_settings($user_guid); //get_metadata_for_entity($user_guid);
	if ($all_metadata) {
		$prefix = "plugin:settings:$plugin_name:";
		$return = new stdClass;

		foreach ($all_metadata as $key => $meta) {
			$name = substr($key, strlen($prefix));
			$value = $meta;

			if (strpos($key, $prefix) === 0) {
				$return->$name = $value;
			}
		}

		return $return;
	}

	return false;
}

/**
 * Set a user specific setting for a plugin.
 *
 * @param string $name        The name - note, can't be "title".
 * @param mixed  $value       The value.
 * @param int    $user_guid   Optional user.
 * @param string $plugin_name Optional plugin name, if not specified then it
 *                            is detected from where you are calling from.
 *
 * @return bool
 */
function set_plugin_usersetting($name, $value, $user_guid = 0, $plugin_name = "") {
	$plugin_name = sanitise_string($plugin_name);
	$user_guid = (int)$user_guid;
	$name = sanitise_string($name);

	if (!$plugin_name) {
		$plugin_name = get_plugin_name();
	}

	$user = get_entity($user_guid);
	if (!$user) {
		$user = get_loggedin_user();
	}

	if (($user) && ($user instanceof ElggUser)) {
		$prefix = "plugin:settings:$plugin_name:$name";
		//$user->$prefix = $value;
		//$user->save();

		// Hook to validate setting
		$value = trigger_plugin_hook('plugin:usersetting', 'user', array(
			'user' => $user,
			'plugin' => $plugin_name,
			'name' => $name,
			'value' => $value
		), $value);

		return set_private_setting($user->guid, $prefix, $value);
	}

	return false;
}

/**
 * Clears a user-specific plugin setting
 *
 * @param str $name        Name of the plugin setting
 * @param int $user_guid   Defaults to logged in user
 * @param str $plugin_name Defaults to contextual plugin name
 *
 * @return bool Success
 */
function clear_plugin_usersetting($name, $user_guid = 0, $plugin_name = '') {
	$plugin_name = sanitise_string($plugin_name);
	$name = sanitise_string($name);

	if (!$plugin_name) {
		$plugin_name = get_plugin_name();
	}

	$user = get_entity((int) $user_guid);
	if (!$user) {
		$user = get_loggedin_user();
	}

	if (($user) && ($user instanceof ElggUser)) {
		$prefix = "plugin:settings:$plugin_name:$name";

		return remove_private_setting($user->getGUID(), $prefix);
	}

	return FALSE;
}

/**
 * Get a user specific setting for a plugin.
 *
 * @param string $name        The name.
 * @param int    $user_guid   Guid of owning user
 * @param string $plugin_name Optional plugin name, if not specified
 *                            then it is detected from where you are calling from.
 *
 * @return mixed
 */
function get_plugin_usersetting($name, $user_guid = 0, $plugin_name = "") {
	$plugin_name = sanitise_string($plugin_name);
	$user_guid = (int)$user_guid;
	$name = sanitise_string($name);

	if (!$plugin_name) {
		$plugin_name = get_plugin_name();
	}

	$user = get_entity($user_guid);
	if (!$user) {
		$user = get_loggedin_user();
	}

	if (($user) && ($user instanceof ElggUser)) {
		$prefix = "plugin:settings:$plugin_name:$name";
		return get_private_setting($user->guid, $prefix);
	}

	return false;
}

/**
 * Set a setting for a plugin.
 *
 * @param string $name        The name - note, can't be "title".
 * @param mixed  $value       The value.
 * @param string $plugin_name Optional plugin name, if not specified
 *                            then it is detected from where you are calling from.
 *
 * @return int|false
 */
function set_plugin_setting($name, $value, $plugin_name = "") {
	if (!$plugin_name) {
		$plugin_name = get_plugin_name();
	}
	$plugin = find_plugin_settings($plugin_name);

	if (!$plugin) {
		$plugin = new ElggPlugin();
	}

	if ($name != 'title') {
		// Hook to validate setting
		$value = trigger_plugin_hook('plugin:setting', 'plugin', array(
			'plugin' => $plugin_name,
			'name' => $name,
			'value' => $value
		), $value);

		$plugin->title = $plugin_name;
		$plugin->access_id = ACCESS_PUBLIC;
		$plugin->save();
		$plugin->$name = $value;

		return $plugin->getGUID();
	}

	return false;
}

/**
 * Get setting for a plugin.
 *
 * @param string $name        The name.
 * @param string $plugin_name Optional plugin name, if not specified
 *                            then it is detected from where you are calling from.
 *
 * @return mixed
 */
function get_plugin_setting($name, $plugin_name = "") {
	$plugin = find_plugin_settings($plugin_name);

	if ($plugin) {
		return $plugin->$name;
	}

	return false;
}

/**
 * Clear a plugin setting.
 *
 * @param string $name        The name.
 * @param string $plugin_name Optional plugin name, if not specified
 *                            then it is detected from where you are calling from.
 *
 * @return bool
 */
function clear_plugin_setting($name, $plugin_name = "") {
	$plugin = find_plugin_settings($plugin_name);

	if ($plugin) {
		return remove_private_setting($plugin->guid, $name);
	}

	return FALSE;
}

/**
 * Clear all plugin settings.
 *
 * @param string $plugin_name Optional plugin name, if not specified
 *                            then it is detected from where you are calling from.
 *
 * @return bool
 * @since 1.7.0
 */
function clear_all_plugin_settings($plugin_name = "") {
	$plugin = find_plugin_settings($plugin_name);

	if ($plugin) {
		return remove_all_private_settings($plugin->guid);
	}

	return FALSE;
}

/**
 * Return an array of installed plugins.
 *
 * @return array
 */
function get_installed_plugins() {
	global $CONFIG;

	$installed_plugins = array();

	if (!empty($CONFIG->pluginspath)) {
		$plugins = get_plugin_list();

		foreach ($plugins as $mod) {
			// require manifest.
			if (!$manifest = load_plugin_manifest($mod)) {
				continue;
			}
			$installed_plugins[$mod] = array();
			$installed_plugins[$mod]['active'] = is_plugin_enabled($mod);
			$installed_plugins[$mod]['manifest'] = $manifest;
		}
	}

	return $installed_plugins;
}

/**
 * Enable a plugin for a site (default current site)
 *
 * Important: You should regenerate simplecache and the viewpath cache after executing this function
 * otherwise you may experience view display artifacts. Do this with the following code:
 *
 * 		elgg_view_regenerate_simplecache();
 *		elgg_filepath_cache_reset();
 *
 * @param string $plugin    The plugin name.
 * @param int    $site_guid The site id, if not specified then this is detected.
 *
 * @return array
 * @throws InvalidClassException
 */
function enable_plugin($plugin, $site_guid = 0) {
	global $CONFIG, $ENABLED_PLUGINS_CACHE;

	$plugin = sanitise_string($plugin);
	$site_guid = (int) $site_guid;
	if ($site_guid == 0) {
		$site_guid = $CONFIG->site_guid;
	}

	$site = get_entity($site_guid);
	if (!($site instanceof ElggSite)) {
		$msg = sprintf(elgg_echo('InvalidClassException:NotValidElggStar'), $site_guid, "ElggSite");
		throw new InvalidClassException($msg);
	}

	if (!$plugin_info = load_plugin_manifest($plugin)) {
		return FALSE;
	}

	// getMetadata() doesn't return an array if only one plugin is enabled
	if ($enabled = $site->enabled_plugins) {
		if (!is_array($enabled)) {
			$enabled = array($enabled);
		}
	} else {
		$enabled = array();
	}

	$enabled[] = $plugin;
	$enabled = array_unique($enabled);

	if ($return = $site->setMetaData('enabled_plugins', $enabled)) {

		// for other plugins that want to hook into this.
		$params = array('plugin' => $plugin, 'manifest' => $plugin_info);
		if ($return && !trigger_elgg_event('enable', 'plugin', $params)) {
			$return = FALSE;
		}

		// for this plugin's on_enable
		if ($return && isset($plugin_info['on_enable'])) {
			// pull in the actual plugin's start so the on_enable function is callabe
			// NB: this will not run re-run the init hooks!
			$start = "{$CONFIG->pluginspath}$plugin/start.php";
			if (!file_exists($start) || !include($start)) {
				$return = FALSE;
			}

			// need language files for the messages
			$translations = "{$CONFIG->pluginspath}$plugin/languages/";
			register_translations($translations);
			if (!is_callable($plugin_info['on_enable'])) {
				$return = FALSE;
			} else {
				$on_enable = call_user_func($plugin_info['on_enable']);
				// allow null to mean "I don't care" like other subsystems
				$return = ($on_disable === FALSE) ? FALSE : TRUE;
			}
		}

		// disable the plugin if the on_enable or trigger results failed
		if (!$return) {
			array_pop($enabled);
			$site->setMetaData('enabled_plugins', $enabled);
		}

		$ENABLED_PLUGINS_CACHE = $enabled;
	}

	return $return;
}

/**
 * Disable a plugin for a site (default current site)
 *
 * Important: You should regenerate simplecache and the viewpath cache after executing this function
 * otherwise you may experience view display artifacts. Do this with the following code:
 *
 * 		elgg_view_regenerate_simplecache();
 *		elgg_filepath_cache_reset();
 *
 * @param string $plugin    The plugin name.
 * @param int    $site_guid The site id, if not specified then this is detected.
 *
 * @return bool
 * @throws InvalidClassException
 */
function disable_plugin($plugin, $site_guid = 0) {
	global $CONFIG, $ENABLED_PLUGINS_CACHE;

	$plugin = sanitise_string($plugin);
	$site_guid = (int) $site_guid;
	if ($site_guid == 0) {
		$site_guid = $CONFIG->site_guid;
	}

	$site = get_entity($site_guid);
	if (!($site instanceof ElggSite)) {
		$msg = sprintf(elgg_echo('InvalidClassException:NotValidElggStar'), $site_guid, "ElggSite");
		throw new InvalidClassException();
	}

	if (!$plugin_info = load_plugin_manifest($plugin)) {
		return FALSE;
	}

	// getMetadata() doesn't return an array if only one plugin is enabled
	if ($enabled = $site->enabled_plugins) {
		if (!is_array($enabled)) {
			$enabled = array($enabled);
		}
	} else {
		$enabled = array();
	}

	$old_enabled = $enabled;

	// remove the disabled plugin from the array
	if (FALSE !== $i = array_search($plugin, $enabled)) {
		unset($enabled[$i]);
	}

	// if we're unsetting all the plugins, this will return an empty array.
	// it will fail with FALSE, though.
	$return = (FALSE === $site->enabled_plugins = $enabled) ? FALSE : TRUE;

	if ($return) {
		// for other plugins that want to hook into this.
		$params = array('plugin' => $plugin, 'manifest' => $plugin_info);
		if ($return && !trigger_elgg_event('disable', 'plugin', $params)) {
			$return = FALSE;
		}

		// for this plugin's on_disable
		if ($return && isset($plugin_info['on_disable'])) {
			if (!is_callable($plugin_info['on_disable'])) {
				$return = FALSE;
			} else {
				$on_disable = call_user_func($plugin_info['on_disable']);
				// allow null to mean "I don't care" like other subsystems
				$return = ($on_disable === FALSE) ? FALSE : TRUE;
			}
		}

		// disable the plugin if the on_enable or trigger results failed
		if (!$return) {
			$site->enabled_plugins = $old_enabled;
			$ENABLED_PLUGINS_CACHE = $old_enabled;
		} else {
			$ENABLED_PLUGINS_CACHE = $enabled;
		}
	}

	return $return;
}

/**
 * Return whether a plugin is enabled or not.
 *
 * @param string $plugin    The plugin name.
 * @param int    $site_guid The site id, if not specified then this is detected.
 *
 * @return bool
 * @throws InvalidClassException
 */
function is_plugin_enabled($plugin, $site_guid = 0) {
	global $CONFIG, $ENABLED_PLUGINS_CACHE;

	if (!file_exists($CONFIG->pluginspath . $plugin)) {
		return false;
	}

	$site_guid = (int) $site_guid;
	if ($site_guid == 0) {
		$site_guid = $CONFIG->site_guid;
	}

	if (!$ENABLED_PLUGINS_CACHE) {
		$site = get_entity($site_guid);
		if (!($site instanceof ElggSite)) {
			$msg = sprintf(elgg_echo('InvalidClassException:NotValidElggStar'), $site_guid, "ElggSite");
			throw new InvalidClassException($msg);
		}

		$enabled_plugins = $site->enabled_plugins;
		if ($enabled_plugins && !is_array($enabled_plugins)) {
			$enabled_plugins = array($enabled_plugins);
		}
		$ENABLED_PLUGINS_CACHE = $enabled_plugins;
	}

	foreach ($ENABLED_PLUGINS_CACHE as $e) {
		if ($e == $plugin) {
			return true;
		}
	}

	return false;
}

/**
 * Register object, plugin entities as ElggPlugin classes
 *
 *  @return void
 */
function plugin_run_once() {
	// Register a class
	add_subtype("object", "plugin", "ElggPlugin");
}

/**
 * Initialise the file modules.
 * Listens to system boot and registers any appropriate file types and classes
 *
 * @return void
 */
function plugin_init() {
	// Now run this stuff, but only once
	run_function_once("plugin_run_once");

	// Register some actions
	register_action("plugins/settings/save", false, "", true);
	register_action("plugins/usersettings/save");

	register_action('admin/plugins/enable', false, "", true);
	register_action('admin/plugins/disable', false, "", true);
	register_action('admin/plugins/enableall', false, "", true);
	register_action('admin/plugins/disableall', false, "", true);

	register_action('admin/plugins/reorder', false, "", true);
}

// Register a startup event
register_elgg_event_handler('init', 'system', 'plugin_init');