Skip to content
Snippets Groups Projects
PluginAdministration.php 17 KiB
Newer Older
<?php
# Lifter010: TODO
/*
 * plugin_administration.php - plugin administration model class
 *
 * Copyright (c) 2009  Dennis Reil, 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.
 */


/**
 * Model code for plugin administration tasks.
 */
class PluginAdministration
{
    /**
     * Install a new plugin. Extracts the contents of the uploaded file,
     * checks the manifest, creates the new plugin directory und finally
     * registers the plugin in the database.
     *
     * @param string $filename path to the uploaded file
     */
    public function installPlugin($filename)
    {
        $packagedir = Config::get()->PLUGINS_PATH . '/tmp_' . md5($filename);

        // extract plugin files
        if (!file_exists($packagedir) && mkdir($packagedir) === false) {
            throw new PluginInstallationException(_('Fehler beim Entpacken des Plugins (fehlende Schreibrechte?).'));
        }

        if (!Studip\ZipArchive::extractToPath($filename, $packagedir)) {
            rmdirr($packagedir);
            throw new PluginInstallationException(_('Fehler beim Entpacken des Plugins.'));
        } else {
            $tmpplugindir = $packagedir;
            $files = scandir($packagedir);
            if (count($files) === 3) {
                foreach ($files as $file) {
                    if (!in_array($file, [".",".."])) {
                        $tmpplugindir .= "/" . $file;
                    }
                }
            }
        }

        // check if the plugin might be located in a subfolder
        $files = glob($packagedir . '/*');
        $dirs  = array_filter($files, 'is_dir');
        if (!file_exists($packagedir . '/plugin.manifest') && count($dirs) === 1) {
            $packagedir = $dirs[0];
        }

        // load the manifest
        $plugin_manager = PluginManager::getInstance();
        $manifest = $plugin_manager->getPluginManifest($tmpplugindir);

        if ($manifest === NULL) {
            rmdirr($packagedir);
            throw new PluginInstallationException(_('Das Manifest des Plugins fehlt.'));
        }

        // get plugin meta data
        $pluginclass = $manifest['pluginclassname'];
        $origin      = $manifest['origin'];
        $min_version = $manifest['studipMinVersion'] ?? null;
        $max_version = $manifest['studipMaxVersion'] ?? null;

        // check for compatible version
        if ((isset($min_version) && StudipVersion::olderThan($min_version)) ||
            (isset($max_version) && StudipVersion::newerThan($max_version))) {
            rmdirr($packagedir);
            throw new PluginInstallationException(_('Das Plugin ist mit dieser Stud.IP-Version nicht kompatibel.'));
        }

        // determine the plugin path
        $basepath = Config::get()->PLUGINS_PATH;
        $pluginpath = $origin . '/' . $pluginclass;
        $plugindir = $basepath . '/' . $pluginpath;

        $pluginregistered = $plugin_manager->getPluginInfo($pluginclass);

        // is the plugin already installed?
        if (file_exists($plugindir)) {
            if ($pluginregistered) {
                $this->updateDBSchema($plugindir, $tmpplugindir, $manifest);
            }

            rmdirr($plugindir);

            // on NFS file system, removing the plugin may fail (see ticket #1892)
            if (file_exists($plugindir)) {
                $plugindir_old = $plugindir . '.old';
                rmdirr($plugindir_old);
                rename($plugindir, $plugindir_old);
            }

            // avoid loading old version of the class from opcache (see ticket #569)
            ini_set('opcache.enable', 0);
        }

        // move directory to final destination
        if (!file_exists($basepath . '/' . $origin) && mkdir($basepath . '/' . $origin) === false) {
            throw new PluginInstallationException(sprintf(
                _('Der Ordner "%s" konnte nicht erstellt werden.'),
                studip_relative_path($basepath . '/' . $origin)
            ));
        }

        if (!is_writable($basepath . '/' . $origin)) {
            throw new PluginInstallationException(sprintf(
                _('Der Ordner "%s" ist nicht schreibbar.'),
                studip_relative_path($basepath . '/' . $origin)
            ));
        }

        rename($tmpplugindir, $plugindir);

        // create database schema if needed
        $this->createDBSchema($plugindir, $manifest, $pluginregistered);

        // now register the plugin in the database
        $pluginid = $plugin_manager->registerPlugin($manifest['pluginname'], $pluginclass, $pluginpath);

        if ($pluginid === NULL) {
            rmdirr($plugindir);
            throw new PluginInstallationException(_('Das Plugin enthält keine gültige Plugin-Klasse.'));
        }

        // register additional plugin classes in this package
        $additionalclasses = $manifest['additionalclasses'] ?? null;

        if (is_array($additionalclasses)) {
            foreach ($additionalclasses as $class) {
                $plugin_manager->registerPlugin($class, $class, $pluginpath, $pluginid);
            }
        }
        rmdirr($packagedir);
        return $pluginid;
    }

