<?php # Lifter010: TODO /* * PluginManager.class.php - plugin manager for Stud.IP * * Copyright (c) 2009 Elmar Ludwig * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. */ 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.'.class.php'; if (!file_exists($pluginfile)) { $pluginfile = $basepath.'/'.$path.'/'.$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 object */ 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 = $plugin_class->newInstance(); } return $this->plugin_cache[$class] = $plugin; } /** * Get instance of the plugin specified by plugin class name. * * @param string $class class name of plugin * @return object */ 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 object $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 string $type plugin type or null (all types) * @param string $context context range id (optional) */ 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; } }