aboutsummaryrefslogtreecommitdiff
path: root/lib/plugins
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:07:19 +0200
committerJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:19:12 +0200
commita3da1483a9e689846179159355badfec8073dbec (patch)
tree770dcca6bdf5f6f2a11b0e7fcbbeda6919a3fc52 /lib/plugins
current code from svn, revision 62608
Diffstat (limited to 'lib/plugins')
-rw-r--r--lib/plugins/core/AdminCourseAction.class.php22
-rw-r--r--lib/plugins/core/AdminCourseContents.class.php23
-rw-r--r--lib/plugins/core/AdministrationPlugin.class.php19
-rw-r--r--lib/plugins/core/CorePlugin.php151
-rw-r--r--lib/plugins/core/DetailspagePlugin.class.php27
-rw-r--r--lib/plugins/core/FileUploadHook.class.php12
-rw-r--r--lib/plugins/core/FilesystemPlugin.class.php82
-rw-r--r--lib/plugins/core/ForumModule.class.php140
-rw-r--r--lib/plugins/core/HomepagePlugin.class.php33
-rw-r--r--lib/plugins/core/LibraryPlugin.class.php55
-rw-r--r--lib/plugins/core/MetricsPlugin.class.php23
-rw-r--r--lib/plugins/core/PluginAssetsTrait.php215
-rw-r--r--lib/plugins/core/PortalPlugin.class.php33
-rw-r--r--lib/plugins/core/PrivacyPlugin.class.php24
-rw-r--r--lib/plugins/core/QuestionnaireAssignmentPlugin.class.php55
-rw-r--r--lib/plugins/core/RESTAPIPlugin.class.php23
-rw-r--r--lib/plugins/core/Role.class.php89
-rw-r--r--lib/plugins/core/ScorePlugin.class.php29
-rw-r--r--lib/plugins/core/StandardPlugin.class.php17
-rw-r--r--lib/plugins/core/StudIPPlugin.class.php218
-rw-r--r--lib/plugins/core/SystemPlugin.class.php17
-rw-r--r--lib/plugins/core/TranslatablePluginTrait.php104
-rw-r--r--lib/plugins/core/WebServicePlugin.class.php14
-rw-r--r--lib/plugins/db/RolePersistence.class.php517
-rw-r--r--lib/plugins/engine/PluginEngine.class.php154
-rw-r--r--lib/plugins/engine/PluginManager.class.php715
-rw-r--r--lib/plugins/engine/PluginRepository.class.php160
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;
+ }
+}