    /**
     * Download and install a new plugin from the given URL.
     *
     * @param string $plugin_url the URL of the plugin package
     */
    public function installPluginFromURL($plugin_url)
    {
        $temp_name = tempnam(Config::get()->TMP_PATH, 'plugin');

        if (!@copy($plugin_url, $temp_name, get_default_http_stream_context($plugin_url))) {
            throw new PluginInstallationException(_('Das Herunterladen des Plugins ist fehlgeschlagen.'));
        }

        $pluginid = $this->installPlugin($temp_name);
        unlink($temp_name);
        return $pluginid;
    }

    /**
     * Download and install a plugin with the given name from the
     * plugin repository.
     *
     * @param string $pluginname name of the plugin to install
     */
    public function installPluginByName($pluginname)
    {
        $repository = new PluginRepository();
        $plugin = $repository->getPlugin($pluginname);

        if (!isset($plugin)) {
            throw new PluginInstallationException(_('Das Plugin konnte nicht gefunden werden.'));
        }

        $this->installPluginFromURL($plugin['url']);
    }

    /**
     * Uninstall the given plugin from the system. It will remove
     * the database schema and all the plugin's files.
     *
     * @param array $plugin meta data of plugin
     */
    public function uninstallPlugin($plugin)
    {
        $plugin_manager = PluginManager::getInstance();

        // check if there are dependent plugins
        foreach ($plugin_manager->getPluginInfos() as $dep_plugin) {
            if ($dep_plugin['depends'] === $plugin['id']) {
                $plugin_manager->unregisterPlugin($dep_plugin['id']);
            }
        }

        $plugin_manager->unregisterPlugin($plugin['id']);
        $plugindir = Config::get()->PLUGINS_PATH . '/' . $plugin['path'];
        $manifest = $plugin_manager->getPluginManifest($plugindir);

        // delete database if needed
        $this->deleteDBSchema($plugindir, $manifest);
        PluginAsset::deleteBySQL('plugin_id = ?', [$plugin['id']]);

        rmdirr($plugindir);
    }

    /**
     * Create the initial database schema for the plugin.
     *
     * @param string  $plugindir  absolute path to the plugin
     * @param array   $manifest   plugin manifest information
     * @param boolean $update     update installed plugin
     */
    private function createDBSchema($plugindir, $manifest, $update)
    {
        $pluginname = $manifest['pluginname'];

        if (isset($manifest['dbscheme']) && !$update) {
            $schemafile = $plugindir . '/' . $manifest['dbscheme'];
            $contents   = file_get_contents($schemafile);
            $statements = preg_split("/;[[:space:]]*\n/", $contents, -1, PREG_SPLIT_NO_EMPTY);
            $db = DBManager::get();
            foreach ($statements as $statement) {
                $db->exec($statement);
            }
        }

        if (is_dir($plugindir . '/migrations')) {
            $schema_version = new DBSchemaVersion($pluginname);
            $migrator = new Migrator($plugindir . '/migrations', $schema_version);
            $migrator->migrateTo(null);
        }
    }

