_db = DB::getInstance(); } /** * Get an array of forums a user can access, including topic information * * @param array $groups Users groups * @param int $user_id User ID * @return array Array of forums a user can access */ public function listAllForums(array $groups = [0], int $user_id = 0): array { if (in_array(0, $groups)) { $user_id = 0; } if (!$user_id) { $user_id = 0; } $groups_in = implode(',', $groups); // Get a list of parent forums $parent_forums = $this->_db->query( <<count()) { foreach ($parent_forums->results() as $forum) { $return[$forum->id]['description'] = Output::getClean($forum->forum_description); $return[$forum->id]['title'] = Output::getClean($forum->forum_title); $return[$forum->id]['icon'] = Output::getPurified($forum->icon); // Get discussion forums $forums = $this->_db->query( <<id] ); if ($forums->count()) { foreach ($forums->results() as $item) { $return[$forum->id]['subforums'][$item->id] = $item; $return[$forum->id]['subforums'][$item->id]->forum_title = Output::getClean($item->forum_title); $return[$forum->id]['subforums'][$item->id]->forum_description = Output::getClean($item->forum_description); $return[$forum->id]['subforums'][$item->id]->icon = Output::getPurified($item->icon); $return[$forum->id]['subforums'][$item->id]->link = URL::build('/forum/view/' . urlencode($item->id) . '-' . $this->titleToURL($item->forum_title)); $return[$forum->id]['subforums'][$item->id]->redirect_to = Output::getClean($item->redirect_url); // Get latest post from any sub-subforums $subforums = $this->getAnySubforums($item->id, $groups, 0, true, $user_id); if ($item->view_other_topics) { $latest_post = [$item->last_post_date, $item->last_user_posted, $item->last_topic_posted]; } else { $latest_post = $this->getLatestPostInOwnTopicForum($item->id, $user_id); } if (count($subforums)) { $return[$forum->id]['subforums'][$item->id]->subforums = []; foreach ($subforums as $subforum) { if (isset($subforum->last_post_date) && $subforum->last_post_date > $latest_post[0]) { $latest_post = [$subforum->last_post_date, $subforum->last_user_posted, $subforum->last_topic_posted]; } $return[$forum->id]['subforums'][$item->id]->subforums[$subforum->id] = new stdClass(); $return[$forum->id]['subforums'][$item->id]->subforums[$subforum->id]->title = $subforum->forum_title; $return[$forum->id]['subforums'][$item->id]->subforums[$subforum->id]->link = URL::build('/forum/view/' . urlencode($subforum->id) . '-' . $this->titleToURL($subforum->forum_title)); $return[$forum->id]['subforums'][$item->id]->subforums[$subforum->id]->icon = $subforum->icon; } } // Can the user view other topics? if ($this->canViewOtherTopics($item->id, $groups)) { $topics = $this->_db->query( <<id] )->first()->count; $posts = $this->_db->query( <<id] )->first()->count; } else { $topics = $this->_db->query( <<id, $user_id], )->first()->count; $posts = $this->_db->query( <<id, $user_id] )->first()->count; } $return[$forum->id]['subforums'][$item->id]->topics = $topics; $return[$forum->id]['subforums'][$item->id]->posts = $posts; // Get latest topic info if ($latest_post[0]) { $latest_topic = $this->_db->query( <<count() && $latest_topic = $latest_topic->first()) { $return[$forum->id]['subforums'][$item->id]->last_post = $latest_topic; $return[$forum->id]['subforums'][$item->id]->last_post->title = Output::getClean($latest_topic->topic_title); $return[$forum->id]['subforums'][$item->id]->last_post->link = URL::build('/forum/topic/' . urlencode($latest_post[2]) . '-' . $this->titleToURL($latest_topic->topic_title), 'pid=' . $latest_topic->pid); } } } } } } return $return; } /** * Determine if a forum exists (in the context of a specific user) * * @param int $forum_id ID of the forum * @param array $groups Array of groups the user is in * @return bool Whether the forum exists or not */ public function forumExist(int $forum_id, array $groups = [0]): bool { $exists = $this->_db->get('forums', ['id', $forum_id])->results(); if (count($exists)) { return $this->hasPermission($forum_id, 'view', $groups); } return false; } /** * Determines if any groups have permission to do a certain action on a forum * * @param int $forum_id ID of the forum * @param string $required_permission Required permission * @param array $groups Array of groups the user is in * @return bool Whether the groups have permission or not */ private function hasPermission(int $forum_id, string $required_permission, array $groups): bool { $cache_key = 'forum_permissions_' . $forum_id . '_' . $required_permission . '_' . implode('_', $groups); if (isset(self::$_permission_cache[$cache_key])) { return true; } $permissions = $this->_db->get('forums_permissions', ['forum_id', $forum_id])->results(); foreach ($permissions as $permission) { if (in_array($permission->group_id, $groups)) { if ($permission->{$required_permission} == 1) { self::$_permission_cache[$cache_key] = true; return true; } } } return false; } public function titleToURL(string $topic = null): string { return URL::urlSafe($topic ?? ''); } // Returns true/false depending on whether the current user can view a forum // Params: $forum_id (integer) - forum id to check, $groups (array) - user groups public function canViewOtherTopics(int $forum_id, array $groups = [0]): bool { $cache_key = 'topics_view_' . $forum_id . '_' . implode('_', $groups); if (isset(self::$_permission_cache[$cache_key])) { return true; } // Does the forum exist? $exists = $this->_db->get('forums', ['id', $forum_id])->results(); if (count($exists)) { // Can the user view other topics? $access = $this->_db->get('forums_permissions', ['forum_id', $forum_id])->results(); foreach ($access as $item) { if (in_array($item->group_id, $groups)) { if ($item->view_other_topics == 1) { self::$_permission_cache[$cache_key] = true; return true; } } } } return false; } /** * Get the newest x topics this user/group can view * * @param array $groups Array of groups the user is in * @param int $user_id User ID * @param int $limit Limit of topics to return, default 50 * @return array Latest topics */ public function getLatestDiscussions(array $groups = [0], int $user_id = 0, int $limit = 50): array { if (!$user_id) { $user_id = 0; } $all_topics_forums = DB::getInstance()->query('SELECT forum_id FROM nl2_forums_permissions WHERE group_id IN (' . rtrim(implode(',', $groups), ',') . ') AND `view` = 1 AND view_other_topics = 1')->results(); $own_topics_forums = []; if ($user_id > 0) { $own_topics_forums = DB::getInstance()->query('SELECT forum_id FROM nl2_forums_permissions WHERE group_id IN (' . rtrim(implode(',', $groups), ',') . ') AND `view` = 1 AND view_other_topics = 0')->results(); } if (!count($all_topics_forums) && !count($own_topics_forums)) { return []; } $all_topics_forums_string = ''; foreach ($all_topics_forums as $forum) { $all_topics_forums_string .= $forum->forum_id . ','; } $all_topics_forums_string = rtrim($all_topics_forums_string, ','); if (count($own_topics_forums)) { $own_topics_forums_string = ''; foreach ($own_topics_forums as $forum) { $own_topics_forums_string .= $forum->forum_id . ','; } $own_topics_forums_string = rtrim($own_topics_forums_string, ','); return DB::getInstance()->query( "(SELECT topics.id as id, topics.forum_id as forum_id, topics.topic_title as topic_title, topics.topic_creator as topic_creator, topics.topic_last_user as topic_last_user, topics.topic_date as topic_date, topics.topic_reply_date as topic_reply_date, topics.topic_views as topic_views, topics.locked as locked, topics.sticky as sticky, topics.label as label, topics.deleted as deleted, posts.id as last_post_id FROM nl2_topics topics LEFT JOIN nl2_posts posts ON topics.id = posts.topic_id AND posts.id = (SELECT MAX(id) FROM nl2_posts p WHERE p.topic_id = topics.id AND p.deleted = 0) WHERE topics.deleted = 0 AND topics.forum_id IN ($all_topics_forums_string) ORDER BY topics.topic_reply_date DESC LIMIT $limit) UNION (SELECT topics.id as id, topics.forum_id as forum_id, topics.topic_title as topic_title, topics.topic_creator as topic_creator, topics.topic_last_user as topic_last_user, topics.topic_date as topic_date, topics.topic_reply_date as topic_reply_date, topics.topic_views as topic_views, topics.locked as locked, topics.sticky as sticky, topics.label as label, topics.deleted as deleted, posts.id as last_post_id FROM nl2_topics topics LEFT JOIN nl2_posts posts ON topics.id = posts.topic_id AND posts.id = (SELECT MAX(id) FROM nl2_posts p WHERE p.topic_id = topics.id AND p.deleted = 0) WHERE topics.deleted = 0 AND ((topics.forum_id IN ($own_topics_forums_string) AND topics.topic_creator = ?) OR topics.sticky = 1) ORDER BY topics.topic_reply_date DESC LIMIT $limit) ORDER BY topic_reply_date DESC LIMIT $limit", [$user_id], true )->results(); } return DB::getInstance()->query( "SELECT topics.id as id, topics.forum_id as forum_id, topics.topic_title as topic_title, topics.topic_creator as topic_creator, topics.topic_last_user as topic_last_user, topics.topic_date as topic_date, topics.topic_reply_date as topic_reply_date, topics.topic_views as topic_views, topics.locked as locked, topics.sticky as sticky, topics.label as label, topics.deleted as deleted, posts.id as last_post_id FROM nl2_topics topics LEFT JOIN nl2_posts posts ON topics.id = posts.topic_id AND posts.id = (SELECT MAX(id) FROM nl2_posts p WHERE p.topic_id = topics.id AND p.deleted = 0) WHERE topics.deleted = 0 AND topics.forum_id IN ($all_topics_forums_string) ORDER BY topics.topic_reply_date DESC LIMIT $limit", )->results(); } /** * Determine if a topic exists or not. * * @param int $topic_id The topic ID * @return bool Whether the topic exists or not */ public function topicExist(int $topic_id): bool { // Does the topic exist? $exists = $this->_db->get('topics', ['id', $topic_id])->results(); return count($exists) > 0; } /** * Determine if the groups can view the forum or not. * * @param int $forum_id The forum ID * @param array $groups The user's groups * @return bool Whether the groups can view the forum or not */ public function canViewForum(int $forum_id, array $groups = [0]): bool { return $this->hasPermission($forum_id, 'view', $groups); } /** * Determine if the groups can post topics in the forum or not. * * @param int $forum_id The forum ID * @param array $groups The user's groups * @return bool Whether the groups can post topics in the forum or not */ public function canPostTopic(int $forum_id, array $groups = [0]): bool { return $this->hasPermission($forum_id, 'create_topic', $groups); } /** * Determine if the user can view a topic in a forum * * @param int $forum_id The forum ID for the topic * @param int $author_id The topic author ID * @param bool $is_topic_sticky Whether the topic is sticky or not * @param ?User $user The user * @return bool Whether the user can view the topic */ public function canViewTopicInForum(int $forum_id, int $author_id, bool $is_topic_sticky, ?User $user): bool { if ($user) { $groups = $user->getAllGroupIds(); $userId = $user->data()->id; } else { $groups = [0]; $userId = 0; } return $this->canViewForum($forum_id, $groups) && ($is_topic_sticky || $this->canViewOtherTopics($forum_id, $groups) || $author_id === $userId); } /** * Determine if the groups can post replies in the forum or not. * * @param int $forum_id The forum ID * @param array $groups The user's groups * @return bool Whether the groups can post replies in the forum or not */ public function canPostReply(int $forum_id, array $groups = [0]): bool { return $this->hasPermission($forum_id, 'create_post', $groups); } /** * Determine if the groups can edit [psts] in the forum or not. * * @param int $forum_id The forum ID * @param array $groups The user's groups * @return bool Whether the groups can edit posts in the forum or not */ public function canEditTopic(int $forum_id, array $groups = [0]): bool { return $this->hasPermission($forum_id, 'edit_topic', $groups); } /** * Update the database with the new latest forum posts. * * @param int $forum_id The forum ID to update */ public function updateForumLatestPosts(int $forum_id): void { $latest_post = $this->_db->query( <<count() && $latest_post = $latest_post->first()) { $this->_db->update('forums', $forum_id, [ 'last_post_date' => $latest_post->created ?? strtotime($latest_post->post_date), 'last_user_posted' => $latest_post->post_creator, 'last_topic_posted' => $latest_post->topic_id, ]); } else { $this->_db->update('forums', $forum_id, [ 'last_post_date' => null, 'last_user_posted' => null, 'last_topic_posted' => null, ]); } } /** * Update the database with the new latest forum topic posts. * * @param int $topic_id The topic ID to update * @param int|null $forum_id The forum ID to update if topic does not exist */ public function updateTopicLatestPosts(int $topic_id, ?int $forum_id): void { $latest_post = $this->_db->query( <<count() && $latest_post = $latest_post->first()) { $this->_db->update('topics', $topic_id, [ 'topic_reply_date' => $latest_post->created ?? strtotime($latest_post->post_date), 'topic_last_user' => $latest_post->post_creator, ]); } else if ($forum_id !== null) { $this->_db->update('forums', $forum_id, [ 'last_post_date' => null, 'last_user_posted' => null, 'last_topic_posted' => null, ]); } } /** * Get the title of a specific forum. * * @param int $forum_id The forum ID to get the title of. * @return string The forum title. */ public function getForumTitle(int $forum_id): string { $data = $this->_db->get('forums', ['id', $forum_id])->results(); return $data[0]->forum_title; } /** * Get data of a specific post. * * @param int $post_id The post ID to data about. * @return array|false The post data or false on failure. */ public function getIndividualPost(int $post_id) { $data = $this->_db->query( <<count()) { $data = $data->first(); return [ 'creator' => $data->post_creator, 'content' => $data->post_content, 'date' => $data->post_date, 'forum_id' => $data->forum_id, 'topic_id' => $data->topic_id, 'topic_sticky' => $data->sticky, ]; } return false; } /** * Get the latest news posts to display on homepage. * * TODO: can be optimised by including posts in the initial query? * * @param int $number The number of posts to get. * @param array $groups Array containing user's group IDs, default [0] for guests * @return array The latest news posts. */ public function getLatestNews(int $number = 5, array $groups = [0]): array { $return = []; // Array to return containing news $labels_cache = []; // Array to contain labels $groups_in = implode(',', array_map(static fn () => '?', $groups)); $news_items = $this->_db->query( <<results(); foreach ($news_items as $item) { $news_post = $this->_db->get('posts', ['topic_id', $item->id]); $posts = $news_post->count(); $news_post = $news_post->first(); if (is_null($news_post->created)) { $post_date = date(DATE_FORMAT, strtotime($news_post->post_date)); } else { $post_date = date(DATE_FORMAT, $news_post->created); } $labels = []; if ($item->labels) { // Get label $label_ids = explode(',', $item->labels); if ($label_ids !== false) { foreach ($label_ids as $label_id) { if (isset($labels_cache[$label_id])) { $labels[] = $labels_cache[$label_id]; } else { $label = $this->_db->get('forums_topic_labels', ['id', $label_id]); if ($label->count()) { $label = $label->first(); $label_html = $this->_db->get('forums_labels', ['id', $label->label]); if ($label_html->count()) { $label_html = $label_html->first()->html; $label = str_replace('{x}', Output::getClean($label->name), Output::getPurified($label_html)); } else { $label = ''; } } else { $label = ''; } $labels_cache[$label_id] = $label; $labels[] = $label; } } } } $post = $news_post->post_content; $return[] = [ 'topic_id' => $item->id, 'topic_date' => $post_date, 'topic_title' => $item->topic_title, 'topic_views' => $item->topic_views, 'author' => $item->topic_creator, 'content' => Text::truncate($post), 'replies' => $posts, 'label' => count($labels) ? $labels[0] : null, 'labels' => $labels ]; } return $return; } /** * Determine if groups have permission to moderate a forum. * * @param int|null $forum_id The forum ID to check. * @param array $groups The groups to check. * @return bool Whether the groups can moderate the forum. */ public function canModerateForum(int $forum_id = null, array $groups = [0]): bool { if (!$forum_id || in_array(0, $groups)) { return false; } $cache_key = 'moderate_' . $forum_id . '_' . implode('_', $groups); if (isset(self::$_permission_cache[$cache_key])) { return true; } $permissions = $this->_db->get('forums_permissions', ['forum_id', $forum_id])->results(); // Check the forum foreach ($permissions as $permission) { if (in_array($permission->group_id, $groups)) { if ($permission->moderate == 1) { self::$_permission_cache[$cache_key] = true; return true; } } } return false; } /** * Get a user's post count * * @param int|null $user_id User ID to check * @return int Number of posts */ public function getPostCount(int $user_id = null): int { if ($user_id) { if (isset(self::$_count_cache["posts_$user_id"])) { return self::$_count_cache["posts_$user_id"]; } $count = $this->_db->query('SELECT COUNT(*) AS c FROM nl2_posts WHERE deleted = 0 AND post_creator = ?', [$user_id])->first()->c; self::$_count_cache["posts_$user_id"] = $count; return $count; } return 0; } /** * Get a user's topic count * * @param int|null $user_id User ID to check * @return int Number of topics */ public function getTopicCount(int $user_id = null): int { if ($user_id) { if (isset(self::$_count_cache["topics_$user_id"])) { return self::$_count_cache["topics_$user_id"]; } $count = $this->_db->query('SELECT COUNT(*) AS c FROM nl2_topics WHERE deleted = 0 AND topic_creator = ?', [$user_id])->first()->c; self::$_count_cache["topics_$user_id"] = $count; return $count; } return 0; } /** * Get posts on a specific topic. * * @param int|null $tid The topic ID to check. * @return array|false Array of topics or false on failure. */ public function getPosts(int $tid = null) { if ($tid) { // Get posts from database $posts = $this->_db->get('posts', ['topic_id', $tid]); if ($posts->count()) { $posts = $posts->results(); // Remove deleted posts foreach ($posts as $key => $post) { if ($post->deleted == 1) { unset($posts[$key]); } } return array_values($posts); } } return false; } /** * Get any subforums at any level for a forum * * @param int $forum_id The forum ID * @param array $groups The user groups * @param int $depth The depth of the subforums to get * @param ?bool $onlyOwnTopics Whether to only get forums in which the user can only view their own topics (default false) * @param ?int $user_id Current user ID (default 0) - only used if $onlyOwnTopics is true * @return array Subforums at any level for a forum */ public function getAnySubforums( int $forum_id, array $groups = [0], int $depth = 0, ?bool $onlyOwnTopics = false, ?int $user_id = 0 ): array { if ($depth == 10) { return []; } $ret = []; $groups_in = implode(',', $groups); $subforums_query = $this->_db->query( <<count()) { return $ret; } foreach ($subforums_query->results() as $result) { $to_add = new stdClass(); $to_add->id = $result->id; $to_add->forum_title = Output::getClean($result->forum_title); $to_add->icon = Output::getPurified($result->icon); $to_add->category = false; // Latest post if ($onlyOwnTopics && $result->view_other_topics !== 1) { [ $to_add->last_post_date, $to_add->last_user_posted, $to_add->last_topic_posted, ] = $this->getLatestPostInOwnTopicForum($result->id, $user_id); } else { $to_add->last_post_date = $result->last_post_date; $to_add->last_user_posted = $result->last_user_posted; $to_add->last_topic_posted = $result->last_topic_posted; } $ret[] = $to_add; $subforums = $this->getAnySubforums($result->id, $groups, ++$depth, $onlyOwnTopics, $user_id); if (count($subforums)) { foreach ($subforums as $subforum) { $ret[] = $subforum; } } } return $ret; } /** * Get an array returned of accessible labels when providing group ids * * @param array $labels of label ids * @param array $user_groups array of a user their group ids * @return array An array of the ids of the labels the user has access to */ public static function getAccessibleLabels(array $labels, array $user_groups): array { return array_reduce($labels, function(&$prev, $topic_label) use ($user_groups) { $label = DB::getInstance()->get('forums_topic_labels', ['id', $topic_label])->first(); if ($label) { $label_group_ids = explode(',', $label->gids); $hasPerm = array_reduce($user_groups, fn($prev, $group_id) => $prev || in_array($group_id, $label_group_ids)); if ($hasPerm) { $prev[] = $label->id; } } return $prev; }, []); } /** * Get banned terms from the Forum module * * @return array Array of banned terms */ public static function getBannedTerms(): array { $terms = Settings::get('banned_terms', null, 'forum'); if (!$terms) { return []; } return explode("\n", $terms); } /** * Get the latest post in a "View own topic" forum * This could be a topic created by the user, or a sticky topic * * @param int $forumId * @param int $userId * @return array|null Time of latest post, post creator ID, topic ID */ private function getLatestPostInOwnTopicForum(int $forumId, int $userId): ?array { $latest_post = $this->_db->query( <<count() && $latest_post = $latest_post->first()) { return [ $latest_post->created ?? strtotime($latest_post->post_date), $latest_post->post_creator, $latest_post->topic_id, ]; } return null; } }