aboutsummaryrefslogtreecommitdiff
path: root/lib/classes
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/classes
parentc054faf90288a75fc3680480434ba93b7f5b287b (diff)
convert old core plugins to new model, re #814
Merge request studip/studip!440
Diffstat (limited to 'lib/classes')
-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
14 files changed, 2752 insertions, 8 deletions
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,