    /**
     * Update the database schema maintained by the plugin.
     *
     * @param string $plugindir      absolute path to the plugin
     * @param string $new_pluginpath absolute path to updated plugin
     * @param array  $manifest       plugin manifest information
     */
    private function updateDBSchema($plugindir, $new_pluginpath, $manifest)
    {
        $pluginname = $manifest['pluginname'];

        if (is_dir($plugindir . '/migrations')) {
            $schema_version = new DBSchemaVersion($pluginname);
            $new_version = 0;

            if (is_dir($new_pluginpath . '/migrations')) {
                $migrator = new Migrator($new_pluginpath . '/migrations', $schema_version);
                $all_branches = array_fill_keys($schema_version->getAllBranches(), 0);
                $new_version = $migrator->topVersion(true) + $all_branches;
            }

            $migrator = new Migrator($plugindir . '/migrations', $schema_version);
            $migrator->migrateTo($new_version);
        }
    }

    /**
     * Delete the database schema maintained by the plugin.
     *
     * @param string $plugindir  absolute path to the plugin
     * @param array  $manifest   plugin manifest information
     */
    private function deleteDBSchema($plugindir, $manifest)
    {
        $pluginname = $manifest['pluginname'];

        if (is_dir($plugindir . '/migrations')) {
            $schema_version = new DBSchemaVersion($pluginname);
            $migrator = new Migrator($plugindir . '/migrations', $schema_version);
            $migrator->migrateTo(0);
        }

        if (isset($manifest['uninstalldbscheme'])) {
            $schemafile = $plugindir . '/' . $manifest['uninstalldbscheme'];
            $contents   = file_get_contents($schemafile);
            $statements = preg_split("/;[[:space:]]*\n/", $contents, -1, PREG_SPLIT_NO_EMPTY);
            $db = DBManager::get();
            foreach ($statements as $statement) {
                $db->exec($statement);
            }
        }
    }

    /**
     * Get a list of the types of all installed plugins.
     *
     * @return array    list of plugin types
     */
    public function getPluginTypes()
    {
        $plugin_manager = PluginManager::getInstance();
        $plugin_infos   = $plugin_manager->getPluginInfos();
        $plugin_types   = [];

        foreach ($plugin_infos as $plugin) {
            $plugin_types = array_merge($plugin_types, $plugin['type']);
        }

        sort($plugin_types);
        return array_unique($plugin_types);
    }

    /**
     * Fetch update information for a list of plugins. This method
     * returns for each plugin: the plugin name, current version and
     * meta data of the plugin update, if available.
     *
     * @param array  $plugins    array of plugin meta data
     */
    public function getUpdateInfo($plugins)
    {
        $default_repository = new PluginRepository();
        $plugin_manager = PluginManager::getInstance();
        $update_info = [];

        foreach ($plugins as $plugin) {
            $repository = $default_repository;
            $plugindir = Config::get()->PLUGINS_PATH . '/' . $plugin['path'];
            $manifest = $plugin_manager->getPluginManifest($plugindir);

Moritz Strohm's avatar
Moritz Strohm committed
            if (!$manifest) {
                continue;
            }

            if (isset($manifest['updateURL'])) {
                $repository = new PluginRepository($manifest['updateURL']);
            }

            $meta_data = $repository->getPlugin($manifest['pluginname']);

            if (isset($meta_data) &&
                version_compare($meta_data['version'], $manifest['version']) > 0) {
                $manifest['update'] = $meta_data;
            }

            $update_info[$plugin['id']] = $manifest;
        }

        return $update_info;
    }

    /**
     * Fetch migration information plugins. This method
     * returns for each plugin:
     * current schema version and top migration version, if available.
     *
     * @return array
     */
    public function getMigrationInfo()
    {
        $info = [];
        $plugin_manager = PluginManager::getInstance();
        $plugins = $plugin_manager->getPluginInfos();
        $basepath = Config::get()->PLUGINS_PATH;
        foreach ($plugins as $id => $plugin) {
            $plugindir = $basepath . '/' . $plugin['path'];
            if (is_dir($plugindir . '/migrations')) {
                $schema_version = new DBSchemaVersion($plugin['name']);
                $migrator = new Migrator($plugindir . '/migrations', $schema_version);
                $info[$id]['pending_migrations'] = $migrator->pendingMigrations();
                $info[$id]['schema_version'] = $schema_version->get();
            }
        }
        return $info;
    }

