mirror of
https://github.com/OPSnet/Gazelle.git
synced 2026-01-16 18:04:34 -05:00
291 lines
12 KiB
PHP
291 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Gazelle\Manager;
|
|
|
|
class Forum extends \Gazelle\BaseManager {
|
|
protected const CACHE_TOC_MAIN = 'forum_toc_main';
|
|
protected const CACHE_LIST = 'forum_list';
|
|
protected const ID_KEY = 'zz_f_%d';
|
|
|
|
/**
|
|
* Create a forum
|
|
*/
|
|
public function create(
|
|
\Gazelle\User $user,
|
|
int $sequence,
|
|
int $categoryId,
|
|
string $name,
|
|
string $description,
|
|
int $minClassRead,
|
|
int $minClassWrite,
|
|
int $minClassCreate,
|
|
bool $autoLock,
|
|
int $autoLockWeeks,
|
|
): \Gazelle\Forum {
|
|
self::$db->prepared_query("
|
|
INSERT INTO forums
|
|
(Sort, CategoryID, Name, Description, MinClassRead, MinClassWrite, MinClassCreate, AutoLock, AutoLockWeeks, LastPostAuthorID)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
", $sequence, $categoryId, trim($name), trim($description), $minClassRead, $minClassWrite, $minClassCreate,
|
|
$autoLock ? '1' : '0', $autoLockWeeks, $user->id
|
|
);
|
|
$id = self::$db->inserted_id();
|
|
$this->flushToc();
|
|
return new \Gazelle\Forum($id);
|
|
}
|
|
|
|
/**
|
|
* Instantiate a forum by its ID
|
|
*/
|
|
public function findById(int $id): ?\Gazelle\Forum {
|
|
$key = sprintf(self::ID_KEY, $id);
|
|
$forumId = self::$cache->get_value($key);
|
|
if ($forumId === false) {
|
|
$forumId = (int)self::$db->scalar("
|
|
SELECT ID FROM forums WHERE ID = ?
|
|
", $id
|
|
);
|
|
if ($forumId) {
|
|
self::$cache->cache_value($key, $forumId, 7200);
|
|
}
|
|
}
|
|
return $forumId ? new \Gazelle\Forum($forumId) : null;
|
|
}
|
|
|
|
public function forumList(): array {
|
|
$list = self::$cache->get_value(self::CACHE_LIST);
|
|
if ($list === false) {
|
|
self::$db->prepared_query("
|
|
SELECT f.ID
|
|
FROM forums f
|
|
INNER JOIN forums_categories cat ON (cat.ID = f.CategoryID)
|
|
ORDER BY cat.Sort, cat.Name, f.Sort, f.Name
|
|
");
|
|
$list = self::$db->collect('ID', false);
|
|
self::$cache->cache_value(self::CACHE_LIST, $list, 86400);
|
|
}
|
|
return array_map(fn ($id) => $this->findById($id), $list);
|
|
}
|
|
|
|
/**
|
|
* The forum table of contents (the main /forums.php view)
|
|
*
|
|
* - string category name "Community"
|
|
* containing an array of (one per forum):
|
|
* - int 'ID' Forum id
|
|
* - string 'Name' Forum name "The Lounge"
|
|
* - string 'Description' Forum description "The Lounge"
|
|
* - int 'NumTopics' Number of threads (topics)
|
|
* - int 'NumPosts' Number of posts (sum of posts of all threads)
|
|
* - int 'LastPostTopicID' Thread id of most recent post
|
|
* - int 'MinClassRead' Min class read \
|
|
* - int 'MinClassWrite' Min class write -+-- ACLs
|
|
* - int 'MinClassCreate' Min class create /
|
|
* - int 'Sort' Positional rank
|
|
* - bool 'AutoLock' if true, forum will lock after AutoLockWeeks of inactivity
|
|
* - int 'AutoLockWeeks' number of weeks for inactivity timer
|
|
* - string 'Title' Title of most recent thread
|
|
* - int 'LastPostAuthorID' User id of author of most recent post
|
|
* - int 'LastPostID' Post id of most recent post
|
|
* - timestamp 'LastPostTime' Date of most recent thread (creation or post)
|
|
* - int 'IsSticky' Last post is locked (0/1)
|
|
* - int 'IsLocked' Last post is sticky (0/1)
|
|
*/
|
|
public function tableOfContentsMain(): array {
|
|
$toc = self::$cache->get_value(self::CACHE_TOC_MAIN);
|
|
if ($toc === false) {
|
|
self::$db->prepared_query("
|
|
SELECT cat.Name AS categoryName, cat.ID AS categoryId,
|
|
f.ID, f.Name, f.Description, f.NumTopics, f.NumPosts,
|
|
f.LastPostTopicID, f.MinClassRead, f.MinClassWrite, f.MinClassCreate,
|
|
f.Sort, f.AutoLock, f.AutoLockWeeks,
|
|
ft.Title, ft.LastPostAuthorID, ft.LastPostID, ft.LastPostTime, ft.IsSticky, ft.IsLocked,
|
|
(fp.TopicID IS NOT NULL AND fp.Closed = '0') AS has_poll
|
|
FROM forums f
|
|
INNER JOIN forums_categories cat ON (cat.ID = f.CategoryID)
|
|
LEFT JOIN forums_topics ft ON (ft.ID = f.LastPostTopicID)
|
|
LEFT JOIN forums_polls fp ON (fp.TopicID = ft.ID)
|
|
ORDER BY cat.Sort, cat.Name, f.Sort, f.Name
|
|
");
|
|
$toc = [];
|
|
while ($row = self::$db->next_row(MYSQLI_ASSOC)) {
|
|
$category = $row['categoryName'];
|
|
$row['AutoLock'] = ($row['AutoLock'] == '1');
|
|
if (!isset($toc[$category])) {
|
|
$toc[$category] = [];
|
|
}
|
|
$toc[$category][] = $row;
|
|
}
|
|
self::$cache->cache_value(self::CACHE_TOC_MAIN, $toc, 86400 * 10);
|
|
}
|
|
return $toc;
|
|
}
|
|
|
|
public function tableOfContents(\Gazelle\User $user): array {
|
|
$toc = $this->tableOfContentsMain();
|
|
$userToc = [];
|
|
foreach ($toc as $category => $forumList) {
|
|
$seen = 0;
|
|
foreach ($forumList as $f) {
|
|
$forum = $this->findById($f['ID']);
|
|
if (!$user->readAccess($forum)) {
|
|
continue;
|
|
}
|
|
$autosubList = $forum->autoSubscribeForUserList($user);
|
|
$userLastRead = $forum->userLastRead($user);
|
|
if (isset($userLastRead[$f['LastPostTopicID']])) {
|
|
$isRead = true;
|
|
$lastReadPage = (int)$userLastRead[$f['LastPostTopicID']]['Page'];
|
|
$lastReadPost = $userLastRead[$f['LastPostTopicID']]['PostID'];
|
|
$catchup = $userLastRead[$f['LastPostTopicID']]['PostID'] >= $f['LastPostID']
|
|
|| $user->forumCatchupEpoch() >= strtotime($f['LastPostTime']);
|
|
} else {
|
|
$isRead = false;
|
|
$lastReadPage = null;
|
|
$lastReadPost = null;
|
|
$catchup = $f['LastPostTime'] && $user->forumCatchupEpoch() >= strtotime($f['LastPostTime']);
|
|
}
|
|
|
|
if (!isset($toc[$category])) {
|
|
$userToc[$category] = [];
|
|
}
|
|
$userToc[$category][] = [
|
|
'autosub' => in_array($f['ID'], $autosubList),
|
|
'creator' => $f['MinClassCreate'] <= $user->classLevel(),
|
|
'category' => $category,
|
|
'category_id' => $f['categoryId'],
|
|
'cut_title' => shortenString($f['Title'] ?? '', 50, true),
|
|
'description' => $f['ID'] == DONOR_FORUM
|
|
? DONOR_FORUM_DESCRIPTION[random_int(0, count(DONOR_FORUM_DESCRIPTION) - 1)]
|
|
: $f['Description'],
|
|
'forum' => $forum,
|
|
'forum_id' => $f['ID'],
|
|
'icon_class' => (($f['IsLocked'] && !$f['IsSticky']) || $catchup ? 'read' : 'unread')
|
|
. ($f['IsLocked'] ? '_locked' : '')
|
|
. ($f['IsSticky'] ? '_sticky' : ''),
|
|
'id' => $f['LastPostTopicID'],
|
|
'is_read' => $isRead,
|
|
'has_poll' => $f['has_poll'],
|
|
'last_post_time' => $f['LastPostTime'],
|
|
'last_post_user' => $f['LastPostAuthorID'],
|
|
'name' => $f['Name'],
|
|
'num_posts' => $f['NumPosts'],
|
|
'num_topics' => $f['NumTopics'],
|
|
'threads' => $f['NumPosts'] > 0,
|
|
'title' => $f['Title'],
|
|
'tooltip' => $f['ID'] == DONOR_FORUM ? 'tooltip_gold' : 'tooltip',
|
|
'first' => (++$seen == 1), // implies <table> needs to be emitted
|
|
'last_read_page' => $lastReadPage,
|
|
'last_read_post' => $lastReadPost,
|
|
];
|
|
}
|
|
}
|
|
return $userToc;
|
|
}
|
|
|
|
public function flushToc(): static {
|
|
self::$cache->delete_multi([
|
|
self::CACHE_TOC_MAIN,
|
|
self::CACHE_LIST,
|
|
]);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Transform a list of forum ids into forum names (returning the id as-is if
|
|
* the forum does not exist. Main use is in templates.
|
|
*/
|
|
public function nameList(array $idList): array {
|
|
return array_map(fn ($id) => $this->findById($id)?->name() ?? $id, $idList);
|
|
}
|
|
|
|
/**
|
|
* Configure forum ACLs for a user (what they can see from their class, what they
|
|
* have explicit permission to access, less what they have been forbidden.
|
|
* It is expected to implode(' AND ', ...) the first return parameter within a
|
|
* larger query, and merge the second return parameter into the prepared_query()
|
|
* call.
|
|
*
|
|
* It is a pre-requisite that the `forums` table have the alias f.
|
|
*
|
|
* @return array of [conditions, args]
|
|
*/
|
|
public function configureForUser(\Gazelle\User $user): array {
|
|
$permitted = $user->permittedForums();
|
|
if (!empty($permitted)) {
|
|
$cond = ['(f.MinClassRead <= ? OR f.ID IN (' . placeholders($permitted) . '))'];
|
|
$args = array_merge([$user->classLevel()], $permitted);
|
|
} else {
|
|
$cond = ['f.MinClassRead <= ?'];
|
|
$args = [$user->classLevel()];
|
|
}
|
|
$forbidden = $user->forbiddenForums();
|
|
if (!empty($forbidden)) {
|
|
$cond[] = 'f.ID NOT IN (' . placeholders($forbidden) . ')';
|
|
$args = array_merge($args, $forbidden);
|
|
}
|
|
return [$cond, $args];
|
|
}
|
|
|
|
public function subscribedForumTotal(\Gazelle\User $user): int {
|
|
[$cond, $args] = $this->configureForUser($user);
|
|
return (int)self::$db->scalar("
|
|
SELECT count(*)
|
|
FROM users_subscriptions AS s
|
|
LEFT JOIN forums_last_read_topics AS l ON (l.UserID = s.UserID AND l.TopicID = s.TopicID)
|
|
INNER JOIN forums_topics AS t ON (t.ID = s.TopicID)
|
|
INNER JOIN forums AS f ON (f.ID = t.ForumID)
|
|
WHERE s.UserID = ?
|
|
AND " . implode(' AND ', $cond),
|
|
$user->id, ...$args
|
|
);
|
|
}
|
|
|
|
public function unreadSubscribedForumTotal(\Gazelle\User $user): int {
|
|
[$cond, $args] = $this->configureForUser($user);
|
|
return (int)self::$db->scalar("
|
|
SELECT count(*)
|
|
FROM users_subscriptions AS s
|
|
LEFT JOIN forums_last_read_topics AS l ON (l.UserID = s.UserID AND l.TopicID = s.TopicID)
|
|
INNER JOIN forums_topics AS t ON (t.ID = s.TopicID)
|
|
INNER JOIN forums AS f ON (f.ID = t.ForumID)
|
|
WHERE if(t.IsLocked = '1' AND t.IsSticky = '0', t.LastPostID, coalesce(l.PostID, 0)) < t.LastPostID
|
|
AND s.UserID = ?
|
|
AND " . implode(' AND ', $cond),
|
|
$user->id, ...$args
|
|
);
|
|
}
|
|
|
|
public function latestPostsList(\Gazelle\User $user, bool $showUnread, int $limit, int $offset): array {
|
|
[$cond, $args] = $this->configureForUser($user);
|
|
if ($showUnread) {
|
|
$cond[] = "if(t.IsLocked = '1' AND t.IsSticky = '0', t.LastPostID, flrt.PostID) < t.LastPostID";
|
|
}
|
|
array_push($cond,
|
|
"s.UserID = ?"
|
|
);
|
|
array_push($args, $user->id, $limit, $offset);
|
|
|
|
self::$db->prepared_query("
|
|
SELECT f.ID AS forumId,
|
|
f.Name AS forumName,
|
|
t.ID AS threadId,
|
|
t.Title AS threadTitle,
|
|
t.LastPostID AS lastPostId,
|
|
(t.IsLocked = '1') AS locked,
|
|
(p.ID < t.LastPostID AND t.IsLocked != '1') AS new
|
|
FROM users_subscriptions AS s
|
|
INNER JOIN forums_last_read_topics AS flrt ON (flrt.TopicID = s.TopicID AND flrt.UserID = ?)
|
|
INNER JOIN forums_topics AS t ON (t.ID = s.TopicID)
|
|
INNER JOIN forums_posts AS p ON (p.ID = flrt.PostID and p.TopicID = flrt.TopicID)
|
|
INNER JOIN forums AS f ON (f.ID = t.ForumID)
|
|
WHERE " . implode(' AND ', $cond) . "
|
|
GROUP BY p.TopicID
|
|
ORDER BY t.LastPostID DESC
|
|
LIMIT ? OFFSET ?
|
|
", $user->id, ...$args
|
|
);
|
|
return self::$db->to_array(false, MYSQLI_ASSOC, false);
|
|
}
|
|
}
|