WebOrbiton
v2.0.0.1

Publisium

202 lines · 10.5 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/includes/config.php';
  6. require_once __DIR__ . '/includes/database.php';
  7. require_once __DIR__ . '/includes/auth.php';
  8. require_once __DIR__ . '/includes/csrf.php';
  9. require_once __DIR__ . '/includes/activity-log.php';
  10. require_once __DIR__ . '/includes/site-front.php';
  11. require_once __DIR__ . '/includes/site-comments.php';
  12. ​
  13. Auth::boot();
  14. Auth::requireRoleAtLeast(Auth::ROLE_WRITER);
  15. ​
  16. $currentUser = Auth::user();
  17. $currentRole = Auth::role();
  18. $canModerate = Auth::canModerate();
  19. $db = Database::site();
  20. ​
  21. $flashMessage = null;
  22. $flashType = 'success';
  23. ​
  24. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  25. if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
  26. $flashMessage = 'Your session expired. Please try again.';
  27. $flashType = 'error';
  28. } else {
  29. require __DIR__ . '/dashboard-actions/manage-comments.php';
  30. }
  31. }
  32. ​
  33. $statusFilters = $canModerate ? ['pending' => 'Waiting', 'approved' => 'Published', 'all' => 'All'] : ['approved' => 'Published'];
  34. $defaultFilter = $canModerate ? 'pending' : 'approved';
  35. $statusFilter = array_key_exists((string) ($_GET['status'] ?? ''), $statusFilters) ? (string) $_GET['status'] : $defaultFilter;
  36. $perPage = 50;
  37. $currentPage = max(1, (int) ($_GET['page'] ?? 1));
  38. ​
  39. $ownArticlesSql = $canModerate ? '' : ' AND a.author_id = ' . (int) $currentUser['id'];
  40. ​
  41. $counts = ['pending' => 0, 'approved' => 0, 'all' => 0];
  42. foreach ($db->query('SELECT c.status, COUNT(*) AS total FROM article_comments c LEFT JOIN articles a ON a.id = c.article_id WHERE 1 = 1' . $ownArticlesSql . ' GROUP BY c.status') as $countRow) {
  43. if (isset($counts[$countRow['status']])) {
  44. $counts[$countRow['status']] = (int) $countRow['total'];
  45. }
  46. $counts['all'] += (int) $countRow['total'];
  47. }
  48. ​
  49. $totalPages = max(1, (int) ceil($counts[$statusFilter] / $perPage));
  50. $currentPage = min($currentPage, $totalPages);
  51. $where = 'WHERE 1 = 1' . $ownArticlesSql . ($statusFilter === 'all' ? '' : ' AND c.status = :status');
  52. $statement = $db->prepare(
  53. 'SELECT c.*, a.title AS article_title, a.slug AS article_slug, a.status AS article_status
  54. FROM article_comments c
  55. LEFT JOIN articles a ON a.id = c.article_id
  56. ' . $where . '
  57. ORDER BY c.created_at DESC, c.id DESC
  58. LIMIT ' . $perPage . ' OFFSET ' . (($currentPage - 1) * $perPage)
  59. );
  60. $statement->execute($statusFilter === 'all' ? [] : ['status' => $statusFilter]);
  61. $comments = $statement->fetchAll();
  62. ​
  63. $dashActivePage = 'comments';
  64. $dashPageTitle = 'Comments';
  65. ​
  66. require __DIR__ . '/includes/dash-header.php';
  67. ​
  68. ?>
  69. ​
  70. <?php if ($flashMessage !== null): ?>
  71. <div class="dash-flash dash-flash-<?= htmlspecialchars($flashType) ?>"><?= Icons::icon($flashType === 'error' ? 'x' : 'check', 'icon icon-sm') ?><?= htmlspecialchars($flashMessage) ?></div>
  72. <?php endif; ?>
  73. ​
  74. <div class="dash-header-row">
  75. <h1 class="dash-title"><?= Icons::icon('message', 'icon icon-lg') ?>Comments</h1>
  76. <?php if ($canModerate && $statusFilter === 'pending' && $counts['pending'] > 0): ?>
  77. <form method="post" onsubmit="return confirm('Delete all <?= (int) $counts['pending'] ?> waiting comments? This can’t be undone.');">
  78. <?= Csrf::field() ?>
  79. <input type="hidden" name="action" value="delete_pending_comments">
  80. <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete all waiting</button>
  81. </form>
  82. <?php endif; ?>
  83. </div>
  84. ​
  85. <?php if (!SiteComments::isActive()): ?>
  86. <p class="comments-note">Publisium comments are not the selected comment service right now, so nothing new comes in and published comments aren’t shown on the site. You can switch in Settings → Comments.</p>
  87. <?php endif; ?>
  88. ​
  89. <nav class="comments-filter">
  90. <?php foreach ($statusFilters as $filterKey => $filterLabel): ?>
  91. <a href="comments.php?status=<?= $filterKey ?>" class="dash-btn<?= $statusFilter === $filterKey ? ' dash-btn-primary' : '' ?>"><?= htmlspecialchars($filterLabel) ?> (<?= $counts[$filterKey] ?>)</a>
  92. <?php endforeach; ?>
  93. </nav>
  94. ​
  95. <table class="dash-table comments-table">
  96. <thead>
  97. <tr>
  98. <th><?= Icons::icon('message', 'icon icon-sm') ?>Comment</th>
  99. <th><?= Icons::icon('user', 'icon icon-sm') ?>Author</th>
  100. <th><?= Icons::icon('articles', 'icon icon-sm') ?>Article</th>
  101. <th><?= Icons::icon('calendar', 'icon icon-sm') ?>Date</th>
  102. <th><?= Icons::icon('eye', 'icon icon-sm') ?>Status</th>
  103. <th></th>
  104. </tr>
  105. </thead>
  106. <tbody>
  107. <?php foreach ($comments as $comment): ?>
  108. <?php $commentId = (int) $comment['id']; ?>
  109. <tr>
  110. <td class="comments-table-body">
  111. <?php if ($comment['parent_id'] !== null): ?><span class="status-pill status-draft">Reply</span><?php endif; ?>
  112. <?= SiteComments::formatBody((string) $comment['body']) ?>
  113. <?php if ($comment['status'] === 'approved' && $comment['article_status'] === 'published'): ?>
  114. <details class="comments-reply">
  115. <summary class="dash-btn-small"><?= Icons::icon('message', 'icon icon-sm') ?>Reply</summary>
  116. <form method="post">
  117. <?= Csrf::field() ?>
  118. <input type="hidden" name="action" value="reply_comment">
  119. <input type="hidden" name="comment_id" value="<?= $commentId ?>">
  120. <textarea name="reply_body" rows="3" required maxlength="<?= SiteComments::MAX_BODY ?>" placeholder="Your reply, shown with your name and a team badge"></textarea>
  121. <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Publish reply</button>
  122. </form>
  123. </details>
  124. <?php endif; ?>
  125. </td>
  126. <td>
  127. <?= htmlspecialchars((string) $comment['author_name']) ?>
  128. <span class="status-pill status-draft"><?= !empty($comment['team_account_id']) ? 'Team' : ($comment['user_account_id'] !== null ? 'Reader' : 'Guest') ?></span>
  129. <?php if ($canModerate && !empty($comment['author_email'])): ?>
  130. <div class="comments-email"><a href="mailto:<?= htmlspecialchars((string) $comment['author_email'], ENT_QUOTES) ?>"><?= htmlspecialchars((string) $comment['author_email']) ?></a></div>
  131. <?php endif; ?>
  132. </td>
  133. <td>
  134. <?php if ($comment['article_title'] === null): ?>
  135. <em>Deleted article</em>
  136. <?php elseif ($comment['article_status'] === 'published'): ?>
  137. <a href="<?= htmlspecialchars(SiteFront::articleUrl((string) $comment['article_slug'], '') . '#comment-' . $commentId, ENT_QUOTES) ?>" target="_blank" rel="noopener"><?= htmlspecialchars((string) $comment['article_title']) ?></a>
  138. <?php else: ?>
  139. <?= htmlspecialchars((string) $comment['article_title']) ?>
  140. <?php endif; ?>
  141. </td>
  142. <td><?= htmlspecialchars(date('M j, Y H:i', strtotime((string) $comment['created_at']) ?: time())) ?></td>
  143. <td>
  144. <?php if ($comment['status'] === 'approved'): ?>
  145. <span class="status-pill status-published">Published</span>
  146. <?php else: ?>
  147. <span class="status-pill status-pending_review">Waiting</span>
  148. <?php endif; ?>
  149. </td>
  150. <td class="dash-table-actions">
  151. <?php if ($canModerate): ?>
  152. <form method="post" style="display:inline;">
  153. <?= Csrf::field() ?>
  154. <input type="hidden" name="comment_id" value="<?= $commentId ?>">
  155. <?php if ($comment['status'] === 'approved'): ?>
  156. <button type="submit" name="action" value="hide_comment" class="dash-btn-small"><?= Icons::icon('eye', 'icon icon-sm') ?>Hide</button>
  157. <?php else: ?>
  158. <button type="submit" name="action" value="approve_comment" class="dash-btn-small"><?= Icons::icon('check', 'icon icon-sm') ?>Approve</button>
  159. <?php endif; ?>
  160. </form>
  161. <form method="post" style="display:inline;" onsubmit="return confirm('Delete this comment? Replies to it are deleted too.');">
  162. <?= Csrf::field() ?>
  163. <input type="hidden" name="action" value="delete_comment">
  164. <input type="hidden" name="comment_id" value="<?= $commentId ?>">
  165. <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete</button>
  166. </form>
  167. <?php endif; ?>
  168. </td>
  169. </tr>
  170. <?php endforeach; ?>
  171. <?php if (empty($comments)): ?>
  172. <tr>
  173. <td colspan="6"><?= $statusFilter === 'pending' ? 'Nothing is waiting for approval.' : ($canModerate ? 'No comments here yet.' : 'No comments on your articles yet.') ?></td>
  174. </tr>
  175. <?php endif; ?>
  176. </tbody>
  177. </table>
  178. ​
  179. <?php if ($totalPages > 1): ?>
  180. <nav class="comments-filter">
  181. <?php for ($pageNumber = 1; $pageNumber <= $totalPages; $pageNumber++): ?>
  182. <a href="comments.php?status=<?= $statusFilter ?>&amp;page=<?= $pageNumber ?>" class="dash-btn<?= $pageNumber === $currentPage ? ' dash-btn-primary' : '' ?>"><?= $pageNumber ?></a>
  183. <?php endfor; ?>
  184. </nav>
  185. <?php endif; ?>
  186. ​
  187. <style>
  188. .comments-filter { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
  189. .comments-note { color: var(--muted); font-size: 13px; }
  190. .comments-table-body { max-width: 420px; overflow-wrap: anywhere; white-space: normal; }
  191. .comments-table-body .status-pill { margin-right: 6px; }
  192. .comments-email { margin-top: 4px; font-size: 12px; overflow-wrap: anywhere; }
  193. .comments-reply { margin-top: 10px; }
  194. .comments-reply summary { display: inline-flex; cursor: pointer; list-style: none; }
  195. .comments-reply summary::-webkit-details-marker { display: none; }
  196. .comments-reply form { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
  197. .comments-reply textarea { width: 100%; }
  198. .comments-reply button { align-self: flex-start; }
  199. </style>
  200. ​
  201. <?php require __DIR__ . '/includes/dash-footer.php'; ?>
  202. ​