aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorElmar Ludwig <elmar.ludwig@uni-osnabrueck.de>2022-05-11 07:19:42 +0000
committerElmar Ludwig <elmar.ludwig@uni-osnabrueck.de>2022-05-11 07:19:42 +0000
commit20240b2aacb15ab3264afbfbbc9dae952db4bb63 (patch)
tree597cae73c5ae7ab9eda6d066d45430c3f17a4560 /lib
parentc054faf90288a75fc3680480434ba93b7f5b287b (diff)
convert old core plugins to new model, re #814
Merge request studip/studip!440
Diffstat (limited to 'lib')
-rw-r--r--lib/activities/ForumProvider.php4
-rw-r--r--lib/calendar/CalendarWidgetView.php52
-rw-r--r--lib/classes/ForumAbo.php186
-rw-r--r--lib/classes/ForumActivity.php149
-rw-r--r--lib/classes/ForumBulkMail.php135
-rw-r--r--lib/classes/ForumEntry.php1385
-rw-r--r--lib/classes/ForumFavorite.php41
-rw-r--r--lib/classes/ForumHelpers.php282
-rw-r--r--lib/classes/ForumIssue.php96
-rw-r--r--lib/classes/ForumLike.php99
-rw-r--r--lib/classes/ForumPerm.php215
-rw-r--r--lib/classes/ForumVisit.php162
-rw-r--r--lib/classes/JsonApi/Routes/Forum/ForumAuthority.php2
-rw-r--r--lib/classes/JsonApi/Routes/Forum/ForumCategoriesShow.php2
-rw-r--r--lib/classes/Privacy.php2
-rw-r--r--lib/classes/globalsearch/GlobalSearchForum.php4
-rw-r--r--lib/models/BlubberThread.php2
-rw-r--r--lib/models/ForumCat.php252
-rw-r--r--lib/modules/ActivityFeed.php77
-rw-r--r--lib/modules/Blubber.class.php126
-rw-r--r--lib/modules/ContentsWidget.php30
-rw-r--r--lib/modules/CoreForum.class.php209
-rw-r--r--lib/modules/EvaluationsWidget.php62
-rw-r--r--lib/modules/NewsWidget.php65
-rw-r--r--lib/modules/QuickSelection.php57
-rw-r--r--lib/modules/ScheduleWidget.php54
-rw-r--r--lib/modules/TerminWidget.php43
-rw-r--r--lib/plugins/engine/PluginManager.class.php2
28 files changed, 3782 insertions, 13 deletions
diff --git a/lib/activities/ForumProvider.php b/lib/activities/ForumProvider.php
index 49260c3..f543947 100644
--- a/lib/activities/ForumProvider.php
+++ b/lib/activities/ForumProvider.php
@@ -9,8 +9,6 @@
namespace Studip\Activity;
-require_once 'public/plugins_packages/core/Forum/models/ForumEntry.php';
-
class ForumProvider implements ActivityProvider
{
/**
@@ -28,7 +26,7 @@ class ForumProvider implements ActivityProvider
$activity->content = formatReady($post['content']);
- $url = \PluginEngine::getURL('CoreForum', [], 'index/index/' . $post['topic_id']
+ $url = \URLHelper::getURL('dispatch.php/course/forum/index/index/' . $post['topic_id']
.'?cid='. $post['seminar_id'] .'&highlight_topic='. $post['topic_id']
.'#'. $post['topic_id']);
diff --git a/lib/calendar/CalendarWidgetView.php b/lib/calendar/CalendarWidgetView.php
new file mode 100644
index 0000000..946592c
--- /dev/null
+++ b/lib/calendar/CalendarWidgetView.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Calendar widget view, links to details page of courses.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @since Stud.IP 3.4
+ */
+class CalendarWidgetView extends CalendarWeekView
+{
+ /**
+ * Creates a widget view from a week view.
+ *
+ * @param CalendarWeekView $view The CalendarWeekView object
+ * @return CalendarWidgetView object with the data from the
+ * CalendarWeekView
+ */
+ public static function createFromWeekView(CalendarWeekView $view)
+ {
+ $new_view = new self($view->getColumns(), $view->getContext());
+ $new_view->setReadOnly(true);
+ return $new_view;
+ }
+
+ /**
+ * Returns all columns of the calendar-view and removes everything that
+ * is not needed and links the entry to the details page of the course.
+ *
+ * @return array of CalendarColumn
+ */
+ public function getColumns()
+ {
+ foreach ($this->entries as $column) {
+ $column->setURL(false);
+ foreach ($column->entries as $key => $entry) {
+ if (isset($entry['cycle_id'])) {
+ list($course_id, $cycle_id) = explode('-', $entry['id']);
+
+ $url = URLHelper::getLink('dispatch.php/course/details/?sem_id=' . $course_id);
+ $column->entries[$key]['url'] = $url;
+ } else {
+ unset($column->entries[$key]['url']);
+ }
+
+ unset($column->entries[$key]['onClick']);
+ unset($column->entries[$key]['icons']);
+ }
+ }
+
+ return $this->entries;
+ }
+}
diff --git a/lib/classes/ForumAbo.php b/lib/classes/ForumAbo.php
new file mode 100644
index 0000000..fa84a48
--- /dev/null
+++ b/lib/classes/ForumAbo.php
@@ -0,0 +1,186 @@
+<?php
+
+/**
+ * ForumAbo.php - Handle abonnements of areas/threads or even the whole forum
+ *
+ * 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
+ */
+
+class ForumAbo
+{
+ /**
+ * add the passed user as a watcher for the passed topic (including all
+ * current and future childs)
+ *
+ * @param string $topic_id
+ * @param string $user_id
+ */
+ public static function add($topic_id, $user_id = null)
+ {
+ if (!$user_id) $user_id = $GLOBALS['user']->id;
+
+ $stmt = DBManager::get()->prepare("REPLACE INTO forum_abo_users
+ (topic_id, user_id) VALUEs (?, ?)");
+ $stmt->execute([$topic_id, $user_id]);
+ }
+
+ /**
+ * remove the passed user as a watcher from the passed topic (including all
+ * current and future childs)
+ *
+ * @param string $topic_id
+ * @param string $user_id
+ */
+ public static function delete($topic_id, $user_id = null)
+ {
+ if (!$user_id) $user_id = $GLOBALS['user']->id;
+
+ $stmt = DBManager::get()->prepare("DELETE FROM forum_abo_users
+ WHERE topic_id = ? AND user_id = ?");
+ $stmt->execute([$topic_id, $user_id]);
+ }
+
+ /**
+ * check, if the passed user watches the passed topic. If no user_id is passed,
+ * the currently logged in user is used
+ *
+ * @param string $topic_id
+ * @param string $user_id
+ *
+ * @return boolean returns true if user is watching, false otherwise
+ */
+ public static function has($topic_id, $user_id = null)
+ {
+ if (!$user_id) $user_id = $GLOBALS['user']->id;
+
+ $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_abo_users
+ WHERE topic_id = ? AND user_id = ?");
+ $stmt->execute([$topic_id, $user_id]);
+
+ return $stmt->fetchColumn() > 0 ? true : false;
+ }
+
+ /**
+ * send out the notification messages for the passed topic. The contents
+ * and a link directly to the topic are added to the message.
+ *
+ * @param string $topic_id
+ */
+ public static function notify($topic_id)
+ {
+ // send message to all abo-users
+ $db = DBManager::get();
+ $messaging = new ForumBulkMail();
+ // $messaging = new Messaging();
+
+ // get all parent topic-ids, to find out which users to notify
+ $path = ForumEntry::getPathToPosting($topic_id);
+
+ // fetch all users to notify, exclude current user
+ $stmt = $db->prepare("SELECT DISTINCT user_id
+ FROM forum_abo_users
+ WHERE topic_id IN (:topic_ids)
+ AND user_id != :user_id");
+ $stmt->bindParam(':topic_ids', array_keys($path), StudipPDO::PARAM_ARRAY);
+ $stmt->bindParam(':user_id', $GLOBALS['user']->id);
+ $stmt->execute();
+
+ // get details for topic
+ $topic = ForumEntry::getConstraints($topic_id);
+
+ $template = $GLOBALS['template_factory']->open('mail/forum_notification');
+
+ // notify users
+ while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ $user_id = $data['user_id'];
+
+ // don't notify user if view permission is not granted
+ if (!ForumPerm::has('view', $topic['seminar_id'], $user_id)) {
+ continue;
+ }
+
+ $user = User::find($user_id);
+
+ // check if user wants an email for all or selected messages only
+ $force_email = false;
+ if ($messaging->user_wants_email($user_id)) {
+ $force_email = true;
+ }
+ // do not send mails when account is locked or expired
+ $expiration = UserConfig::get($user->id)->EXPIRATION_DATE;
+ if ($user->locked || ($expiration > 0 && $expiration < time())) {
+ $force_email = false;
+ }
+ $parent_id = ForumEntry::getParentTopicId($topic['topic_id']);
+
+ setTempLanguage($data['user_id']);
+ $notification = sprintf(_("%s hat einen Beitrag geschrieben"), ($topic['anonymous'] ? _('Anonym') : $topic['author']));
+ restoreLanguage();
+
+ PersonalNotifications::add(
+ $user_id,
+ URLHelper::getURL(
+ 'dispatch.php/course/forum/index/index/' . $topic['topic_id'] . '#' . $topic['topic_id'],
+ ['cid' => $topic['seminar_id']],
+ true
+ ),
+ $notification,
+ "forumposting_" . $topic['topic_id'],
+ Icon::create('forum', 'clickable')
+ );
+
+ if ($force_email) {
+ $title = implode(' >> ', ForumEntry::getFlatPathToPosting($topic_id));
+
+ $subject = _('[Forum]') . ' ' . ($title ?: _('Neuer Beitrag'));
+
+ $htmlMessage = $template->render(
+ compact('user_id', 'topic', 'path')
+ );
+
+ $textMessage = trim(kill_format($htmlMessage));
+
+ $userWantsHtml = UserConfig::get($user_id)->MAIL_AS_HTML;
+
+ StudipMail::sendMessage(
+ $user->email,
+ $subject,
+ $textMessage,
+ $userWantsHtml ? $htmlMessage : null
+ );
+ }
+ }
+
+ $messaging->bulkSend();
+ }
+
+ /**
+ * Removes all abos for a given course and user
+ *
+ * @param String $course_id Id of the course
+ * @param String $user_id Id of the user
+ * @return int number of removed abos
+ */
+ public static function removeForCourseAndUser($course_id, $user_id)
+ {
+ $query = "DELETE FROM `forum_abo_users`
+ WHERE `user_id` = :user_id
+ AND `topic_id` IN (
+ SELECT `topic_id`
+ FROM `forum_entries`
+ WHERE `seminar_id` = :course_id
+ )";
+ $statement = DBManager::get()->prepare($query);
+ $statement->bindValue(':course_id', $course_id);
+ $statement->bindValue(':user_id', $user_id);
+ $statement->execute();
+ return $statement->rowCount();
+ }
+}
diff --git a/lib/classes/ForumActivity.php b/lib/classes/ForumActivity.php
new file mode 100644
index 0000000..20ebeb6
--- /dev/null
+++ b/lib/classes/ForumActivity.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * File - description
+ *
+ * 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 Till Glöggler <tgloeggl@uos.de>
+ * @license https://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ */
+
+class ForumActivity
+{
+ /**
+ * Post activity for new forum post
+ *
+ * @param string $event
+ * @param string $topic_id
+ * @param array $post
+ */
+ public static function newEntry($event, $topic_id, $post)
+ {
+ $verb = $post['depth'] == 3 ? 'answered' : 'created';
+
+ if ($verb == 'created') {
+ if ($post['depth'] == 1) {
+ $summary = _('%s hat im Forum der Veranstaltung "%s" einen Bereich erstellt.');
+ } else {
+ $summary = _('%s hat im Forum der Veranstaltung "%s" ein Thema erstellt.');
+ }
+ } else {
+ $summary = _('%s hat im Forum der Veranstaltung "%s" auf ein Thema geantwortet.');
+ }
+
+ self::post($post, $verb, $summary);
+ }
+
+ /**
+ * Post activity for updating a forum post
+ * @param string $event
+ * @param string $topic_id
+ * @param string $post
+ */
+ public static function updateEntry($event, $topic_id, $post)
+ {
+ $summary = _('%s hat im Forum der Veranstaltung "%s" einen Beitrag editiert.');
+
+ if ($post['user_id'] == $GLOBALS['user']->id) {
+ $content = sprintf(
+ _('%s hat seinen eigenen Beitrag vom %s editiert.'),
+ self::getPostUsername($post),
+ date('d.m.y, H:i', $post['mkdate'])
+ );
+ } else {
+ $content = sprintf(
+ _('%s hat den Beitrag von %s vom %s editiert.'),
+ get_fullname($GLOBALS['user']->id),
+ self::getPostUsername($post),
+ date('d.m.y, H:i', $post['mkdate'])
+ );
+ }
+
+ self::post($post, 'edited', $summary, $content);
+ }
+
+ /**
+ * Post activity for deleting a forum post
+ * $param string $event
+ * @param string $topic_id
+ * @param string $post
+ */
+ public static function deleteEntry($event, $topic_id, $post)
+ {
+ // Remove all previous activities for the post
+ Studip\Activity\Activity::deleteBySQL(
+ "provider = ? AND object_type = 'forum' AND object_id = ?",
+ [Studip\Activity\ForumProvider::class, $topic_id]
+ );
+
+ $summary = _('%s hat im Forum der Veranstaltung "%s" einen Beitrag gelöscht.');
+
+ if ($post['user_id'] == $GLOBALS['user']->id) {
+ $content = sprintf(
+ _('%s hat seinen Beitrag vom %s gelöscht.'),
+ self::getPostUsername($post),
+ date('d.m.y, H:i', $post['mkdate'])
+ );
+ } else {
+ $content = sprintf(
+ _('%s hat den Beitrag von %s vom %s gelöscht.'),
+ get_fullname($GLOBALS['user']->id),
+ self::getPostUsername($post),
+ date('d.m.y, H:i', $post['mkdate'])
+ );
+ }
+
+ self::post($post, 'deleted', $summary, $content);
+ }
+
+ private static function post($post, $verb, $summary, $content = null)
+ {
+ // skip system-created entries like "Allgemeine Diskussionen"
+ if (!$post['user_id']) {
+ return;
+ }
+
+ $range_id = $post['seminar_id'];
+ $type = get_object_type($range_id);
+
+ $obj = get_object_name($range_id, $type);
+
+ $data = [
+ 'provider' => 'Studip\Activity\ForumProvider',
+ 'context' => $type === 'sem' ? 'course' : 'institute',
+ 'context_id' => $post['seminar_id'],
+ 'content' => null,
+ 'actor_type' => 'user', // who initiated the activity?
+ 'actor_id' => $post['user_id'], // id of initiator
+ 'verb' => $verb, // the activity type
+ 'object_id' => $post['topic_id'], // the id of the referenced object
+ 'object_type' => 'forum', // type of activity object
+ 'mkdate' => $post['mkdate'] ?: time()
+ ];
+
+ if ($post['anonymous']) {
+ $data['actor_type'] = 'anonymous';
+ $data['actor_id'] = '';
+ }
+
+ $activity = Studip\Activity\Activity::create($data);
+ }
+
+ /**
+ * Returns the poster's name for a forum post.
+ *
+ * @param array $post
+ * @return string
+ */
+ private static function getPostUsername($post)
+ {
+ if ($post['anonymous']) {
+ return _('Anonym');
+ }
+
+ return get_fullname($post['user_id']);
+ }
+}
diff --git a/lib/classes/ForumBulkMail.php b/lib/classes/ForumBulkMail.php
new file mode 100644
index 0000000..ec18de9
--- /dev/null
+++ b/lib/classes/ForumBulkMail.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * ForumBulkMail.php - Experimental mailer to handle large amounts of mails at high speed
+ *
+ * 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
+ */
+
+class ForumBulkMail extends Messaging {
+ var $bulk_mail;
+
+ /**
+ * Overwrites the parent method. This method combines messages with the same
+ * content and prepares them for sending them as a mail with multiple
+ * recepients instead of one mail for each recipient.
+ * The actual sending task is done bulkSend().
+ *
+ * @global object $user
+ *
+ * @param string $rec_user_id user_id of recipient
+ * @param string $snd_user_id user_id of sender
+ * @param string $message the message
+ * @param string $subject subject for the message
+ * @param string $message_id the message_id in the database
+ */
+ function sendingEmail($rec_user_id, $snd_user_id, $message, $subject, $message_id)
+ {
+ $receiver = User::find($rec_user_id);
+
+ if ($receiver && $receiver->email) {
+ $rec_fullname = 'Sie';
+
+ setTempLanguage($receiver->id);
+
+ if (empty($this->bulk_mail[md5($message)][getenv('LANG')])) {
+
+ $title = "[Stud.IP - " . Config::get()->UNI_NAME_CLEAN . "] ".stripslashes(kill_format(str_replace(["\r","\n"], '', $subject)));
+
+ if ($snd_user_id != "____%system%____") {
+ $sender = User::find($snd_user_id);
+ $reply_to = $sender->email;
+ }
+
+ $template = $GLOBALS['template_factory']->open('mail/text');
+ $template->message = kill_format(stripslashes($message));
+ $template->rec_fullname = $receiver->getFullname();
+ $mailmessage = $template->render();
+
+ $template = $GLOBALS['template_factory']->open('mail/html');
+ $template->lang = getUserLanguagePath($rec_user_id);
+ $template->message = stripslashes($message);
+ $template->rec_fullname = $receiver->getFullname();
+ $mailhtml = $template->render();
+
+ $this->bulk_mail[md5($message)][getenv('LANG')] = [
+ 'text' => $mailmessage,
+ 'html' => $mailhtml,
+ 'title' => $title,
+ 'reply_to' => $reply_to,
+ 'message_id' => $message_id,
+ 'users' => []
+ ];
+ }
+
+ $this->bulk_mail[md5($message)][getenv('LANG')]['users'][$receiver->id] = $receiver->email;
+
+ restoreLanguage();
+ }
+ }
+
+
+ /**
+ * Sends the collected messages from sendingMail as e-mail.
+ */
+ function bulkSend()
+ {
+ // if nothing to do, return
+ if (empty($this->bulk_mail)) return;
+
+ // send a mail, for each language one
+ foreach ($this->bulk_mail as $lang_data) {
+ foreach ($lang_data as $data) {
+ $mail = new StudipMail();
+ $mail->setSubject($data['title']);
+
+ foreach ($data['users'] as $user_id => $to) {
+ $mail->addRecipient($to, get_fullname($user_id), 'Bcc');
+ }
+
+ $mail->setReplyToEmail('')
+ ->setBodyText($data['text']);
+
+ if (mb_strlen($data['reply_to'])) {
+ $mail->setSenderEmail($data['reply_to'])
+ ->setSenderName($snd_fullname);
+ }
+
+ $user_cfg = UserConfig::get($user_id);
+ if ($user_cfg->getValue('MAIL_AS_HTML')) {
+ $mail->setBodyHtml($mailhtml);
+ }
+
+ if($GLOBALS["ENABLE_EMAIL_ATTACHMENTS"]){
+ $message = Message::find($data['message_id']);
+
+ $current_user = User::findCurrent();
+
+ $message_folder = MessageFolder::findMessageTopFolder(
+ $message->id,
+ $current_user->id
+ );
+
+ $message_folder = $message_folder->getTypedFolder();
+
+ $attachments = FileManager::getFolderFilesRecursive(
+ $message_folder,
+ $current_user->id
+ );
+
+
+ foreach($attachments as $attachment) {
+ $mail->addStudipAttachment($attachment);
+ }
+ }
+ $mail->send();
+ }
+ }
+ }
+}
diff --git a/lib/classes/ForumEntry.php b/lib/classes/ForumEntry.php
new file mode 100644
index 0000000..bd62b08
--- /dev/null
+++ b/lib/classes/ForumEntry.php
@@ -0,0 +1,1385 @@
+<?php
+/**
+ * ForumEntry.php - Allows the retrieval and handling of forum-entrys
+ *
+ * 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
+ */
+
+class ForumEntry implements PrivacyObject
+{
+ const WITH_CHILDS = true;
+ const WITHOUT_CHILDS = false;
+ const THREAD_PREVIEW_LENGTH = 100;
+ const POSTINGS_PER_PAGE = 10;
+ const FEED_POSTINGS = 100;
+
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * H E L P E R - F U N C T I O N S *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ /**
+ * is used for posting-preview. replaces all newlines with spaces
+ *
+ * @param string $text the text to work on
+ * @returns string
+ */
+ public static function br2space($text)
+ {
+ return str_replace("\n", ' ', str_replace("\r", '', $text));
+ }
+
+ /**
+ * remove the edit-html from a posting
+ *
+ * @param string $description the posting-content
+ * @return string the content stripped by the edit-mark
+ */
+ public static function killEdit($description)
+ {
+ // wurde schon mal editiert
+ if (preg_match('/^(.*)(<admin_msg.*?)$/s', $description, $match)) {
+ return $match[1];
+ }
+ return $description;
+ }
+
+ /**
+ * add the edit-html to a posting
+ *
+ * @param string $description the posting-content
+ * @return string the content with the edit-mark
+ */
+ public static function appendEdit($description)
+ {
+ $edit = "<admin_msg autor=\"" . addslashes(get_fullname()) . "\" chdate=\"" . time() . "\">";
+ return $description . $edit;
+ }
+
+ /**
+ * convert the edit-html to raw text
+ *
+ * @param string $description the posting-content
+ * @return string the content with the raw text version of the edit-mark
+ */
+ public static function parseEdit($description, $anonymous = false)
+ {
+ // TODO figure out if this function can be removed
+ // has been replaced with getContentAsHTML in core code
+ $content = ForumEntry::killEdit($description);
+ $comment = ForumEntry::getEditComment($description, $anonymous);
+ return $content . ($comment ? "\n\n%%" . $comment .'%%' : '');
+ }
+
+ /**
+ * Get content with appended edit comment as HTML.
+ *
+ * @param string $description Database entry of forum entry's body.
+ * @param bool $anonymous True, if only root is allowed to see
+ * authors.
+ * @return string Content and edit comment as HTML.
+ */
+ public static function getContentAsHtml($description, $anonymous = false)
+ {
+ $raw_content = ForumEntry::killEdit($description);
+
+ $comment = ForumEntry::getEditComment($description, $anonymous);
+ $content = formatReady($raw_content);
+
+ if ($comment) {
+ $content .= '<br><em>' . htmlReady($comment) . '</em>';
+ }
+
+ return $content;
+ }
+
+ /**
+ * Get author and time of an edited forum entry as a string.
+ *
+ * @param string $description Database entry of forum entry's body.
+ * @param bool $anonymous True, if only root is allowed to see
+ * authors.
+ * @return string Author and time or empty string if not edited.
+ */
+ public static function getEditComment($description, $anonymous = false)
+ {
+ $info = ForumEntry::getEditInfo($description);
+ if ($info) {
+ $root = $GLOBALS['perm']->have_perm('root');
+ $author = ($anonymous && !$root) ? _('Anonym') : $info['author'];
+ $time = date('d.m.y - H:i', $info['time']);
+ return '[' . _('Zuletzt editiert von') . " $author - $time]";
+ }
+ return '';
+ }
+
+ /**
+ * Get author and time of an edited forum entry.
+ *
+ * @param string $description Database entry of forum entry's body.
+ * @return array Associative array containing author and time.
+ * boolean False if edit tag was not found.
+ */
+ public static function getEditInfo($description) {
+ if (preg_match('/<admin_msg autor="([^"]*)" chdate="([^"]*)">\s*$/i', $description, $matches)) {
+ // wurde schon mal editiert
+ return ['author' => $matches[1], 'time' => $matches[2]];
+ }
+ return false;
+ }
+
+ /**
+ * Remove all quote blocks AND the quoted text from a forum post.
+ *
+ * @param String $string The string to remove the quote blocks from
+ * @return String the posting without the [quote]-blocks (not just tags!)
+ */
+ public static function removeQuotes($description)
+ {
+ if (Studip\Markup::isHtml($description)) {
+ // remove all blockquote tags
+ $dom = new DOMDocument();
+ $dom->loadHtml($description, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
+ $nodes = iterator_to_array($dom->getElementsByTagName('blockquote'));
+
+ foreach ($nodes as $node) {
+ $node->parentNode->removeChild($node);
+ }
+
+ return $dom->saveHTML();
+ } else {
+ $description = preg_replace('/\[quote(=.*)\].*\[\/quote\]/isU', '', $description);
+ $description = str_replace('[/quote]', '', $description);
+ }
+ return $description;
+ }
+
+
+ /**
+ * calls Stud.IP's kill_format and additionally removes any found smiley-tag
+ *
+ * @param string $text the text to parse
+ * @return string the text without format-tags and without smileys
+ */
+ public static function killFormat($text)
+ {
+ $text = kill_format($text);
+
+ // find stuff which is enclosed between to colons
+ preg_match('/' . SmileyFormat::REGEXP . '/U', $text, $matches);
+
+ // remove the match if it is a smiley
+ foreach ($matches as $match) {
+ if (Smiley::getByName($match) || Smiley::getByShort($match)) {
+ $text = str_replace($match, '', $text);
+ }
+ }
+
+ return $text;
+ }
+
+ /**
+ * returns the entry for the passed topic_id
+ *
+ * @param string $topic_id
+ * @return array array('lft' => ..., 'rgt' => ..., seminar_id => ...)
+ *
+ * @throws Exception
+ */
+ public static function getConstraints($topic_id)
+ {
+ //very bad performance if topic_id is 0 or false
+ if (!$topic_id) return false;
+
+ // look up the range of postings
+ $range_stmt = DBManager::get()->prepare("SELECT *
+ FROM forum_entries WHERE topic_id = ?");
+ $range_stmt->execute([$topic_id]);
+ if (!$data = $range_stmt->fetch(PDO::FETCH_ASSOC)) {
+ return false;
+ // throw new Exception("Could not find entry with id >>$topic_id<< in forum_entries, " . __FILE__ . " on line " . __LINE__);
+ }
+
+ if ($data['depth'] == 1) {
+ $data['area'] = 1;
+ }
+
+ return $data;
+ }
+
+ /**
+ * return the topic_id of the parent element, false if there is none (ie the
+ * passed topic_id is already the upper-most node in the tree)
+ *
+ * @param string $topic_id the topic_id for which the parent shall be found
+ *
+ * @return string the topic_id of the parent element or false
+ */
+ public static function getParentTopicId($topic_id)
+ {
+ $path = ForumEntry::getPathToPosting($topic_id);
+ array_pop($path);
+ $data = array_pop($path);
+
+ return $data['id'] ?: false;
+ }
+
+
+ /**
+ * get the topic_ids of all childs of the passed topic including itself
+ *
+ * @param string $topic_id the topic_id to find the childs for
+ * @return array a list if topic_ids
+ */
+ public static function getChildTopicIds($topic_id)
+ {
+ $constraints = ForumEntry::getConstraints($topic_id);
+
+ $stmt = DBManager::get()->prepare("SELECT topic_id
+ FROM forum_entries WHERE lft >= ? AND rgt <= ?
+ AND seminar_id = ?");
+ $stmt->execute([$constraints['lft'], $constraints['rgt'], $constraints['seminar_id']]);
+
+ return $stmt->fetchAll(PDO::FETCH_COLUMN);
+ }
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * D A T A - R E T R I E V A L *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ /**
+ * get the page the passed posting is on
+ *
+ * @param string $topic_id
+ * @return int
+ */
+ public static function getPostingPage($topic_id, $constraint = null)
+ {
+ if (!$constraint) {
+ $constraint = ForumEntry::getConstraints($topic_id);
+ }
+
+ // this calculation only works for postings
+ if ($constraint['depth'] <= 2) return ForumHelpers::getPage();
+
+ if ($parent_id = ForumEntry::getParentTopicId($topic_id)) {
+ $parent_constraint = ForumEntry::getConstraints($parent_id);
+
+ return ceil((($constraint['lft'] - $parent_constraint['lft'] + 3) / 2) / ForumEntry::POSTINGS_PER_PAGE);
+ }
+
+ return 0;
+ }
+
+ /**
+ * return the id for the oldest unread child-posting for the passed topic.
+ *
+ * @param string $parent_id
+ * @return string id of oldest unread posting
+ */
+ public static function getLastUnread($parent_id)
+ {
+ $constraint = ForumEntry::getConstraints($parent_id);
+
+ // take users visitdate into account
+ $visitdate = ForumVisit::getLastVisit($constraint['seminar_id']);
+
+ // get the first unread entry
+ $stmt = DBManager::get()->prepare("SELECT * FROM forum_entries
+ WHERE lft > ? AND rgt < ? AND seminar_id = ?
+ AND mkdate >= ?
+ ORDER BY mkdate ASC LIMIT 1");
+ $stmt->execute([$constraint['lft'], $constraint['rgt'], $constraint['seminar_id'], $visitdate]);
+ $last_unread = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ return $last_unread ? $last_unread['topic_id'] : null;
+ }
+
+ /**
+ * retrieve the the latest posting under $parent_id
+ * or false if the postings itself is the latest
+ *
+ * @param string $parent_id the node to lookup the childs in
+ * @return mixed the data for the latest postings or false
+ */
+ public static function getLatestPosting($parent_id)
+ {
+ $constraint = ForumEntry::getConstraints($parent_id);
+
+ // get last entry
+ $stmt = DBManager::get()->prepare("SELECT * FROM forum_entries
+ WHERE lft > ? AND rgt < ? AND seminar_id = ?
+ ORDER BY mkdate DESC LIMIT 1");
+ $stmt->execute([$constraint['lft'], $constraint['rgt'], $constraint['seminar_id']]);
+
+ if (!$data = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ return false;
+ }
+
+ return $data;
+ }
+
+ /**
+ * returns a hashmap with arrays containing id and name with the entries
+ * which lead to the passed topic
+ *
+ * @param string $topic_id the topic to get the path for
+ *
+ * @return array
+ */
+ public static function getPathToPosting($topic_id)
+ {
+ $data = ForumEntry::getConstraints($topic_id);
+ $ret = [];
+
+ $stmt = DBManager::get()->prepare("SELECT * FROM forum_entries
+ WHERE lft <= ? AND rgt >= ? AND seminar_id = ? ORDER BY lft ASC");
+ $stmt->execute([$data['lft'], $data['rgt'], $data['seminar_id']]);
+
+ while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ $ret[$data['topic_id']] = $data;
+ $ret[$data['topic_id']]['id'] = $data['topic_id'];
+ }
+
+ // set the name of the first entry to the name of the category the entry is in
+ if (sizeof($ret) > 1) {
+ reset($ret);
+ $tmp = array_slice($ret, 1, 1);
+ $area = array_pop($tmp);
+ $top = current($ret);
+ $ret[$top['id']]['name'] = ForumCat::getCategoryNameForArea($area['id']) ?: _('Allgemein');
+ }
+
+ return $ret;
+ }
+
+ /**
+ * returns a hashmap where key is topic_id and value a posting-title from the
+ * entries which lead to the passed topic.
+ *
+ * WARNING: This function ommits postings with an empty title. For a full
+ * list please use ForumEntry::getPathToPosting()!
+ *
+ * @param string $topic_id the topic to get the path for
+ *
+ * @return array
+ */
+ public static function getFlatPathToPosting($topic_id)
+ {
+ // use only the part of the path until the thread, no posting title
+ $postings = array_slice(self::getPathToPosting($topic_id), 0, 3);
+
+ // var_dump($postings);
+
+ foreach ($postings as $post) {
+ if ($post['name']) {
+ $ret[$post['id']] = $post['name'];
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * fill the passed postings with additional data
+ *
+ * @param array $postings
+ * @return array
+ */
+ public static function parseEntries($postings)
+ {
+ $posting_list = [];
+
+ // retrieve the postings
+ foreach ($postings as $data) {
+ // we throw away all formatting stuff, tags, etc, leaving the important bit of information
+ $desc_short = ForumEntry::br2space(ForumEntry::killFormat(strip_tags($data['content'])));
+ if (mb_strlen($desc_short) > (ForumEntry::THREAD_PREVIEW_LENGTH + 2)) {
+ $desc_short = mb_substr($desc_short, 0, ForumEntry::THREAD_PREVIEW_LENGTH) . '...';
+ } else {
+ $desc_short = $desc_short;
+ }
+
+ $posting_list[$data['topic_id']] = [
+ 'author' => $data['author'],
+ 'topic_id' => $data['topic_id'],
+ 'name' => formatReady($data['name']),
+ 'name_raw' => $data['name'],
+ 'content' => ForumEntry::getContentAsHtml($data['content'], $data['anonymous']),
+ 'content_raw' => ForumEntry::killEdit($data['content']),
+ 'content_short' => $desc_short,
+ 'chdate' => $data['chdate'],
+ 'mkdate' => $data['mkdate'],
+ 'user_id' => $data['user_id'],
+ 'raw_title' => $data['name'],
+ 'raw_description' => ForumEntry::killEdit($data['content']),
+ 'fav' => ($data['fav'] == 'fav'),
+ 'depth' => $data['depth'],
+ 'anonymous' => $data['anonymous'],
+ 'closed' => $data['closed'],
+ 'sticky' => $data['sticky'],
+ 'seminar_id' => $data['seminar_id']
+ ];
+ } // retrieve the postings
+
+ return $posting_list;
+ }
+
+ /**
+ * Get all entries for the passed parent_id.
+ * Returns an array of the following structure:
+ * Array (
+ * 'list' => Array (
+ * 'author' =>
+ * 'topic_id' =>
+ * 'name' => formatReady()
+ * 'name_raw' =>
+ * 'content' => formatReady()
+ * 'content_raw' =>
+ * 'content_short' =>
+ * 'chdate' =>
+ * 'mkdate' =>
+ * 'user_id' =>
+ * 'raw_title' =>
+ * 'raw_description' =>
+ * 'fav' =>
+ * 'depth' =>
+ * 'sticky' =>
+ * 'closed' =>
+ * 'seminar_id' =>
+ * )
+ * 'count' =>
+ * )
+ *
+ * @param string $parent_id id of parent-element to get entries for.
+ * @param boolean $with_childs if true, the whole subtree is fetched
+ * @param string $add for additional constraints in the WHERE-part of the query
+ * @param string $sort_order can be ASC or DESC
+ * @param int $start can be used for pagination, is used for the LIMIT-part of the query
+ * @param int $limit number of entries to fetch, defaults to ForumEntry::POSTINGS_PER_PAGE
+ *
+ * @return array
+ *
+ * @throws Exception if the retrieval failed, an Exception is thrown
+ */
+ public static function getEntries($parent_id, $with_childs = false, $add = '',
+ $sort_order = 'DESC', $start = 0, $limit = ForumEntry::POSTINGS_PER_PAGE)
+ {
+ $constraint = ForumEntry::getConstraints($parent_id);
+ $seminar_id = $constraint['seminar_id'];
+ $depth = $constraint['depth'] + 1;
+
+ // count the entries and set correct page if necessary
+ if ($with_childs) {
+ $count_stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries
+ LEFT JOIN forum_favorites as ou ON (ou.topic_id = forum_entries.topic_id AND ou.user_id = ?)
+ WHERE (forum_entries.seminar_id = ?
+ AND forum_entries.seminar_id != forum_entries.topic_id
+ AND lft > ? AND rgt < ?) "
+ . ($depth > 2 ? " OR forum_entries.topic_id = ". DBManager::get()->quote($parent_id) : '')
+ . $add
+ . " ORDER BY forum_entries.mkdate $sort_order");
+ $count_stmt->execute([$GLOBALS['user']->id, $seminar_id, $constraint['lft'], $constraint['rgt']]);
+ $count = $count_stmt->fetchColumn();
+ } else {
+ $count_stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries
+ LEFT JOIN forum_favorites as ou ON (ou.topic_id = forum_entries.topic_id AND ou.user_id = ?)
+ WHERE ((depth = ? AND forum_entries.seminar_id = ?
+ AND forum_entries.seminar_id != forum_entries.topic_id
+ AND lft > ? AND rgt < ?) "
+ . ($depth > 2 ? " OR forum_entries.topic_id = ". DBManager::get()->quote($parent_id) : '')
+ . ') '. $add
+ . " ORDER BY forum_entries.mkdate $sort_order");
+ $count_stmt->execute([$GLOBALS['user']->id, $depth, $seminar_id, $constraint['lft'], $constraint['rgt']]);
+ $count = $count_stmt->fetchColumn();
+ }
+
+ // use the last page if the requested page does not exist
+ if ($start > $count) {
+ $page = ceil($count / ForumEntry::POSTINGS_PER_PAGE);
+ ForumHelpers::setPage($page);
+ $start = max(1, $page - 1) * ForumEntry::POSTINGS_PER_PAGE;
+ }
+
+ if ($with_childs) {
+ $stmt = DBManager::get()->prepare("SELECT forum_entries.*, IF(ou.topic_id IS NOT NULL, 'fav', NULL) as fav
+ FROM forum_entries
+ LEFT JOIN forum_favorites as ou ON (ou.topic_id = forum_entries.topic_id AND ou.user_id = ?)
+ WHERE (forum_entries.seminar_id = ?
+ AND forum_entries.seminar_id != forum_entries.topic_id
+ AND lft > ? AND rgt < ?) "
+ . ($depth > 2 ? " OR forum_entries.topic_id = ". DBManager::get()->quote($parent_id) : '')
+ . $add
+ . " ORDER BY forum_entries.mkdate $sort_order"
+ . ($limit ? " LIMIT $start, $limit" : ''));
+ $stmt->execute([$GLOBALS['user']->id, $seminar_id, $constraint['lft'], $constraint['rgt']]);
+ } else {
+ $stmt = DBManager::get()->prepare("SELECT forum_entries.*, IF(ou.topic_id IS NOT NULL, 'fav', NULL) as fav
+ FROM forum_entries
+ LEFT JOIN forum_favorites as ou ON (ou.topic_id = forum_entries.topic_id AND ou.user_id = ?)
+ WHERE ((depth = ? AND forum_entries.seminar_id = ?
+ AND lft > ? AND rgt < ?) "
+ . ($depth > 2 ? " OR forum_entries.topic_id = ". DBManager::get()->quote($parent_id) : '')
+ . ') '. $add
+ . " ORDER BY forum_entries.mkdate $sort_order"
+ . ($limit ? " LIMIT $start, $limit" : ''));
+ $stmt->execute([$GLOBALS['user']->id, $depth, $seminar_id, $constraint['lft'], $constraint['rgt']]);
+ }
+
+ if (!$stmt) {
+ throw new Exception("Error while retrieving postings in " . __FILE__ . " on line " . __LINE__);
+ }
+
+ return ['list' => ForumEntry::parseEntries($stmt->fetchAll(PDO::FETCH_ASSOC)), 'count' => $count];
+ }
+
+
+ /**
+ * Takes a posting-array like the one generated by ForumEntry::getList()
+ * and adds the child-posting with the freshest creation-date to it.
+ *
+ * @param array $postings
+ * @return array
+ */
+ public static function getLastPostings($postings)
+ {
+ foreach ($postings as $key => $posting) {
+
+ if ($data = ForumEntry::getLatestPosting($posting['topic_id'])) {
+ $last_posting['topic_id'] = $data['topic_id'];
+ $last_posting['date'] = $data['mkdate'];
+ $last_posting['user_id'] = $data['user_id'];
+ $last_posting['user_fullname'] = $data['author'];
+ $last_posting['username'] = get_username($data['user_id']);
+ $last_posting['anonymous'] = $data['anonymous'];
+
+ // we throw away all formatting stuff, tags, etc, so we have just the important bit of information
+ $text = strip_tags($data['name']);
+ $text = ForumEntry::br2space($text);
+ $text = ForumEntry::killFormat(ForumEntry::removeQuotes($text));
+
+ if (mb_strlen($text) > 42) {
+ $text = mb_substr($text, 0, 40) . '...';
+ }
+
+ $last_posting['text'] = $text;
+ }
+
+ $postings[$key]['last_posting'] = $last_posting;
+ if (!$postings[$key]['last_unread'] = ForumEntry::getLastUnread($posting['topic_id'])) {
+ $postings[$key]['last_unread'] = $last_posting['topic_id'];
+ }
+ $postings[$key]['num_postings'] = ForumEntry::countEntries($posting['topic_id']);
+
+ unset($last_posting);
+ }
+
+ return $postings;
+ }
+
+ /**
+ * get a list of postings of a special type
+ *
+ * @param string $type one of 'area', 'list', 'postings', 'latest', 'favorites', 'dump', 'flat'
+ * @param string $parent_id the are to fetch from
+ * @return array array('list' => ..., 'count' => ...);
+ */
+ public static function getList($type, $parent_id)
+ {
+ $start = (ForumHelpers::getPage() - 1) * ForumEntry::POSTINGS_PER_PAGE;
+
+ switch ($type) {
+ case 'area':
+ $list = ForumEntry::getEntries($parent_id, ForumEntry::WITHOUT_CHILDS, '', 'DESC', 0, 1000);
+ $postings = $list['list'];
+
+ $postings = ForumEntry::getLastPostings($postings);
+ return ['list' => $postings, 'count' => $list['count']];
+
+ break;
+
+ case 'list':
+ $constraint = ForumEntry::getConstraints($parent_id);
+
+ // purpose of the following query is to retrieve the threads
+ // for an area ordered by the mkdate of their latest posting
+ $stmt = DBManager::get()->prepare("SELECT SQL_CALC_FOUND_ROWS
+ fe.*, IF(ou.topic_id IS NOT NULL, 'fav', NULL) as fav
+ FROM forum_entries AS fe
+ LEFT JOIN forum_favorites as ou ON (ou.topic_id = fe.topic_id AND ou.user_id = :user_id)
+ WHERE fe.seminar_id = :seminar_id AND fe.lft > :left
+ AND fe.rgt < :right AND fe.depth = 2
+ ORDER BY sticky DESC, latest_chdate DESC
+ LIMIT $start, ". ForumEntry::POSTINGS_PER_PAGE);
+ $stmt->bindParam(':seminar_id', $constraint['seminar_id']);
+ $stmt->bindParam(':left', $constraint['lft'], PDO::PARAM_INT);
+ $stmt->bindParam(':right', $constraint['rgt'], PDO::PARAM_INT);
+ $stmt->bindParam(':user_id', $GLOBALS['user']->id);
+ $stmt->execute();
+
+ $postings = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ $count = DBManager::get()->query("SELECT FOUND_ROWS()")->fetchColumn();
+ $postings = ForumEntry::parseEntries($postings);
+ $postings = ForumEntry::getLastPostings($postings);
+
+ return ['list' => $postings, 'count' => $count];
+ break;
+
+ case 'postings':
+ return ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, '', 'ASC', $start);
+ break;
+
+ case 'newest':
+ $constraint = ForumEntry::getConstraints($parent_id);
+
+ // get postings
+ $stmt = DBManager::get()->prepare("SELECT forum_entries.*, IF(ou.topic_id IS NOT NULL, 'fav', NULL) as fav
+ FROM forum_entries
+ LEFT JOIN forum_favorites as ou ON (ou.topic_id = forum_entries.topic_id AND ou.user_id = :user_id)
+ WHERE seminar_id = :seminar_id AND lft > :left
+ AND rgt < :right AND (mkdate >= :mkdate OR chdate >= :mkdate)
+ ORDER BY mkdate ASC
+ LIMIT $start, ". ForumEntry::POSTINGS_PER_PAGE);
+
+ $stmt->bindParam(':seminar_id', $constraint['seminar_id']);
+ $stmt->bindParam(':left', $constraint['lft']);
+ $stmt->bindParam(':right', $constraint['rgt']);
+ $stmt->bindParam(':mkdate', ForumVisit::getLastVisit($constraint['seminar_id']));
+ $stmt->bindParam(':user_id', $GLOBALS['user']->id);
+ $stmt->execute();
+
+ $postings = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ $postings = ForumEntry::parseEntries($postings);
+ // var_dump($postings);
+
+ // count found postings
+ $stmt_count = DBManager::get()->prepare("SELECT COUNT(*)
+ FROM forum_entries
+ WHERE seminar_id = :seminar_id AND lft > :left
+ AND rgt < :right AND mkdate >= :mkdate
+ ORDER BY mkdate ASC");
+
+ $stmt_count->bindParam(':seminar_id', $constraint['seminar_id']);
+ $stmt_count->bindParam(':left', $constraint['lft']);
+ $stmt_count->bindParam(':right', $constraint['rgt']);
+ $stmt_count->bindParam(':mkdate', ForumVisit::getLastVisit($constraint['seminar_id']));
+ $stmt_count->execute();
+
+
+ // return results
+ return ['list' => $postings, 'count' => $stmt_count->fetchColumn()];
+ break;
+
+ case 'latest':
+ return ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, '', 'DESC', $start);
+ break;
+
+ case 'favorites':
+ $add = "AND ou.topic_id IS NOT NULL";
+ return ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, $add, 'DESC', $start);
+ break;
+
+ case 'dump':
+ $constraint = ForumEntry::getConstraints($parent_id);
+ $seminar_id = $constraint['seminar_id'];
+ $depth = $constraint['depth'] + 1;
+
+ $stmt = DBManager::get()->prepare("SELECT * FROM forum_entries
+ WHERE (forum_entries.seminar_id = ?
+ AND forum_entries.seminar_id != forum_entries.topic_id
+ AND lft > ? AND rgt < ?) "
+ . ($depth > 2 ? " OR forum_entries.topic_id = ". DBManager::get()->quote($parent_id) : '')
+ . " ORDER BY forum_entries.lft ASC");
+ $stmt->execute([$seminar_id, $constraint['lft'], $constraint['rgt']]);
+
+ return ForumEntry::parseEntries($stmt->fetchAll(PDO::FETCH_ASSOC));
+ break;
+
+ case 'flat':
+ $constraint = ForumEntry::getConstraints($parent_id);
+
+ $stmt = DBManager::get()->prepare("SELECT SQL_CALC_FOUND_ROWS * FROM forum_entries
+ WHERE lft > ? AND rgt < ? AND seminar_id = ? AND depth = ?
+ ORDER BY name ASC");
+ $stmt->execute([$constraint['lft'], $constraint['rgt'], $constraint['seminar_id'], $constraint['depth'] + 1]);
+
+ $count = DBManager::get()->query("SELECT FOUND_ROWS()")->fetchColumn();
+
+ $posting_list = [];
+
+ // speed up things a bit by leaving out the formatReady fields
+ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $data) {
+ // we throw away all formatting stuff, tags, etc, leaving the important bit of information
+ $desc_short = ForumEntry::br2space(ForumEntry::killFormat(strip_tags($data['content'])));
+ if (mb_strlen($desc_short) > (ForumEntry::THREAD_PREVIEW_LENGTH + 2)) {
+ $desc_short = mb_substr($desc_short, 0, ForumEntry::THREAD_PREVIEW_LENGTH) . '...';
+ } else {
+ $desc_short = $desc_short;
+ }
+ $posting_list[$data['topic_id']] = [
+ 'author' => $data['author'],
+ 'topic_id' => $data['topic_id'],
+ 'name_raw' => $data['name'],
+ 'content_raw' => ForumEntry::killEdit($data['content']),
+ 'content_short' => $desc_short,
+ 'chdate' => $data['chdate'],
+ 'mkdate' => $data['mkdate'],
+ 'user_id' => $data['user_id'],
+ 'raw_title' => $data['name'],
+ 'raw_description' => ForumEntry::killEdit($data['content']),
+ 'fav' => ($data['fav'] == 'fav'),
+ 'depth' => $data['depth'],
+ 'seminar_id' => $data['seminar_id']
+ ];
+ }
+
+ return ['list' => $posting_list, 'count' => $count];
+ break;
+
+ case 'depth_to_large':
+ $constraint = ForumEntry::getConstraints($parent_id);
+
+ $stmt = DBManager::get()->prepare("SELECT SQL_CALC_FOUND_ROWS * FROM forum_entries
+ WHERE lft > ? AND rgt < ? AND seminar_id = ? AND depth > 3
+ ORDER BY name ASC");
+ $stmt->execute([$constraint['lft'], $constraint['rgt'], $constraint['seminar_id']]);
+
+ $count = DBManager::get()->query("SELECT FOUND_ROWS()")->fetchColumn();
+
+ return ['list' => $stmt->fetchAll(PDO::FETCH_ASSOC), 'count' => $count];
+ break;
+ }
+ }
+
+ /**
+ * Get the latest forum entries for the passed entries childs
+ *
+ * @param string $parent_id
+ * @param int $start_date timestamp
+ * @param int $end_date timestamp
+ *
+ * @return array list of postings
+ */
+ public static function getLatestSince($parent_id, $start_date, $end_date)
+ {
+ $constraint = ForumEntry::getConstraints($parent_id);
+
+ $stmt = DBManager::get()->prepare("SELECT SQL_CALC_FOUND_ROWS * FROM forum_entries
+ WHERE lft > ? AND rgt < ? AND seminar_id = ?
+ AND mkdate BETWEEN ? AND ?
+ ORDER BY name ASC");
+ $stmt->execute([$constraint['lft'], $constraint['rgt'], $constraint['seminar_id'], $start_date, $end_date]);
+
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ ** returns a list of postings for the passed search-term
+ *
+ * @param string $parent_id the area to search in (can be a whole seminar)
+ * @param string $_searchfor the term to search for
+ * @param array $options filter-options: search_title, search_content, search_author
+ * @return array array('list' => ..., 'count' => ...);
+ */
+ public static function getSearchResults($parent_id, $_searchfor, $options)
+ {
+ $start = (ForumHelpers::getPage() - 1) * ForumEntry::POSTINGS_PER_PAGE;
+
+ // if there are quoted parts, they should not be separated
+ $suchmuster = '/".*"/U';
+ preg_match_all($suchmuster, $_searchfor, $treffer);
+ array_walk($treffer[0], function(&$value) { $value = trim($value, '"'); });
+
+ // remove the quoted parts from $_searchfor
+ $_searchfor = trim(preg_replace($suchmuster, '', $_searchfor));
+
+ // split the searchstring $_searchfor at every space
+ $parts = explode(' ', $_searchfor);
+
+ foreach ($parts as $key => $val) {
+ if ($val == '') {
+ unset($parts[$key]);
+ }
+ }
+
+ if (!empty($parts)) {
+ $_searchfor = array_merge($parts, $treffer[0]);
+ } else {
+ $_searchfor = $treffer[0];
+ }
+
+ // make an SQL-statement out of the searchstring
+ $search_string = [];
+ foreach ($_searchfor as $key => $val) {
+ if (!$val) {
+ unset($_searchfor[$key]);
+ } else {
+ $search_word = '%'. $val .'%';
+ $zw_search_string = [];
+ if ($options['search_title']) {
+ $zw_search_string[] .= "name LIKE " . DBManager::get()->quote($search_word);
+ }
+
+ if ($options['search_content']) {
+ $zw_search_string[] .= "content LIKE " . DBManager::get()->quote($search_word);
+ }
+
+ if ($options['search_author']) {
+ $zw_search_string[] .= "author LIKE " . DBManager::get()->quote($search_word);
+ }
+
+ if (!empty($zw_search_string)) {
+ $search_string[] = '(' . implode(' OR ', $zw_search_string) . ')';
+ }
+ }
+ }
+
+ if (!empty($search_string)) {
+ $add = "AND (" . implode(' AND ', $search_string) . ")";
+ return array_merge(
+ ['highlight' => $_searchfor],
+ ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, $add, 'DESC', $start)
+ );
+ }
+
+ return ['num_postings' => 0, 'list' => []];
+ }
+
+ /**
+ * returns the entry for the passed topic_id
+ *
+ * @param string $topic_id
+ * @return array hash-array with the entries fields
+ */
+ public static function getEntry($topic_id)
+ {
+ return ForumEntry::getConstraints($topic_id);
+ }
+
+ /**
+ * Count the number of child-elements that the passed entry has and return it.
+ *
+ * @param string $parent_id
+ *
+ * @return int the number of child entries for the passed entry
+ */
+ public static function countEntries($parent_id)
+ {
+ $data = ForumEntry::getConstraints($parent_id);
+ return max((($data['rgt'] - $data['lft'] - 1) / 2) + 1, 0);
+ }
+
+ /**
+ * Count the number of postings in a given course and return it.
+ *
+ * @param string $course_id the id of the given course
+ *
+ * @return int the number of postings in the course
+ */
+ public static function countPostings($course_id)
+ {
+ $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries
+ WHERE seminar_id = ? AND depth >= 2");
+ $stmt->execute([$course_id]);
+
+ return $stmt->fetchColumn(0);
+ }
+
+ /**
+ * Count all entries the passed user has ever written and return the result
+ *
+ * @staticvar type $entries
+ *
+ * @param string $user_id
+ *
+ * @return int number of entries user has ever written
+ */
+ public static function countUserEntries($user_id, $seminar_id = null)
+ {
+ static $entries;
+
+ if (!$entries[$user_id]) {
+ $stmt = DBManager::get()->prepare("SELECT COUNT(*)
+ FROM forum_entries
+ WHERE user_id = ? AND seminar_id = IFNULL(?, seminar_id)");
+ $stmt->execute([$user_id, $seminar_id]);
+
+ $entries[$user_id] = $stmt->fetchColumn();
+ }
+
+ return $entries[$user_id];
+ }
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * D A T A - C R E A T I O N *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ /**
+ * insert a node into the table
+ *
+ * @param array $data an array containing the following fields:
+ * topic_id the id of the new topic
+ * seminar_id the id of the seminar to add the topic to
+ * user_id the id of the user who created the topic
+ * name the title of the entry
+ * content the content of the entry
+ * author the author's name as a plaintext string
+ * author_host ip-address of creator
+ * @param string $parent_id the node to add the topic to
+ *
+ * @return void
+ */
+ public static function insert($data, $parent_id)
+ {
+ $constraint = ForumEntry::getConstraints($parent_id);
+
+ // #TODO: Zusammenfassen in eine Transaktion!!!
+ DBManager::get()->exec('UPDATE forum_entries SET lft = lft + 2
+ WHERE lft > '. $constraint['rgt'] ." AND seminar_id = '". $constraint['seminar_id'] ."'");
+ DBManager::get()->exec('UPDATE forum_entries SET rgt = rgt + 2
+ WHERE rgt >= '. $constraint['rgt'] ." AND seminar_id = '". $constraint['seminar_id'] ."'");
+
+ $stmt = DBManager::get()->prepare("INSERT INTO forum_entries
+ (topic_id, seminar_id, user_id, name, content, mkdate, latest_chdate,
+ chdate, author, author_host, lft, rgt, depth, anonymous)
+ VALUES (? ,?, ?, ?, ?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$data['topic_id'], $data['seminar_id'], $data['user_id'],
+ $data['name'], transformBeforeSave($data['content']), $data['author'], $data['author_host'],
+ $constraint['rgt'], $constraint['rgt'] + 1, $constraint['depth'] + 1, $data['anonymous'] ? : 0]);
+
+ // update "latest_chdate" for easier sorting of actual threads
+ DBManager::get()->exec("UPDATE forum_entries SET latest_chdate = UNIX_TIMESTAMP()
+ WHERE topic_id = '" . $constraint['topic_id'] . "'");
+
+ NotificationCenter::postNotification('ForumAfterInsert', $data['topic_id'], $data);
+ }
+
+
+ /**
+ * update the passed topic
+ *
+ * @param string $topic_id the id of the topic to update
+ * @param string $name the new name
+ * @param string $content the new content
+ *
+ * @return void
+ */
+ public static function update($topic_id, $name, $content)
+ {
+ $post = ForumEntry::getConstraints($topic_id);
+
+ if (time() - $post['mkdate'] > 5 * 60) {
+ $content = ForumEntry::appendEdit($content);
+ }
+
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries
+ SET name = ?, content = ?, chdate = UNIX_TIMESTAMP(), latest_chdate = UNIX_TIMESTAMP()
+ WHERE topic_id = ?");
+ $stmt->execute([$name, transformBeforeSave($content), $topic_id]);
+
+ // update "latest_chdate" for easier sorting of actual threads
+ $parent_id = ForumEntry::getParentTopicId($topic_id);
+ DBManager::get()->exec("UPDATE forum_entries SET latest_chdate = UNIX_TIMESTAMP()
+ WHERE topic_id = '" . $parent_id . "'");
+
+ $post['name'] = $name;
+ $post['content'] = $content;
+
+ NotificationCenter::postNotification('ForumAfterUpdate', $topic_id, $post);
+ }
+
+ /**
+ * delete an entry and all his descendants from the mptt-table
+ *
+ * @param string $topic_id the id of the entry to delete
+ *
+ * @return void
+ */
+ public static function delete($topic_id)
+ {
+ $post = ForumEntry::getConstraints($topic_id);
+ $parent = ForumEntry::getConstraints(ForumEntry::getParentTopicId($topic_id));
+
+ NotificationCenter::postNotification('ForumBeforeDelete', $topic_id, $post);
+
+ // #TODO: Zusammenfassen in eine Transaktion!!!
+ // get all entry-ids to delete them from the category-reference-table
+ $stmt = DBManager::get()->prepare("SELECT topic_id FROM forum_entries
+ WHERE seminar_id = ? AND lft >= ? AND rgt <= ? AND depth = 1");
+ $stmt->execute([$post['seminar_id'], $post['lft'], $post['rgt']]);
+ $ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
+
+ if ($ids != false && !is_array($ids)) $ids = [$ids];
+
+ if (!empty($ids)) {
+ $stmt = DBManager::get()->prepare("DELETE FROM forum_categories_entries
+ WHERE topic_id IN (:ids)");
+ $stmt->bindParam(':ids', $ids, StudipPDO::PARAM_ARRAY);
+ $stmt->execute();
+ }
+
+ // delete all entries
+ $stmt = DBManager::get()->prepare("DELETE FROM forum_entries
+ WHERE seminar_id = ? AND lft >= ? AND rgt <= ?");
+
+ $stmt->execute([$post['seminar_id'], $post['lft'], $post['rgt']]);
+
+ // update lft and rgt
+ $diff = $post['rgt'] - $post['lft'] + 1;
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries SET lft = lft - $diff
+ WHERE lft > ? AND seminar_id = ?");
+ $stmt->execute([$post['rgt'], $post['seminar_id']]);
+
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries SET rgt = rgt - $diff
+ WHERE rgt > ? AND seminar_id = ?");
+ $stmt->execute([$post['rgt'], $post['seminar_id']]);
+
+
+ // set the latest_chdate to the latest child's chdate
+ $stmt = DBManager::get()->prepare("SELECT chdate FROM forum_entries
+ WHERE lft > ? AND rgt < ? AND seminar_id = ?
+ ORDER BY chdate DESC LIMIT 1");
+ $stmt->execute([$parent['lft'], $parent['rgt'], $parent['seminar_id']]);
+ $chdate = $stmt->fetchColumn();
+
+ $stmt_insert = DBManager::get()->prepare("UPDATE forum_entries
+ SET chdate = ? WHERE topic_id = ?");
+ if ($chdate) {
+ $stmt_insert->execute([$chdate, $parent['topic_id']]);
+ } else {
+ $stmt_insert->execute([$parent['chdate'], $parent['topic_id']]);
+ }
+ }
+
+ /**
+ * move the passed topic to the passed area
+ *
+ * @param string $topic_id the topic to move
+ * @param string $destination the area_id where the topic is moved to
+ *
+ * @return void
+ */
+ public static function move($topic_id, $destination)
+ {
+ // #TODO: Zusammenfassen in eine Transaktion!!!
+ $constraints = ForumEntry::getConstraints($topic_id);
+
+ // move the affected entries "outside" the tree
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries
+ SET lft = lft * -1, rgt = rgt * -1
+ WHERE seminar_id = ? AND lft >= ? AND rgt <= ?");
+ $stmt->execute([$constraints['seminar_id'], $constraints['lft'], $constraints['rgt']]);
+
+ // update the lft and rgt values of the parent to reflect the "deletion"
+ $diff = $constraints['rgt'] - $constraints['lft'] + 1;
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries SET lft = lft - ?
+ WHERE lft > ? AND seminar_id = ?");
+ $stmt->execute([$diff, $constraints['rgt'], $constraints['seminar_id']]);
+
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries SET rgt = rgt - ?
+ WHERE rgt > ? AND seminar_id = ?");
+ $stmt->execute([$diff, $constraints['rgt'], $constraints['seminar_id']]);
+
+ // make some space by updating the lft and rgt values of the target node
+ $constraints_destination = ForumEntry::getConstraints($destination);
+ $size = $constraints['rgt'] - $constraints['lft'] + 1;
+
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries SET lft = lft + ?
+ WHERE lft > ? AND seminar_id = ?");
+ $stmt->execute([$size, $constraints_destination['rgt'], $constraints_destination['seminar_id']]);
+
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries SET rgt = rgt + ?
+ WHERE rgt >= ? AND seminar_id = ?");
+ $stmt->execute([$size, $constraints_destination['rgt'], $constraints_destination['seminar_id']]);
+
+ //move the entries from "outside" the tree to the target node
+ $constraints_destination = ForumEntry::getConstraints($destination);
+
+
+ // update the depth to reflect the new position in the tree
+ // determine if we need to add, subtract or even do nothing to/from the depth
+ $depth_mod = $constraints_destination['depth'] - $constraints['depth'] + 1;
+
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries
+ SET depth = depth + ?
+ WHERE seminar_id = ? AND lft < 0");
+ $stmt->execute([$depth_mod, $constraints_destination['seminar_id']]);
+
+ // if the depth is larger than 3, fix it
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries
+ SET depth = 3
+ WHERE seminar_id = ? AND depth > 3 AND lft < 0");
+ $stmt->execute([$constraints_destination['seminar_id']]);
+
+ // move the tree to its destination
+ $diff = ($constraints_destination['rgt'] - ($constraints['rgt'] - $constraints['lft'])) - 1 - $constraints['lft'];
+
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries
+ SET lft = (lft * -1) + ?, rgt = (rgt * -1) + ?
+ WHERE seminar_id = ? AND lft < 0");
+ $stmt->execute([$diff, $diff, $constraints_destination['seminar_id']]);
+
+ if ($depth_mod != 0) {
+ self::fix_ordering($topic_id);
+ }
+ }
+
+ private static function fix_ordering($parent_id)
+ {
+ $db = DBManager::get();
+
+ $entry = ForumEntry::getConstraints($parent_id);
+
+ $stmt= $db->prepare('SELECT topic_id FROM forum_entries
+ WHERE lft > ? AND rgt < ? AND depth = 3
+ AND seminar_id = ?
+ ORDER BY mkdate');
+
+ $stmt->execute([$entry['lft'], $entry['rgt'], $entry['seminar_id']]);
+
+ $lft = $entry['lft'] + 1;
+ $rgt = $lft + 1;
+
+ $inner_stmt = $db->prepare("UPDATE forum_entries SET lft=?, rgt=?
+ WHERE topic_id = ?");
+ while ($topic_id = $stmt->fetchColumn()) {
+ $inner_stmt->execute([$lft, $rgt, $topic_id]);
+
+ $lft += 2;
+ $rgt += 2;
+ }
+ }
+
+ /**
+ * close the passed topic
+ *
+ * @param string $topic_id the topic to close
+ *
+ * @return void
+ */
+ public static function close($topic_id)
+ {
+ // close all entries belonging to the topic
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries
+ SET closed = 1
+ WHERE topic_id = ?");
+ $stmt->execute([$topic_id]);
+ }
+
+ /**
+ * open the passed topic
+ *
+ * @param string $topic_id the topic to open
+ *
+ * @return void
+ */
+ public static function open($topic_id)
+ {
+ // open all entries belonging to the topic
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries
+ SET closed = 0
+ WHERE topic_id = ?");
+ $stmt->execute([$topic_id]);
+ }
+
+ /**
+ * make the passed topic sticky
+ *
+ * @param string $topic_id the topic to make sticky
+ *
+ * @return void
+ */
+ public static function sticky($topic_id)
+ {
+ // open all entries belonging to the topic
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries
+ SET sticky = 1
+ WHERE topic_id = ?");
+ $stmt->execute([$topic_id]);
+ }
+
+ /**
+ * make the passed topic unsticky
+ *
+ * @param string $topic_id the topic to make unsticky
+ *
+ * @return void
+ */
+ public static function unsticky($topic_id)
+ {
+ // open all entries belonging to the topic
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries
+ SET sticky = 0
+ WHERE topic_id = ?");
+ $stmt->execute([$topic_id]);
+ }
+
+ /**
+ * check, if the default root-node for this seminar exists and make sure
+ * the default category exists as well
+ *
+ * @param string $seminar_id
+ *
+ * @return void
+ */
+ public static function checkRootEntry($seminar_id)
+ {
+ setTempLanguage();
+
+ // check, if the root entry in the topic tree exists
+ $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries
+ WHERE topic_id = ? AND seminar_id = ?");
+ $stmt->execute([$seminar_id, $seminar_id]);
+ if ($stmt->fetchColumn() == 0) {
+ $stmt = DBManager::get()->prepare("INSERT INTO forum_entries
+ (topic_id, seminar_id, name, mkdate, chdate, lft, rgt, depth)
+ VALUES (?, ?, 'Übersicht', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 1, 0)");
+ $stmt->execute([$seminar_id, $seminar_id]);
+ }
+
+
+ // make sure, that the category "Allgemein" exists
+ $stmt = DBManager::get()->prepare("INSERT IGNORE INTO forum_categories
+ (category_id, seminar_id, entry_name) VALUES (?, ?, ?)");
+ $stmt->execute([$seminar_id, $seminar_id, _('Allgemein')]);
+
+ // make sure that the default area "Allgemeine Diskussionen" exists, if there is nothing else present
+ $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries
+ WHERE seminar_id = ? AND depth = 1");
+ $stmt->execute([$seminar_id]);
+
+ // add default area
+ if ($stmt->fetchColumn() == 0) {
+ $data = [
+ 'topic_id' => md5(uniqid()),
+ 'seminar_id' => $seminar_id,
+ 'user_id' => '',
+ 'name' => _('Allgemeine Diskussion'),
+ 'content' => _('Hier ist Raum für allgemeine Diskussionen'),
+ 'author' => '',
+ 'author_host' => ''
+ ];
+ ForumEntry::insert($data, $seminar_id);
+ }
+
+ restoreLanguage();
+ }
+
+ /**
+ * returns the ten most active seminars
+ *
+ * @return array
+ */
+ public static function getTopTenSeminars()
+ {
+ return DBManager::get()->query("SELECT a.seminar_id, b.name AS display,
+ count( a.seminar_id ) AS count FROM forum_entries a
+ INNER JOIN seminare b USING ( seminar_id )
+ WHERE b.visible = 1
+ AND a.mkdate > UNIX_TIMESTAMP( NOW( ) - INTERVAL 2 WEEK )
+ GROUP BY a.seminar_id
+ ORDER BY count DESC
+ LIMIT 10")->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * count all entries that exists in the whole installation and return it.
+ *
+ * @return int
+ */
+ public static function countAllEntries()
+ {
+ return count_table_rows('forum_entries');
+ }
+
+ /**
+ * updates the user-entries and replaces the old user-id by the new one
+ *
+ * @param string $user_from
+ * @param string $user_to
+ */
+ public static function migrateUser($user_from, $user_to)
+ {
+ $stmt = DBManager::get()->prepare("UPDATE forum_entries SET user_id = ? WHERE user_id = ?");
+ $stmt->execute([$user_to, $user_from]);
+
+ $stmt = DBManager::get()->prepare("UPDATE IGNORE forum_favorites SET user_id = ? WHERE user_id = ?");
+ $stmt->execute([$user_to, $user_from]);
+
+ $stmt = DBManager::get()->prepare("UPDATE IGNORE forum_visits SET user_id = ? WHERE user_id = ?");
+ $stmt->execute([$user_to, $user_from]);
+
+ $stmt = DBManager::get()->prepare("UPDATE IGNORE forum_likes SET user_id = ? WHERE user_id = ?");
+ $stmt->execute([$user_to, $user_from]);
+
+ $stmt = DBManager::get()->prepare("UPDATE IGNORE forum_abo_users SET user_id = ? WHERE user_id = ?");
+ $stmt->execute([$user_to, $user_from]);
+ }
+
+ /**
+ * returns the complete seminar or only the passed sub-tree as a html-string
+ *
+ * @param string $seminar_id
+ *
+ * @return string
+ */
+ public static function getDump($seminar_id, $parent_id = null)
+ {
+ $seminar_name = get_object_name($seminar_id, 'sem');
+ $content = '<h1>'. _('Forum') .': ' . $seminar_name['name'] .'</h1>';
+ $data = ForumEntry::getList('dump', $parent_id ?: $seminar_id);
+
+ foreach ($data as $entry) {
+ if ($entry['depth'] == 1) {
+ $content .= '<h2>'. _('Bereich') .': '. $entry['name'] .'</h2>';
+ $content .= $entry['content'] .'<br><br>';
+ } else if ($entry['depth'] == 2) {
+ $content .= '<h3 style="margin-bottom: 0px;">'. _('Thema') .': '. $entry['name'] .'</h3>';
+ $content .= '<i>' . sprintf(_('erstellt von %s am %s'), htmlReady($entry['author']),
+ strftime('%A %d. %B %Y, %H:%M', (int)$entry['mkdate'])) . '</i><br>';
+ $content .= $entry['content'] .'<br><br>';
+ } else if ($entry['depth'] == 3) {
+ $content .= '<b>'.$entry['name'] .'</b><br>';
+ $content .= '<i>' . sprintf(_('erstellt von %s am %s'), htmlReady($entry['author']),
+ strftime('%A %d. %B %Y, %H:%M', (int)$entry['mkdate'])) . '</i><br>';
+ $content .= $entry['content'] .'<hr><br>';
+ }
+ }
+
+ return $content;
+ }
+
+ public static function isClosed($topic_id)
+ {
+ foreach(ForumEntry::getPathToPosting($topic_id) as $entry) {
+ if ($entry['closed']) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 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 static function exportUserData(StoredUserData $storage)
+ {
+ $field_data = DBManager::get()->fetchAll("SELECT * FROM forum_entries WHERE user_id = ?", [$storage->user_id]);
+ if ($field_data) {
+ $storage->addTabularData(_('Forum Einträge'), 'forum_entries', $field_data);
+ }
+ }
+
+}
diff --git a/lib/classes/ForumFavorite.php b/lib/classes/ForumFavorite.php
new file mode 100644
index 0000000..9ffebf5
--- /dev/null
+++ b/lib/classes/ForumFavorite.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * ForumFavorite.php - Add and remove favorite postings
+ *
+ * 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
+ */
+
+class ForumFavorite {
+
+ /**
+ * Set the topic denoted by the passed id as favorite for the
+ * currently logged in user
+ *
+ * @param string $topic_id
+ */
+ static function set($topic_id) {
+ $stmt = DBManager::get()->prepare("REPLACE INTO
+ forum_favorites (topic_id, user_id)
+ VALUES (?, ?)");
+ $stmt->execute([$topic_id, $GLOBALS['user']->id]);
+ }
+
+ /**
+ * Remove the topic denoted by the passed id as favorite for the
+ * currently logged in user
+ *
+ * @param string $topic_id
+ */
+ static function remove($topic_id) {
+ $stmt = DBManager::get()->prepare("DELETE FROM forum_favorites
+ WHERE topic_id = ? AND user_id = ?");
+ $stmt->execute([$topic_id, $GLOBALS['user']->id]);
+ }
+} \ No newline at end of file
diff --git a/lib/classes/ForumHelpers.php b/lib/classes/ForumHelpers.php
new file mode 100644
index 0000000..ed478ad
--- /dev/null
+++ b/lib/classes/ForumHelpers.php
@@ -0,0 +1,282 @@
+<?php
+/**
+ * ForumHelpers.php - Some useful helpers for the forum
+ *
+ * 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
+ */
+
+class ForumHelpers {
+
+ /**
+ * The page for the current script run, modified by a global page-handle
+ * @var int
+ */
+ static $page = 1;
+
+ /**
+ * helper_function for highlight($text, $highlight)
+ *
+ * @param string $text
+ * @param array $highlight
+ * @return string
+ */
+ public static function do_highlight($text, $highlight)
+ {
+ foreach ($highlight as $hl) {
+ $text = preg_replace(
+ '/' . preg_quote(htmlReady($hl), '/') . '/i',
+ '<span class="highlight">$0</span>',
+ $text
+ );
+ }
+ return $text;
+ }
+
+ /**
+ * This function highlights Text HTML-safe
+ * (tags or words in tags are not highlighted, words between tags ARE highlighted)
+ *
+ * @param string $text the text where to words shall be highlighted, may contain tags
+ * @param array $highlight an array of words to be highlighted
+ * @return string the highlighted text
+ */
+ public static function highlight($text, $highlight)
+ {
+ if (empty($highlight)) {
+ return $text;
+ }
+
+ $data = [];
+ $treffer = [];
+
+ // split text at every tag
+ $pattern = '/<[^<]*>/U';
+ preg_match_all($pattern, $text, $treffer, PREG_OFFSET_CAPTURE);
+
+ if (sizeof($treffer[0]) == 0) {
+ return self::do_highlight($text, $highlight);
+ }
+
+ // cycle trough the text between the tags and highlight all hits
+ $last_pos = 0;
+ foreach ($treffer[0] as $taginfo) {
+ $size = mb_strlen($taginfo[0]);
+ if ($taginfo[1] != 0) {
+ $data[] = self::do_highlight(mb_substr($text, $last_pos, $taginfo[1] - $last_pos), $highlight);
+ }
+
+ $data[] = mb_substr($text, $taginfo[1], $size);
+ $last_pos = $taginfo[1] + $size;
+ }
+
+ // don't miss the last portion of a posting
+ if ($last_pos < mb_strlen($text)) {
+ $data[] = self::do_highlight(mb_substr($text, $last_pos, mb_strlen($text) - $last_pos), $highlight);
+ }
+
+ return implode('', $data);
+ }
+
+ /**
+ * Returns a human-readable version of the passed global Stud.IP permission.
+ *
+ * @param string $perm
+ * @return string
+ */
+ public static function translate_perm($perm)
+ {
+ $mapping = [
+ 'root' => _('Root'),
+ 'admin' => _('Administrator/-in'),
+ 'dozent' => _('Lehrende/-r'),
+ 'tutor' => _('Tutor/-in'),
+ 'autor' => _('Autor/-in'),
+ 'user' => _('Leser/-in'),
+ ];
+
+ // TODO: Activate next when devboard reliably runs on PHP7
+ // return $mapping[$perm] ?? '';
+
+ return isset($mapping[$perm]) ? $mapping[$perm] : '';
+ }
+
+ /**
+ * return the currently chosen page
+ *
+ * @return int
+ */
+ public static function getPage()
+ {
+ return self::$page;
+ }
+
+ /**
+ * set the current page
+ *
+ * @param int $page_num the page
+ */
+ public static function setPage($page_num)
+ {
+ self::$page = $page_num;
+ }
+
+ /**
+ * Return an info-text explaining the visit-status of the passed topic_di
+ * which has the passed number of new entries.
+ *
+ * @param string $num_entries the number of new entries
+ * @param string $topic_id the id of the topic
+ *
+ * @return string a human readable, localized text
+ */
+ public static function getVisitText($num_entries, $topic_id)
+ {
+ if ($num_entries > 0) {
+ $text = sprintf(_('Seit Ihrem letzten Besuch gibt es %s neue Beiträge'), $num_entries);
+ } else {
+ $all_entries = ForumEntry::countPostings($topic_id);
+
+ if ($all_entries == 0) {
+ $text = sprintf(_('Es gibt bisher keine Beiträge.'));
+ } else if ($all_entries == 1) {
+ $text = sprintf(_('Seit Ihrem letzten Besuch gab es nichts Neues.'
+ . ' Es ist ein alter Beitrag vorhanden.'));
+ } else {
+ $text = sprintf(_('Seit Ihrem letzten Besuch gab es nichts Neues.'
+ . ' Es sind %s alte Beiträge vorhanden.'), $all_entries);
+ }
+ }
+
+ return $text;
+ }
+
+ /**
+ * return the online status of the passed user, one of three possible
+ * states is returned:
+ * - available
+ * - away
+ * - offline
+ *
+ * @staticvar type $online_status
+ *
+ * @param string $user_id
+ *
+ * @return string
+ */
+ public static function getOnlineStatus($user_id)
+ {
+ static $online_status;
+
+ // check if the corresponding user's profile is visible
+ if (get_visibility_by_id($user_id) == false) {
+ return 'offline';
+ }
+
+ if ($GLOBALS['user']->id == $user_id) {
+ return 'available';
+ }
+
+ if (!$online_status) {
+ $online_users = get_users_online(10);
+ foreach ($online_users as $username => $data) {
+ if ($data['last_action'] >= 300) {
+ $online_status[$data['user_id']] = 'away';
+ } else {
+ $online_status[$data['user_id']] = 'available';
+ }
+ }
+ }
+
+ return $online_status[$user_id] ?: 'offline';
+ }
+
+ /**
+ * Create a pdf of all postings belonging to the passed seminar located
+ * under the passed topic_id. The PDF is dispatched automatically.
+ *
+ * BEWARE: This function never returns, it dies after the PDF has been
+ * (succesfully or not) dispatched.
+ *
+ * @param string $seminar_id
+ * @param string $parent_id
+ */
+ public static function createPdf($seminar_id, $parent_id = null)
+ {
+ $seminar_name = get_object_name($seminar_id, 'sem');
+ $data = ForumEntry::getList('dump', $parent_id ?: $seminar_id);
+ $first_page = true;
+
+ $document = new ExportPDF();
+ $document->SetTitle(_('Forum'));
+ $document->setHeaderTitle(sprintf(_("Forum \"%s\""), $seminar_name['name']));
+ $document->addPage();
+
+ foreach ($data as $entry) {
+ if (Config::get()->FORUM_ANONYMOUS_POSTINGS && $entry['anonymous']) {
+ $author = _('anonym');
+ } else {
+ $author = $entry['author'];
+ }
+ if ($entry['depth'] == 1) {
+ if (!$first_page) {
+ $document->addPage();
+ }
+ $first_page = false;
+ $document->addContent('!! '. _('Bereich') . ": {$entry['name_raw']}\n");
+ $document->addContent($entry['content_raw']);
+ $document->addContent("\n\n");
+ } else if ($entry['depth'] == 2) {
+ $document->addContent('! '. _('Thema') . ": {$entry['name_raw']}\n");
+ $document->addContent('%%' . sprintf(_('erstellt von %s am %s'), $author,
+ strftime('%A %d. %B %Y, %H:%M', (int)$entry['mkdate'])) . '%%' . "\n");
+ $document->addContent($entry['content_raw']);
+ $document->addContent("\n--\n");
+ } else if ($entry['depth'] == 3) {
+ $document->addContent("**{$entry['name_raw']}**\n");
+ $document->addContent('%%' . sprintf(_('erstellt von %s am %s'), $author,
+ strftime('%A %d. %B %Y, %H:%M', (int)$entry['mkdate'])) . '%%' . "\n");
+ $document->addContent($entry['content_raw']);
+ $document->addContent("\n--\n");
+ }
+ }
+
+ $document->dispatch($seminar_name['name'] ." - Forum");
+ die;
+ }
+
+
+ /**
+ * Returns the id of the currently selected seminar or false, if no seminar
+ * is selected
+ *
+ * @return mixed seminar_id or false
+ */
+ public static function getSeminarId()
+ {
+ return Context::getId();
+ }
+
+ /**
+ * replace in the passed text every %%% with <% and every ### with %>
+ * This is used to work around a limitation of the Button-API in combination
+ * with the underscore.js way of inserting template vars.
+ *
+ * The Button-API correctly replaces < > with tags, but underscore.js is
+ * unable to find them in their tag-represenation
+ *
+ * @param string $text the text to apply the replacements on
+ *
+ * @return string the modified text
+ */
+ public static function replace($text)
+ {
+ return str_replace('%%%', '<%', str_replace('###', '%>', $text));
+ }
+}
diff --git a/lib/classes/ForumIssue.php b/lib/classes/ForumIssue.php
new file mode 100644
index 0000000..8be894f
--- /dev/null
+++ b/lib/classes/ForumIssue.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * ForumIssue.php - Manage issues linked to postings
+ *
+ * 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
+ */
+
+class ForumIssue
+{
+ /**
+ * Get the id of the topic linked to the issue denoted by the passed id.
+ *
+ * @param string $issue_id
+ * @return string the id of the linked topic
+ */
+ static function getThreadIdForIssue($issue_id)
+ {
+ $stmt = DBManager::get()->prepare("SELECT topic_id FROM forum_entries_issues
+ WHERE issue_id = ?");
+ $stmt->execute([$issue_id]);
+
+ return ($stmt->fetchColumn());
+ }
+
+
+ /**
+ * Get the id of the issue linked to the topic denoted by the passed id.
+ *
+ * @param string $topic_id
+ * @return string the id of the linked topic
+ */
+ static function getIssueIdForThread($topic_id)
+ {
+ $stmt = DBManager::get()->prepare("SELECT issue_id FROM forum_entries_issues
+ WHERE topic_id = ?");
+ $stmt->execute([$topic_id]);
+
+ return ($stmt->fetchColumn());
+ }
+
+
+ /**
+ * Create/Update the linked posting for the passed issue_id
+ *
+ * @param string $seminar_id
+ * @param string $issue_id issue id to link to
+ * @param string $title (new) title of the posting
+ * @param string $content (new) content of the posting
+ */
+ static function setThreadForIssue($seminar_id, $issue_id, $title, $content)
+ {
+ if ($topic_id = self::getThreadIdForIssue($issue_id)) { // update
+ ForumEntry::update($topic_id, $title ?: _('Ohne Titel'), $content);
+
+ } else { // create
+ // make sure the forum is set up properly
+ ForumEntry::checkRootEntry($seminar_id);
+
+ $topic_id = md5(uniqid(rand()));
+
+ ForumEntry::insert([
+ 'topic_id' => $topic_id,
+ 'seminar_id' => $seminar_id,
+ 'user_id' => $GLOBALS['user']->id,
+ 'name' => $title ?: _('Ohne Titel'),
+ 'content' => $content,
+ 'author' => get_fullname($GLOBALS['user']->id),
+ 'author_host' => ($GLOBALS['user']->id == 'nobody') ? getenv('REMOTE_ADDR') : ''
+ ], $seminar_id);
+
+ $stmt = DBManager::get()->prepare("INSERT INTO forum_entries_issues
+ (issue_id, topic_id) VALUES (?, ?)");
+ $stmt->execute([$issue_id, $topic_id]);
+ }
+ }
+
+ /**
+ * Remove the link for the posting denoted by the passed topic_id
+ *
+ * @param object $notification
+ * @param string $topic_id
+ */
+ static function unlinkIssue($notification, $topic_id)
+ {
+ $stmt = DBManager::get()->prepare("DELETE FROM forum_entries_issues
+ WHERE topic_id = ?");
+ $stmt->execute([$topic_id]);
+ }
+}
diff --git a/lib/classes/ForumLike.php b/lib/classes/ForumLike.php
new file mode 100644
index 0000000..24cb478
--- /dev/null
+++ b/lib/classes/ForumLike.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * ForumLike.php - Manage the likes for postings
+ *
+ * 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
+ */
+
+class ForumLike {
+
+ /**
+ * Set the posting denoted by the passed topic_id as liked for the
+ * currently logged in user
+ *
+ * @param string $topic_id
+ */
+ static function like($topic_id) {
+ $stmt = DBManager::get()->prepare("REPLACE INTO
+ forum_likes (topic_id, user_id)
+ VALUES (?, ?)");
+ $stmt->execute([$topic_id, $GLOBALS['user']->id]);
+
+ // get posting owner
+ $data = ForumEntry::getConstraints($topic_id);
+
+ // notify owner of posting about the like
+ setTempLanguage($data['user_id']);
+ $notification = get_fullname($GLOBALS['user']->id) . _(' gefällt einer deiner Forenbeiträge!');
+ restoreLanguage();
+
+ PersonalNotifications::add(
+ $data['user_id'], URLHelper::getURL('dispatch.php/course/forum/index/index/' . $topic_id .'?highlight_topic='. $topic_id .'#'. $topic_id),
+ $notification, $topic_id,
+ Icon::create('forum', 'clickable')
+ );
+ }
+
+ /**
+ * Revoke the liking of the posting denoted by the passed topic_id for the
+ * currently logged in user
+ *
+ * @param string $topic_id
+ */
+ static function dislike($topic_id) {
+ $stmt = DBManager::get()->prepare("DELETE FROM forum_likes
+ WHERE topic_id = ? AND user_id = ?");
+ $stmt->execute([$topic_id, $GLOBALS['user']->id]);
+ }
+
+ /**
+ * Get the user_id for all likers of the topic denoted by the passed id
+ *
+ * @param string $topic_id
+ * @return array an array of user_id's
+ */
+ static function getLikes($topic_id) {
+ $stmt = DBManager::get()->prepare("SELECT
+ auth_user_md5.user_id FROM forum_likes
+ LEFT JOIN auth_user_md5 USING (user_id)
+ LEFT JOIN user_info USING (user_id)
+ WHERE topic_id = ?");
+ $stmt->execute([$topic_id]);
+
+ return $stmt->fetchAll(PDO::FETCH_COLUMN);
+ }
+
+ /**
+ * count the number of likes the user has received - system-wide
+ *
+ * @staticvar type $entries
+ * @param string $user_id the user's id to count the received likes for
+ *
+ * @return int the number of likes received
+ */
+ static function receivedForUser($user_id)
+ {
+ static $entries;
+
+ if (!$entries[$user_id]) {
+ $stmt = DBManager::get()->prepare("SELECT COUNT(*)
+ FROM forum_entries
+ LEFT JOIN forum_likes USING (topic_id)
+ WHERE forum_entries.user_id = ?
+ AND forum_likes.topic_id IS NOT NULL
+ AND forum_likes.user_id != ?");
+ $stmt->execute([$user_id, $user_id]);
+
+ $entries[$user_id] = $stmt->fetchColumn();
+ }
+
+ return $entries[$user_id];
+ }
+}
diff --git a/lib/classes/ForumPerm.php b/lib/classes/ForumPerm.php
new file mode 100644
index 0000000..a5441fa
--- /dev/null
+++ b/lib/classes/ForumPerm.php
@@ -0,0 +1,215 @@
+<?php
+/**
+ * filename - Short description for file
+ *
+ * Long description for file (if any)...
+ *
+ * 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
+ */
+
+class ForumPerm {
+
+ /**
+ * Check, if the a user has the passed permission in a seminar.
+ * Possible permissions are:
+ * edit_category - Editing the name of a category<br>
+ * add_category - Adding a new category<br>
+ * remove_category - Removing an existing category<br>
+ * sort_category - Sorting categories<br>
+ * edit_area - Editing an area (title + content)<br>
+ * add_area - Adding a new area<br>
+ * remove_area - Removing an area and all belonging threads<br>
+ * sort_area - Sorting of areas in categories and between categories<br>
+ * search - Searching in postings<br>
+ * edit_entry - Editing of foreign threads/postings<br>
+ * add_entry - Creating a new thread/posting<br>
+ * remove_entry - Removing of foreign threads/postings<br>
+ * fav_entry - Marking a Posting as "favorite"<br>
+ * like_entry - Liking a posting<br>
+ * move_thread - Moving a thrad between ares<br>
+ * close_thread - Close or open a thread<br>
+ * make_sticky - Make a thread sticky<br>
+ * abo - Signing up for mail-notifications for new entries<br>
+ * forward_entry - Forwarding an existing entry as a message<br>
+ * pdfexport - Exporting parts of the forum as PDF<br>
+ * admin - Allowed to mass-administrate the forum<br>
+ * view - Allowed to view the forum at all<br>
+ * edit_closed - Editing entries in a closed thread
+ *
+ * @param string $perm one of the modular permissions
+ * @param string $seminar_id the seminar to check for
+ * @param string $user_id the user to check for
+ * @return boolean true, if the user has the perms, false otherwise
+ */
+ public static function has($perm, $seminar_id, $user_id = null)
+ {
+ static $permissions = [];
+
+ // if no user-id is passed, use the current user (for your convenience)
+ if (!$user_id) {
+ $user_id = $GLOBALS['user']->id;
+ }
+
+ // get the status for the user in the passed seminar
+ if (!$permissions[$seminar_id][$user_id]) {
+ $permissions[$seminar_id][$user_id] = $GLOBALS['perm']->get_studip_perm($seminar_id, $user_id);
+ }
+
+ $status = $permissions[$seminar_id][$user_id];
+
+ // take care of the not logged in user
+ if ($user_id == 'nobody' || $status == false) {
+ // which status has nobody - read only or read/write?
+ if (get_object_type($seminar_id) == 'sem') {
+ $sem = Seminar::getInstance($seminar_id);
+
+ if ($sem->write_level == 0) {
+ $status = 'nobody_write';
+ } else if ($sem->read_level == 0) {
+ $status = 'nobody_read';
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ // root and admins have all possible perms
+ if (in_array($status, words('root admin')) !== false) {
+ return true;
+ }
+
+ // eCULT Notlösung
+ if ($status == 'tutor' && $seminar_id == '30e0b89dcc9173d5fccf9f22b13b87bd') {
+ $status = 'autor';
+ }
+
+ // check the status and the passed permission
+ if (($status == 'dozent' || $status == 'tutor') && in_array($perm,
+ words('edit_category add_category remove_category sort_category '
+ . 'edit_area add_area remove_area sort_area '
+ . 'search edit_entry add_entry remove_entry fav_entry like_entry move_thread '
+ . 'make_sticky close_thread abo forward_entry pdfexport view edit_closed')
+ ) !== false) {
+ return true;
+ } else if ($status == 'autor' && in_array($perm, words('search add_entry fav_entry like_entry forward_entry abo pdfexport view')) !== false) {
+ return true;
+ } else if ($status == 'user' && in_array($perm, words('search forward_entry pdfexport view')) !== false) {
+ return true;
+ } else if ($status == 'nobody_write' && in_array($perm, words('search add_entry pdfexport view')) !== false) {
+ return true;
+ } else if ($status == 'nobody_read' && in_array($perm, words('search pdfexport view')) !== false) {
+ return true;
+ }
+
+ // user has no permission
+ return false;
+ }
+
+ /**
+ * If the user has not the passed perm in a seminar, an AccessDeniedException
+ * is thrown.
+ * An optional topic_id can be passed which is checked against the passed
+ * seminar if the topic_id belongs to that seminar
+ *
+ * @param string $perm for the list of possible perms and their function see @ForumPerm::hasPerm()
+ * @param string $seminar_id the seminar to check for
+ * @param string $topic_id if passed, this topic_id is checked if it belongs to the passed seminar
+ *
+ * @throws AccessDeniedException
+ */
+ public static function check($perm, $seminar_id, $topic_id = null)
+ {
+ if (!self::has($perm, $seminar_id)) {
+ throw new AccessDeniedException(sprintf(
+ _("Sie haben keine Berechtigung für diese Aktion! Benötigte Berechtigung: %s"),
+ $perm)
+ );
+ }
+
+ // check the topic id (if any)
+ if ($topic_id) {
+ self::checkTopicId($seminar_id, $topic_id);
+ }
+ }
+
+ /**
+ * Check if the current user is allowed to edit the topic
+ * denoted by the passed id
+ *
+ * @staticvar array $perms
+ *
+ * @param string $topic_id the id for the topic to check for
+ *
+ * @return bool true if the user has the necessary perms, false otherwise
+ */
+ public static function hasEditPerms($topic_id)
+ {
+ static $perms = [];
+
+ if (!$perms[$topic_id]) {
+ // find out if the posting is the last in the thread
+ $constraints = ForumEntry::getConstraints($topic_id);
+
+ $stmt = DBManager::get()->prepare("SELECT user_id, seminar_id
+ FROM forum_entries WHERE topic_id = ?");
+ $stmt->execute([$topic_id]);
+
+ $data = $stmt->fetch();
+
+ $closed = ForumEntry::isClosed($topic_id);
+
+ $perms[$topic_id] = (($GLOBALS['user']->id == $data['user_id'] && $GLOBALS['user']->id != 'nobody') ||
+ ForumPerm::has('edit_entry', $constraints['seminar_id']))
+ && (!$closed || $closed && ForumPerm::has('edit_closed', $constraints['seminar_id']));
+ }
+
+ return $perms[$topic_id];
+ }
+
+ /**
+ * check if the passed category_id belongs to the passed seminar_id.
+ * Throws an AccessDenied denied exception if this is not the case
+ *
+ * @param string $seminar_id id of the seminar, the category should belong to
+ * @param string $category_id the id of the category to check
+ */
+ public static function checkCategoryId($seminar_id, $category_id)
+ {
+ $data = ForumCat::get($category_id);
+
+ if ($data['seminar_id'] != $seminar_id) {
+ throw new AccessDeniedException(sprintf(
+ _('Forum: Sie haben keine Berechtigung auf die Kategorie mit der ID %s zuzugreifen!'),
+ $category_id
+ ));
+ }
+ }
+
+ /**
+ * check if the passed topic_id belongs to the passed seminar_id.
+ * Throws an AccessDenied denied exception if this is not the case
+ *
+ * @param string $seminar_id id of the seminar, the category should belong to
+ * @param string $topic_id the id of the topic to check
+ */
+ public static function checkTopicId($seminar_id, $topic_id)
+ {
+ $data = ForumEntry::getConstraints($topic_id);
+
+ if ($data['seminar_id'] != $seminar_id) {
+ throw new AccessDeniedException(sprintf(
+ _('Forum: Sie haben keine Berechtigung auf den Eintrag mit der ID %s zuzugreifen!'),
+ $topic_id
+ ));
+ }
+ }
+}
diff --git a/lib/classes/ForumVisit.php b/lib/classes/ForumVisit.php
new file mode 100644
index 0000000..befe116
--- /dev/null
+++ b/lib/classes/ForumVisit.php
@@ -0,0 +1,162 @@
+<?php
+/**
+ * ForumVisit - Functions for visit-dates for threads
+ *
+ * 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
+ */
+
+class ForumVisit {
+
+ /**
+ * This is the maximum number of seconds that unread entries are
+ * marked as new.
+ */
+ const LAST_VISIT_MAX = 7776000; // 90 days
+
+ /**
+ * return number of new entries since last visit up to 3 month ago
+ *
+ * @param string $parent_id the seminar_id for the entries
+ * @param string $visitdate count all entries newer than this timestamp
+ *
+ * @return int the number of entries
+ */
+ static function getCount($parent_id, $visitdate)
+ {
+ if ($visitdate < time() - ForumVisit::LAST_VISIT_MAX) {
+ $visitdate = time() - ForumVisit::LAST_VISIT_MAX;
+ }
+
+ $constraints = ForumEntry::getConstraints($parent_id);
+
+ $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries
+ WHERE lft >= :lft AND rgt <= :rgt AND user_id != :user_id
+ AND seminar_id = :seminar_id
+ AND topic_id != seminar_id
+ AND chdate > :lastvisit");
+
+ $stmt->bindParam(':user_id', $GLOBALS['user']->id);
+ $stmt->bindParam(':lft', $constraints['lft']);
+ $stmt->bindParam(':rgt', $constraints['rgt']);
+ $stmt->bindParam(':seminar_id', $constraints['seminar_id']);
+ $stmt->bindParam(':lastvisit', $visitdate);
+
+ $stmt->execute();
+
+ return $stmt->fetchColumn();
+ }
+
+ /**
+ * Set the seminar denoted by the passed id as visited by the currently
+ * logged in user
+ *
+ * @param string $seminar_id
+ */
+ static function setVisit($seminar_id) {
+ $type = get_object_type($seminar_id, words('fak inst sem'));
+ if ($type === 'fak') {
+ $type = 'inst';
+ }
+ if (self::getVisit($seminar_id) < object_get_visit($seminar_id, $type, false, false)) {
+ self::setVisitdates($seminar_id);
+ }
+ }
+
+ /**
+ * Stores the visitdate in last_visitdate and sets the current time for as new visitdate
+ *
+ * @param string $seminar_id the seminar that has been entered
+ */
+ static function setVisitdates($seminar_id) {
+ $stmt = DBManager::get()->prepare('SELECT visitdate FROM forum_visits
+ WHERE user_id = ? AND seminar_id = ?');
+ $stmt->execute([$GLOBALS['user']->id, $seminar_id]);
+ $visitdate = $stmt->fetchColumn();
+
+ $stmt = DBManager::get()->prepare("REPLACE INTO forum_visits
+ (user_id, seminar_id, visitdate, last_visitdate)
+ VALUES (?, ?, UNIX_TIMESTAMP(), ?)");
+ $stmt->execute([$GLOBALS['user']->id, $seminar_id, $visitdate]);
+
+ }
+
+
+ /**
+ * returns visitdate and last_visitdate for the passed seminar and the
+ * currently logged in user
+ *
+ * @staticvar array $visit
+ *
+ * @param string $seminar_id the seminar to fetch the visitdates for
+ * @return mixed an array containing visitdate and last_visitdate
+ */
+ private static function getVisitDates($seminar_id)
+ {
+ static $visit = [];
+
+ // no costly checking for root or nobody necessary
+ if ($GLOBALS['perm']->have_perm('root') || $GLOBALS['user']->id == 'nobody') {
+ $tstamp = mktime(23, 59, 00, date('m'), 31, date('y'));
+ return ['visit' => $tstamp, 'last_visitdate' => $tstamp];
+ }
+
+ if (!isset($visit[$seminar_id])) {
+ $visit[$seminar_id] = [];
+ }
+ if (!isset($visit[$seminar_id][$GLOBALS['user']->id])) {
+ $stmt = DBManager::get()->prepare("SELECT visitdate, last_visitdate FROM forum_visits
+ WHERE seminar_id = ? AND user_id = ?");
+ $stmt->execute([$seminar_id, $GLOBALS['user']->id]);
+ $visit[$seminar_id][$GLOBALS['user']->id] = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ // no entry for this seminar yet present
+ if (!$visit[$seminar_id][$GLOBALS['user']->id]) {
+ // set visitdate to current time
+ $visit[$seminar_id][$GLOBALS['user']->id] = [
+ 'visit' => time() - ForumVisit::LAST_VISIT_MAX,
+ 'last_visitdate' => time() - ForumVisit::LAST_VISIT_MAX
+ ];
+ }
+
+ // prevent visit-dates from being older than LAST_VISIT_MAX allows
+ foreach ($visit[$seminar_id][$GLOBALS['user']->id] as $type => $date) {
+ if ($date < time() - ForumVisit::LAST_VISIT_MAX) {
+ $visit[$seminar_id][$GLOBALS['user']->id][$type] = time() - ForumVisit::LAST_VISIT_MAX;
+ }
+ }
+ }
+
+ return $visit[$seminar_id][$GLOBALS['user']->id];
+ }
+
+ /**
+ * return the last_visitdate for the passed seminar and currently logged in user
+ *
+ * @param string $seminar_id the seminar to get the last_visitdate for
+ * @return int a timestamp
+ */
+ static function getLastVisit($seminar_id)
+ {
+ $visit = self::getVisitDates($seminar_id);
+ return $visit['last_visitdate'];
+ }
+
+ /**
+ * return the visitdate for the passed seminar and currently logged in user
+ *
+ * @param string $seminar_id the seminar to get the visitdate for
+ * @return int a timestamp
+ */
+ static function getVisit($seminar_id)
+ {
+ $visit = self::getVisitDates($seminar_id);
+ return $visit['visitdate'];
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Forum/ForumAuthority.php b/lib/classes/JsonApi/Routes/Forum/ForumAuthority.php
index c895a7b..acc5d2a 100644
--- a/lib/classes/JsonApi/Routes/Forum/ForumAuthority.php
+++ b/lib/classes/JsonApi/Routes/Forum/ForumAuthority.php
@@ -8,8 +8,6 @@ class ForumAuthority
{
public static function has(\User $user, $perm, \Course $course, ForumEntry $topic = null)
{
- require_once 'public/plugins_packages/core/Forum/models/ForumPerm.php';
-
if (!\ForumPerm::has($perm, $course->id, $user->id)) {
return false;
}
diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesShow.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoriesShow.php
index 9e935e0..190efb2 100644
--- a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesShow.php
+++ b/lib/classes/JsonApi/Routes/Forum/ForumCategoriesShow.php
@@ -2,8 +2,6 @@
namespace JsonApi\Routes\Forum;
-// require_once 'public/plugins_packages/core/Forum/models/ForumCat.php';
-
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use JsonApi\Errors\AuthorizationFailedException;
diff --git a/lib/classes/Privacy.php b/lib/classes/Privacy.php
index 90ba9f7..bb3bcdd 100644
--- a/lib/classes/Privacy.php
+++ b/lib/classes/Privacy.php
@@ -85,8 +85,6 @@ class Privacy
*/
public static function getUserdataInformation($user_id, $section = null)
{
- //workaround make Forum Model available
- PluginEngine::getPlugin('CoreForum');
$storage = new StoredUserData($user_id);
if ($section && !isset(self::$privacy_classes[$section])) {
diff --git a/lib/classes/globalsearch/GlobalSearchForum.php b/lib/classes/globalsearch/GlobalSearchForum.php
index 83558bd..37efd26 100644
--- a/lib/classes/globalsearch/GlobalSearchForum.php
+++ b/lib/classes/globalsearch/GlobalSearchForum.php
@@ -144,7 +144,7 @@ class GlobalSearchForum extends GlobalSearchModule implements GlobalSearchFullte
'id' => $data['topic_id'],
'name' => $name,
'url' => URLHelper::getURL(
- "plugins.php/coreforum/index/index/{$data['topic_id']}#{$data['topic_id']}",
+ "dispatch.php/course/forum/index/index/{$data['topic_id']}#{$data['topic_id']}",
['cid' => $data['seminar_id'], 'highlight_topic' => $data['topic_id']],
true
),
@@ -152,7 +152,7 @@ class GlobalSearchForum extends GlobalSearchModule implements GlobalSearchFullte
'date' => strftime('%x', $data['chdate']),
'description' => self::mark($filtered_content, $search, true),
'additional' => htmlReady($additional),
- 'expand' => URLHelper::getURL('plugins.php/coreforum/index/search', [
+ 'expand' => URLHelper::getURL('dispatch.php/course/forum/index/search', [
'cid' => $data['seminar_id'],
'backend' => 'search',
'searchfor' => $search,
diff --git a/lib/models/BlubberThread.php b/lib/models/BlubberThread.php
index 0be5cd2..430950f 100644
--- a/lib/models/BlubberThread.php
+++ b/lib/models/BlubberThread.php
@@ -593,7 +593,7 @@ class BlubberThread extends SimpleORMap implements PrivacyObject
public function getURL()
{
if (($this['context_type'] === "course") || ($this['context_type'] === "institute")) {
- return URLHelper::getURL('plugins.php/blubber/messenger/course/' . $this->getId(), ['cid' => $this['context_id']]);
+ return URLHelper::getURL('dispatch.php/course/messenger/course/' . $this->getId(), ['cid' => $this['context_id']]);
}
return URLHelper::getURL('dispatch.php/blubber/index/' . $this->getId());
}
diff --git a/lib/models/ForumCat.php b/lib/models/ForumCat.php
new file mode 100644
index 0000000..c963b69
--- /dev/null
+++ b/lib/models/ForumCat.php
@@ -0,0 +1,252 @@
+<?php
+/**
+ * ForumCat.php - Class to handle categories for areas
+ *
+ * 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
+ */
+
+class ForumCat extends SimpleORMap
+{
+ /**
+ * Configures this model.
+ *
+ * @param array $config Configuration array
+ */
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'forum_categories';
+ parent::configure($config);
+ }
+
+ /**
+ * Return a list of all areas with their categories.
+ * Empty categories are excluded by default
+ *
+ * @param string $seminar_id the seminar_id the retrieve the categories for
+ * @param string $exclude_null if false, empty categories are returned as well
+ * @return array list of categories
+ */
+ public static function getListWithAreas($seminar_id, $exclude_null = true)
+ {
+ $stmt = DBManager::get()->prepare("SELECT * FROM forum_categories AS fc
+ LEFT JOIN forum_categories_entries AS fce USING (category_id)
+ WHERE seminar_id = ? "
+ . ($exclude_null ? 'AND fce.topic_id IS NOT NULL ' : '')
+ . "ORDER BY fc.pos ASC, fce.pos ASC");
+
+ $stmt->execute([$seminar_id]);
+
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Returns the name of the associated category for an area denoted by the
+ * passed topic_id
+ *
+ * @param string $topic_id
+ * @return string the name of the category
+ */
+ public static function getCategoryNameForArea($topic_id)
+ {
+ $stmt = DBManager::get()->prepare("SELECT fc.entry_name FROM forum_categories AS fc
+ LEFT JOIN forum_categories_entries AS fce USING (category_id)
+ WHERE fce.topic_id = ?");
+ $stmt->execute([$topic_id]);
+
+ return $stmt->fetchColumn();
+ }
+
+
+ /**
+ * Adds a new category with the passed name to the passed seminar and
+ * returns the id of the newly created category
+ *
+ * @param string $seminar_id
+ * @param string $name the name of the new category
+ * @return string the id of the newly created category
+ */
+ public static function add($seminar_id, $name)
+ {
+ $stmt = DBManager::get()->prepare("INSERT INTO forum_categories
+ (category_id, seminar_id, entry_name)
+ VALUES (?, ?, ?)");
+
+ $category_id = md5(uniqid(rand()));
+
+ $stmt->execute([$category_id, $seminar_id, $name]);
+
+ return $category_id;
+ }
+
+
+ /**
+ * Remove the category with the passed id. The seminar_id is used only
+ * to be certain.
+ *
+ * @param string $category_id The ID of the category to be deleted
+ * @param string $seminar_id Seminar-ID the category belongs to
+ */
+ public static function remove($category_id, $seminar_id)
+ {
+ // delete the category itself
+ $stmt = DBManager::get()->prepare("DELETE FROM
+ forum_categories
+ WHERE category_id = ?");
+ $stmt->execute([$category_id]);
+
+ // set all entries to default category
+ $stmt = DBManager::get()->prepare("UPDATE
+ forum_categories_entries
+ SET category_id = ?, pos = 999
+ WHERE category_id = ?");
+ $stmt->execute([$seminar_id, $category_id]);
+ }
+
+
+ /**
+ * Set the position for the passed category to the passed value
+ *
+ * @param string $category_id the ID of the category to update
+ * @param int $pos the new position
+ */
+ public static function setPosition($category_id, $pos)
+ {
+ $stmt = DBManager::get()->prepare("UPDATE
+ forum_categories
+ SET pos = ? WHERE category_id = ?");
+ $stmt->execute([$pos, $category_id]);
+ }
+
+
+ /**
+ * Add the passed area to the passed category and remove it from all
+ * other categories.
+ *
+ * @param string $category_id the ID of the category
+ * @param string $area_id the ID of the area to add the category to
+ */
+ public static function addArea($category_id, $area_id)
+ {
+ // remove area from all other categories
+ $stmt = DBManager::get()->prepare("DELETE FROM
+ forum_categories_entries
+ WHERE topic_id = ?");
+ $stmt->execute([$area_id]);
+
+ // add area to this category, make sure it is at the end
+ $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM
+ forum_categories_entries
+ WHERE category_id = ?");
+ $stmt->execute([$category_id]);
+ $new_pos = $stmt->fetchColumn() + 1;
+
+ $stmt = DBManager::get()->prepare("REPLACE INTO
+ forum_categories_entries
+ (category_id, topic_id, pos) VALUES (?, ?, ?)");
+ $stmt->execute([$category_id, $area_id, $new_pos]);
+ }
+
+
+ /**
+ * Remove the passed area from all categories.
+ *
+ * @param string $area_id the ID of the area to be removed
+ */
+ public static function removeArea($area_id)
+ {
+ $stmt = DBManager::get()->prepare("DELETE FROM
+ forum_categories_entries
+ WHERE topic_id = ?");
+ $stmt->execute([$area_id]);
+ }
+
+
+ /**
+ * Set the position for the passed category to the passed value
+ *
+ * @param string $area_id the ID of the area to update
+ * @param int $pos the new position
+ */
+ public static function setAreaPosition($area_id, $pos)
+ {
+ $stmt = DBManager::get()->prepare("UPDATE
+ forum_categories_entries
+ SET pos = ? WHERE topic_id = ?");
+ $stmt->execute([$pos, $area_id]);
+ }
+
+
+ /**
+ * Set the name for the passed category
+ *
+ * @param string $category_id the ID of the category to update
+ * @param string $name the name to set
+ */
+ public static function setName($category_id, $name)
+ {
+ $stmt = DBManager::get()->prepare("UPDATE
+ forum_categories
+ SET entry_name = ? WHERE category_id = ?");
+ $stmt->execute([$name, $category_id]);
+ }
+
+ /**
+ * Return the data for the passed category_id
+ *
+ * @param string $category_id
+ *
+ * @return array the data for the passed category_id
+ */
+ public static function get($category_id)
+ {
+ $stmt = DBManager::get()->prepare("SELECT * FROM forum_categories
+ WHERE category_id = ?");
+ $stmt->execute([$category_id]);
+
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Return the areas for the passed category_id
+ *
+ * @param string $category_id
+ * @param int $start limit start (optional)
+ * @param int $num number of entries to fetch (optional, default is 20)
+ *
+ * @return array the data for the passed category_id
+ */
+ public static function getAreas($category_id, $start = null, $num = 20)
+ {
+ $category = self::get($category_id);
+
+ $limit = '';
+ if ($start !== null && $num) {
+ $limit = " LIMIT $start, $num";
+ }
+
+ if ($category_id == $category['seminar_id']) {
+ $stmt = DBManager::get()->prepare("SELECT fe.* FROM forum_entries AS fe
+ LEFT JOIN forum_categories_entries AS fce USING (topic_id)
+ WHERE seminar_id = ? AND depth = 1 AND (
+ fce.category_id = ? OR fce.category_id IS NULL
+ ) ORDER BY category_id DESC, pos ASC" . $limit);
+ $stmt->execute([$category_id, $category_id]);
+ } else {
+ $stmt = DBManager::get()->prepare("SELECT forum_entries.* FROM forum_categories_entries
+ LEFT JOIN forum_entries USING(topic_id)
+ WHERE category_id = ?
+ ORDER BY pos ASC" . $limit);
+
+ $stmt->execute([$category_id]);
+ }
+
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+}
diff --git a/lib/modules/ActivityFeed.php b/lib/modules/ActivityFeed.php
new file mode 100644
index 0000000..6fa9ce3
--- /dev/null
+++ b/lib/modules/ActivityFeed.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * @author André Klaßen <klassen@elan-ev.de>
+ * @author Till Glöggler <tgloeggl@uos.de>
+ * @license GPL 2 or later
+ */
+class ActivityFeed extends CorePlugin implements PortalPlugin
+{
+ public function getPluginName()
+ {
+ return _('Aktivitäten');
+ }
+
+ public function getMetadata()
+ {
+ return [
+ 'description' => _('Mit diesem Widget haben Sie alle Aktivitäten im Überblick.')
+ ];
+ }
+
+ public function getPortalTemplate()
+ {
+ $template = $GLOBALS['template_factory']->open('start/activityfeed');
+
+ $template->user_id = $GLOBALS['user']->id;
+ $template->scrolledfrom = strtotime('+1 day');
+ $template->config = WidgetHelper::getWidgetUserConfig($GLOBALS['user']->id, 'ACTIVITY_FEED');
+
+ $navigation = new Navigation('', 'dispatch.php/activityfeed/configuration');
+ $navigation->setImage(Icon::create('edit', 'clickable', ["title" => _('Konfigurieren')]), ['data-dialog'=>'size=auto']);
+ $icons[] = $navigation;
+
+ $navigation = new Navigation('', '#', ['cid' => null]);
+ $navigation->setImage(Icon::create('headache+visibility-visible', 'clickable'));
+ $navigation->setLinkAttributes([
+ 'id' => 'toggle-user-activities',
+ 'title' => _('Eigene Aktivitäten ein-/ausblenden'),
+ ]);
+ $icons[] = $navigation;
+
+ $navigation = new Navigation('', '#', ['cid' => null]);
+ $navigation->setImage(Icon::create('no-activity', 'clickable'));
+ $navigation->setLinkAttributes([
+ 'id' => 'toggle-all-activities',
+ 'title' => _('Aktivitätsdetails ein-/ausblenden'),
+ ]);
+ $icons[] = $navigation;
+
+ $template->icons = $icons;
+
+ return $template;
+ }
+
+ public static function onEnable($pluginId)
+ {
+ $errors = [];
+ if (!Config::get()->API_ENABLED) {
+ $errors[] = sprintf(
+ _('Die REST-API ist nicht aktiviert (%s "API_ENABLED")'),
+ formatReady(sprintf('[%s]%s',
+ _('Konfiguration'),
+ URLHelper::getLink('dispatch.php/admin/configuration/configuration')
+ ))
+ );
+ } elseif (!RESTAPI\ConsumerPermissions::get('global')->check('/user/:user_id/activitystream', 'get')) {
+ $errors[] = sprintf(
+ _('Die REST-API-Route ist nicht aktiviert (%s "/user/:user_id/activitystream"")'),
+ formatReady(sprintf('[%s]%s',
+ _('Konfiguration'),
+ URLHelper::getLink('dispatch.php/admin/api/permissions')
+ ))
+ );
+ }
+
+ return count($errors) === 0;
+ }
+}
diff --git a/lib/modules/Blubber.class.php b/lib/modules/Blubber.class.php
new file mode 100644
index 0000000..d9b5579
--- /dev/null
+++ b/lib/modules/Blubber.class.php
@@ -0,0 +1,126 @@
+<?php
+/*
+ * Copyright (c) 2012-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.
+ */
+
+/**
+ * Class Blubber - the Blubber-plugin
+ * This is only used to manage blubber within a course.
+ */
+class Blubber extends CorePlugin implements StudipModule
+{
+ /**
+ * Returns a navigation for the tab displayed in the course.
+ * @param string $course_id of the course
+ * @return \Navigation
+ */
+ public function getTabNavigation($course_id)
+ {
+ $tab = new Navigation(
+ _('Blubber'),
+ 'dispatch.php/course/messenger/course'
+ );
+ $tab->setImage(Icon::create('blubber', Icon::ROLE_INFO_ALT));
+ return ['blubber' => $tab];
+ }
+
+ /**
+ * Returns a navigation-object with the grey/red icon for displaying in the
+ * my_courses.php page.
+ * @param string $course_id
+ * @param int $last_visit
+ * @param string|null $user_id
+ * @return \Navigation
+ */
+ public function getIconNavigation($course_id, $last_visit, $user_id = null)
+ {
+ $user_id || $user_id = $GLOBALS['user']->id;
+ $icon = new Navigation(
+ _('Blubber'),
+ 'dispatch.php/course/messenger/course'
+ );
+ $icon->setImage(Icon::create('blubber', Icon::ROLE_CLICKABLE, ['title' => _('Blubber-Messenger')]));
+
+ $condition = "INNER JOIN blubber_threads USING (thread_id)
+ WHERE blubber_threads.context_type = 'course'
+ AND blubber_threads.context_id = :course_id
+ AND blubber_comments.mkdate >= :last_visit
+ AND blubber_comments.user_id != :me
+ AND blubber_threads.visible_in_stream = 1
+ ";
+ $comments = BlubberComment::findBySQL($condition, [
+ 'course_id' => $course_id,
+ 'last_visit' => $last_visit,
+ 'me' => $user_id,
+ ]);
+ foreach ($comments as $comment) {
+ if ($comment->thread->isVisibleInStream() && $comment->thread->isReadable() && ($comment->thread->getLatestActivity() > UserConfig::get($user_id)->getValue("BLUBBERTHREAD_VISITED_".$comment['thread_id']))) {
+ $icon->setImage(Icon::create('blubber', Icon::ROLE_NEW, ['title' => _('Es gibt neue Blubber')]));
+ $icon->setTitle(_('Es gibt neue Blubber'));
+ $icon->setBadgeNumber(count($comments));
+ $icon->setURL('dispatch.php/course/messenger/course', ['thread' => 'new']);
+ break;
+ }
+ }
+
+ $condition = "context_type = 'course'
+ AND context_id = :course_id
+ AND mkdate >= :last_visit
+ AND user_id != :me
+ AND visible_in_stream = 1
+ AND (
+ blubber_threads.display_class IS NOT NULL
+ OR blubber_threads.`content` IS NOT NULL
+ )";
+ $threads = BlubberThread::findBySQL($condition, [
+ 'course_id' => $course_id,
+ 'last_visit' => $last_visit,
+ 'me' => $GLOBALS['user']->id,
+ ]);
+ foreach ($threads as $thread) {
+ if ($thread->isVisibleInStream() && $thread->isReadable() && ($thread['mkdate'] > UserConfig::get($user_id)->getValue("BLUBBERTHREAD_VISITED_".$thread->getId()))) {
+ $icon->setImage(Icon::create('blubber', Icon::ROLE_ATTENTION, ['title' => _('Es gibt neue Blubber')]));
+ $icon->setTitle(_('Es gibt neue Blubber'));
+ break;
+ }
+ }
+ return $icon;
+ }
+
+ /**
+ * Returns no template, because this plugin doesn't want to insert an
+ * info-template in the course-overview.
+ * @param string $course_id
+ * @return null
+ */
+ public function getInfoTemplate($course_id)
+ {
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMetadata()
+ {
+ return [
+ 'summary' => _('Schneller und einfacher Austausch von Informationen in Gesprächsform'),
+ 'description' => _('Blubber ist eine Kommunikationsform mit Ähnlichkeiten zu einem Forum, in dem aber in Echtzeit miteinander kommuniziert werden kann und das durch den etwas informelleren Charakter eher einem Chat anmutet. Anders als im Forum ist es nicht notwendig, die Seiten neu zu laden, um die neuesten Einträge (z. B. Antworten auf eigene Postings) sehen zu können: Die Seite aktualisiert sich selbst bei neuen Einträgen. Dateien (z.B. Fotos, Audiodateien, Links) können per Drag and Drop in das Feld gezogen und somit verlinkt werden. Auch Textformatierungen und das Verwenden von Smileys sind möglich.'),
+ 'descriptionlong' => _('Kommunikationsform mit Ähnlichkeiten zu einem Forum. Im Gegensatz zum Forum kann mit Blubber jedoch in Echtzeit miteinander kommuniziert werden. Das Tool ähnelt durch den etwas informelleren Charakter einem Messenger. Anders als im Forum ist es nicht notwendig, die Seiten neu zu laden, um die neuesten Einträge (z. B. Antworten auf eigene Postings) sehen zu können. Dateien (z. B. Fotos, Audiodateien, Links) können per drag and drop in das Feld gezogen und somit verlinkt werden. Auch Textformatierungen und das Verwenden von Smileys sind möglich.'),
+ 'category' => _('Kommunikation und Zusammenarbeit'),
+ 'keywords' => _('Einfach Text schreiben und mit <Enter> abschicken; Direktes Kontaktieren anderer Stud.IP-NutzerInnen (@Vorname Nachname); Setzen von und Suche nach Stichworten über Hashtags (#Stichwort); Einbinden von Dateien per drag and drop'),
+ 'icon' => Icon::create('blubber', Icon::ROLE_INFO),
+ 'screenshots' => [
+ 'path' => 'assets/images/plus/screenshots/Blubber',
+ 'pictures' => [
+ ['source' => 'blubberscreenshot.png', 'title' => 'Blubbern']
+ ]
+ ]
+ ];
+ }
+}
diff --git a/lib/modules/ContentsWidget.php b/lib/modules/ContentsWidget.php
new file mode 100644
index 0000000..1907ba6
--- /dev/null
+++ b/lib/modules/ContentsWidget.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Contents widget.
+ *
+ * @author David Siegfried <ds.siegfried@gmail.com>
+ * @license GPL2 or any later version
+ * @since Stud.IP 5.0
+ */
+
+class ContentsWidget extends CorePlugin implements PortalPlugin
+{
+ public function getPluginName()
+ {
+ return _('Mein Arbeitsplatz');
+ }
+
+ public function getMetadata()
+ {
+ return [
+ 'description' => _('Mit diesem Widget haben Sie einen Überblick über Ihre Inhalte.')
+ ];
+ }
+
+ public function getPortalTemplate()
+ {
+ $template = $GLOBALS['template_factory']->open('start/contents');
+ $template->tiles = Navigation::getItem('/contents');
+ return $template;
+ }
+}
diff --git a/lib/modules/CoreForum.class.php b/lib/modules/CoreForum.class.php
new file mode 100644
index 0000000..88dd5dd
--- /dev/null
+++ b/lib/modules/CoreForum.class.php
@@ -0,0 +1,209 @@
+<?php
+/*
+ * Forum.class.php - Forum
+ *
+ * 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 Till Glöggler <till.gloeggler@elan-ev.de>
+ * @copyright 2011 ELAN e.V. <http://www.elan-ev.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+
+// Notifications
+NotificationCenter::addObserver('CoreForum', 'overviewDidClear', 'OverviewDidClear');
+NotificationCenter::addObserver('CoreForum', 'removeAbosForUserAndCourse', 'UserDidLeaveCourse');
+
+NotificationCenter::addObserver('ForumActivity', 'newEntry', 'ForumAfterInsert');
+NotificationCenter::addObserver('ForumActivity', 'updateEntry', 'ForumAfterUpdate');
+NotificationCenter::addObserver('ForumActivity', 'deleteEntry', 'ForumBeforeDelete');
+
+NotificationCenter::addObserver('ForumIssue', 'unlinkIssue', 'ForumBeforeDelete');
+
+class CoreForum extends CorePlugin implements ForumModule
+{
+ /* interface method */
+ public function getTabNavigation($course_id)
+ {
+ $navigation = new Navigation(_('Forum'), 'dispatch.php/course/forum/index');
+ $navigation->setImage(Icon::create('forum', 'info_alt'));
+
+ // add main third-level navigation-item
+ $navigation->addSubNavigation('index', new Navigation(_('Übersicht'), 'dispatch.php/course/forum/index'));
+
+ if (ForumPerm::has('fav_entry', $course_id)) {
+ $navigation->addSubNavigation('newest', new Navigation(_("Neue Beiträge"), 'dispatch.php/course/forum/index/newest'));
+ $navigation->addSubNavigation('latest', new Navigation(_("Letzte Beiträge"), 'dispatch.php/course/forum/index/latest'));
+ $navigation->addSubNavigation('favorites', new Navigation(_('Gemerkte Beiträge'), 'dispatch.php/course/forum/index/favorites'));
+
+ // mass-administrate the forum
+ if (ForumPerm::has('admin', $course_id)) {
+ $navigation->addSubNavigation('admin', new Navigation(_('Administration'), 'dispatch.php/course/forum/admin'));
+ }
+ }
+
+ return ['forum2' => $navigation];
+ }
+
+ /* interface method */
+ public function getIconNavigation($course_id, $last_visit, $user_id = null)
+ {
+ if ($GLOBALS['perm']->have_studip_perm('user', $course_id)) {
+ $num_entries = ForumVisit::getCount($course_id, ForumVisit::getVisit($course_id));
+ $text = ForumHelpers::getVisitText($num_entries, $course_id);
+ } else {
+ $num_entries = 0;
+ $text = 'Forum';
+ }
+
+ $navigation = new Navigation('forum', 'dispatch.php/course/forum/index/enter_seminar');
+ $navigation->setBadgeNumber($num_entries);
+
+ if ($num_entries > 0) {
+ $navigation->setImage(Icon::create('forum', Icon::ROLE_ATTENTION, ['title' => $text]));
+ } else {
+ $navigation->setImage(Icon::create('forum', Icon::ROLE_CLICKABLE, ['title' => $text]));
+ }
+
+ return $navigation;
+ }
+
+ /**
+ * This method is called, whenever an user clicked to clear the visit timestamps
+ * and set everything as visited
+ *
+ * @param object $notification
+ * @param string $user_id
+ */
+ public static function overviewDidClear($notification, $user_id)
+ {
+ $query = "REPLACE INTO `forum_visits`
+ SELECT `user_id`, `Seminar_id`, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
+ FROM `seminar_user`
+ WHERE `user_id` = ?";
+ DBManager::get()->execute($query, [$user_id]);
+ }
+
+ /**
+ * This method is called whenever a user is removed from a course and thus
+ * the forum abos will be removed.
+ *
+ * @param object $notification
+ * @param string $course_id
+ * @param string $user_id
+ */
+ public static function removeAbosForUserAndCourse($notification, $course_id, $user_id)
+ {
+ ForumAbo::removeForCourseAndUser($course_id, $user_id);
+ }
+
+ public function getInfoTemplate($course_id)
+ {
+ return null;
+ }
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+ /* * IMPLEMENTATION OF METHODS FROM FORUMMODULE-INTERFACE * */
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ public function getLinkToThread($issue_id)
+ {
+ if ($topic_id = ForumIssue::getThreadIdForIssue($issue_id)) {
+ return URLHelper::getLink('dispatch.php/course/forum/index/index/' . $topic_id);
+ }
+
+ return false;
+ }
+
+ public function setThreadForIssue($issue_id, $title, $content)
+ {
+ ForumIssue::setThreadForIssue(Context::getId(), $issue_id, $title, $content);
+ }
+
+ public function getNumberOfPostingsForUser($user_id, $seminar_id = null)
+ {
+ return ForumEntry::countUserEntries($user_id, $seminar_id);
+ }
+
+ public function getNumberOfPostingsForIssue($issue_id)
+ {
+ $topic_id = ForumIssue::getThreadIdForIssue($issue_id);
+
+ return $topic_id ? ForumEntry::countEntries($topic_id) : 0;
+ }
+
+ public function getNumberOfPostingsForSeminar($seminar_id)
+ {
+ return floor(ForumEntry::countEntries($seminar_id));
+ }
+
+ public function getNumberOfPostings()
+ {
+ return ForumEntry::countAllEntries();
+ }
+
+ public function getEntryTableInfo()
+ {
+ return [
+ 'table' => 'forum_entries',
+ 'content' => 'content',
+ 'chdate' => 'chdate',
+ 'seminar_id' => 'seminar_id',
+ 'user_id' => 'user_id'
+ ];
+ }
+
+ public function getTopTenSeminars()
+ {
+ return ForumEntry::getTopTenSeminars();
+ }
+
+ public function migrateUser($user_from, $user_to)
+ {
+ return ForumEntry::migrateUser($user_from, $user_to);
+ }
+
+ public function deleteContents($seminar_id)
+ {
+ return ForumEntry::delete($seminar_id);
+ }
+
+ public function getDump($seminar_id)
+ {
+ return ForumEntry::getDump($seminar_id);
+ }
+
+ public static function getDescription()
+ {
+ return _('Textbasierte und zeit- und ortsunabhängige '.
+ 'Diskursmöglichkeit. Lehrende können parallel zu '.
+ 'Veranstaltungsthemen Fragen stellen, die von den Studierenden '.
+ 'per Meinungsaustausch besprochen werden.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMetadata()
+ {
+ return [
+ 'summary' => _('Veranstaltungsbegleitender Meinungsaustausch zu bestimmten Themen'),
+ 'description' => _('Textbasierte zeit- und ortsunabhängige Möglichkeit zum Austausch von Gedanken, Meinungen und Erfahrungen. Lehrende und/oder Studierende können parallel zu Veranstaltungsthemen Fragen stellen, die in Form von Textbeiträgen besprochen werden können. Diese Beiträge können von allen Teilnehmenden der Veranstaltung gemerkt, verlinkt, positiv bewertet (sog. "Gefällt mir") und editiert werden (Letzeres nur von Lehrenden). Lehrende können zusätzlich Themen in Bereiche gliedern, zwischen den Bereichen verschiebe, im Bereich hervorheben und diesen öffnen und schließen.'),
+ 'descriptionlong' => _('Textbasierte zeit- und ortsunabhängige Möglichkeit zum Austausch von Gedanken, Meinungen und Erfahrungen. Lehrende und/oder Studierende können parallel zu Veranstaltungsthemen Fragen stellen, die in Form von Textbeiträgen besprochen werden können. Diese Beiträge können von allen Teilnehmenden der Veranstaltung gemerkt, verlinkt, positiv bewertet (sog. "Gefällt mir") und editiert werden (Letzeres nur von Lehrenden). Lehrende können zusätzlich Themen in Bereiche gliedern, zwischen den Bereichen verschieben, im Bereich hervorheben und diesen öffnen und schließen.'),
+ 'category' => _('Kommunikation und Zusammenarbeit'),
+ 'keywords' => _('Möglichkeit zum intensiven, nachhaltigen textbasierten Austausch; (nachträgliche) Strukturierung der Beiträge; Editierfunktion für Lehrende'),
+ 'icon' => Icon::create('forum', Icon::ROLE_INFO),
+ 'screenshots' => [
+ 'path' => 'assets/images/plus/screenshots/Forum',
+ 'pictures' => [
+ ['source' => 'Lehrendensicht_-_Kategorien_mit_Bereichen_und_Beitraegen.jpg'],
+ ['source' => 'Studentische_Sicht_-_Kategorien_mit_Bereichen_und_Beitraegen.jpg'],
+ ['source' => 'Einen_Forumsbeitrag_erstellen.jpg'],
+ ]
+ ]
+ ];
+ }
+}
diff --git a/lib/modules/EvaluationsWidget.php b/lib/modules/EvaluationsWidget.php
new file mode 100644
index 0000000..4c902c7
--- /dev/null
+++ b/lib/modules/EvaluationsWidget.php
@@ -0,0 +1,62 @@
+<?php
+/*
+ * EvaluationsWidget.php - widget plugin for start page
+ *
+ * Copyright (C) 2014 - Nadine Werner <nadwerner@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.
+ */
+
+class EvaluationsWidget extends CorePlugin implements PortalPlugin
+{
+ public function getPluginName()
+ {
+ return _('Fragebögen');
+ }
+
+ public function getMetadata()
+ {
+ return [
+ 'description' => _('Mit diesem Widget haben Sie Zugriff auf systemweite Umfragen.')
+ ];
+ }
+
+ /**
+ * Returns the portal widget template.
+ *
+ * Due to a seriously messed up architecture, the suppress_empty_output
+ * variable is used to determine when an according message should be
+ * presented to the user. If no evaluations and questionnaires are present,
+ * the message from the questionnaires should be displayed. The according
+ * message from evaluations should never be shown in the widget. Thus, the
+ * evaluation controller will always suppress this message and the variable
+ * is adjusted if the evaluation returned no content so that the
+ * questionnaire controller will display it's message. If you think, we're
+ * slowly running out of duct tape, you might be absolutely right...
+ */
+ public function getPortalTemplate()
+ {
+ // include and show votes and tests
+ if (Config::get()->VOTE_ENABLE) {
+ $controller = new AuthenticatedController(new StudipDispatcher());
+ $controller->suppress_empty_output = true;
+
+ $response = $controller->relay('evaluation/display/studip')->body;
+
+ $controller->suppress_empty_output = (bool)$response;
+ $response .= $controller->relay('questionnaire/widget/start')->body;
+
+ $template = $GLOBALS['template_factory']->open('shared/string');
+ $template->content = $response;
+
+ if ($GLOBALS['perm']->have_perm('root')) {
+ $navigation = new Navigation('', 'dispatch.php/questionnaire/overview');
+ $navigation->setImage(Icon::create('add', 'clickable', ["title" => _('Umfragen bearbeiten')]));
+ $template->icons = [$navigation];
+ }
+ return $template;
+ }
+ }
+}
diff --git a/lib/modules/NewsWidget.php b/lib/modules/NewsWidget.php
new file mode 100644
index 0000000..60af752
--- /dev/null
+++ b/lib/modules/NewsWidget.php
@@ -0,0 +1,65 @@
+<?php
+/*
+ * news.php - News controller
+ *
+ * Copyright (C) 2014 - Nadine Werner <nadwerner@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.
+ */
+
+require_once 'app/controllers/news.php';
+
+class NewsWidget extends CorePlugin implements PortalPlugin
+{
+ public function getPluginName()
+ {
+ return _('Ankündigungen');
+ }
+
+ public function getMetadata()
+ {
+ return [
+ 'description' => _('Mit diesem Widget haben Sie Zugriff auf systemweite Ankündigungen.')
+ ];
+ }
+
+ function getPortalTemplate()
+ {
+ $dispatcher = new StudipDispatcher();
+ $controller = new NewsController($dispatcher);
+ $response = $controller->relay('news/display/studip');
+ $template = $GLOBALS['template_factory']->open('shared/string');
+ $template->content = $response->body;
+
+ if (StudipNews::CountUnread() > 0) {
+ $navigation = new Navigation('', 'dispatch.php/news/visit_all');
+ $navigation->setImage(Icon::create('refresh', 'clickable', ["title" => _('Alle als gelesen markieren')]));
+ $icons[] = $navigation;
+ }
+
+ if (Config::get()->NEWS_RSS_EXPORT_ENABLE) {
+ if ($rss_id = StudipNews::GetRssIdFromRangeId('studip')) {
+ $navigation = new Navigation('', 'rss.php', ['id' => $rss_id]);
+ $navigation->setImage(Icon::create('rss', 'clickable', ["title" => _('RSS-Feed')]));
+ $icons[] = $navigation;
+ }
+ }
+
+ if ($GLOBALS['perm']->have_perm('root')) {
+ $navigation = new Navigation('', 'dispatch.php/news/edit_news/new/studip');
+ $navigation->setImage(Icon::create('add', 'clickable', ["title" => _('Ankündigungen bearbeiten')]), ['rel' => 'get_dialog']);
+ $icons[] = $navigation;
+ if (Config::get()->NEWS_RSS_EXPORT_ENABLE) {
+ $navigation = new Navigation('', 'dispatch.php/news/rss_config/studip');
+ $navigation->setImage(Icon::create('rss+add', 'clickable', ["title" => _('RSS-Feed konfigurieren')]), ["rel" => 'size=auto']);
+ $icons[] = $navigation;
+ }
+ }
+
+ $template->icons = $icons;
+
+ return $template;
+ }
+}
diff --git a/lib/modules/QuickSelection.php b/lib/modules/QuickSelection.php
new file mode 100644
index 0000000..f1a7a2c
--- /dev/null
+++ b/lib/modules/QuickSelection.php
@@ -0,0 +1,57 @@
+<?php
+/*
+ * QuickSelection.php - widget plugin for start page
+ *
+ * Copyright (C) 2014 - Nadine Werner <nadwerner@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.
+ */
+
+class QuickSelection extends CorePlugin implements PortalPlugin
+{
+ public function getPluginName()
+ {
+ return _('Schnellzugriff');
+ }
+
+ public function getMetadata()
+ {
+ return [
+ 'description' => _('Mit dem Schnellzugriff-Widget können Sie Links zu bestimmten Bereichen in Stud.IP verwalten.')
+ ];
+ }
+
+ public function getPortalTemplate()
+ {
+ $names = WidgetHelper::getWidgetUserConfig($GLOBALS['user']->id, 'QUICK_SELECTION');
+
+ $template = $GLOBALS['template_factory']->open('start/quickselection');
+ $template->navigation = $this->getFilteredNavigation($names);
+
+ $navigation = new Navigation('', 'dispatch.php/quickselection/configuration');
+ $navigation->setImage(Icon::create('edit', 'clickable', ["title" => _('Konfigurieren')]), ['data-dialog'=>'size=auto']);
+
+ $template->icons = [$navigation];
+
+ return $template;
+ }
+
+ private function getFilteredNavigation($items)
+ {
+ $result = [];
+
+ $navigation = Navigation::getItem('/start');
+ foreach ($navigation as $name => $nav) {
+ // if config is new (key:value) display values which are not in config array
+ // otherwise hide items which are not in config array
+ // This is important for patching.
+ if (!isset($items[$name]) || $items[$name] !== 'deactivated') {
+ $result[] = $nav;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/modules/ScheduleWidget.php b/lib/modules/ScheduleWidget.php
new file mode 100644
index 0000000..339beb3
--- /dev/null
+++ b/lib/modules/ScheduleWidget.php
@@ -0,0 +1,54 @@
+<?php
+/*
+ * This class displays a seminar-schedule for
+ * users on a seminar-based view and for admins on an institute based view
+ *
+ * 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 Till Glöggler <tgloeggl@uos.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+
+class ScheduleWidget extends CorePlugin implements PortalPlugin
+{
+ /**
+ * Returns the name of the plugin/widget.
+ *
+ * @return String containing the name
+ */
+ public function getPluginName()
+ {
+ return _('Mein Stundenplan');
+ }
+
+ public function getMetadata()
+ {
+ return [
+ 'description' => _('Mit diesem Widget haben Sie eine Übersicht Ihres aktuellen Stundenplans.')
+ ];
+ }
+
+ /**
+ * Return the template for the widget.
+ *
+ * @return Flexi_PhpTemplate The template containing the widget contents
+ */
+ public function getPortalTemplate()
+ {
+ $view = CalendarScheduleModel::getUserCalendarView(
+ $GLOBALS['user']->id,
+ false,
+ false,
+ $days = array(0,1,2,3,4)
+ );
+
+ $template = $GLOBALS['template_factory']->open('shared/string');
+ $template->content = CalendarWidgetView::createFromWeekView($view)->render();
+
+ return $template;
+ }
+}
diff --git a/lib/modules/TerminWidget.php b/lib/modules/TerminWidget.php
new file mode 100644
index 0000000..c92f1d2
--- /dev/null
+++ b/lib/modules/TerminWidget.php
@@ -0,0 +1,43 @@
+<?php
+/*
+ * TerminWidget.php - A portal plugin for dates
+ *
+ * Copyright (C) 2014 - André Klaßen <klassen@elan-ev.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.
+ */
+
+require_once 'app/controllers/calendar/contentbox.php';
+
+class TerminWidget extends CorePlugin implements PortalPlugin
+{
+ public function getPluginName()
+ {
+ return _('Meine aktuellen Termine');
+ }
+
+ public function getMetadata()
+ {
+ return [
+ 'description' => _('Mit diesem Widget haben Sie ihre aktuellen Termine im Überlick.')
+ ];
+ }
+
+ public function getPortalTemplate()
+ {
+ $dispatcher = new StudipDispatcher();
+ $controller = new Calendar_ContentboxController($dispatcher);
+ $response = $controller->relay('calendar/contentbox/display/'.$GLOBALS['user']->id);
+ $template = $GLOBALS['template_factory']->open('shared/string');
+ $template->content = $response->body;
+
+ $navigation = new Navigation('', 'dispatch.php/calendar/single/week', ['self' => true]);
+ $navigation->setImage(Icon::create('add', 'clickable', ["title" => _('Neuen Termin anlegen')]));
+ $template->icons = [$navigation];
+
+ return $template;
+ }
+}
diff --git a/lib/plugins/engine/PluginManager.class.php b/lib/plugins/engine/PluginManager.class.php
index ee5b43f..7193f31 100644
--- a/lib/plugins/engine/PluginManager.class.php
+++ b/lib/plugins/engine/PluginManager.class.php
@@ -83,7 +83,7 @@ class PluginManager
'enabled' => $plugin['enabled'] === 'yes',
'position' => $plugin['navigationpos'],
'depends' => (int) $plugin['dependentonid'],
- 'core' => $plugin['pluginpath'] === '' || strpos($plugin['pluginpath'], 'core/') === 0,
+ 'core' => $plugin['pluginpath'] === '',
'automatic_update_url' => $plugin['automatic_update_url'],
'automatic_update_secret' => $plugin['automatic_update_secret']
];