diff options
Diffstat (limited to 'lib/plugins/engine/PluginManager.php')
| -rw-r--r-- | lib/plugins/engine/PluginManager.php | 719 |
1 files changed, 719 insertions, 0 deletions
diff --git a/lib/plugins/engine/PluginManager.php b/lib/plugins/engine/PluginManager.php new file mode 100644 index 0000000..ec434a9 --- /dev/null +++ b/lib/plugins/engine/PluginManager.php @@ -0,0 +1,719 @@ +<?php +/** + * PluginManager.php - plugin manager for Stud.IP + * + * @copyright 2009 Elmar Ludwig + * @license GPL2 or any later version + * + * @template P of StudIPPlugin + */ + +class PluginManager +{ + /** + * meta data of installed plugins + */ + private $plugins; + + /** + * cache of created plugin instances + */ + private $plugin_cache; + + /** + * cache of activated plugins by context + */ + private $plugins_activated_cache; + + /** + * Returns the PluginManager singleton instance. + */ + public static function getInstance () + { + static $instance; + + if (isset($instance)) { + return $instance; + } + + return $instance = new PluginManager(); + } + + /** + * Initialize a new PluginManager instance. + */ + private function __construct () + { + $this->readPluginInfos(); + $this->plugin_cache = []; + + $this->plugins_activated_cache = new StudipCachedArray('/PluginActivations'); + } + + /** + * Comparison function used to order plugins by position. + */ + private static function positionCompare ($plugin1, $plugin2) + { + return $plugin1['position'] - $plugin2['position']; + } + + /** + * Read meta data for all plugins registered in the data base. + */ + private function readPluginInfos () + { + $db = DBManager::get(); + $this->plugins = []; + + $result = $db->query('SELECT * FROM plugins ORDER BY pluginname'); + + foreach ($result as $plugin) { + $id = (int) $plugin['pluginid']; + + $this->plugins[$id] = [ + 'id' => $id, + 'name' => $plugin['pluginname'], + 'class' => $plugin['pluginclassname'], + 'path' => $plugin['pluginpath'], + 'type' => explode(',', $plugin['plugintype']), + 'enabled' => $plugin['enabled'] === 'yes', + 'position' => $plugin['navigationpos'], + 'depends' => (int) $plugin['dependentonid'], + 'core' => $plugin['pluginpath'] === '', + 'automatic_update_url' => $plugin['automatic_update_url'], + 'automatic_update_secret' => $plugin['automatic_update_secret'], + 'description' => $plugin['description'], + 'description_mode' => $plugin['description_mode'] ?? null, + 'highlight_until' => $plugin['highlight_until'] ?? null, + 'highlight_text' => $plugin['highlight_text'] ?? null, + ]; + } + } + + /** + * @addtogroup notifications + * + * Enabling or disabling a plugin triggers a PluginDidEnable or + * respectively PluginDidDisable notification. The plugin's ID + * is transmitted as subject of the notification. + */ + /** + * Set the enabled/disabled status of the given plugin. + * + * If the plugin implements the method "onEnable" or "onDisable", this + * method will be called accordingly. If the method returns false or + * throws and exception, the plugin's activation state is not updated. + * + * @param int $id id of the plugin + * @param bool $enabled plugin status (true or false) + * @param bool $force force (de)activation regardless of the result + * of on(en|dis)able + * @return bool indicating whether the plugin was updated or null if the + * passed state equals the current state or if the plugin is + * missing. + */ + public function setPluginEnabled ($id, $enabled, $force = false) + { + $info = $this->getPluginInfoById($id); + $plugin_class = null; + + // Plugin is not present or no changes + if (!$info || $info['enabled'] == $enabled) { + return null; + } + + if ($info['core'] || !$this->isPluginsDisabled()) { + $plugin_class = $this->loadPlugin($info['class'], $info['path']); + } + + if ($plugin_class) { + $method = $enabled ? 'onEnable' : 'onDisable'; + $result = $plugin_class->getMethod($method)->invoke(null, $id); + + // if callback returns false, don't enable or disable the plugin + if ($result === false && !$force) { + return false; + } + } + + // Update plugin + $state = $enabled ? 'yes' : 'no'; + + $query = "UPDATE plugins SET enabled = ? WHERE pluginid = ?"; + DBManager::get()->execute($query, [$state, $id]); + + $this->plugins[$id]['enabled'] = (boolean) $enabled; + + NotificationCenter::postNotification( + $enabled ? 'PluginDidEnable' : 'PluginDidDisable', + $id + ); + + return true; + } + + /** + * Set the navigation position of the given plugin. + * + * @param int $id id of the plugin + * @param int $position plugin navigation position + * @return bool indicating whether any change occured + */ + public function setPluginPosition ($id, $position) + { + $info = $this->getPluginInfoById($id); + $position = (int) $position; + + if (!$info || $info['position'] == $position) { + return false; + } + + $query = "UPDATE plugins SET navigationpos = ? WHERE pluginid = ?"; + DBManager::get()->execute($query, [$position, $id]); + + $this->plugins[$id]['position'] = $position; + $this->readPluginInfos(); + + return true; + } + + /** + * Get the activation status of a plugin in the given context. + * This also checks the plugin default activations and sem_class-settings. + * + * @param int $id id of the plugin + * @param string $context range id + * @returns bool + */ + public function isPluginActivated ($id, $context) + { + if (!$context) { + return null; + } + if (!isset($this->plugins_activated_cache[$context])) { + $query = "SELECT plugin_id, 1 as state + FROM tools_activated + WHERE range_id = ?"; + $statement = DBManager::get()->prepare($query); + $statement->execute([$context]); + $this->plugins_activated_cache[$context] = $statement->fetchGrouped(PDO::FETCH_COLUMN); + } + return isset($this->plugins_activated_cache[$context][$id]); + } + + /** + * Get the activation status of a plugin for the given user. + * This also checks the plugin default activations and sem_class-settings. + * + * @param int $pluginId id of the plugin + * @param string $userId id of the user + */ + public function isPluginActivatedForUser($pluginId, $userId) + { + if (!$userId) { + $userId = $GLOBALS['user']->id; + } + if (!isset($this->plugins_activated_cache[$userId])) { + $query = "SELECT pluginid, state + FROM plugins_activated + WHERE range_type = 'user' + AND range_id = ?"; + $statement = DBManager::get()->prepare($query); + $statement->execute([$userId]); + $this->plugins_activated_cache[$userId] = $statement->fetchGrouped(PDO::FETCH_COLUMN); + } + $state = $this->plugins_activated_cache[$userId][$pluginId] ?? null; + if ($state === null) { + $activated = (bool) Config::get()->HOMEPAGEPLUGIN_DEFAULT_ACTIVATION; + } else { + $activated = (bool) $state; + } + + return $activated; + } + + /** + * Sets the activation status of a plugin in the given context. + * + * @param int $id id of the plugin + * @param string $rangeId context range id + * @param bool $active plugin status (true or false) + */ + public function setPluginActivated ($id, $rangeId, $active) + { + unset($this->plugins_activated_cache[$rangeId]); + $activation = ToolActivation::find([$rangeId, $id]); + if (!$activation) { + $range = get_object_by_range_id($rangeId); + $activation = new ToolActivation(); + $activation->range_id = $rangeId; + $activation->plugin_id = $id; + $activation->range_type = $range->getRangeType(); + } + $plugin = $this->getPluginById($id); + + if ($active) { + call_user_func([get_class($plugin), 'onActivation'], $id, $rangeId); + StudipLog::log('PLUGIN_ENABLE', $rangeId, $id, User::findCurrent()->id); + NotificationCenter::postNotification('PluginDidActivate', $rangeId, $id); + return $activation->store(); + } else { + call_user_func([get_class($plugin), 'onDeactivation'], $id, $rangeId); + StudipLog::log('PLUGIN_DISABLE', $rangeId, $id, User::findCurrent()->id); + NotificationCenter::postNotification('PluginDidDeactivate', $rangeId, $id); + return $activation->delete(); + } + } + + /** + * Sets the activation status of a plugin in the given context. + * + * @param int $pluginid id of the plugin + * @param string $user_id user id + * @param bool $active plugin status (true or false) + */ + public function setPluginActivatedForUser($pluginid, $user_id, $active) + { + $db = DBManager::get(); + $state = $active ? 1 : 0; + unset($this->plugins_activated_cache[$user_id]); + + $query = "REPLACE INTO plugins_activated (pluginid, range_type, range_id, state) + VALUES (?, 'user', ?, ?)"; + $result = $db->execute($query, [$pluginid, $user_id, $state]); + + if ($result > 0) { + $plugin = $this->getPluginById($pluginid); + if ($active) { + call_user_func([get_class($plugin), 'onActivation'], $pluginid, $user_id); + } else { + call_user_func([get_class($plugin), 'onDeactivation'], $pluginid, $user_id); + } + } + + return $result; + } + + /** + * Deactivate all plugins for the given range. + * + * @param string $range_type Type of range (sem, inst or user) + * @param string $range_id Id of range + * @return int number of deactivated/removed plugins for range + */ + public function deactivateAllPluginsForRange($range_type, $range_id) + { + unset($this->plugins_activated_cache[$range_id]); + + $query = "DELETE FROM `plugins_activated` + WHERE `range_type` = :range_type + AND `range_id` = :range_id"; + $statement = DBManager::get()->prepare($query); + $statement->bindValue(':range_type', $range_type); + $statement->bindValue(':range_id', $range_id); + $statement->execute(); + + return $statement->rowCount(); + } + + /** + * Disable loading of all non-core plugins for the current session. + * + * @param bool $status true: disable non-core plugins + */ + public function setPluginsDisabled($status) + { + $_SESSION['plugins_disabled'] = (bool) $status; + } + + /** + * Check whether loading of non-core plugins is currently disabled. + */ + public function isPluginsDisabled() + { + return $_SESSION['plugins_disabled'] ?? false; + } + + /** + * Load a plugin class from the given file system path and + * return the ReflectionClass instance for the plugin. + * + * @param string $class plugin class name + * @param string $path plugin relative path + */ + private function loadPlugin ($class, $path) + { + if ($path) { + $basepath = Config::get()->PLUGINS_PATH; + } else { + $basepath = $GLOBALS['STUDIP_BASE_PATH']; + $path = 'lib/modules'; + } + + $pluginfile = $basepath.'/'.$path.'/'.$class.'.php'; + + if (!file_exists($pluginfile)) { + $pluginfile = $basepath.'/'.$path.'/'.$class.'.class.php'; + + if (!file_exists($pluginfile)) { + return null; + } + } + + require_once $pluginfile; + + return new ReflectionClass($class); + } + + /** + * Determine the type of a plugin to be installed. + * + * @param string $class plugin class name + * @param string $path plugin relative path + */ + private function getPluginType ($class, $path) + { + $plugin_class = $this->loadPlugin($class, $path); + $types = []; + + if ($plugin_class) { + $plugin_base = new ReflectionClass('StudIPPlugin'); + $interfaces = $plugin_class->getInterfaces(); + + if ($plugin_class->isSubclassOf($plugin_base)) { + foreach ($interfaces as $interface) { + $types[] = $interface->getName(); + } + } + } + + sort($types); + + return $types; + } + + /** + * Register a new plugin or update an existing plugin entry in the + * data base. Returns the id of the new or updated plugin. + * + * @param string $name plugin name + * @param string $class plugin class name + * @param string $path plugin relative path + * @param int $depends id of plugin this plugin depends on + */ + public function registerPlugin ($name, $class, $path, $depends = null) + { + $db = DBManager::get(); + $info = $this->getPluginInfo($class); + $type = $this->getPluginType($class, $path); + $position = 1; + + // plugin must implement at least one interface + if (count($type) == 0) { + throw new Exception(_("Plugin implementiert kein gültiges Interface.")); + } + + if ($info) { + $id = $info['id']; + $sql = 'UPDATE plugins SET pluginname = ?, pluginpath = ?, + plugintype = ? WHERE pluginid = ?'; + $stmt = $db->prepare($sql); + $stmt->execute([$name, $path, join(',', $type), $id]); + + $this->plugins[$id]['name'] = $name; + $this->plugins[$id]['path'] = $path; + $this->plugins[$id]['type'] = $type; + } else { + foreach ($this->plugins as $plugin) { + $common_types = array_intersect($type, $plugin['type']); + + if (count($common_types) > 0 && $plugin['position'] >= $position) { + $position = $plugin['position'] + 1; + } + } + + $sql = 'INSERT INTO plugins ( + pluginname, pluginclassname, pluginpath, + plugintype, navigationpos, dependentonid + ) VALUES (?,?,?,?,?,?)'; + $stmt = $db->prepare($sql); + $stmt->execute([$name, $class, $path, join(',', $type), $position, $depends]); + $id = $db->lastInsertId(); + + $this->plugins[$id] = [ + 'id' => $id, + 'name' => $name, + 'class' => $class, + 'path' => $path, + 'type' => $type, + 'enabled' => false, + 'position' => $position, + 'depends' => $depends + ]; + + $this->readPluginInfos(); + + $db->exec("INSERT INTO roles_plugins (roleid, pluginid) + SELECT roleid, $id FROM roles WHERE `system` = 'y' AND rolename != 'Nobody'"); + } + + if (!in_array(StandardPlugin::class, $type)) { + ToolActivation::findEachBySQL( + function (ToolActivation $activation) use ($id) { + $this->setPluginActivated($id, $activation->range_id, false); + }, + 'plugin_id = ?', + [$id] + ); + } + + return $id; + } + + /** + * Remove registration for the given plugin from the data base. + * + * @param int $id id of the plugin + */ + public function unregisterPlugin ($id) + { + $info = $this->getPluginInfoById($id); + + if ($info) { + $db = DBManager::get(); + + $db->execute("DELETE FROM plugins WHERE pluginid = ?", [$id]); + $db->execute("DELETE FROM plugins_activated WHERE pluginid = ?", [$id]); + $db->execute("DELETE FROM roles_plugins WHERE pluginid = ?", [$id]); + + unset($this->plugins[$id]); + + unset($this->plugins_activated_cache[$id]); + } + } + + /** + * Get meta data for the plugin specified by plugin class name. + * + * @param string $class class name of plugin + */ + public function getPluginInfo ($class) + { + foreach ($this->plugins as $plugin) { + if (strcasecmp($plugin['class'], $class) == 0) { + return $plugin; + } + } + + return null; + } + + /** + * Get meta data for the plugin specified by plugin id. + * + * @param int $id id of the plugin + */ + public function getPluginInfoById ($id) + { + if (isset($this->plugins[$id])) { + return $this->plugins[$id]; + } + + return null; + } + + /** + * Get meta data for all plugins of the specified type. A type of null + * returns meta data for all installed plugins. + * + * @param string $type plugin type or null (all types) + */ + public function getPluginInfos ($type = null) + { + $result = []; + + foreach ($this->plugins as $id => $plugin) { + if ($type === null || in_array($type, $plugin['type'])) { + $result[$id] = $plugin; + } + } + + return $result; + } + + /** + * Check user access permission for the given plugin. + * + * @param array $plugin plugin meta data + * @param string $user_id id of user + * @return bool + */ + protected function checkUserAccess ($plugin, $user_id) + { + if (!$plugin['enabled']) { + return false; + } + + $plugin_roles = RolePersistence::getAssignedPluginRoles($plugin['id']); + $user_roles = RolePersistence::getAssignedRoles($user_id, true); + + foreach ($plugin_roles as $plugin_role) { + foreach ($user_roles as $user_role) { + if ($plugin_role->getRoleid() === $user_role->getRoleid()) { + return true; + } + } + } + + return false; + } + + /** + * Get instance of the plugin specified by plugin meta data. + * + * @param array $plugin_info plugin meta data + * @return P + */ + protected function getCachedPlugin ($plugin_info) + { + $class = $plugin_info['class']; + $path = $plugin_info['path']; + $plugin_class = ''; + $plugin = null; + + if (isset($this->plugin_cache[$class])) { + return $this->plugin_cache[$class]; + } + + if ($plugin_info['core'] || !$this->isPluginsDisabled()) { + $plugin_class = $this->loadPlugin($class, $path); + } + + if ($plugin_class) { + $plugin = app()->get($class); + } + + return $this->plugin_cache[$class] = $plugin; + } + + /** + * Get instance of the plugin specified by plugin class name. + * + * @param class-string<P> $class class name of plugin + * @return P|null + */ + public function getPlugin ($class) + { + $user = $GLOBALS['user']->id; + $plugin_info = $this->getPluginInfo($class); + $plugin = null; + + if (isset($plugin_info) && $this->checkUserAccess($plugin_info, $user)) { + $plugin = $this->getCachedPlugin($plugin_info); + } + + return $plugin; + } + + /** + * Get instance of the plugin specified by plugin id. + * + * @param int $id id of the plugin + * @return P|null $plugin + */ + public function getPluginById ($id) + { + $user = $GLOBALS['user']->id; + $plugin_info = $this->getPluginInfoById($id); + $plugin = null; + + if (isset($plugin_info) && $this->checkUserAccess($plugin_info, $user)) { + $plugin = $this->getCachedPlugin($plugin_info); + } + + return $plugin; + } + + /** + * Get instances of all plugins of the specified type. A type of null + * returns all enabled plugins. The optional context parameter can be + * used to get only plugins that are activated in the given context. + * + * @param class-string<P>|null $type plugin type or null (all types) + * @param string $context context range id (optional) + * @return P[]|StudIPPlugin[] + */ + public function getPlugins ($type, $context = null) + { + $user = isset($GLOBALS['user']) ? $GLOBALS['user']->id : 'nobody'; + $plugin_info = $this->getPluginInfos($type); + $plugins = []; + + usort($plugin_info, [self::class, 'positionCompare']); + + foreach ($plugin_info as $info) { + $activated = $context == null + || $this->isPluginActivated($info['id'], $context); + + if ($this->checkUserAccess($info, $user) && $activated) { + $plugin = $this->getCachedPlugin($info); + + if ($plugin !== null) { + $plugins[] = $plugin; + } + } + } + + return $plugins; + } + + /** + * Read the manifest of the plugin in the given directory. + * Returns null if the manifest cannot be found. + * + * @return array containing the manifest information + */ + public function getPluginManifest($plugindir) + { + if (!file_exists($plugindir . '/plugin.manifest')) { + return null; + } + $manifest = file($plugindir . '/plugin.manifest'); + $result = []; + + if ($manifest === false) { + return null; + } + + foreach ($manifest as $line) { + $key_and_value = explode('=', $line); + $key = trim($key_and_value[0]); + $value = trim($key_and_value[1] ?? ''); + + // skip empty lines and comments + if ($key === '' || $key[0] === '#') { + continue; + } + + $key_array = explode('.',$key,2); + if(count($key_array) > 1){ + if($key_array[0] === 'screenshots'){ + $screenshot_data['source'] = $key_array[1]; + $screenshot_data['title'] = $value; + $result['screenshots']['pictures'][] = $screenshot_data; + } + } elseif($key === 'screenshots') { + $result['screenshots']['path'] = $value; + } elseif ($key === 'pluginclassname' && isset($result[$key])) { + $result['additionalclasses'][] = $value; + } elseif ($key === 'screenshot' && isset($result[$key])) { + $result['additionalscreenshots'][] = $value; + } else { + $result[$key] = $value; + } + } + + return $result; + } +} |