    /**
     * migrate plugin to top migration
     *
     * @param integer $plugin_id
     * @return string output from migrator
     */
    public function migratePlugin($plugin_id)
    {
        $plugin_manager = PluginManager::getInstance();
        $plugin = $plugin_manager->getPluginInfoById($plugin_id);
        $plugindir = Config::get()->PLUGINS_PATH . '/' . $plugin['path'];
        if (is_dir($plugindir . '/migrations')) {
            $schema_version = new DBSchemaVersion($plugin['name']);
            $migrator = new Migrator($plugindir . '/migrations', $schema_version, true);
            ob_start();
            $migrator->migrateTo(null);
            $log = ob_get_clean();
        }
        return $log;
    }

    /**
     * scans PLUGINS_PATH for plugin.manifest files
     * belonging to not registered plugins
     *
     * @return array with manifest meta data
     */
    public function scanPluginDirectory()
    {
        $found = [];
        $basepath = Config::get()->PLUGINS_PATH;
        $plugin_manager = PluginManager::getInstance();
        $iterator = new RegexIterator(
                        new RecursiveIteratorIterator(
                            new RecursiveDirectoryIterator($basepath, FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::UNIX_PATHS)),
                        '/plugin\.manifest$/', RecursiveRegexIterator::MATCH);
        foreach ($iterator as $manifest_file) {
            $manifest = $plugin_manager->getPluginManifest($manifest_file->getPath());
            if (!isset($manifest['pluginclassname'])) {
                continue;
            }
            $pluginpath = $basepath . '/' . $manifest['origin'] . '/' . $manifest['pluginclassname'];
            if (!$plugin_manager->getPluginInfo($manifest['pluginclassname'])
                && $pluginpath === $manifest_file->getPath()) {
                $manifest['path'] = $manifest_file->getPath();
                $found[] = $manifest;
            }
        }
        return $found;
    }

    /**
     * registers plugin at given path in database
     *
     * @param string $plugindir path to plugin
     * @throws PluginInstallationException
     */
    public function registerPlugin($plugindir)
    {
        $plugin_manager = PluginManager::getInstance();
        $manifest = $plugin_manager->getPluginManifest($plugindir);
        if (!$manifest) {
            throw new PluginInstallationException(_('Das Manifest des Plugins fehlt.'));
        }

        // get plugin meta data
        $pluginclass = $manifest['pluginclassname'];
        $origin      = $manifest['origin'];
        $min_version = $manifest['studipMinVersion'];
        $max_version = $manifest['studipMaxVersion'];

        // check for compatible version
        if ((isset($min_version) && StudipVersion::olderThan($min_version)) ||
            (isset($max_version) && StudipVersion::newerThan($max_version))) {
            throw new PluginInstallationException(_('Das Plugin ist mit dieser Stud.IP-Version nicht kompatibel.'));
        }

        // determine the plugin path
        $basepath = Config::get()->PLUGINS_PATH;
        $pluginpath = $origin . '/' . $pluginclass;

        $pluginregistered = $plugin_manager->getPluginInfo($pluginclass);

        if ($pluginregistered) {
            new PluginInstallationException(_('Das Plugin ist bereits registriert.'));
        }

        // create database schema if needed
        $this->createDBSchema($plugindir, $manifest, $pluginregistered);

        // now register the plugin in the database
        $pluginid = $plugin_manager->registerPlugin($manifest['pluginname'], $pluginclass, $pluginpath);

        // register additional plugin classes in this package
        $additionalclasses = $manifest['additionalclasses'];

        if (is_array($additionalclasses)) {
            foreach ($additionalclasses as $class) {
                $plugin_manager->registerPlugin($class, $class, $pluginpath, $pluginid);
            }
        }
    }
}