Select Git revision
BlubberThread.vue
Forked from
Stud.IP / Stud.IP
Source project has a limited visibility.
-
Jan-Hendrik Willms authored
Closes #1809 Merge request studip/studip!1189
Jan-Hendrik Willms authoredCloses #1809 Merge request studip/studip!1189
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ForumEntry.php 53.23 KiB
<?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();
$old_libxml_error = libxml_use_internal_errors(true);
$dom->loadHtml($description, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
libxml_use_internal_errors($old_libxml_error);
$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' => (!empty($data['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)
{
$last_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']];
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];
case 'postings':
return ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, '', 'ASC', $start);
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()];
case 'latest':
return ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, '', 'DESC', $start);
case 'favorites':
$add = "AND ou.topic_id IS NOT NULL";
return ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, $add, 'DESC', $start);
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));
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' => (!empty($data['fav']) && $data['fav'] == 'fav'),
'depth' => $data['depth'],
'seminar_id' => $data['seminar_id']
];
}
return ['list' => $posting_list, 'count' => $count];
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];
}
throw new InvalidArgumentException("Invalid type {$type}");
}
/**
* 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 (empty($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, user_id, name, content, author, author_host, 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);
}
}
}