diff options
| author | Jan-Hendrik Willms <tleilax+github@gmail.com> | 2021-07-22 16:07:19 +0200 |
|---|---|---|
| committer | Jan-Hendrik Willms <tleilax+github@gmail.com> | 2021-07-22 16:19:12 +0200 |
| commit | a3da1483a9e689846179159355badfec8073dbec (patch) | |
| tree | 770dcca6bdf5f6f2a11b0e7fcbbeda6919a3fc52 /lib/plugins | |
current code from svn, revision 62608
Diffstat (limited to 'lib/plugins')
27 files changed, 2971 insertions, 0 deletions
diff --git a/lib/plugins/core/AdminCourseAction.class.php b/lib/plugins/core/AdminCourseAction.class.php new file mode 100644 index 0000000..046502c --- /dev/null +++ b/lib/plugins/core/AdminCourseAction.class.php @@ -0,0 +1,22 @@ +<?php + +interface AdminCourseAction +{ + public function getAdminActionURL(); + + /** + * Defines if the Plugin wants to use the multimode to edit multiple courses at once. + * @return boolean|string: false, if multimode is not important, else true. But you can also set it to a string (means true) that is the label of the send-button like _("Veranstaltungen archivieren") + */ + public function useMultimode(); + + /** + * Returns a template for a small table cell (the <td> wraps the template-content) + * in which you can set inputs and links to display special actions for an admin + * for the given course. + * @param $course_id + * @param null $values + * @return null|Flex_Template + */ + public function getAdminCourseActionTemplate($course_id, $values = null); +} diff --git a/lib/plugins/core/AdminCourseContents.class.php b/lib/plugins/core/AdminCourseContents.class.php new file mode 100644 index 0000000..065cc9e --- /dev/null +++ b/lib/plugins/core/AdminCourseContents.class.php @@ -0,0 +1,23 @@ +<?php + +/** + * Interface AdminCourseContents + * With this interface a plugin is able to add columns to the course-overview table for admins and roots. + */ +interface AdminCourseContents +{ + /** + * The available columns for the course-overview table for admins. Index is the identifier of the column + * for the method adminAreaGetCourseContent. The value is the display name of the column. + * @return array : an associative array like array('index' => _("Translated display name")) + */ + public function adminAvailableContents(); + + /** + * Returns the value of the additional column for the course-overview table in the admin-area. + * @param Course $course : A Course-object of the given ... course + * @param string $index : the index that comes from adminAvailableContents to identify the column. + * @return Flexi_Template | String : Either one will do, but string is preferred, because it can exported as CSV-file more easily. + */ + public function adminAreaGetCourseContent($course, $index); +} diff --git a/lib/plugins/core/AdministrationPlugin.class.php b/lib/plugins/core/AdministrationPlugin.class.php new file mode 100644 index 0000000..c9aa8da --- /dev/null +++ b/lib/plugins/core/AdministrationPlugin.class.php @@ -0,0 +1,19 @@ +<?php +# Lifter010: TODO +/* + * AdministrationPlugin.class.php - administration plugin interface + * + * NOTE: This interface is deprecated, use SystemPlugin instead. + * + * Copyright (c) 2008 - Marcus Lunzenauer <mlunzena@uos.de> + * 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. + */ + +interface AdministrationPlugin +{ +} diff --git a/lib/plugins/core/CorePlugin.php b/lib/plugins/core/CorePlugin.php new file mode 100644 index 0000000..e16b77a --- /dev/null +++ b/lib/plugins/core/CorePlugin.php @@ -0,0 +1,151 @@ +<?php +/** + * CorePlugin.class.php - base class + * + * @author André Noack <noack@data-quest.de> + * @copyright 2021 Authors + * @license GPL2 or any later version + */ +abstract class CorePlugin +{ + + /** + * plugin meta data + */ + protected $plugin_info; + + /** + * plugin constructor + * TODO bindtextdomain() + */ + public function __construct() + { + $plugin_manager = PluginManager::getInstance(); + $this->plugin_info = $plugin_manager->getPluginInfo(static::class); + } + + /** + * Return the ID of this plugin. + */ + public function getPluginId() + { + return $this->plugin_info['id']; + } + + public function isEnabled() + { + return $this->plugin_info['enabled']; + } + + /** + * Return the name of this plugin. + */ + public function getPluginName() + { + return $this->plugin_info['name']; + } + + + public function getPluginURL() + { + return $GLOBALS['ABSOLUTE_URI_STUDIP']; + } + + /** + * Returns the version of this plugin as defined in manifest. + * @return string + */ + public function getPluginVersion() + { + return ''; + } + + /** + * Checks if the plugin is a core-plugin. Returns true if this is the case. + * + * @return boolean + */ + public function isCorePlugin() + { + return true; + } + + /** + * Get the activation status of this plugin in the given context. + * This also checks the plugin default activations. + * + * @param $context context range id (optional) + */ + public function isActivated($context = null) + { + $plugin_id = $this->getPluginId(); + $plugin_manager = PluginManager::getInstance(); + + if (!isset($context)) { + $context = Context::getId(); + } + $activated = $plugin_manager->isPluginActivated($plugin_id, $context); + return $activated; + } + + /** + * Returns whether the plugin may be activated in a certain context. + * + * @param Range $context + * @return bool + */ + public function isActivatableForContext(Range $context) + { + return true; + } + + /** + * Callback function called after enabling a plugin. + * The plugin's ID is transmitted for convenience. + * + * @param $plugin_id string The ID of the plugin just enabled. + */ + public static function onEnable($plugin_id) + { + } + + /** + * Callback function called after disabling a plugin. + * The plugin's ID is transmitted for convenience. + * + * @param $plugin_id string The ID of the plugin just disabled. + */ + public static function onDisable($plugin_id) + { + } + + /** + * Callback function called after enabling a plugin. + * The plugin's ID is transmitted for convenience. + * + * @param $plugin_id string The ID of the plugin just enabled. + */ + public static function onActivation($plugin_id, $range_id) + { + } + + /** + * Callback function called after disabling a plugin. + * The plugin's ID is transmitted for convenience. + * + * @param $plugin_id string The ID of the plugin just disabled. + */ + public static function onDeactivation($plugin_id, $range_id) + { + } + + /** + * @param $range_id string + * @return bool + */ + public static function checkActivation($range_id) + { + $core_plugin = PluginEngine::getPlugin(static::class); + return $core_plugin && $core_plugin->isActivated($range_id); + } +} diff --git a/lib/plugins/core/DetailspagePlugin.class.php b/lib/plugins/core/DetailspagePlugin.class.php new file mode 100644 index 0000000..15eda09 --- /dev/null +++ b/lib/plugins/core/DetailspagePlugin.class.php @@ -0,0 +1,27 @@ +<?php +/* + * DetailspagePlugin.class.php + * + * Copyright (c) 2019 - Rasmus Fuhse <fuhse@data-quest.de> + * + * 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. + */ +interface DetailspagePlugin +{ + /** + * Return a template (an instance of the Flexi_Template class) + * to be rendered on the details page. Return NULL to + * render nothing for this plugin or this course. + * + * The template will automatically get a standard layout, which + * can be configured via attributes set on the template: + * + * title title to display, defaults to plugin name + * + * @return object template object to render or NULL + */ + public function getDetailspageTemplate($course); +} diff --git a/lib/plugins/core/FileUploadHook.class.php b/lib/plugins/core/FileUploadHook.class.php new file mode 100644 index 0000000..27323fd --- /dev/null +++ b/lib/plugins/core/FileUploadHook.class.php @@ -0,0 +1,12 @@ +<?php +interface FileUploadHook +{ + /** + * If this method returns a URL the new page will be shown right after adding a file + * into a filesystem. + * + * @param $file_ref + * @return null|string: URL or null if no page should be added + */ + public function getAdditionalUploadWizardPage($file_ref); +} diff --git a/lib/plugins/core/FilesystemPlugin.class.php b/lib/plugins/core/FilesystemPlugin.class.php new file mode 100644 index 0000000..6d4c46c --- /dev/null +++ b/lib/plugins/core/FilesystemPlugin.class.php @@ -0,0 +1,82 @@ +<?php +interface FilesystemPlugin +{ + /** + * Returns a Navigation-object. Only the title and the image will be used. + * + * @return null|Navigation with title and image + */ + public function getFileSelectNavigation(); + + /** + * Returns an URL to a page, where the filesystem can be configured. + * + * @return mixed + */ + public function filesystemConfigurationURL(); + + /** + * Determines if this filesystem plugin should be a source for copying or a search. + * This may be dependend on the current user and his/her configurations. + * + * @return boolean + */ + public function isSource(); + + /** + * Determines if this filesystem-plugin should show up as a personal file-area and be a destination + * for copied files. + * This may be dependend on the current user and his/her configurations. + * + * @return boolean + */ + public function isPersonalFileArea(); + + /** + * This method is used to get a folder-object for this plugin. + * Not recommended but still possible is to return a Flexi_Template for the folder, if you want to + * take care of the frontend of displaying the folder as well. + * + * @param null $folder_id : folder_id of folder to get or null if you want the top-folder + * @return FolderType|Flexi_Template + */ + public function getFolder($folder_id = null); + + /** + * @param $file_id : The id for the file in the given filesystem of the plugin. + * @return array : the already prepared File just like a file-upload-array + */ + public function getPreparedFile($file_id, $with_blob = false); + + /** + * Defines if the filesystem-plugin has a search-function. + * + * @return mixed + */ + public function hasSearch(); + + /** + * Returns an array for each special search parameter. Each parameter is itself represented by as associative array + * like + * array( + * 'name' => "name of this parameter in the form", + * 'type' => "one of 'text', 'checkbox', 'select'", + * 'options' => array() //only neccesary if type is 'select' - a key-value array with the key key as the value of the select and the value as the label of the option + * 'placeholder' => "only possible for type 'text' but not mandatory" + * ) + * This method can also return an empty array or null if no search parameters are needed or no search is provided at all. + * + * @return null|array(array(), ...) + */ + public function getSearchParameters(); + + /** + * Returns a virtual folder that 'contains' all the files as a search-result. Only return null + * if search is not implemented. + * + * @param string $text a string + * @param array $parameters : an associative array of additional search parameters as defined in getSearchParameters() + * @return FolderType|null + */ + public function search($text, $parameters = []); +} diff --git a/lib/plugins/core/ForumModule.class.php b/lib/plugins/core/ForumModule.class.php new file mode 100644 index 0000000..33784db --- /dev/null +++ b/lib/plugins/core/ForumModule.class.php @@ -0,0 +1,140 @@ +<?php +/** + * ForumModule.class.php - Interface for all intersections between the Stud.IP + * Core and something that behaves like a forum + * + * Implement all interface methods and you can integrate your plugin like + * a real core-module into Stud.IP + * + * 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 3 of + * the License, or (at your option) any later version. + * + * @author Till Glöggler <tgloeggl@uos.de> + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 + * @category Stud.IP + */ + +interface ForumModule extends StandardPlugin +{ + /** + * Issues can be connected with an entry in a forum. This method + * has to return an url to the connected topic for the passed issue_id. + * If no topic is connected, it has to return "false" + * + * @param string $issue_id + * @return mixed URL or false + */ + function getLinkToThread($issue_id); + + /** + * This method is called in case of an creation OR an update of an issue. + * Normally one would update the title and the content of the linked topic + * when called + * + * @param string $issue_id + * @param string $title the title of the issue + * @param string $content the description of the issue + */ + function setThreadForIssue($issue_id, $title, $content); + + /** + * Return the number of postings the connected topic contains for + * the issue with the passed id + * + * @param type $issue_id + * + * @return int + */ + function getNumberOfPostingsForIssue($issue_id); + + /** + * Return the number of postings for the passed user + * + * @param type $user_id + * + * @return int + */ + function getNumberOfPostingsForUser($user_id); + + /** + * Return the number of postings for the passed seminar + * + * @param type $seminar_id + * + * @return int + */ + function getNumberOfPostingsForSeminar($seminar_id); + + /** + * Return the number of all postings served by your module. The + * results are used for statistics. + * + * @return int + */ + function getNumberOfPostings(); + + /** + * This function is called whenever Stud.IP needs to directly operate + * on your entries-table. Your entries-table MUST have at least fields + * for a date (a change-date is preferred, but make-date will suffice), + * posting-content, seminar_id and user_id. + * + * The returning array must have the following structure: + * Array ( + * 'table' => 'your_entry_table, + * 'content' => 'your_content_field', + * 'chdate' => 'your_date_field', + * 'seminar_id' => 'your_seminar_id_field', + * 'user_id' => 'your_user_id_field' + * ) + * + * @return array + */ + function getEntryTableInfo(); + + /** + * The caller expects an array of the ten seminars with the most postings + * in your module. + * + * Return an array of the following structure: + * Array ( + * Array ( + * 'seminar_id' => + * 'display' => + * 'count' => + * ) + * ) + * + * @return array + */ + function getTopTenSeminars(); + + /** + * Is called when the data of a user is moved to another user. + * Update all user_ids with the passed new one. + * + * @param string $user_from the user_id of the user who has the data + * @param string $user_to the user_id of the user who shall receive the data + */ + function migrateUser($user_from, $user_to); + + /** + * Clean up everything for the passed seminar, because the seminar + * is beeing deleted. + * + * @param string $seminar_id + */ + function deleteContents($seminar_id); + + /** + * Return a complete HTML-Dump of all entries in the forum-module. This is + * used for archiving purposes, so make it pretty! + * + * @param string $seminar_id + * + * @return string a single-page HTML-view of all contents in one string + */ + function getDump($seminar_id); +}
\ No newline at end of file diff --git a/lib/plugins/core/HomepagePlugin.class.php b/lib/plugins/core/HomepagePlugin.class.php new file mode 100644 index 0000000..74d0204 --- /dev/null +++ b/lib/plugins/core/HomepagePlugin.class.php @@ -0,0 +1,33 @@ +<?php +# Lifter010: TODO +/* + * HomepagePlugin.class.php - home page plugin interface + * + * Copyright (c) 2008 - Marcus Lunzenauer <mlunzena@uos.de> + * 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. + */ + +interface HomepagePlugin +{ + /** + * Return a template (an instance of the Flexi_Template class) + * to be rendered on the given user's home page. Return NULL to + * render nothing for this plugin. + * + * The template will automatically get a standard layout, which + * can be configured via attributes set on the template: + * + * title title to display, defaults to plugin name + * icon_url icon for this plugin (if any) + * admin_url admin link for this plugin (if any) + * admin_title title for admin link (default: Administration) + * + * @return object template object to render or NULL + */ + function getHomepageTemplate($user_id); +} diff --git a/lib/plugins/core/LibraryPlugin.class.php b/lib/plugins/core/LibraryPlugin.class.php new file mode 100644 index 0000000..a7fb873 --- /dev/null +++ b/lib/plugins/core/LibraryPlugin.class.php @@ -0,0 +1,55 @@ +<?php + + +/* + * LibraryPlugin.class.php - A plugin class for library plugins. + * + * Copyright (c) 2020 Moritz Strohm + * + * 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. + */ + + +interface LibraryPlugin +{ + /** + * Generates the URL that leads to the plugin action to create a request. + * The URL may vary depending on the ID of the library file that shall be + * requested. Therefore, the library file ID is passed to this method. + * + * @param $library_file_id The library file ID to which a request URL shall + * be generated. + * + * @returns string The URL for the request action of the plugin. + */ + public function getRequestURL(string $library_file_id) : string; + + + /** + * Generates the title for the plugin action to create a request. + * That title may vary depending on the library file that shall be requested. + * Therefore, the file is passed to this method. + * + * @param LibraryFile $file The file to which the request URL title shall be + * generated. + * + * @returns string The title for the request URL action of the plugin. + */ + public function getRequestTitle() : string; + + + /** + * Generates the icon for the plugin action to create a request. + * That icon may vary depending on the library file that shall be requested. + * Therefore, the file is passed to this method. + * + * @param LibraryFile $file The file to which the request URL icon shall be + * generated. + * + * @returns Icon The icon for the request URL action of the plugin. + */ + public function getRequestIcon() : Icon; +} diff --git a/lib/plugins/core/MetricsPlugin.class.php b/lib/plugins/core/MetricsPlugin.class.php new file mode 100644 index 0000000..95edaf1 --- /dev/null +++ b/lib/plugins/core/MetricsPlugin.class.php @@ -0,0 +1,23 @@ +<?php +/* + * MetricsPlugins take countings and measurements and transfer them to + * a specific backend like statsd. + * + * 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. + * + * @author <mlunzena@uos.de> + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ + +interface MetricsPlugin +{ + public function count($stat, $value, $sampleRate = null); + + public function timing($stat, $time, $sampleRate = null); + + public function gauge($stat, $value, $sampleRate = null); +} diff --git a/lib/plugins/core/PluginAssetsTrait.php b/lib/plugins/core/PluginAssetsTrait.php new file mode 100644 index 0000000..326809c --- /dev/null +++ b/lib/plugins/core/PluginAssetsTrait.php @@ -0,0 +1,215 @@ +<?php +/** + * Trait for assets handling in plugins. + * + * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> + * @license GPL2 or any later version + * @since Stud.IP 4.4 + */ +trait PluginAssetsTrait +{ + /** + * Adds many stylesheeets at once. + * @param array $filenames List of relative filenames + * @param array $variables Optional array of variables to pass to the + * LESS compiler + * @param array $link_attr Attributes to pass to the link elements + * @param string $path Common path prefix for all filenames + */ + protected function addStylesheets(array $filenames, array $variables = [], array $link_attr = [], $path = '') + { + if (Studip\ENV === 'development') { + foreach ($filenames as $filename) { + $this->addStylesheet("{$path}{$filename}", $variables, $link_attr); + } + } + + $hash = substr(md5(serialize($filenames)), -8); + $filename = "combined-{$hash}.css"; + + // Get asset file from storage + $asset = Assets\Storage::getFactory()->createCSSFile( + $filename, + $this->createMetaData() + ); + + // Compile asset if neccessary + if ($asset->isNew()) { + $content = ''; + foreach ($filenames as $filename) { + $file = $this->resolveFilename($filename, $path); + $content .= $this->readPluginAssetFile($file, $variables); + } + $asset->setContent($content); + } + + $this->includeStyleAsset($asset, $link_attr); + } + + /** + * Includes given stylesheet in page, compiles less if neccessary + * + * @param string $filename Name of the stylesheet (css or less) to include + * (relative to plugin directory) + * @param array $variables Optional array of variables to pass to the + * LESS compiler + * @param array $link_attr Attributes to pass to the link element + */ + protected function addStylesheet($filename, array $variables = [], array $link_attr = []) + { + $extension = pathinfo($filename, PATHINFO_EXTENSION); + if (!in_array($extension, ['less', 'scss'])) { + PageLayout::addStylesheet( + "{$this->getPluginURL()}/{$filename}?v={$this->getPluginVersion()}", + $link_attr + ); + return; + } + + // Create absolute path to assets file + $file = $this->resolveFilename($filename); + + // Get asset file from storage + $asset = Assets\Storage::getFactory()->createCSSFile( + $file, + $this->createMetaData() + ); + + // Compile asset if neccessary + if ($asset->isNew()) { + $css = $this->readPluginAssetFile($file, $variables); + $asset->setContent($css); + } + + $this->includeStyleAsset($asset, $link_attr); + } + + private function includeStyleAsset(Assets\PluginAsset $asset, array $link_attr) + { + // Include asset in page by reference or directly + $download_uri = $asset->getDownloadLink(); + if ($download_uri === false) { + PageLayout::addStyle($asset->getContent(), $link_attr); + } else { + $link_attr['rel'] = 'stylesheet'; + $link_attr['href'] = $download_uri; + $link_attr['type'] = 'text/css'; + PageLayout::addHeadElement('link', $link_attr); + } + } + + /** + * Adds many scripts at once. + * @param array $filenames List of relative filenames + * @param array $link_attr Attributes to pass to the script elements + * @param string $path Common path prefix for all filenames + */ + protected function addScripts(array $filenames, array $link_attr = [], $path = '') + { + if (Studip\ENV === 'development') { + foreach ($filenames as $filename) { + $this->addScript("{$path}{$filename}", $link_attr); + } + return; + } + + $hash = substr(md5(serialize($filenames)), -8); + $filename = "combined-{$hash}.js"; + + // Get asset file from storage + $asset = Assets\Storage::getFactory()->createJSFile( + $filename, + $this->createMetaData() + ); + + // Compile asset if neccessary + if ($asset->isNew()) { + $content = ''; + foreach ($filenames as $filename) { + $file = $this->resolveFilename($filename, $path); + $content .= $this->readPluginAssetFile($file) . ';'; + } + $asset->setContent($content); + } + + // Include asset in page by reference or directly + $download_uri = $asset->getDownloadLink(); + if ($download_uri === false) { + PageLayout::addHeadElement('script', $link_attr, $asset->getContent()); + } else { + $link_attr['src'] = $download_uri; + PageLayout::addHeadElement('script', $link_attr); + } + } + + /** + * Includes given script in page. + * + * @param string $filename Name of script file + * @param array $link_attr Attributes to pass to the script element + */ + protected function addScript($filename, array $link_attr = []) + { + PageLayout::addScript( + "{$this->getPluginURL()}/{$filename}?v={$this->getPluginVersion()}", + $link_attr + ); + } + + /** + * Create metadata for plugin assets factory + * @return array + */ + private function createMetaData() + { + return [ + 'plugin_id' => $this->plugin_info['depends'] ?: $this->getPluginId(), + 'plugin_version' => $this->getPluginVersion(), + ]; + } + + /** + * Resolves relative filename to absolute filename. + * + * @param string $filename Relative filename + * @param string $path Optional relative path the file is stored in + * @return string + * @throws RuntimeException when absolute file is missing + */ + private function resolveFilename($filename, $path = '') + { + $file = $GLOBALS['ABSOLUTE_PATH_STUDIP'] + . $this->getPluginPath() . '/' + . "{$path}{$filename}"; + + // Fail if file does not exist + if (!file_exists($file)) { + throw new RuntimeException("Could not locate assets file '{$filename}'"); + } + + return $file; + } + + /** + * Reads assets file (and compiles if neccessary). + * @param string $filename Name of the file to read + * @param array $variables Additional variables for compiler (if appropriate) + * @return string + */ + private function readPluginAssetFile($filename, array $variables = []) + { + $contents = file_get_contents($filename); + + $extension = pathinfo($filename, PATHINFO_EXTENSION); + if ($extension === 'less') { + $contents = Assets\LESSCompiler::getInstance()->compile($contents, $variables + [ + 'plugin-path' => $this->getPluginURL(), + ]); + } elseif ($extension === 'scss') { + $contents = Assets\SASSCompiler::getInstance()->compile($contents, $variables + [ + 'plugin-path' => '"' . $this->getPluginURL() . '"', + ]); + } + return $contents; + } +} diff --git a/lib/plugins/core/PortalPlugin.class.php b/lib/plugins/core/PortalPlugin.class.php new file mode 100644 index 0000000..ed157a8 --- /dev/null +++ b/lib/plugins/core/PortalPlugin.class.php @@ -0,0 +1,33 @@ +<?php +# Lifter010: TODO +/* + * PortalPlugin.class.php - start / portal page plugin interface + * + * Copyright (c) 2008 - Marcus Lunzenauer <mlunzena@uos.de> + * 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. + */ + +interface PortalPlugin +{ + /** + * Return a template (an instance of the Flexi_Template class) + * to be rendered on the start or portal page. Return NULL to + * render nothing for this plugin. + * + * The template will automatically get a standard layout, which + * can be configured via attributes set on the template: + * + * title title to display, defaults to plugin name + * icon_url icon for this plugin (if any) + * admin_url admin link for this plugin (if any) + * admin_title title for admin link (default: Administration) + * + * @return object template object to render or NULL + */ + function getPortalTemplate(); +} diff --git a/lib/plugins/core/PrivacyPlugin.class.php b/lib/plugins/core/PrivacyPlugin.class.php new file mode 100644 index 0000000..73a971d --- /dev/null +++ b/lib/plugins/core/PrivacyPlugin.class.php @@ -0,0 +1,24 @@ +<?php +/** + * PrivacyPlugin are able to handle user data according the privacy policy. + * + * 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. + * + * @author Timo Hartge <hartge@data-quest.de> + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ + +interface PrivacyPlugin +{ + /** + * Export available data of a given user into a storage object + * (an instance of the StoredUserData class) for that user. + * + * @param StoredUserData $storage object to store data into + */ + public function exportUserData(StoredUserData $storage); +} diff --git a/lib/plugins/core/QuestionnaireAssignmentPlugin.class.php b/lib/plugins/core/QuestionnaireAssignmentPlugin.class.php new file mode 100644 index 0000000..6892f8a --- /dev/null +++ b/lib/plugins/core/QuestionnaireAssignmentPlugin.class.php @@ -0,0 +1,55 @@ +<?php + +/** + * Interface QuestionnaireAssignmentPlugin + * Implement this interface if you want to relate Stud.IP-questionnaires to your plugin-contents. + * By storing an assignment you should set the range_type to something unique that is + * related to your plugin like "mytaskplugin". In all methods you should first check if the + * assignment is related to your plugin, because other QuestionnaireAssignmentPlugins might + * be installed as well. + */ +interface QuestionnaireAssignmentPlugin +{ + /** + * Returns if the questionnaire is viewable dependend on the assignment. Check for the range_type + * and range_id to see if the the assignment has to do with your plugin (and not with + * somebody else's plugin) and if type and id fit. + * @param QuestionnaireAssignment $questionnaire + * @return boolean + */ + public function isQuestionnaireViewable(QuestionnaireAssignment $questionnaire); + + /** + * Returns if the questionnaire is editable dependend on the assignment. Check for the range_type + * and range_id to see if the the assignment has to do with your plugin (and not with + * somebody else's plugin) and if type and id fit. + * @param QuestionnaireAssignment $questionnaire + * @return boolean + */ + public function isQuestionnaireEditable(QuestionnaireAssignment $questionnaire); + + /** + * The display name of the assignment. + * @param QuestionnaireAssignment $questionnaire + * @return string + */ + public function getQuestionnaireAssignmentName(QuestionnaireAssignment $questionnaire); + + /** + * This template will get displayed when someone at tools -> questionnaires + * wants to edit the contexts of the questionnaire. Maybe you don't want to provide a + * template here, so return null or just a readonly html-snippet. + * @param Questionnaire $questionnaire + * @return null|Flexi_Template + */ + public function getQuestionnaireAssignmentEditTemplate(Questionnaire $questionnaire); + + /** + * When the context of the questionnaire is stored at tools -> questionnaires (where + * the template from getQuestionnaireAssignmentEditTemplate was displayed) you should + * use this method to store your assignments as well. + * @param Questionnaire $questionnaire + * @return null + */ + public function storeQuestionnaireAssignments(Questionnaire $questionnaire); +} diff --git a/lib/plugins/core/RESTAPIPlugin.class.php b/lib/plugins/core/RESTAPIPlugin.class.php new file mode 100644 index 0000000..d395dea --- /dev/null +++ b/lib/plugins/core/RESTAPIPlugin.class.php @@ -0,0 +1,23 @@ +<?php +/* + * REST-API Plugins add maps to the REST-API router. + * + * Copyright (c) 2014 - Marcus Lunzenauer <mlunzena@uos.de> + * + * 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. + */ + +interface RESTAPIPlugin +{ + /** + * Returns one or more instances of RESTAPI\RouteMap to register + * to the Router. + * + * @return RouteMap|Array either a single instance of class + * RouteMap or an array of them + */ + public function getRouteMaps(); +} diff --git a/lib/plugins/core/Role.class.php b/lib/plugins/core/Role.class.php new file mode 100644 index 0000000..3ca8a11 --- /dev/null +++ b/lib/plugins/core/Role.class.php @@ -0,0 +1,89 @@ +<?php +/** + * Role.class.php + * + * @author Dennis Reil <dennis.reil@offis.de> + * @author Michael Riehemann <michael.riehemann@uni-oldenburg.de> + * @package pluginengine + * @subpackage core + * @copyright 2009 Stud.IP + * @license http://www.gnu.org/licenses/gpl.html GPL Licence 3 + */ +class Role +{ + const UNKNOWN_ROLE_ID = null; + + public $roleid; + public $rolename; + public $systemtype; + + /** + * Constructor + */ + public function __construct($id = self::UNKNOWN_ROLE_ID, $name = '', $system = false) + { + $this->setRoleid($id); + $this->setRolename($name); + $this->setSystemtype($system); + } + + /** + * Returns the role's id. + * + * @return int + */ + public function getRoleid() + { + return $this->roleid; + } + + /** + * Set the role's id. + * + * @param int $newid + */ + public function setRoleid($newid) + { + $this->roleid = $newid; + } + + /** + * Returns the role's name. + * + * @return string + */ + public function getRolename() + { + return $this->rolename; + } + + /** + * Set the role's name. + * + * @param string $newrole + */ + public function setRolename($newrole) + { + $this->rolename = $newrole; + } + + /** + * Returns whether the role is a system role. + * + * @return boolean + */ + public function getSystemtype() + { + return $this->systemtype; + } + + /** + * Sets whether the role is a system role. + * + * @param boolean $newtype + */ + public function setSystemtype($newtype) + { + $this->systemtype = (bool) $newtype; + } +} diff --git a/lib/plugins/core/ScorePlugin.class.php b/lib/plugins/core/ScorePlugin.class.php new file mode 100644 index 0000000..064ddc3 --- /dev/null +++ b/lib/plugins/core/ScorePlugin.class.php @@ -0,0 +1,29 @@ +<?php +/* + * 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. + */ + +interface ScorePlugin +{ + /** + * Returns null or an array of associated arrays - each one to + * indicate a db-table in which the plugin stores user activities. + * + * For a bullitin-board-plugin this array could look like this: + * + * return array( + * array( + * 'table' => "bullitin_board_entries", + * 'user_id_column' => "user_id", + * 'date_column' => "mkdate", + * 'where' => "public = '1'" //only public entries should be counted + * ) + * ); + * + * @return null|array of associated arrays + */ + function getPluginActivityTables(); +} diff --git a/lib/plugins/core/StandardPlugin.class.php b/lib/plugins/core/StandardPlugin.class.php new file mode 100644 index 0000000..960ffd0 --- /dev/null +++ b/lib/plugins/core/StandardPlugin.class.php @@ -0,0 +1,17 @@ +<?php +# Lifter010: TODO +/* + * StandardPlugin.class.php - course or institute plugin interface + * + * Copyright (c) 2008 - Marcus Lunzenauer <mlunzena@uos.de> + * 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. + */ + +interface StandardPlugin extends StudipModule +{ +} diff --git a/lib/plugins/core/StudIPPlugin.class.php b/lib/plugins/core/StudIPPlugin.class.php new file mode 100644 index 0000000..2fa9703 --- /dev/null +++ b/lib/plugins/core/StudIPPlugin.class.php @@ -0,0 +1,218 @@ +<?php +/** + * StudIPPlugin.class.php - generic plugin base class + * + * @author Elmar Ludwig <ludwig@uos.de> + * @copyright 2009 Authors + * @license GPL2 or any later version + */ +abstract class StudIPPlugin +{ + use PluginAssetsTrait; + use TranslatablePluginTrait; + + /** + * plugin meta data + */ + protected $plugin_info; + + /** + * Plugin manifest + */ + protected $manifest = null; + + /** + * plugin constructor + */ + public function __construct() + { + $plugin_manager = PluginManager::getInstance(); + $this->plugin_info = $plugin_manager->getPluginInfo(static::class); + } + + /** + * Return the ID of this plugin. + */ + public function getPluginId() + { + return $this->plugin_info['id']; + } + + public function isEnabled() + { + return $this->plugin_info['enabled']; + } + + /** + * Return the name of this plugin. + */ + public function getPluginName() + { + return $this->plugin_info['name']; + } + + /** + * Return the filesystem path to this plugin. + */ + public function getPluginPath() + { + return "plugins_packages/{$this->plugin_info['path']}"; + } + + /** + * Return the URL of this plugin. Can be used to refer to resources + * (images, style sheets, etc.) inside the installed plugin package. + */ + public function getPluginURL() + { + return $GLOBALS['ABSOLUTE_URI_STUDIP'] . $this->getPluginPath(); + } + + /** + * Return metadata stored in the manifest of this plugin. + */ + public function getMetadata() + { + if ($this->manifest === null) { + $plugin_manager = PluginManager::getInstance(); + $this->manifest = $plugin_manager->getPluginManifest($this->getPluginPath()); + } + return $this->manifest; + } + + /** + * Returns the version of this plugin as defined in manifest. + * @return string + */ + public function getPluginVersion() + { + return $this->getMetadata()['version']; + } + + /** + * Checks if the plugin is a core-plugin. Returns true if this is the case. + * + * @return boolean + */ + public function isCorePlugin() + { + return $this->plugin_info['core']; + } + + /** + * Get the activation status of this plugin in the given context. + * This also checks the plugin default activations. + * + * @param $context context range id (optional) + * @param $type type of activation (optional), can be set to 'user' + * in order to point to a homepage plugin + */ + public function isActivated($context = null, $type = 'sem') + { + $plugin_id = $this->getPluginId(); + $plugin_manager = PluginManager::getInstance(); + + /* + * Context can be a Seminar ID or the current user ID if not set. + * Identification is done via the "username" parameter. + */ + if (!isset($context)) { + if ($type === 'user') { + $context = get_userid(Request::username('username', $GLOBALS['user']->username)); + } else { + $context = Context::getId(); + } + } + + if ($type === 'user') { + $activated = $plugin_manager->isPluginActivatedForUser($plugin_id, $context); + } else { + $activated = $plugin_manager->isPluginActivated($plugin_id, $context); + } + + return $activated; + } + + /** + * Returns whether the plugin may be activated in a certain context. + * + * @param Range $context + * @return bool + */ + public function isActivatableForContext(Range $context) + { + return true; + } + + /** + * This method dispatches all actions. + * + * @param string part of the dispatch path that was not consumed + * + * @return void + */ + public function perform($unconsumed_path) + { + $args = explode('/', $unconsumed_path); + $action = $args[0] !== '' ? array_shift($args).'_action' : 'show_action'; + + if (!method_exists($this, $action)) { + $trails_root = $this->getPluginPath(); + $trails_uri = rtrim(PluginEngine::getLink($this, [], null, true), '/'); + + $dispatcher = new Trails_Dispatcher($trails_root, $trails_uri, 'index'); + $dispatcher->current_plugin = $this; + try { + $dispatcher->dispatch($unconsumed_path); + } catch (Trails_UnknownAction $exception) { + if (count($args) > 0) { + throw $exception; + } else { + throw new Exception(_('unbekannte Plugin-Aktion: ') . $unconsumed_path); + } + } + } else { + call_user_func_array([$this, $action], $args); + } + } + + /** + * Callback function called after enabling a plugin. + * The plugin's ID is transmitted for convenience. + * + * @param $plugin_id string The ID of the plugin just enabled. + */ + public static function onEnable($plugin_id) + { + } + + /** + * Callback function called after disabling a plugin. + * The plugin's ID is transmitted for convenience. + * + * @param $plugin_id string The ID of the plugin just disabled. + */ + public static function onDisable($plugin_id) + { + } + + /** + * Callback function called after enabling a plugin. + * The plugin's ID is transmitted for convenience. + * + * @param $plugin_id string The ID of the plugin just enabled. + */ + public static function onActivation($plugin_id, $range_id) + { + } + + /** + * Callback function called after disabling a plugin. + * The plugin's ID is transmitted for convenience. + * + * @param $plugin_id string The ID of the plugin just disabled. + */ + public static function onDeactivation($plugin_id, $range_id) + { + } +} diff --git a/lib/plugins/core/SystemPlugin.class.php b/lib/plugins/core/SystemPlugin.class.php new file mode 100644 index 0000000..c204567 --- /dev/null +++ b/lib/plugins/core/SystemPlugin.class.php @@ -0,0 +1,17 @@ +<?php +# Lifter010: TODO +/* + * SystemPlugin.class.php - generic system plugin interface + * + * Copyright (c) 2008 - Marcus Lunzenauer <mlunzena@uos.de> + * 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. + */ + +interface SystemPlugin +{ +} diff --git a/lib/plugins/core/TranslatablePluginTrait.php b/lib/plugins/core/TranslatablePluginTrait.php new file mode 100644 index 0000000..13e0903 --- /dev/null +++ b/lib/plugins/core/TranslatablePluginTrait.php @@ -0,0 +1,104 @@ +<?php +/** + * Trait used to allow plugins to be translated in a generic way. + * + * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> + * @license GPL2 or any later version + * @since Stud.IP 5.0 + */ +trait TranslatablePluginTrait +{ + protected $translation_domain = null; + + /** + * Initializes the translation for the plugin. + * + * @param string $domain + */ + protected function initializeTranslation($domain) + { + bindtextdomain($domain, $this->getPluginPath() . '/locale'); + bind_textdomain_codeset($domain, 'UTF-8'); + } + + /** + * Returns the defined translation domain from plugin manifest. If none + * is set, false is returned. + * + * @return false|string The translation domain from manifest, if set + */ + protected function getTranslationDomain() + { + if ($this->translation_domain === null) { + $manifest = $this->getMetadata(); + $this->translation_domain = $manifest['localedomain'] ?? false; + + if ($this->translation_domain !== false) { + $this->initializeTranslation($this->translation_domain); + } + } + return $this->translation_domain; + } + + /** + * Returns whether the plugin has a translation defined or not. + * + * @return bool + */ + public function hasTranslation() + { + return $this->getTranslationDomain() !== false; + } + + /** + * Plugin localization for a single string. + * + * @param string $string String to translate + * @return string + */ + public function _($string) + { + $domain = $this->getTranslationDomain(); + if (!$domain) { + return $string; + } + + $result = dgettext($domain, $string); + + // Fallback to possible translations from core system + if ($result === $string) { + $result = _($string); + } + + return $result; + } + + /** + * Plugin localization for plural strings. + * + * @param string $string0 String to translate (singular) + * @param string $string1 String to translate (plural) + * @param mixed $n Quantity factor (may be an array or array-like) + * @return string + */ + public function _n($string0, $string1, $n) + { + if (is_array($n)) { + $n = count($n); + } + + $domain = $this->getTranslationDomain(); + if (!$domain) { + return $n == 1 ? $string0 : $string1; + } + + $result = dngettext($domain, $string0, $string1, $n); + + // Fallback to possible translations from core system + if ($result === $string0 || $result === $string1) { + $result = ngettext($string0, $string1, $n); + } + + return $result; + } +} diff --git a/lib/plugins/core/WebServicePlugin.class.php b/lib/plugins/core/WebServicePlugin.class.php new file mode 100644 index 0000000..ff3e757 --- /dev/null +++ b/lib/plugins/core/WebServicePlugin.class.php @@ -0,0 +1,14 @@ +<?php +/* + * Copyright (c) 2011 <mlunzena@uos.de> + * + * 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. + */ + +interface WebServicePlugin +{ + function getWebServices(); +} diff --git a/lib/plugins/db/RolePersistence.class.php b/lib/plugins/db/RolePersistence.class.php new file mode 100644 index 0000000..358e8f1 --- /dev/null +++ b/lib/plugins/db/RolePersistence.class.php @@ -0,0 +1,517 @@ +<?php +/** + * RolePersistence.class.php + * + * Funktionen für das Rollenmanagement + * + * @author Dennis Reil <dennis.reil@offis.de> + * @author Michael Riehemann <michael.riehemann@uni-oldenburg.de> + * @package pluginengine + * @subpackage db + * @copyright 2009 Stud.IP + * @license http://www.gnu.org/licenses/gpl.html GPL Licence 3 + */ +class RolePersistence +{ + /** + * Returns all available roles. + * + * @return array Roles + */ + public static function getAllRoles() + { + // read cache + $cache = self::getRolesCache(); + + // cache miss, retrieve from database + if (count($cache) === 0) { + $query = "SELECT `roleid`, `rolename`, `system` = 'y' AS `is_system` + FROM `roles` + ORDER BY `rolename`"; + $statement = DBManager::get()->query($query); + $statement->setFetchMode(PDO::FETCH_ASSOC); + + foreach ($statement as $row) { + extract($row); + + $cache[$roleid] = new Role($roleid, $rolename, $is_system); + } + } + + return $cache->getArrayCopy(); + } + + public static function getRoleIdByName($name) + { + foreach (self::getAllRoles() as $id => $role) { + if ($role->getRolename() === $name) { + return $id; + } + } + return false; + } + + /** + * Inserts the role into the database or does an update, if it's already there + * + * @param Role $role + * @return the role id + */ + public static function saveRole($role) + { + // sweep roles cache, see #getAllRoles + self::expireCaches(); + + // role is not in database + $query = "INSERT INTO `roles` (`roleid`, `rolename`, `system`) + VALUES (?, ?, 'n') + ON DUPLICATE KEY UPDATE `rolename` = VALUES(`rolename`)"; + $statement = DBManager::get()->prepare($query); + $statement->execute([$role->getRoleId(), $role->getRolename()]); + + if ($role->getRoleid() === Role::UNKNOWN_ROLE_ID) { + $role_id = DBManager::get()->lastInsertId(); + $role->setRoleid($role_id); + + $event = 'RoleDidCreate'; + } else { + $event = 'RoleDidUpdate'; + } + + NotificationCenter::postNotification( + $event, + $role->getRoleid(), + $role->getRolename() + ); + + return $role->getRoleid(); + } + + /** + * Delete role if not a permanent role. System roles cannot be deleted. + * + * @param Role $role + */ + public static function deleteRole($role) + { + $id = $role->getRoleid(); + $name = $role->getRolename(); + + // sweep roles cache + self::expireCaches(); + + $query = "SELECT `pluginid` FROM `roles_plugins` WHERE `roleid` = ?"; + $statement = DBManager::get()->prepare($query); + $statement->execute([$id]); + $statement->setFetchMode(PDO::FETCH_COLUMN, 0); + + foreach ($statement as $plugin_id) { + unset(self::getPluginRolesCache()[$plugin_id]); + } + + DBManager::get()->execute( + "DELETE `roles`, `roles_user`, `roles_plugins`, `roles_studipperms` + FROM `roles` + LEFT JOIN `roles_user` USING (`roleid`) + LEFT JOIN `roles_plugins` USING (`roleid`) + LEFT JOIN `roles_studipperms` USING (`roleid`) + WHERE `roleid` = ? AND `system` = 'n'", + [$id] + ); + + NotificationCenter::postNotification('RoleDidDelete', $id, $name); + } + + /** + * Saves a role assignment to the database + * + * @param User $user + * @param Role $role + * @param string $institut_id + */ + public static function assignRole(User $user, $role, $institut_id = '') + { + // role is not in database + // save it to the database first + if ($role->getRoleid() !== Role::UNKNOWN_ROLE_ID) { + $roleid = self::saveRole($role); + } else { + $roleid = $role->getRoleid(); + } + + $query = "REPLACE INTO `roles_user` (`roleid`, `userid`, `institut_id`) + VALUES (?, ?, ?)"; + $statement = DBManager::get()->prepare($query); + $statement->execute([$roleid, $user->id, $institut_id]); + + unset(self::getUserRolesCache()[$user->id]); + + NotificationCenter::postNotification( + 'RoleAssignmentDidCreate', + $roleid, + $user->id, + $institut_id + ); + } + + /** + * Gets all assigned roles from the database for a user + * + * @param int $userid + * @param boolean $implicit + * @return array + */ + public static function getAssignedRoles($user_id, $implicit = false) + { + return array_intersect_key( + self::getAllRoles(), + self::loadUserRoles($user_id, $implicit) + ); + } + + /** + * Returns institutes for which the given user has the given role. + * @param string $user_id User id + * @param int $role_id Role id + * @return array of institute ids + */ + public static function getAssignedRoleInstitutes($user_id, $role_id) + { + $roles = self::loadUserRoles($user_id); + return isset($roles[$role_id]) + ? $roles[$role_id]['institutes'] + : []; + } + + /** + * Checks a role assignment for an user + * optionally check for institute + * + * @param string $userid + * @param string $assignedrole + * @param string $institut_id + * @return boolean + */ + public static function isAssignedRole($userid, $assignedrole, $institut_id = '') + { + $faculty_id = $institut_id + ? Institute::find($institut_id)->fakultaets_id + : null; + + $role_id = self::getRoleIdByName($assignedrole); + $user_roles = self::loadUserRoles($userid, true); + + return isset($user_roles[$role_id]) + ? ( + in_array($institut_id, $user_roles[$role_id]['institutes']) + || in_array($faculty_id, $user_roles[$role_id]['institutes']) + ) + : false; + } + + private static function loadUserRoles($user_id, $implicit = false) + { + $cache = self::getUserRolesCache(); + + if (!isset($cache[$user_id])) { + $query = "SELECT DISTINCT * + FROM ( + SELECT `roleid`, `institut_id`, 1 AS explicit + FROM `roles_user` + WHERE `userid` = :user_id + + UNION + + SELECT `roleid`, `fakultaets_id` AS `institut_id`, 1 AS explicit + FROM `roles_user` + JOIN `Institute` USING (`institut_id`) + WHERE `userid` = :user_id + + UNION + + SELECT `roleid`, '' AS institut_id, 0 AS explicit + FROM `roles_studipperms` + WHERE `permname` = :perm + ) AS tmp"; + $statement = DBManager::get()->prepare($query); + $statement->bindValue(':user_id', $user_id); + $statement->bindValue(':perm', $GLOBALS['perm']->get_perm($user_id)); + $statement->execute(); + $statement->setFetchMode(PDO::FETCH_ASSOC); + + $roles = []; + foreach ($statement as $row) { + if (!isset($roles[$row['roleid']])) { + $roles[$row['roleid']] = [ + 'institutes' => [], + 'explicit' => (bool) $row['explicit'], + ]; + } + $roles[$row['roleid']]['institutes'][] = $row['institut_id']; + } + + $cache[$user_id] = $roles; + } + return array_filter( + $cache[$user_id], + function ($role) use ($implicit) { + return $implicit || $role['explicit']; + } + ); + } + + /** + * Deletes a role assignment from the database + * + * @param User $user + * @param Role $role + * @param String $institut_id + */ + public static function deleteRoleAssignment(User $user, $role, $institut_id = null) + { + $query = "DELETE FROM `roles_user` + WHERE `roleid` = ? + AND `userid` = ? + AND `institut_id` = IFNULL(?, `institut_id`)"; + DBManager::get()->execute( + $query, + [$role->getRoleid(), $user->id, $institut_id] + ); + + unset(self::getUserRolesCache()[$user->id]); + + NotificationCenter::postNotification( + 'RoleAssignmentDidDelete', + $role->getRoleid(), + $user->id, + $institut_id + ); + } + + /** + * Get's all Role-Assignments for a certain user. + * If no user is set, all role assignments are returned. + * + * @param User $user + * @return array with roleids and the assigned userids + * @deprecated seems to be unused (and was corrupt for some versions) + */ + public static function getAllRoleAssignments($user = null) + { + $query = "SELECT `roleid`, `userid` + FROM `roles_user` + WHERE `userid` = IFNULL(?, `userid`)"; + return DBManager::get()->fetchPairs($query, [$user]); + } + + /** + * Enter description here... + * + * @param int $pluginid + * @param array $roleids + */ + public static function assignPluginRoles($plugin_id, $role_ids) + { + $plugin_id = (int) $plugin_id; + + unset(self::getPluginRolesCache()[$plugin_id]); + + $query = "REPLACE INTO `roles_plugins` (`roleid`, `pluginid`) + VALUES (:role_id, :plugin_id)"; + $statement = DBManager::get()->prepare($query); + $statement->bindValue(':plugin_id', $plugin_id); + + foreach ($role_ids as $role_id) { + $statement->bindValue(':role_id', $role_id); + $statement->execute(); + + NotificationCenter::postNotification( + 'PluginRoleAssignmentDidCreate', + $role_id, + $plugin_id + ); + + } + } + + /** + * Removes the given roles' assignments from the given plugin. + * + * @param int $pluginid + * @param array $roleids + */ + public static function deleteAssignedPluginRoles($plugin_id, $role_ids) + { + $plugin_id = (int) $plugin_id; + + unset(self::getPluginRolesCache()[$plugin_id]); + + $query = "DELETE FROM `roles_plugins` + WHERE `pluginid` = :plugin_id + AND `roleid` = :role_id"; + $statement = DBManager::get()->prepare($query); + $statement->bindValue(':plugin_id', $plugin_id); + + foreach ($role_ids as $role_id) { + $statement->bindValue(':role_id', $role_id); + $statement->execute(); + + NotificationCenter::postNotification( + 'PluginRoleAssignmentDidDelete', + $role_id, + $plugin_id + ); + + } + } + + /** + * Return all roles assigned to a plugin. + * + * @param int $pluginid + * @return array + */ + public static function getAssignedPluginRoles($plugin_id) + { + $plugin_id = (int) $plugin_id; + + // read plugin roles from cache + $cache = self::getPluginRolesCache(); + + // cache miss, retrieve roles from database + if (!isset($cache[$plugin_id])) { + $query = "SELECT `roleid` FROM `roles_plugins` WHERE `pluginid` = ?"; + $statement = DBManager::get()->prepare($query); + $statement->execute([$plugin_id]); + $role_ids = $statement->fetchAll(PDO::FETCH_COLUMN); + + // write to cache + $cache[$plugin_id] = $role_ids; + } + + $roles = self::getAllRoles(); + return array_filter(array_map( + function ($role_id) use ($roles) { + if (!isset($roles[$role_id])) { + return false; + } + return $roles[$role_id]; + }, + $cache[$plugin_id] + )); + } + + /** + * Returns statistic values for each role: + * + * - number of explicitely assigned users + * - number of implicitely assigned users + * - number of assigned plugins + * + * @return array + */ + public static function getStatistics() + { + // Get basic statistics + $query = "SELECT r.`roleid`, + COUNT(DISTINCT ru.`userid`) AS explicit, + COUNT(DISTINCT rp.`pluginid`) AS plugins + FROM roles AS r + -- Explicit assignment + LEFT JOIN `roles_user` AS ru + ON r.`roleid` = ru.`roleid` AND ru.`userid` IN (SELECT `user_id` FROM `auth_user_md5`) + -- Plugins + LEFT JOIN `roles_plugins` AS rp + ON r.`roleid` = rp.`roleid` AND rp.`pluginid` IN (SELECT `pluginid` FROM `plugins`) + GROUP BY r.`roleid`"; + $result = DBManager::get()->fetchGrouped($query); + + // Fetch implicit assignments in a second query due to performance + // reasons + foreach (self::countImplicitUsers(array_keys($result)) as $id => $count) { + $result[$id]['implicit'] = $count; + } + + return $result; + } + + /** + * Counts the implicitely assigned users for a role. + * @param mixed $role_id Role id or array of role ids + * @return mixed number of implictit for the role (if one role id is given) + * or associative array [role id => number of implicit users] + * when given a list of role ids + */ + public static function countImplicitUsers($role_id) + { + // Ensure that the result array has an entry for every role id + $result = array_fill_keys((array) $role_id, 0); + + $query = "SELECT rsp.`roleid`, COUNT(*) AS implicit + FROM `roles_studipperms` AS rsp + JOIN `auth_user_md5` AS a ON rsp.`permname` = a.`perms` + LEFT JOIN `roles_user` AS ru + ON a.`user_id` = ru.`userid` AND rsp.`roleid` = ru.`roleid` + WHERE rsp.`roleid` IN (?) + AND ru.`userid` IS NULL + GROUP BY rsp.`roleid`"; + $statement = DBManager::get()->prepare($query); + $statement->execute([$role_id]); + $statement->setFetchMode(PDO::FETCH_ASSOC); + + foreach ($statement as $row) { + $result[$row['roleid']] = (int) $row['implicit']; + } + + return is_array($role_id) + ? $result + : $result[$role_id]; + } + + // Cache operations + private static $roles_cache = null; + private static $user_roles_cache = null; + private static $plugin_roles_cache = null; + + private static function getRolesCache() + { + if (self::$roles_cache === null) { + self::$roles_cache = new StudipCachedArray( + '/Roles', + function () { + return 'All'; + }, + StudipCachedArray::ENCODE_SERIALIZE + ); + } + return self::$roles_cache; + } + + private static function getUserRolesCache() + { + if (self::$user_roles_cache === null) { + self::$user_roles_cache = new StudipCachedArray('/UserRoles'); + } + return self::$user_roles_cache; + } + + private static function getPluginRolesCache() + { + if (self::$plugin_roles_cache === null) { + self::$plugin_roles_cache = new StudipCachedArray('/PluginRoles'); + } + return self::$plugin_roles_cache; + } + + public static function expireCaches() + { + self::getRolesCache()->clear(); + self::getUserRolesCache()->clear(); + self::getPluginRolesCache()->clear(); + } + + public static function expireUserCache($user_id) + { + unset(self::getUserRolesCache()[$user_id]); + } +}
\ No newline at end of file diff --git a/lib/plugins/engine/PluginEngine.class.php b/lib/plugins/engine/PluginEngine.class.php new file mode 100644 index 0000000..9018cd8 --- /dev/null +++ b/lib/plugins/engine/PluginEngine.class.php @@ -0,0 +1,154 @@ +<?php +# Lifter007: TODO +# Lifter010: TODO +/** + * Factory Class for the plugin engine + * @author Dennis Reil, <dennis.reil@offis.de> + * @package pluginengine + * @subpackage engine + */ + +class PluginEngine +{ + /** + * This function maps an incoming request to a tuple + * (pluginclassname, unconsumed rest). + * + * @return array the above mentioned tuple + */ + public static function routeRequest($dispatch_to) + { + $dispatch_to = ltrim($dispatch_to, '/'); + if (mb_strlen($dispatch_to) === 0) { + throw new PluginNotFoundException(_('Es wurde kein Plugin gewählt.')); + } + $pos = mb_strpos($dispatch_to, '/'); + return $pos === FALSE + ? [$dispatch_to, ''] + : [mb_substr($dispatch_to, 0, $pos), mb_substr($dispatch_to, $pos + 1)]; + } + + /** + * Load the default set of plugins. This currently loads plugins of + * type Homepage, Standard (if a course is selected), Administration + * (if user has admin status) and System. The exact type of plugins + * loaded here may change in the future. + */ + public static function loadPlugins() + { + global $user, $perm; + + // load system plugins + self::getPlugins('SystemPlugin'); + + // load homepage plugins + self::getPlugins('HomepagePlugin'); + + // load course plugins + if (Context::getId()) { + self::getPlugins('StudipModule'); + self::getPlugins('StandardPlugin'); + } + + // load admin plugins + if (is_object($user) && $perm->have_perm('admin')) { + self::getPlugins('AdministrationPlugin'); + } + } + + /** + * Get instance of the plugin specified by plugin class name. + * + * @param $class class name of plugin + */ + public static function getPlugin ($class) + { + return PluginManager::getInstance()->getPlugin($class); + } + + /** + * 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 $type plugin type or NULL (all types) + * @param $context context range id (optional) + */ + public static function getPlugins ($type, $context = NULL) + { + return PluginManager::getInstance()->getPlugins($type, $context); + } + + /** + * Sends a message to all activated plugins of a type and returns an array of + * the return values. + * + * @param type plugin type or NULL (all types) + * @param string the method name that should be send to all plugins + * @param mixed a variable number of arguments + * + * @return array an array containing the return values + */ + public static function sendMessage($type, $method /* ... */) + { + $args = func_get_args(); + array_splice($args, 1, 0, [NULL]); + return call_user_func_array([__CLASS__, 'sendMessageWithContext'], $args); + } + + /** + * Sends a message to all activated plugins of a type enabled in a context and + * returns an array of the return values. + * + * @param type plugin type or NULL (all types) + * @param context context range id (may be NULL) + * @param string the method name that should be send to all plugins + * @param mixed a variable number of arguments + * + * @return array an array containing the return values + */ + public static function sendMessageWithContext($type, $context, $method /* ... */) + { + $args = func_get_args(); + $args = array_slice($args, 3); + $results = []; + foreach (self::getPlugins($type, $context) as $plugin) { + $results[] = call_user_func_array([$plugin, $method], $args); + } + return $results; + } + + /** + * Generates a URL which can be shown in user interfaces + * @param $plugin - the plugin to which should be linked + * @param $params - an array with name value pairs + * @param $cmd - command to execute by clicking the link + * @param bool $ignore_registered_params do not add registered params + * @return a link to the current plugin with the additional $params + */ + public static function getURL($plugin, $params = [], $cmd = 'show', $ignore_registered_params = false) + { + if (is_null($plugin)) { + throw new InvalidArgumentException(_('Es wurde kein Plugin gewählt.')); + } else if (is_object($plugin)) { + $plugin = mb_strtolower(get_class($plugin)) . '/' . $cmd; + } else if (mb_strpos($plugin, '/') === false) { + $plugin = $plugin . '/' . $cmd; + } + + return URLHelper::getURL('plugins.php/' . $plugin, $params, $ignore_registered_params); + } + + /** + * Generates a link (entity encoded URL) which can be shown in user interfaces + * @param $plugin - the plugin to which should be linked + * @param $params - an array with name value pairs + * @param $cmd - command to execute by clicking the link + * @param bool $ignore_registered_params do not add registeredparams + * @return a link to the current plugin with the additional $params + */ + public static function getLink($plugin, $params = [], $cmd = 'show', $ignore_registered_params = false) + { + return htmlReady(self::getURL($plugin, $params, $cmd, $ignore_registered_params)); + } +} diff --git a/lib/plugins/engine/PluginManager.class.php b/lib/plugins/engine/PluginManager.class.php new file mode 100644 index 0000000..cd14841 --- /dev/null +++ b/lib/plugins/engine/PluginManager.class.php @@ -0,0 +1,715 @@ +<?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; + + /** + * cache of plugin default activations + */ + private $plugins_default_activations_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'); + $this->plugins_default_activations_cache = new StudipCachedArray('/PluginDefaultActivations'); + } + + /** + * 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'] === '' || strpos($plugin['pluginpath'], 'core/') === 0, + 'automatic_update_url' => $plugin['automatic_update_url'], + 'automatic_update_secret' => $plugin['automatic_update_secret'] + ]; + } + } + + /** + * @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 string $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 is not present or no changes + if (!$info || $info['enabled'] == $enabled) { + return; + } + + 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 $id id of the plugin + * @param $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 $id int of the plugin + * @param $context string range id + * @returns bool + */ + public function isPluginActivated ($id, $context) + { + if (!DBSchemaVersion::exists('studip', '20210201')) { + return null; + } + + 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 $pluginId id of the plugin + * @param $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]; + 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 $id id of the plugin + * @param $rangeId context range id + * @param $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); + return $activation->store(); + } else { + call_user_func([get_class($plugin), 'onDeactivation'], $id, $rangeId); + return $activation->delete(); + } + } + + /** + * 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(); + } + + /** + * Returns the list of institutes for which a specific plugin is + * enabled by default. + * + * @param $id id of the plugin + */ + public function getDefaultActivations ($id) + { + $db = DBManager::get(); + + $result = $db->query("SELECT institutid FROM plugins_default_activations + WHERE pluginid = '$id'"); + + return $result->fetchAll(PDO::FETCH_COLUMN); + } + + /** + * Set the list of institutes for which a specific plugin should + * be enabled by default. + * + * @param $id id of the plugin + * @param $institutes array of institute ids + */ + public function setDefaultActivations ($id, $institutes) + { + $db = DBManager::get(); + + $result = $db->query("DELETE FROM plugins_default_activations + WHERE pluginid = '$id'"); + + $stmt = $db->prepare("INSERT INTO plugins_default_activations + (pluginid, institutid) VALUES (?,?)"); + foreach ($institutes as $instid) { + $stmt->execute([$id, $instid]); + } + + $this->plugins_default_activations_cache->clear(); + } + + /** + * Disable loading of all non-core plugins for the current session. + * + * @param $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']; + } + + /** + * Load a plugin class from the given file system path and + * return the ReflectionClass instance for the plugin. + * + * @param $class plugin class name + * @param $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 $class plugin class name + * @param $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 $name plugin name + * @param $class plugin class name + * @param $path plugin relative path + * @param $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; + + return $id; + } + + 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'"); + + return $id; + } + + /** + * Remove registration for the given plugin from the data base. + * + * @param $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 plugins_default_activations WHERE pluginid = ?", [$id]); + $db->execute("DELETE FROM roles_plugins WHERE pluginid = ?", [$id]); + + unset($this->plugins[$id]); + + $this->plugins_default_activations_cache->clear(); + $this->plugins_activated_cache->clear(); + } + } + + /** + * Get meta data for the plugin specified by plugin class name. + * + * @param $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 $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 $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 $plugin plugin meta data + * @param $user user id of user + */ + protected function checkUserAccess ($plugin, $user) + { + if (!$plugin['enabled']) { + return false; + } + + $plugin_roles = RolePersistence::getAssignedPluginRoles($plugin['id']); + $user_roles = RolePersistence::getAssignedRoles($user, 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 $plugin_info plugin meta data + */ + protected function getCachedPlugin ($plugin_info) + { + $class = $plugin_info['class']; + $path = $plugin_info['path']; + + 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 $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 $id id of the 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 $type plugin type or NULL (all types) + * @param $context context range id (optional) + */ + public function getPlugins ($type, $context = NULL) + { + $user = $GLOBALS['user']->id ?: 'nobody'; + $plugin_info = $this->getPluginInfos($type); + $plugins = []; + + usort($plugin_info, ['self', '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) + { + $manifest = @file($plugindir . '/plugin.manifest'); + $result = []; + + if ($manifest === false) { + return NULL; + } + + foreach ($manifest as $line) { + list($key, $value) = explode('=', $line); + $key = trim($key); + $value = trim($value); + + // 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; + } +} diff --git a/lib/plugins/engine/PluginRepository.class.php b/lib/plugins/engine/PluginRepository.class.php new file mode 100644 index 0000000..cca8aad --- /dev/null +++ b/lib/plugins/engine/PluginRepository.class.php @@ -0,0 +1,160 @@ +<?php +# Lifter010: TODO +/* + * PluginRepository.class.php - query plugin meta data + * + * Copyright (c) 2008 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 used to locate plugins available from a plugin repository. + */ +class PluginRepository +{ + /** + * list and meta data of available plugins + */ + private $plugins = []; + + /** + * Initialize a new PluginRepository and read meta data from + * the given URL or the default list of URLs (if $url is NULL). + */ + public function __construct($url = NULL) + { + if (isset($url)) { + $this->readMetadata($url); + } else { + foreach ($GLOBALS['PLUGIN_REPOSITORIES'] as $url) { + $this->readMetadata($url); + } + } + } + + /** + * Read plugin meta data from the given URL (using XML format). + * The structure of the XML is: + * + * <plugins> + * <plugin name="DummyPlugin" + * <release + * version="2.0" + * url="http://plugins.example.com/dummy-2.0.zip" + * studipMinVersion="1.4" + * studipMaxVersion="1.9"> + * </plugin> + * [...] + * </plugins> + */ + public function readMetadata($url) + { + global $SOFTWARE_VERSION; + + $cache = StudipCacheFactory::getCache(); + $cache_key = 'plugin_metadata/'.$url; + $metadata = $cache->read($cache_key); + + if ($metadata === false) { + // Set small timeout for the rare case that the repository is not + // available + $context = get_default_http_stream_context($url); + stream_context_set_option($context, ['http' => [ + 'timeout' => 5, + ]]); + $metadata = @file_get_contents($url, false, $context); + + if ($metadata === false) { + throw new Exception(sprintf(_('Fehler beim Zugriff auf %s'), $url)); + } + + $cache->write($cache_key, $metadata, 3600); + } + + $xml = new SimpleXMLElement($metadata); + + if (!isset($xml->plugin)) { + $cache->expire($cache_key); + throw new Exception(_('Keine Plugin Meta-Daten gefunden')); + } + + foreach ($xml->plugin as $plugin) { + foreach ($plugin->release as $release) { + $min_version = trim($release['studipMinVersion']); + $max_version = trim($release['studipMaxVersion']); + + if (($min_version && StudipVersion::olderThan($min_version)) || + ($max_version && StudipVersion::newerThan($max_version))) { + // plugin is not compatible, so skip it + continue; + } + + $meta_data = [ + 'version' => (string) $release['version'], + 'url' => (string) $release['url'], + 'description' => (string) $plugin['description'], + 'plugin_url' => (string) $plugin['homepage'], + 'image' => (string) $plugin['image'], + 'score' => (float) $plugin['score'], + 'marketplace_url' => (string) $plugin['marketplace_url'], + ]; + + $this->registerPlugin((string) $plugin['name'], $meta_data); + } + } + } + + /** + * Register a new plugin in this repository. + * + * @param $name string plugin name + * @param $meta_data array of plugin meta data + */ + protected function registerPlugin($name, $meta_data) + { + $old_data = $this->plugins[$name]; + + if (!isset($old_data) || + version_compare($meta_data['version'], $old_data['version']) > 0) { + $this->plugins[$name] = $meta_data; + } + } + + /** + * Get meta data for the plugin with the given name (if available). + * Always chooses the newest compatible version of the plugin. + * + * @return array meta data for plugin (or NULL) + */ + public function getPlugin($name) + { + return $this->plugins[$name]; + } + + /** + * Get meta data for all plugins whose names contain the given + * string. You may omit the search string to get a list of all + * available plugins. Returns the newest compatible version of + * each plugin. + * + * @return array array of meta data for matching plugins + */ + public function getPlugins($search = NULL) + { + $result = []; + + foreach ($this->plugins as $name => $data) { + if ($search === NULL || $search === '' || + is_int(mb_stripos($name, $search)) || + is_int(mb_stripos($data['description'], $search))) { + $result[$name] = $data; + } + } + + return $result; + } +} |
