Files
ops-Gazelle/app/Comment/AbstractComment.php
2025-08-07 16:03:10 +02:00

330 lines
12 KiB
PHP

<?php
namespace Gazelle\Comment;
abstract class AbstractComment extends \Gazelle\BaseObject {
final public const tableName = 'comments';
final protected const PAGE_TOTAL = '%s_comments_%d';
protected int $userId;
protected int $lastRead = 0;
protected int $total; // number of comments
protected array $thread = []; // the page of comments
abstract public function page(): string;
abstract public function pageUrl(): string;
public function flush(): static {
// No-op: There is no such thing as an individual comment cache
return $this;
}
public function link(): string {
return sprintf('<a href="%s">%s</a>', $this->url(), "Comment #" . $this->id);
}
public function location(): string {
return $this->pageUrl() . "{$this->pageId}&postid={$this->id}#post{$this->id}";
}
public function __construct(
protected int $pageId,
protected int $pageNum,
public readonly int $id,
) {}
public function body(): string {
return (string)self::$db->scalar("
SELECT Body FROM comments WHERE Page = ? AND ID = ?
", $this->page(), $this->id
);
}
public function isAuthor(\Gazelle\User $user): bool {
return $this->userId() === $user->id;
}
public function lastRead(): int {
return $this->lastRead;
}
public function pageNum(): int {
return $this->pageNum;
}
public function thread(): array {
return $this->thread;
}
public function threadList(\Gazelle\Manager\User $manager): array {
$cache = [];
$list = [];
foreach ($this->thread() as $post) {
[$postId, $userId, $created, $body, $editedUserID, $editedTime, $editedUsername] = array_values($post);
if (!isset($cache[$userId])) {
$cache[$userId] = $manager->findById($userId);
}
$author = $cache[$userId];
$list[] = [
'postId' => $postId,
'authorId' => $userId,
'name' => $author->username(),
'donor' => new \Gazelle\User\Donor($author)->isDonor(),
'warned' => $author->isWarned(),
'enabled' => $author->isEnabled(),
'class' => $manager->userclassName($author->primaryClass()),
'addedTime' => $created,
'avatar' => $author->avatar(),
'bbBody' => $body,
'comment' => \Text::full_format($body),
'editedUserId' => $editedUserID,
'editedUsername' => $editedUsername,
'editedTime' => $editedTime
];
}
return $list;
}
public function total(): int {
return $this->total;
}
public function userId(): int {
if (!isset($this->userId)) {
$this->userId = (int)self::$db->scalar("
SELECT AuthorID FROM comments WHERE ID = ?
", $this->id
);
}
return $this->userId;
}
/**
* Load a page of comments
*/
public function load(): static {
$page = $this->page();
$pageId = $this->pageId;
// Get the total number of comments
$key = sprintf(self::PAGE_TOTAL, $page, $pageId);
$total = self::$cache->get_value($key);
if ($total === false) {
$total = (int)self::$db->scalar("
SELECT count(*) FROM comments WHERE Page = ? AND PageID = ?
", $page, $pageId
);
self::$cache->cache_value($key, $total, 0);
}
$this->total = $total;
if (!$this->pageNum) {
// default to final page, or page where specified post is found
if (!$this->id) {
$this->pageNum = $this->total ? (int)ceil($this->total / TORRENT_COMMENTS_PER_PAGE) : 1;
} else {
// If someone clips the post id when pasting and it represents
// a post created prior to the creation of the page, fall back
// to page 1 (instead of page 0 which does not exist).
$this->pageNum = (int)self::$db->scalar("
SELECT greatest(1, ceil(count(*) / ?))
FROM comments
WHERE Page = ?
AND PageID = ?
AND ID <= ?
", TORRENT_COMMENTS_PER_PAGE, $page, $pageId, $this->id
);
}
}
// Cache catalogue from which the page is selected
$CatalogueID = (int)floor(TORRENT_COMMENTS_PER_PAGE * ($this->pageNum - 1) / THREAD_CATALOGUE);
$catKey = sprintf(\Gazelle\Manager\Comment::CATALOG, $page, $pageId, $CatalogueID);
$Catalogue = self::$cache->get_value($catKey);
if ($Catalogue === false) {
self::$db->prepared_query("
SELECT c.ID,
c.AuthorID,
c.AddedTime,
c.Body,
c.EditedUserID,
c.EditedTime,
u.Username,
a.Username AS author_name
FROM comments AS c
LEFT JOIN users_main AS a ON (a.ID = c.AuthorID)
LEFT JOIN users_main AS u ON (u.ID = c.EditedUserID)
WHERE c.Page = ? AND c.PageID = ?
ORDER BY c.ID
LIMIT ? OFFSET ?
", $page, $pageId, THREAD_CATALOGUE, THREAD_CATALOGUE * $CatalogueID
);
$Catalogue = self::$db->to_array(false, MYSQLI_ASSOC);
self::$cache->cache_value($catKey, $Catalogue, 0);
}
//This is a hybrid to reduce the catalogue down to the page elements: We use the page limit % catalogue
$this->thread = array_slice($Catalogue,
(TORRENT_COMMENTS_PER_PAGE * ($this->pageNum - 1)) % THREAD_CATALOGUE, TORRENT_COMMENTS_PER_PAGE, true
);
return $this;
}
public function handleSubscription(\Gazelle\User $user): int {
if (empty($this->thread)) {
return 0;
}
$lastPost = end($this->thread)['ID'];
$page = $this->page();
$pageId = $this->pageId;
$userId = $user->id;
// quote notifications
self::$db->begin_transaction();
self::$db->prepared_query("
UPDATE users_notify_quoted SET
UnRead = false
WHERE Page = ?
AND PageID = ?
AND PostID BETWEEN ? AND ?
AND UserID = ?
", $page, $pageId, current($this->thread)['ID'], $lastPost, $userId
);
$affected = self::$db->affected_rows();
if ($affected) {
new \Gazelle\User\Quote($user)->flush();
}
// last read
$this->lastRead = (int)self::$db->scalar("
SELECT PostID
FROM users_comments_last_read
WHERE Page = ?
AND PageID = ?
AND UserID = ?
", $page, $pageId, $userId
);
if ($this->lastRead < $lastPost) {
self::$db->prepared_query("
INSERT INTO users_comments_last_read
(UserID, Page, PageID, PostID)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
PostID = ?
", $userId, $page, $pageId, $lastPost, $lastPost
);
self::$cache->delete_value("subscriptions_user_new_$userId");
}
self::$db->commit();
return $affected;
}
/**
* Modify a comment (saving the previous revision)
*/
public function modify(): bool {
$body = self::$db->scalar("
SELECT Body FROM comments WHERE ID = ?
", $this->id
);
if (is_null($body)) {
return false;
}
self::$db->begin_transaction();
$page = $this->page();
self::$db->prepared_query("
INSERT INTO comments_edits
(Page, PostID, Body, EditUser)
VALUES (?, ?, ?, ?)
", $page, $this->id, $body, $this->field('EditedUserID')
);
$success = parent::modify();
if (!$success) {
self::$db->rollback();
return false;
}
self::$db->commit();
$commentPage = (int)self::$db->scalar("
SELECT ceil(count(*) / ?) AS Page
FROM comments
WHERE Page = ?
AND PageID = ?
AND ID <= ?
", TORRENT_COMMENTS_PER_PAGE, $page, $this->pageId, $this->id
);
// Update the cache
self::$cache->delete_multi([
"edit_{$page}_" . $this->id,
"{$page}_comments_" . $this->pageId,
sprintf(\Gazelle\Manager\Comment::CATALOG, $page, $this->pageId,
(int)floor((($commentPage - 1) * TORRENT_COMMENTS_PER_PAGE) / THREAD_CATALOGUE)
),
]);
if ($page == 'collages') {
// On collages, we also need to clear the collage key (collage_$CollageID), because it has the comments in it... (why??)
self::$cache->delete_value(sprintf(\Gazelle\Collage::CACHE_KEY, $this->pageId));
}
return true;
}
public function remove(): int {
$page = $this->page();
[$commentPages, $commentPage] = self::$db->row("
SELECT
ceil(count(*) / ?) AS Pages,
ceil(sum(if(ID <= ?, 1, 0)) / ?) AS Page
FROM comments
WHERE Page = ? AND PageID = ?
GROUP BY PageID
", TORRENT_COMMENTS_PER_PAGE, $this->id, TORRENT_COMMENTS_PER_PAGE, $page, $this->pageId
);
if (is_null($commentPages)) {
return 0;
}
self::$db->begin_transaction();
self::$db->prepared_query("
DELETE FROM comments WHERE ID = ?
", $this->id
);
$affected = self::$db->affected_rows();
self::$db->prepared_query("
DELETE FROM comments_edits WHERE Page = ? AND PostID = ?
", $page, $this->id
);
$affected += self::$db->affected_rows();
self::$db->prepared_query("
DELETE FROM users_notify_quoted WHERE Page = ? AND PostID = ?
", $page, $this->id
);
$affected += self::$db->affected_rows();
self::$db->commit();
new \Gazelle\Manager\Subscription()->flushPage($page, $this->pageId);
self::$cache->delete_multi([
"edit_{$page}_" . $this->id,
"{$page}_comments_" . $this->pageId,
]);
// We need to clear all subsequential catalogues as they've all been bumped with the absence of this post
$current = floor((TORRENT_COMMENTS_PER_PAGE * $commentPage - TORRENT_COMMENTS_PER_PAGE) / THREAD_CATALOGUE);
$last = floor((TORRENT_COMMENTS_PER_PAGE * $commentPages - TORRENT_COMMENTS_PER_PAGE) / THREAD_CATALOGUE);
for ($i = $current; $i <= $last; ++$i) {
self::$cache->delete_value(sprintf(\Gazelle\Manager\Comment::CATALOG, $page, $this->pageId, $i));
}
if ($page === 'collages') {
// On collages, we also need to clear the collage key (collage_$CollageID), because it has the comments in it... (why??)
self::$cache->delete_value(sprintf(\Gazelle\Collage::CACHE_KEY, $this->id));
}
return $affected;
}
}