WebOrbiton
v1.0.0.8

Publisium

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