diff options
| author | Elmar Ludwig <elmar.ludwig@uni-osnabrueck.de> | 2022-05-11 07:19:42 +0000 |
|---|---|---|
| committer | Elmar Ludwig <elmar.ludwig@uni-osnabrueck.de> | 2022-05-11 07:19:42 +0000 |
| commit | 20240b2aacb15ab3264afbfbbc9dae952db4bb63 (patch) | |
| tree | 597cae73c5ae7ab9eda6d066d45430c3f17a4560 /lib/classes | |
| parent | c054faf90288a75fc3680480434ba93b7f5b287b (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.php | 186 | ||||
| -rw-r--r-- | lib/classes/ForumActivity.php | 149 | ||||
| -rw-r--r-- | lib/classes/ForumBulkMail.php | 135 | ||||
| -rw-r--r-- | lib/classes/ForumEntry.php | 1385 | ||||
| -rw-r--r-- | lib/classes/ForumFavorite.php | 41 | ||||
| -rw-r--r-- | lib/classes/ForumHelpers.php | 282 | ||||
| -rw-r--r-- | lib/classes/ForumIssue.php | 96 | ||||
| -rw-r--r-- | lib/classes/ForumLike.php | 99 | ||||
| -rw-r--r-- | lib/classes/ForumPerm.php | 215 | ||||
| -rw-r--r-- | lib/classes/ForumVisit.php | 162 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Forum/ForumAuthority.php | 2 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/Forum/ForumCategoriesShow.php | 2 | ||||
| -rw-r--r-- | lib/classes/Privacy.php | 2 | ||||
| -rw-r--r-- | lib/classes/globalsearch/GlobalSearchForum.php | 4 |
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, |
