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/ForumEntry.php | |
| parent | c054faf90288a75fc3680480434ba93b7f5b287b (diff) | |
convert old core plugins to new model, re #814
Merge request studip/studip!440
Diffstat (limited to 'lib/classes/ForumEntry.php')
| -rw-r--r-- | lib/classes/ForumEntry.php | 1385 |
1 files changed, 1385 insertions, 0 deletions
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); + } + } + +} |
