Skip to content
Snippets Groups Projects
PluginManager.class.php 22.6 KiB
Newer Older
<?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) {
        }

        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);
        }
Moritz Strohm's avatar
Moritz Strohm committed
        $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();
        }
    }
André Noack's avatar
André Noack committed

    /**
     * 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)
André Noack's avatar
André Noack committed
     */
    public function setPluginActivatedForUser($pluginid, $user_id, $active)
    {
        $db = DBManager::get();
        $state = $active ? 1 : 0;
        unset($this->plugins_activated_cache[$user_id]);

Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        $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;
André Noack's avatar
André Noack committed
    }

    /**
     * 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()
    {
Moritz Strohm's avatar
Moritz Strohm committed
        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)) {
            }
        }

        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;
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        } else {
            foreach ($this->plugins as $plugin) {
                $common_types = array_intersect($type, $plugin['type']);
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
                if (count($common_types) > 0 && $plugin['position'] >= $position) {
                    $position = $plugin['position'] + 1;
                }
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
            $sql = 'INSERT INTO plugins (
                    pluginname, pluginclassname, pluginpath,
                    plugintype, navigationpos, dependentonid
                ) VALUES (?,?,?,?,?,?)';
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
            $stmt = $db->prepare($sql);
            $stmt->execute([$name, $class, $path, join(',', $type), $position, $depends]);
            $id = $db->lastInsertId();
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
            $this->plugins[$id] = [
                'id'          => $id,
                'name'        => $name,
                'class'       => $class,
                'path'        => $path,
                'type'        => $type,
                'enabled'     => false,
                'position'    => $position,
                'depends'     => $depends
            ];

            $this->readPluginInfos();
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
            $db->exec("INSERT INTO roles_plugins (roleid, pluginid)
                   SELECT roleid, $id FROM roles WHERE `system` = 'y' AND rolename != 'Nobody'");
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        }

        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.
     *
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
     * @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]);

Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
            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;
            }
        }

    }

    /**
     * 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];
        }

     * 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
     */
    public function getPlugin ($class)
    {
        $user = $GLOBALS['user']->id;
        $plugin_info = $this->getPluginInfo($class);

        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);

        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) {
        }

        foreach ($manifest as $line) {
Moritz Strohm's avatar
Moritz Strohm committed
            $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;
    }
}