dbConn = $dbConn; $this->cache = new DbStatementCache($dbConn); } public static function convertCategoryListToTree( array $catInfos, ForumCategoryInfo|string|null $parentInfo = null, ?Colour $colour = null ): array { $colour ??= Colour::none(); $tree = []; $predicate = $parentInfo ? fn($catInfo) => $catInfo->isDirectChildOf($parentInfo) : fn($catInfo) => !$catInfo->hasParent(); foreach($catInfos as $catInfo) { if(!$predicate($catInfo)) continue; $tree[$catInfo->getId()] = $item = new stdClass; $item->info = $catInfo; $item->colour = $catInfo->hasColour() ? $catInfo->getColour() : $colour; $item->children = self::convertCategoryListToTree($catInfos, $catInfo, $item->colour); $item->childIds = []; foreach($item->children as $child) { $item->childIds[] = $child->info->getId(); $item->childIds += $child->childIds; } } return $tree; } public function countCategories( ForumCategoryInfo|string|null|false $parentInfo = false, string|int|null $type = null, ?bool $hidden = null ): array { if($parentInfo instanceof ForumCategoryInfo) $parentInfo = $parentInfo->getId(); $hasParentInfo = $parentInfo !== false; $hasType = $type !== null; $hasHidden = $hidden !== null; $args = 0; $query = 'SELECT COUNT(*) FROM msz_forum_categories'; if($hasParentInfo) { ++$args; $isRootParent = $parentInfo === null; if($isRootParent) { // make a migration that makes the field DEFAULT NULL and update all 0s to NULL $query .= 'WHERE (forum_parent IS NULL OR forum_parent = 0)'; } else { $query .= 'WHERE forum_parent = ?'; } } if($hasType) { if(is_string($type)) { if(!array_key_exists($type, ForumCategoryInfo::TYPE_ALIASES)) throw new InvalidArgumentException('$type is not a valid alias.'); $type = ForumCategoryInfo::TYPE_ALIASES[$type]; } $query .= sprintf(' %s forum_type = ?', ++$args > 1 ? 'AND' : 'WHERE'); } if($hasHidden) $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '='); $args = 0; $stmt = $this->cache->get($query); if($hasParentInfo && !$isRootParent) $stmt->addParameter(++$args, $parentInfo->getId()); if($hasType) $stmt->addParameter(++$args, $type); $stmt->execute(); $result = $stmt->getResult(); return $result->next() ? $result->getInteger(0) : 0; } public function getCategories( ForumCategoryInfo|string|null|false $parentInfo = false, string|int|null $type = null, ?bool $hidden = null, bool $asTree = false, ?Pagination $pagination = null ): array { $hasParentInfo = $parentInfo !== false; $hasType = $type !== null; $hasHidden = $hidden !== null; $hasPagination = $pagination !== null; if($hasParentInfo && $asTree) throw new InvalidArgumentException('$asTree can only be used with $parentInfo set to false.'); $args = 0; $query = 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories'; if($hasParentInfo) { ++$args; $isRootParent = $parentInfo === null; if($isRootParent) { // make a migration that makes the field DEFAULT NULL and update all 0s to NULL $query .= ' WHERE (forum_parent IS NULL OR forum_parent = 0)'; } else { $query .= ' WHERE forum_parent = ?'; } } if($hasType) { if(is_string($type)) { if(!array_key_exists($type, ForumCategoryInfo::TYPE_ALIASES)) throw new InvalidArgumentException('$type is not a valid alias.'); $type = ForumCategoryInfo::TYPE_ALIASES[$type]; } $query .= sprintf(' %s forum_type = ?', ++$args > 1 ? 'AND' : 'WHERE'); } if($hasHidden) $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '='); $query .= ' ORDER BY forum_parent, forum_type <> 1, forum_order'; if($hasPagination) $query .= ' LIMIT ? OFFSET ?'; $args = 0; $stmt = $this->cache->get($query); if($hasParentInfo && !$isRootParent) { if($parentInfo instanceof ForumCategoryInfo) $stmt->addParameter(++$args, $parentInfo->getId()); else $stmt->addParameter(++$args, $parentInfo); } if($hasType) $stmt->addParameter(++$args, $type); if($hasPagination) { $stmt->addParameter(++$args, $pagination->getRange()); $stmt->addParameter(++$args, $pagination->getOffset()); } $stmt->execute(); $result = $stmt->getResult(); $cats = []; while($result->next()) $cats[] = new ForumCategoryInfo($result); if($asTree) $cats = self::convertCategoryListToTree($cats); return $cats; } public function getCategory( ?string $categoryId = null, ForumTopicInfo|string|null $topicInfo = null, ForumPostInfo|string|null $postInfo = null ): ForumCategoryInfo { $hasCategoryId = $categoryId !== null; $hasTopicInfo = $topicInfo !== null; $hasPostInfo = $postInfo !== null; if(!$hasCategoryId && !$hasTopicInfo && !$hasPostInfo) throw new InvalidArgumentException('You must specify an argument.'); if(($hasCategoryId && ($hasTopicInfo || $hasPostInfo)) || ($hasTopicInfo && ($hasCategoryId || $hasPostInfo)) || ($hasPostInfo && ($hasCategoryId || $hasTopicInfo))) throw new InvalidArgumentException('Only one argument may be specified.'); $value = null; $query = 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories'; if($hasCategoryId) { $query .= ' WHERE forum_id = ?'; $value = $categoryId; } if($hasTopicInfo) { if($topicInfo instanceof ForumTopicInfo) { $query .= ' WHERE forum_id = ?'; $value = $topicInfo->getCategoryId(); } else { $query .= ' WHERE forum_id = (SELECT forum_id FROM msz_forum_topics WHERE topic_id = ?)'; $value = $topicInfo; } } if($hasPostInfo) { if($postInfo instanceof ForumPostInfo) { $query .= ' WHERE forum_id = ?'; $value = $postInfo->getCategoryId(); } else { $query .= ' WHERE forum_id = (SELECT forum_id FROM msz_forum_posts WHERE post_id = ?)'; $value = $postInfo; } } $stmt = $this->cache->get($query); $stmt->addParameter(1, $value); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('Forum category info not found.'); return new ForumCategoryInfo($result); } public function updateCategory( ForumCategoryInfo|string $categoryInfo ): void { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); } public function deleteCategory(ForumCategoryInfo|string $categoryInfo): void { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); } public function incrementCategoryClicks(ForumCategoryInfo|string $categoryInfo): void { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); // previous implementation also WHERE'd for forum_type = link but i don't think there's any other way to get here anyhow $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_link_clicks = forum_link_clicks + 1 WHERE forum_id = ? AND forum_link_clicks IS NOT NULL'); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); } public function incrementCategoryTopics(ForumCategoryInfo|string $categoryInfo): void { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_topics = forum_count_topics + 1 WHERE forum_id = ?'); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); } public function incrementCategoryPosts(ForumCategoryInfo|string $categoryInfo): void { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_posts = forum_count_posts + 1 WHERE forum_id = ?'); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); } public function getCategoryAncestry( ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo ): array { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo) $categoryInfo = $categoryInfo->getCategoryId(); $query = 'WITH RECURSIVE msz_cte_ancestry AS (' . 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories WHERE forum_id = ?' . ' UNION ALL' . ' SELECT fc.forum_id, fc.forum_order, fc.forum_parent, fc.forum_name, fc.forum_type, fc.forum_description, fc.forum_icon, fc.forum_colour, fc.forum_link, fc.forum_link_clicks, UNIX_TIMESTAMP(fc.forum_created), fc.forum_archived, fc.forum_hidden, fc.forum_count_topics, fc.forum_count_posts FROM msz_forum_categories AS fc JOIN msz_cte_ancestry AS ca ON fc.forum_id = ca.forum_parent' . ') SELECT * FROM msz_cte_ancestry'; $stmt = $this->cache->get($query); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); $result = $stmt->getResult(); $cats = []; while($result->next()) $cats[] = new ForumCategoryInfo($result); return $cats; } public function getCategoryChildren( ForumCategoryInfo|string $parentInfo, bool $includeSelf = false, ?bool $hidden = null, bool $asTree = false ): array { if($parentInfo instanceof ForumCategoryInfo) $parentInfo = $parentInfo->getId(); $hasHidden = $hidden !== null; $query = 'WITH RECURSIVE msz_cte_children AS (' . 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories WHERE forum_id = ?' . ' UNION ALL' . ' SELECT fc.forum_id, fc.forum_order, fc.forum_parent, fc.forum_name, fc.forum_type, fc.forum_description, fc.forum_icon, fc.forum_colour, fc.forum_link, fc.forum_link_clicks, UNIX_TIMESTAMP(fc.forum_created), fc.forum_archived, fc.forum_hidden, fc.forum_count_topics, fc.forum_count_posts FROM msz_forum_categories AS fc JOIN msz_cte_children AS cc ON fc.forum_parent = cc.forum_id' . ') SELECT * FROM msz_cte_children'; $args = 0; if(!$includeSelf) { ++$args; $query .= ' WHERE forum_id <> ?'; } if($hasHidden) $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '='); $query .= ' ORDER BY forum_parent, forum_order'; $args = 0; $stmt = $this->cache->get($query); $stmt->addParameter(++$args, $parentInfo); if(!$includeSelf) $stmt->addParameter(++$args, $parentInfo); $stmt->execute(); $result = $stmt->getResult(); $cats = []; while($result->next()) $cats[] = new ForumCategoryInfo($result); if($asTree) $cats = self::convertCategoryListToTree($cats, $parentInfo); return $cats; } public function checkCategoryUnread( ForumCategoryInfo|string|array $categoryInfos, UserInfo|string|null $userInfo ): bool { if($userInfo === null) return false; if(!is_array($categoryInfos)) $categoryInfos = [$categoryInfos]; if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $args = 0; $stmt = $this->cache->get(sprintf( 'SELECT COUNT(*) FROM msz_forum_topics AS ft LEFT JOIN msz_forum_topics_track AS ftt ON ftt.topic_id = ft.topic_id AND ftt.user_id = ? WHERE ft.forum_id IN (%s) AND ft.topic_deleted IS NULL AND ft.topic_bumped >= NOW() - INTERVAL 1 MONTH AND (ftt.track_last_read IS NULL OR ftt.track_last_read < ft.topic_bumped)', DbTools::prepareListString($categoryInfos) )); $stmt->addParameter(++$args, $userInfo); foreach($categoryInfos as $categoryInfo) { if($categoryInfo instanceof ForumCategoryInfo) $stmt->addParameter(++$args, $categoryInfo->getId()); elseif(is_string($categoryInfo) || is_int($categoryInfo)) $stmt->addParameter(++$args, $categoryInfo); else throw new InvalidArgumentException('Invalid item in $categoryInfos.'); } $stmt->execute(); $result = $stmt->getResult(); return $result->next() && $result->getInteger(0) > 0; } public function updateUserReadCategory( UserInfo|string|null $userInfo, ForumCategoryInfo|string $categoryInfo ): void { if($userInfo === null) return; if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($categoryInfo instanceof $categoryInfo) $categoryInfo = $categoryInfo->getId(); $stmt = $this->cache->get('REPLACE INTO msz_forum_topics_track (user_id, topic_id, forum_id, track_last_read) SELECT ?, topic_id, forum_id, NOW() FROM msz_forum_topics WHERE forum_id = ? AND topic_bumped >= NOW() - INTERVAL 1 MONTH'); $stmt->addParameter(1, $userInfo); $stmt->addParameter(2, $categoryInfo); $stmt->execute(); } public function getCategoryColour( ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo ): Colour { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo) $categoryInfo = $categoryInfo->getCategoryId(); $query = 'WITH RECURSIVE msz_cte_colours AS (' . 'SELECT forum_id, forum_parent, forum_colour FROM msz_forum_categories WHERE forum_id = ?' . ' UNION ALL' . ' SELECT fc.forum_id, fc.forum_parent, fc.forum_colour FROM msz_forum_categories AS fc JOIN msz_cte_colours AS cc ON fc.forum_id = cc.forum_parent' . ') SELECT forum_colour FROM msz_cte_colours WHERE forum_colour IS NOT NULL'; $stmt = $this->cache->get($query); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); $result = $stmt->getResult(); return $result->next() ? Colour::fromMisuzu($result->getInteger(0)) : Colour::none(); } public function getMostActiveCategoryInfo( UserInfo|string $userInfo, array $exceptCategoryInfos = [], array $exceptTopicInfos = [], ?bool $deleted = null ): object { if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $hasExceptCategoryInfos = !empty($exceptCategoryInfos); $hasExceptTopicInfos = !empty($exceptTopicInfos); $hasDeleted = $deleted !== null; $query = 'SELECT forum_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = ?'; if($hasDeleted) $query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS'); if($hasExceptCategoryInfos) $query .= sprintf(' AND forum_id NOT IN (%s)', DbTools::prepareListString($exceptCategoryInfos)); if($hasExceptTopicInfos) $query .= sprintf(' AND topic_id NOT IN (%s)', DbTools::prepareListString($exceptTopicInfos)); $query .= ' GROUP BY forum_id ORDER BY post_count DESC LIMIT 1'; $args = 0; $stmt = $this->cache->get($query); $stmt->addParameter(++$args, $userInfo); foreach($exceptCategoryInfos as $categoryInfo) { if($categoryInfo instanceof ForumCategoryInfo) $stmt->addParameter(++$args, $categoryInfo->getId()); elseif(is_string($categoryInfo) || is_int($categoryInfo)) $stmt->addParameter(++$args, (string)$categoryInfo); else throw new InvalidArgumentException('$exceptCategoryInfos may only contain string ids or instances of ForumCategoryInfo.'); } foreach($exceptTopicInfos as $topicInfo) { if($topicInfo instanceof ForumTopicInfo) $stmt->addParameter(++$args, $topicInfo->getId()); elseif(is_string($topicInfo) || is_int($topicInfo)) $stmt->addParameter(++$args, (string)$topicInfo); else throw new InvalidArgumentException('$exceptTopicInfos may only contain string ids or instances of ForumTopicInfo.'); } $stmt->execute(); $result = $stmt->getResult(); $info = new stdClass; $info->success = $result->next(); if($info->success) { $info->categoryId = $result->getString(0); $info->postCount = $result->getInteger(1); } return $info; } public function syncForumCounters( ForumCategoryInfo|string|null $categoryInfo = null, bool $updateCounters = true ): object { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); elseif($categoryInfo === null) $categoryInfo = '0'; $counters = new stdClass; $stmt = $this->cache->get('SELECT ? AS target_category_id, (SELECT COUNT(*) FROM msz_forum_topics WHERE forum_id = target_category_id AND topic_deleted IS NULL) AS count_topics, (SELECT COUNT(*) FROM msz_forum_posts WHERE forum_id = target_category_id AND post_deleted IS NULL) AS count_posts'); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('Failed to fetch forum category counters.'); $counters->topics = $result->getInteger(1); $counters->posts = $result->getInteger(2); $stmt = $this->cache->get('SELECT forum_id FROM msz_forum_categories WHERE forum_parent = ?'); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); $children = []; $result = $stmt->getResult(); while($result->next()) $children[] = $result->getString(0); foreach($children as $childId) { $childCounters = $this->syncForumCounters($childId, $updateCounters); $counters->topics += $childCounters->topics; $counters->posts += $childCounters->posts; } if($updateCounters && $categoryInfo !== '0') { $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_topics = ?, forum_count_posts = ? WHERE forum_id = ?'); $stmt->addParameter(1, $counters->topics); $stmt->addParameter(2, $counters->posts); $stmt->addParameter(3, $categoryInfo); $stmt->execute(); } return $counters; } public function countTopics( ForumCategoryInfo|string|array|null $categoryInfo = null, UserInfo|string|null $userInfo = null, ?bool $global = null, ?bool $deleted = null ): int { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $hasCategoryInfo = $categoryInfo !== null; $hasUserInfo = $userInfo !== null; $hasGlobal = $global !== null; $hasDeleted = $deleted !== null; $args = 0; $query = 'SELECT COUNT(*) FROM msz_forum_topics'; if($hasCategoryInfo || $hasGlobal) { ++$args; // wow this sucks $hasGlobalAndCategory = $hasCategoryInfo && $hasGlobal; $query .= ' WHERE '; if($hasGlobalAndCategory) $query .= '('; if($hasCategoryInfo) { if(is_array($categoryInfo)) $query .= sprintf('forum_id IN (%s)', DbTools::prepareListString($categoryInfo)); else $query .= 'forum_id = ?'; } if($hasGlobalAndCategory) $query .= ' OR '; if($hasGlobal) // not sure why you would ever set this to false, but consistency! $query .= sprintf('topic_type %s %d', $global ? '=' : '<>', ForumTopicInfo::TYPE_GLOBAL); if($hasGlobalAndCategory) $query .= ')'; } if($hasUserInfo) $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); if($hasDeleted) $query .= sprintf(' %s topic_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS'); $args = 0; $stmt = $this->cache->get($query); if($hasCategoryInfo) { if(is_array($categoryInfo)) { foreach($categoryInfo as $categoryInfoEntry) $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry); } else $stmt->addParameter(++$args, $categoryInfo); } if($hasUserInfo) $stmt->addParameter(++$args, $userInfo); $stmt->execute(); $result = $stmt->getResult(); return $result->next() ? $result->getInteger(0) : 0; } public function getTopics( ForumCategoryInfo|string|array|null $categoryInfo = null, UserInfo|string|null $userInfo = null, ?array $searchQuery = null, ?bool $global = null, ?bool $deleted = null, ?Pagination $pagination = null ): array { // remove this hack when search server $hasSearchQuery = $searchQuery !== null; $hasAfterTopicId = false; $doSearchOrder = false; if($hasSearchQuery) { if(!empty($searchQuery['type']) && $searchQuery['type'] !== 'forum' && $searchQuery['type'] !== 'forum:topic') return []; $deleted = false; $pagination = null; $doSearchOrder = true; if(!empty($searchQuery['author'])) $userInfo = $searchQuery['author']; if(!empty($searchQuery['after'])) { $hasAfterTopicId = true; $afterTopicId = $searchQuery['after']; } $searchQuery = $searchQuery['query_string']; $hasSearchQuery = !empty($searchQuery); } if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $hasCategoryInfo = $categoryInfo !== null; $hasUserInfo = $userInfo !== null; $hasGlobal = $global !== null; $hasDeleted = $deleted !== null; $hasPagination = $pagination !== null; $args = 0; $query = 'SELECT topic_id, forum_id, user_id, topic_type, topic_title, topic_count_views, UNIX_TIMESTAMP(topic_created), UNIX_TIMESTAMP(topic_bumped), UNIX_TIMESTAMP(topic_deleted), UNIX_TIMESTAMP(topic_locked), (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NULL) AS topic_count_posts, (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NOT NULL) AS topic_count_posts_deleted FROM msz_forum_topics AS ft'; if($hasCategoryInfo || $hasGlobal) { ++$args; // wow this sucks $hasGlobalAndCategory = $hasCategoryInfo && $hasGlobal; $query .= ' WHERE '; if($hasGlobalAndCategory) $query .= '('; if($hasCategoryInfo) { if(is_array($categoryInfo)) $query .= sprintf('forum_id IN (%s)', DbTools::prepareListString($categoryInfo)); else $query .= 'forum_id = ?'; } if($hasGlobalAndCategory) $query .= ' OR '; if($hasGlobal) // not sure why you would ever set this to false, but consistency! $query .= sprintf('topic_type %s %d', $global ? '=' : '<>', ForumTopicInfo::TYPE_GLOBAL); if($hasGlobalAndCategory) $query .= ')'; } if($hasUserInfo) $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); if($hasAfterTopicId) $query .= sprintf(' %s topic_id > ?', ++$args > 1 ? 'AND' : 'WHERE'); if($hasSearchQuery) $query .= sprintf(' %s MATCH(topic_title) AGAINST (? IN NATURAL LANGUAGE MODE)', ++$args > 1 ? 'AND' : 'WHERE'); if($hasDeleted) $query .= sprintf(' %s topic_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS'); if($doSearchOrder) { $query .= ' ORDER BY topic_id ASC LIMIT 20'; } else { $query .= ' ORDER BY topic_type DESC, topic_bumped DESC'; if($hasPagination) $query .= ' LIMIT ? OFFSET ?'; } $args = 0; $stmt = $this->cache->get($query); if($hasCategoryInfo) { if(is_array($categoryInfo)) { foreach($categoryInfo as $categoryInfoEntry) $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry); } else $stmt->addParameter(++$args, $categoryInfo); } if($hasUserInfo) $stmt->addParameter(++$args, $userInfo); if($hasAfterTopicId) $stmt->addParameter(++$args, $afterTopicId); if($hasSearchQuery) $stmt->addParameter(++$args, $searchQuery); if($hasPagination) { $stmt->addParameter(++$args, $pagination->getRange()); $stmt->addParameter(++$args, $pagination->getOffset()); } $stmt->execute(); $result = $stmt->getResult(); $topics = []; while($result->next()) $topics[] = new ForumTopicInfo($result); return $topics; } public function getTopic( ?string $topicId = null, ForumPostInfo|string|null $postInfo = null ): ForumTopicInfo { $hasTopicId = $topicId !== null; $hasPostInfo = $postInfo !== null; if(!$hasTopicId && !$hasPostInfo) throw new InvalidArgumentException('At least one argument must be specified.'); if($hasTopicId && $hasPostInfo) throw new InvalidArgumentException('Only one argument may be specified.'); $value = null; $query = 'SELECT topic_id, forum_id, user_id, topic_type, topic_title, topic_count_views, UNIX_TIMESTAMP(topic_created), UNIX_TIMESTAMP(topic_bumped), UNIX_TIMESTAMP(topic_deleted), UNIX_TIMESTAMP(topic_locked), (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NULL) AS topic_count_posts, (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NOT NULL) AS topic_count_posts_deleted FROM msz_forum_topics AS ft'; if($hasTopicId) { $query .= ' WHERE topic_id = ?'; $value = $topicId; } if($hasPostInfo) { if($postInfo instanceof ForumPostInfo) { $query .= ' WHERE topic_id = ?'; $value = $postInfo->getTopicId(); } else { $query .= ' WHERE topic_id = (SELECT topic_id FROM msz_forum_posts WHERE post_id = ?)'; $value = $postInfo; } } $stmt = $this->cache->get($query); $stmt->addParameter(1, $value); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('Forum topic not found.'); return new ForumTopicInfo($result); } public function createTopic( ForumCategoryInfo|string $categoryInfo, UserInfo|string|null $userInfo, string $title, string|int $type = ForumTopicInfo::TYPE_DISCUSSION ): ForumTopicInfo { if(is_string($type)) { if(!array_key_exists($type, ForumTopicInfo::TYPE_ALIASES)) throw new InvalidArgumentException('$type is not a valid alias.'); $type = ForumTopicInfo::TYPE_ALIASES[$type]; } if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $stmt = $this->cache->get('INSERT INTO msz_forum_topics (forum_id, user_id, topic_type, topic_title) VALUES (?, ?, ?, ?)'); $stmt->addParameter(1, $categoryInfo); $stmt->addParameter(2, $userInfo); $stmt->addParameter(3, $type); $stmt->addParameter(4, $title); $stmt->execute(); return $this->getTopic(topicId: (string)$this->dbConn->getLastInsertId()); } public function updateTopic( ForumTopicInfo|string $topicInfo, ?string $title = null, string|int|null $type = null ): void { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $fields = []; $values = []; if($title !== null) { $fields[] = 'topic_title = ?'; $values[] = $title; } if($type !== null) { if(is_string($type)) { if(!array_key_exists($type, ForumTopicInfo::TYPE_ALIASES)) throw new InvalidArgumentException('$type is not a valid type alias.'); $type = ForumTopicInfo::TYPE_ALIASES[$type]; } $fields[] = 'topic_type = ?'; $values[] = $type; } if(empty($fields)) return; $args = 0; $stmt = $this->cache->get(sprintf('UPDATE msz_forum_topics SET %s WHERE topic_id = ?', implode(', ', $fields))); foreach($values as $value) $stmt->addParameter(++$args, $value); $stmt->addParameter(++$args, $topicInfo); $stmt->execute(); } public function incrementTopicView(ForumTopicInfo|string $topicInfo): void { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_count_views = topic_count_views + 1 WHERE topic_id = ?'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); } public function bumpTopic(ForumTopicInfo|string $topicInfo): void { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_bumped = NOW() WHERE topic_id = ?'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); } public function lockTopic(ForumTopicInfo|string $topicInfo): void { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_locked = NOW() WHERE topic_id = ?'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); } public function unlockTopic(ForumTopicInfo|string $topicInfo): void { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_locked = NULL WHERE topic_id = ?'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); } public function deleteTopic(ForumTopicInfo|string $topicInfo): void { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_deleted = COALESCE(topic_deleted, NOW()) WHERE topic_id = ? AND topic_deleted IS NULL'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); $stmt = $this->cache->get('UPDATE msz_forum_posts AS fp SET post_deleted = (SELECT topic_deleted FROM msz_forum_topics WHERE topic_id = fp.topic_id) WHERE topic_id = ? AND post_deleted = NULL'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); } public function restoreTopic(ForumTopicInfo|string $topicInfo): void { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_posts AS fp SET post_deleted = NULL WHERE topic_id = ? AND post_deleted = (SELECT topic_deleted FROM msz_forum_topics WHERE topic_id = fp.topic_id)'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_deleted = NULL WHERE topic_id = ?'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); } public function nukeTopic(ForumTopicInfo|string $topicInfo): void { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $stmt = $this->cache->get('DELETE FROM msz_forum_topics WHERE topic_id = ?'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); } public function checkTopicParticipated( ForumTopicInfo|string $topicInfo, UserInfo|string|null $userInfo ): bool { if($userInfo === null) return false; if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ? AND user_id = ?'); $stmt->addParameter(1, $topicInfo); $stmt->addParameter(2, $userInfo); $stmt->execute(); $result = $stmt->getResult(); return $result->next() && $result->getInteger(0) > 0; } public function checkTopicUnread( ForumTopicInfo|string $topicInfo, UserInfo|string|null $userInfo ): bool { if($userInfo === null) return false; $topicInfoIsInstance = $topicInfo instanceof ForumTopicInfo; if($topicInfoIsInstance && !$topicInfo->isActive()) return false; $query = 'SELECT UNIX_TIMESTAMP(track_last_read) FROM msz_forum_topics_track AS ftt WHERE user_id = ? AND topic_id = ?'; if(!$topicInfoIsInstance) $query .= ' AND track_last_read = (SELECT topic_bumped FROM msz_forum_topics WHERE topic_id = ftt.topic_id AND topic_bumped >= NOW() - INTERVAL 1 MONTH)'; $stmt = $this->cache->get($query); $stmt->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo); $stmt->addParameter(2, $topicInfoIsInstance ? $topicInfo->getId() : $topicInfo); $stmt->execute(); $result = $stmt->getResult(); // user has never read this topic, return unread if(!$result->next()) return true; return $result->getInteger(0) < $topicInfo->getBumpedTime(); } public function getMostActiveTopicInfo( UserInfo|string $userInfo, array $exceptCategoryInfos = [], array $exceptTopicInfos = [], ?bool $deleted = null ): object { if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $hasExceptCategoryInfos = !empty($exceptCategoryInfos); $hasExceptTopicInfos = !empty($exceptTopicInfos); $hasDeleted = $deleted !== null; $query = 'SELECT topic_id, forum_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = ?'; if($hasDeleted) $query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS'); if($hasExceptCategoryInfos) $query .= sprintf(' AND forum_id NOT IN (%s)', DbTools::prepareListString($exceptCategoryInfos)); if($hasExceptTopicInfos) $query .= sprintf(' AND topic_id NOT IN (%s)', DbTools::prepareListString($exceptTopicInfos)); $query .= ' GROUP BY topic_id ORDER BY post_count DESC LIMIT 1'; $args = 0; $stmt = $this->cache->get($query); $stmt->addParameter(++$args, $userInfo); foreach($exceptCategoryInfos as $categoryInfo) { if($categoryInfo instanceof ForumCategoryInfo) $stmt->addParameter(++$args, $categoryInfo->getId()); elseif(is_string($categoryInfo) || is_int($categoryInfo)) $stmt->addParameter(++$args, (string)$categoryInfo); else throw new InvalidArgumentException('$exceptCategoryInfos may only contain string ids or instances of ForumCategoryInfo.'); } foreach($exceptTopicInfos as $topicInfo) { if($topicInfo instanceof ForumTopicInfo) $stmt->addParameter(++$args, $topicInfo->getId()); elseif(is_string($topicInfo) || is_int($topicInfo)) $stmt->addParameter(++$args, (string)$topicInfo); else throw new InvalidArgumentException('$exceptTopicInfos may only contain string ids or instances of ForumTopicInfo.'); } $stmt->execute(); $result = $stmt->getResult(); $info = new stdClass; $info->success = $result->next(); if($info->success) { $info->topicId = $result->getString(0); $info->categoryId = $result->getString(1); $info->postCount = $result->getInteger(2); } return $info; } public function checkUserHasReadTopic( UserInfo|string|null $userInfo, ForumTopicInfo|string $topicInfo ): bool { // this method is primarily used to check if we should increment the view count // guests shouldn't increment it so we just if($userInfo === null) return true; $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_topics_track WHERE topic_id = ? AND user_id = ?'); $stmt->addParameter(1, $topicInfo instanceof ForumTopicInfo ? $topicInfo->getId() : $topicInfo); $stmt->addParameter(2, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo); $stmt->execute(); $result = $stmt->getResult(); return $result->next() && $result->getInteger(0) > 0; } public function updateUserReadTopic( UserInfo|string|null $userInfo, ForumTopicInfo|string $topicInfo, ForumCategoryInfo|string|null $categoryInfo = null ): void { if($userInfo === null) return; if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($topicInfo instanceof ForumTopicInfo) { $categoryInfo = $topicInfo->getCategoryId(); $topicInfo = $topicInfo->getId(); } else { if($categoryInfo === null) throw new InvalidArgumentException('$categoryInfo must be specified if $topicInfo is not an instance of ForumTopicInfo.'); if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); } $stmt = $this->cache->get('REPLACE INTO msz_forum_topics_track (user_id, topic_id, forum_id, track_last_read) VALUES (?, ?, ?, NOW())'); $stmt->addParameter(1, $userInfo); $stmt->addParameter(2, $topicInfo); $stmt->addParameter(3, $categoryInfo); $stmt->execute(); } public function countTopicRedirects( UserInfo|string|null $userInfo = null ): int { if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $hasUserInfo = $userInfo !== null; $query = 'SELECT COUNT(*) FROM msz_forum_topics_redirects'; if($hasUserInfo) $query .= ' WHERE user_id = ?'; $stmt = $this->cache->get($query); if($hasUserInfo) $stmt->addParameter(1, $userInfo); $stmt->execute(); $result = $stmt->getResult(); return $result->next() ? $result->getInteger(0) : 0; } public function getTopicRedirects( UserInfo|string|null $userInfo = null, ?Pagination $pagination = null ): array { if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $hasUserInfo = $userInfo !== null; $hasPagination = $pagination !== null; $query = 'SELECT topic_id, user_id, topic_redir_url, UNIX_TIMESTAMP(topic_redir_created) FROM msz_forum_topics_redirects'; if($hasUserInfo) $query .= ' WHERE user_id = ?'; if($hasPagination) $query .= ' LIMIT ? OFFSET ?'; $args = 0; $stmt = $this->cache->get($query); if($hasUserInfo) $stmt->addParameter(++$args, $userInfo); if($hasPagination) { $stmt->addParameter(++$args, $pagination->getRange()); $stmt->addParameter(++$args, $pagination->getOffset()); } $stmt->execute(); $result = $stmt->getResult(); $redirs = []; while($result->next()) $redirs[] = new ForumTopicRedirectInfo($result); return $redirs; } public function hasTopicRedirect(ForumTopicInfo|string $topicInfo): bool { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_topics_redirects WHERE topic_id = ?'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('Was unable to check if a redirect exists.'); return $result->getInteger(0) > 0; } public function getTopicRedirect(ForumTopicInfo|string $topicInfo): ForumTopicRedirectInfo { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $stmt = $this->cache->get('SELECT topic_id, user_id, topic_redir_url, UNIX_TIMESTAMP(topic_redir_created) FROM msz_forum_topics_redirects WHERE topic_id = ?'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('Could not find that forum topic redirect.'); return new ForumTopicRedirectInfo($result); } public function createTopicRedirect( ForumTopicInfo|string $topicInfo, UserInfo|string|null $userInfo, string $linkTarget ): ForumTopicRedirectInfo { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $stmt = $this->cache->get('INSERT INTO msz_forum_topics_redirects (topic_id, user_id, topic_redir_url) VALUES (?, ?, ?)'); $stmt->addParameter(1, $topicInfo); $stmt->addParameter(2, $userInfo); $stmt->addParameter(3, $linkTarget); $stmt->execute(); return $this->getTopicRedirect($topicInfo); } public function deleteTopicRedirect(ForumTopicRedirectInfo|ForumTopicInfo|string $topicInfo): void { if($topicInfo instanceof ForumTopicRedirectInfo) $topicInfo = $topicInfo->getTopicId(); elseif($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $stmt = $this->cache->get('DELETE FROM msz_forum_topics_redirects WHERE topic_id = ?'); $stmt->addParameter(1, $topicInfo); $stmt->execute(); } public function countPosts( ForumCategoryInfo|string|null $categoryInfo = null, ForumTopicInfo|string|null $topicInfo = null, UserInfo|string|null $userInfo = null, ForumPostInfo|string|null $upToPostInfo = null, ?bool $deleted = null ): int { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($upToPostInfo instanceof ForumPostInfo) $upToPostInfo = $upToPostInfo->getId(); $hasCategoryInfo = $categoryInfo !== null; $hasTopicInfo = $topicInfo !== null; $hasUserInfo = $userInfo !== null; $hasUpToPostInfo = $upToPostInfo !== null; $hasDeleted = $deleted !== null; $args = 0; $query = 'SELECT COUNT(*) FROM msz_forum_posts'; if($hasCategoryInfo) { ++$args; $query .= ' WHERE forum_id = ?'; } if($hasTopicInfo) $query .= sprintf(' %s topic_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); if($hasUserInfo) $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); if($hasUpToPostInfo) $query .= sprintf(' %s post_id < ?', ++$args > 1 ? 'AND' : 'WHERE'); if($hasDeleted) $query .= sprintf(' %s post_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS'); $args = 0; $stmt = $this->cache->get($query); if($hasCategoryInfo) $stmt->addParameter(++$args, $categoryInfo); if($hasTopicInfo) $stmt->addParameter(++$args, $topicInfo); if($hasUserInfo) $stmt->addParameter(++$args, $userInfo); if($hasUpToPostInfo) $stmt->addParameter(++$args, $upToPostInfo); $stmt->execute(); $result = $stmt->getResult(); return $result->next() ? $result->getInteger(0) : 0; } public function getPosts( ForumCategoryInfo|string|array|null $categoryInfo = null, ForumTopicInfo|string|null $topicInfo = null, UserInfo|string|null $userInfo = null, ForumPostInfo|string|null $upToPostInfo = null, ?array $searchQuery = null, ?bool $deleted = null, ?Pagination $pagination = null ): array { // remove this hack when search server $hasSearchQuery = $searchQuery !== null; $hasAfterPostId = false; $doSearchOrder = false; if($hasSearchQuery) { if(!empty($searchQuery['type']) && $searchQuery['type'] !== 'forum' && $searchQuery['type'] !== 'forum:post') return []; $deleted = false; $pagination = null; $doSearchOrder = true; if(!empty($searchQuery['author'])) $userInfo = $searchQuery['author']; if(!empty($searchQuery['after'])) { $hasAfterPostId = true; $afterPostId = $searchQuery['after']; } $searchQuery = $searchQuery['query_string']; $hasSearchQuery = !empty($searchQuery); } if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($upToPostInfo instanceof ForumPostInfo) $upToPostInfo = $upToPostInfo->getId(); $hasCategoryInfo = $categoryInfo !== null; $hasTopicInfo = $topicInfo !== null; $hasUserInfo = $userInfo !== null; $hasUpToPostInfo = $upToPostInfo !== null; $hasDeleted = $deleted !== null; $hasPagination = $pagination !== null; $args = 0; $query = 'SELECT post_id, topic_id, forum_id, user_id, INET6_NTOA(post_ip), post_text, post_parse, post_display_signature, UNIX_TIMESTAMP(post_created), UNIX_TIMESTAMP(post_edited), UNIX_TIMESTAMP(post_deleted) FROM msz_forum_posts'; if($hasCategoryInfo) { ++$args; if(is_array($categoryInfo)) $query .= sprintf(' WHERE forum_id IN (%s)', DbTools::prepareListString($categoryInfo)); else $query .= ' WHERE forum_id = ?'; } if($hasTopicInfo) $query .= sprintf(' %s topic_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); if($hasUserInfo) $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); if($hasUpToPostInfo) $query .= sprintf(' %s post_id < ?', ++$args > 1 ? 'AND' : 'WHERE'); if($hasAfterPostId) $query .= sprintf(' %s post_id > ?', ++$args > 1 ? 'AND' : 'WHERE'); if($hasSearchQuery) $query .= sprintf(' %s MATCH(post_text) AGAINST (? IN NATURAL LANGUAGE MODE)', ++$args > 1 ? 'AND' : 'WHERE'); if($hasDeleted) $query .= sprintf(' %s post_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS'); if($doSearchOrder) { $query .= ' ORDER BY post_id ASC LIMIT 20'; } else { $query .= ' ORDER BY post_id ASC'; if($hasPagination) $query .= ' LIMIT ? OFFSET ?'; } $args = 0; $stmt = $this->cache->get($query); if($hasCategoryInfo) { if(is_array($categoryInfo)) { foreach($categoryInfo as $categoryInfoEntry) $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry); } else $stmt->addParameter(++$args, $categoryInfo); } if($hasTopicInfo) $stmt->addParameter(++$args, $topicInfo); if($hasUserInfo) $stmt->addParameter(++$args, $userInfo); if($hasUpToPostInfo) $stmt->addParameter(++$args, $upToPostInfo); if($hasAfterPostId) $stmt->addParameter(++$args, $afterPostId); if($hasSearchQuery) $stmt->addParameter(++$args, $searchQuery); if($hasPagination) { $stmt->addParameter(++$args, $pagination->getRange()); $stmt->addParameter(++$args, $pagination->getOffset()); } $stmt->execute(); $result = $stmt->getResult(); $posts = []; while($result->next()) $posts[] = new ForumPostInfo($result); return $posts; } public function getPost( ?string $postId = null, ForumTopicInfo|string|null $topicInfo = null, ForumCategoryInfo|string|array|null $categoryInfos = null, UserInfo|string|null $userInfo = null, bool $getLast = false, ?bool $deleted = null ): ForumPostInfo { $hasPostId = $postId !== null; $hasTopicInfo = $topicInfo !== null; $hasCategoryInfos = $categoryInfos !== null; $hasUserInfo = $userInfo !== null; $hasDeleted = $deleted !== null; if(!$hasPostId && !$hasTopicInfo && !$hasCategoryInfos && !$hasUserInfo) throw new InvalidArgumentException('At least one of the four first arguments must be specified.'); $values = []; $query = 'SELECT post_id, topic_id, forum_id, user_id, INET6_NTOA(post_ip), post_text, post_parse, post_display_signature, UNIX_TIMESTAMP(post_created), UNIX_TIMESTAMP(post_edited), UNIX_TIMESTAMP(post_deleted) FROM msz_forum_posts'; if($hasPostId) { $query .= ' WHERE post_id = ?'; $values[] = $postId; } elseif($hasUserInfo) { $query .= ' WHERE user_id = ?'; $values[] = $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo; $query .= sprintf(' ORDER BY post_id %s', $getLast ? 'DESC' : 'ASC'); } elseif($hasTopicInfo) { if($topicInfo instanceof ForumTopicInfo) $topicInfo = $topicInfo->getId(); $query .= sprintf(' WHERE post_id = (SELECT %s(post_id) FROM msz_forum_posts WHERE topic_id = ?', $getLast ? 'MAX' : 'MIN'); if($hasDeleted) $query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS'); $query .= ')'; $values[] = $topicInfo; } elseif($hasCategoryInfos) { if(!is_array($categoryInfos)) $categoryInfos = [$categoryInfos]; $query .= sprintf( ' WHERE post_id = (SELECT %s(post_id) FROM msz_forum_posts WHERE forum_id IN (%s)', $getLast ? 'MAX' : 'MIN', DbTools::prepareListString($categoryInfos) ); if($hasDeleted) $query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS'); $query .= ')'; foreach($categoryInfos as $categoryInfo) { if($categoryInfo instanceof ForumCategoryInfo) $values[] = $categoryInfo->getId(); elseif(is_string($categoryInfo) || is_int($categoryInfo)) $values[] = (string)$categoryInfo; else throw new InvalidArgumentException('$categoryInfos contains an invalid item.'); } } $args = 0; $stmt = $this->cache->get($query); foreach($values as $value) $stmt->addParameter(++$args, $value); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('Forum post not found.'); return new ForumPostInfo($result); } public function createPost( ForumTopicInfo|string $topicInfo, UserInfo|string|null $userInfo, IPAddress|string $remoteAddr, string $body, int $bodyParser, bool $displaySignature, ForumCategoryInfo|string|null $categoryInfo = null ): ForumPostInfo { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); if($topicInfo instanceof ForumTopicInfo) { $categoryInfo ??= $topicInfo->getCategoryId(); $topicInfo = $topicInfo->getId(); } elseif($categoryInfo === null) throw new InvalidArgumentException('$categoryInfo may only be null if $topicInfo is an instance of ForumTopicInfo.'); if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($remoteAddr instanceof IPAddress) $remoteAddr = (string)$remoteAddr; $stmt = $this->cache->get('INSERT INTO msz_forum_posts (topic_id, forum_id, user_id, post_ip, post_text, post_parse, post_display_signature) VALUES (?, ?, ?, INET6_ATON(?), ?, ?, ?)'); $stmt->addParameter(1, $topicInfo); $stmt->addParameter(2, $categoryInfo); $stmt->addParameter(3, $userInfo); $stmt->addParameter(4, $remoteAddr); $stmt->addParameter(5, $body); $stmt->addParameter(6, $bodyParser); $stmt->addParameter(7, $displaySignature ? 1 : 0); $stmt->execute(); return $this->getPost(postId: (string)$this->dbConn->getLastInsertId()); } public function updatePost( ForumPostInfo|string $postInfo, IPAddress|string|null $remoteAddr = null, ?string $body = null, ?int $bodyParser = null, ?bool $displaySignature = null, bool $bumpEdited = true ): void { if($postInfo instanceof ForumPostInfo) $postInfo = $postInfo->getId(); $fields = []; $values = []; if($remoteAddr !== null) { if($remoteAddr instanceof IPAddress) $remoteAddr = (string)$remoteAddr; $fields[] = 'post_ip = INET6_ATON(?)'; $values[] = $remoteAddr; } if($body !== null) { $fields[] = 'post_text = ?'; $values[] = $body; } if($bodyParser !== null) { $fields[] = 'post_parse = ?'; $values[] = $bodyParser; } if($displaySignature !== null) { $fields[] = 'post_display_signature = ?'; $values[] = $displaySignature ? 1 : 0; } if(empty($fields)) return; if($bumpEdited) $fields[] = 'post_edited = NOW()'; $args = 0; $stmt = $this->cache->get(sprintf('UPDATE msz_forum_posts SET %s WHERE post_id = ?', implode(', ', $fields))); foreach($values as $value) $stmt->addParameter(++$args, $value); $stmt->addParameter(++$args, $postInfo); $stmt->execute(); } public function deletePost(ForumPostInfo|string $postInfo): void { if($postInfo instanceof ForumPostInfo) $postInfo = $postInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_posts SET post_deleted = COALESCE(post_deleted, NOW()) WHERE post_id = ?'); $stmt->addParameter(1, $postInfo); $stmt->execute(); } public function restorePost(ForumPostInfo|string $postInfo): void { if($postInfo instanceof ForumPostInfo) $postInfo = $postInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_posts SET post_deleted = NULL WHERE post_id = ?'); $stmt->addParameter(1, $postInfo); $stmt->execute(); } public function nukePost(ForumPostInfo|string $postInfo): void { if($postInfo instanceof ForumPostInfo) $postInfo = $postInfo->getId(); $stmt = $this->cache->get('DELETE FROM msz_forum_posts WHERE post_id = ?'); $stmt->addParameter(1, $postInfo); $stmt->execute(); } public function getUserLastPostCreatedTime(UserInfo|string $userInfo): int { if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); // intentionally including deleted posts $stmt = $this->cache->get('SELECT UNIX_TIMESTAMP(MAX(post_created)) FROM msz_forum_posts WHERE user_id = ?'); $stmt->addParameter(1, $userInfo); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) return 0; return $result->getInteger(0); } public function getUserLastPostCreatedAt(UserInfo|string $userInfo): DateTime { return DateTime::fromUnixTimeSeconds($this->getUserLastPostCreatedTime($userInfo)); } public function generatePostRankings( int $year = 0, int $month = 0, array $exceptCategoryInfos = [], array $exceptTopicInfos = [] ): array { $hasYear = $year > 0; $hasMonth = $hasYear && $month > 0; $hasExcludedCategoryInfos = !empty($exceptCategoryInfos); $hasExcludedTopicInfos = !empty($exceptTopicInfos); $query = 'SELECT user_id, COUNT(*) AS posts_count FROM msz_forum_posts WHERE post_deleted IS NULL'; if($hasYear) $query .= sprintf( ' AND DATE(post_created) BETWEEN "%1$04d-%2$02d-01" AND "%1$04d-%3$02d-31"', $year, $hasMonth ? $month : 1, $hasMonth ? $month : 12 ); if($hasExcludedCategoryInfos) $query .= sprintf(' AND forum_id NOT IN (%s)', DbTools::prepareListString($exceptCategoryInfos)); if($hasExcludedTopicInfos) $query .= sprintf(' AND topic_id NOT IN (%s)', DbTools::prepareListString($exceptTopicInfos)); $query .= ' GROUP BY user_id HAVING posts_count > 0 ORDER BY posts_count DESC'; $args = 0; $stmt = $this->cache->get($query); foreach($exceptCategoryInfos as $exceptCategoryInfo) $stmt->addParameter(++$args, $exceptCategoryInfo instanceof ForumCategoryInfo ? $exceptCategoryInfo->getId() : $exceptCategoryInfo); foreach($exceptTopicInfos as $exceptTopicInfo) $stmt->addParameter(++$args, $exceptTopicInfo instanceof ForumTopicInfo ? $exceptTopicInfo->getId() : $exceptTopicInfo); $stmt->execute(); $result = $stmt->getResult(); $rankings = []; $rankNo = 0; $lastPostsCount = PHP_INT_MAX; while($result->next()) { $rankings[] = $ranking = new stdClass; $ranking->userId = $result->getString(0); $ranking->postsCount = $result->getInteger(1); if($lastPostsCount > $ranking->postsCount) { ++$rankNo; $lastPostsCount = $ranking->postsCount; } $ranking->position = $rankNo; } return $rankings; } }