set( "module_comments", array( "notify_site_contact" => false, "notify_post_author" => false, "default_comment_status" => Comment::STATUS_DENIED, "allowed_comment_html" => array( "strong", "em", "blockquote", "code", "pre", "a" ), "comments_per_page" => 25, "enable_reload_comments" => false, "auto_reload_comments" => 30 ) ); Group::add_permission("add_comment", "Add Comments"); Group::add_permission("add_comment_private", "Add Comments to Private Posts"); Group::add_permission("edit_comment", "Edit Comments"); Group::add_permission("edit_own_comment", "Edit Own Comments"); Group::add_permission("delete_comment", "Delete Comments"); Group::add_permission("delete_own_comment", "Delete Own Comments"); Group::add_permission("code_in_comments", "Can Use HTML in Comments"); Route::current()->add("comment/(id)/", "comment"); } public static function __uninstall($confirm): void { if ($confirm) Comment::uninstall(); Config::current()->remove("module_comments"); Group::remove_permission("add_comment"); Group::remove_permission("add_comment_private"); Group::remove_permission("edit_comment"); Group::remove_permission("edit_own_comment"); Group::remove_permission("delete_comment"); Group::remove_permission("delete_own_comment"); Group::remove_permission("code_in_comments"); Route::current()->remove("comment/(id)/"); } public function user_logged_in($user): void { $_SESSION['comments'] = array(); } public function user($user): void { $user->has_many[] = "comments"; } public function post($post): void { $post->has_many[] = "comments"; } public function list_permissions($names = array()): array { $names["add_comment"] = __("Add Comments", "comments"); $names["add_comment_private"] = __("Add Comments to Private Posts", "comments"); $names["edit_comment"] = __("Edit Comments", "comments"); $names["edit_own_comment"] = __("Edit Own Comments", "comments"); $names["delete_comment"] = __("Delete Comments", "comments"); $names["delete_own_comment"] = __("Delete Own Comments", "comments"); $names["code_in_comments"] = __("Can Use HTML in Comments", "comments"); return $names; } public function main_comment($main): bool { if (empty($_GET['id']) or !is_numeric($_GET['id'])) Flash::warning( __("Please enter an ID to find a comment.", "comments"), "/" ); $comment = new Comment($_GET['id']); if ($comment->no_results) return false; if ($comment->post->no_results) return false; redirect($comment->post->url()."#comment_".$comment->id); } public function main_most_comments($main): void { $posts = Post::find(array("placeholders" => true)); usort($posts[0], function ($a, $b) { $count_a = $this->get_post_comment_count($a["id"]); $count_b = $this->get_post_comment_count($b["id"]); if ($count_a == $count_b) return 0; return ($count_a > $count_b) ? -1 : 1 ; }); $main->display( array("pages".DIR."most_comments", "pages".DIR."index"), array("posts" => new Paginator($posts, $main->post_limit)), __("Most commented on posts", "comments") ); } public function parse_urls($urls): array { $urls['|/comment/([0-9]+)/|'] = '/?action=comment&id=$1'; return $urls; } private function add_comment(): array { if (!isset($_POST['hash']) or !Session::check_token($_POST['hash'])) show_403( __("Access Denied"), __("Invalid authentication token.") ); if (empty($_POST['post_id']) or !is_numeric($_POST['post_id'])) error( __("No ID Specified"), __("An ID is required to add a comment.", "comments"), code:400 ); $post = new Post( $_POST['post_id'], array("drafts" => true) ); if ($post->no_results) show_404(__("Not Found"), __("Post not found.")); if (!Comment::creatable($post)) show_403( __("Access Denied"), __("You cannot comment on this post.", "comments") ); if (empty($_POST['body'])) return array( false, __("Message can't be blank.", "comments") ); if (empty($_POST['author']) or derezz($_POST['author'])) return array( false, __("Author can't be blank.", "comments") ); if (empty($_POST['author_email'])) return array( false, __("Email address can't be blank.", "comments") ); if (!is_email($_POST['author_email'])) return array( false, __("Invalid email address.", "comments") ); if (!empty($_POST['author_url']) and !is_url($_POST['author_url'])) return array( false, __("Invalid website URL.", "comments") ); if (!empty($_POST['author_url'])) $_POST['author_url'] = add_scheme($_POST['author_url']); if (!logged_in() and !check_captcha()) return array( false, __("Incorrect captcha response.", "comments") ); fallback($_POST['author_url'], ""); $parent = (int) oneof($_POST['parent_id'], 0); $notify = (!empty($_POST['notify']) and logged_in()); $comment = Comment::create( body:$_POST['body'], author:$_POST['author'], author_url:$_POST['author_url'], author_email:$_POST['author_email'], post:$post, parent:$parent, notify:$notify ); return array( true, ($comment->status == Comment::STATUS_APPROVED) ? __("Comment added.", "comments") : __("Your comment is awaiting moderation.", "comments") ); } private function update_comment(): array { if (!isset($_POST['hash']) or !Session::check_token($_POST['hash'])) show_403( __("Access Denied"), __("Invalid authentication token.") ); if (empty($_POST['id']) or !is_numeric($_POST['id'])) error( __("No ID Specified"), __("An ID is required to update a comment.", "comments"), code:400 ); $comment = new Comment($_POST['id']); if ($comment->no_results) show_404( __("Not Found"), __("Comment not found.", "comments") ); if (!$comment->editable()) show_403( __("Access Denied"), __("You do not have sufficient privileges to edit this comment.", "comments") ); fallback($_POST['created_at']); fallback($_POST['status'], $comment->status); fallback($_POST['author_email'], $comment->author_email); fallback($_POST['author_url'], $comment->author_url); if (empty($_POST['body'])) return array( false, __("Message can't be blank.", "comments") ); if (empty($_POST['author']) or derezz($_POST['author'])) return array( false, __("Author can't be blank.", "comments") ); if (empty($_POST['author_email']) and $_POST['status'] != Comment::STATUS_PINGBACK) return array( false, __("Email address can't be blank.", "comments") ); if (!empty($_POST['author_email']) and !is_email($_POST['author_email'])) return array( false, __("Invalid email address.", "comments") ); if (!empty($_POST['author_url']) and !is_url($_POST['author_url'])) return array( false, __("Invalid website URL.", "comments") ); if (!empty($_POST['author_url'])) $_POST['author_url'] = add_scheme($_POST['author_url']); $notify = (!empty($_POST['notify']) and logged_in()); $can_edit_comment = Visitor::current()->group->can("edit_comment"); $status = ($can_edit_comment) ? $_POST['status'] : $comment->status ; $created_at = ($can_edit_comment) ? datetime($_POST['created_at']) : $comment->created_at ; $comment = $comment->update( body:$_POST['body'], author:$_POST['author'], author_url:$_POST['author_url'], author_email:$_POST['author_email'], status:$status, notify:$notify, created_at:$created_at ); return array( true, __("Comment updated.", "comments") ); } public function admin_update_comment()/*: never */{ list($success, $message) = $this->update_comment(); if (!$success) error( __("Error"), $message, code:422 ); Flash::notice( $message, "manage_comments" ); } public function ajax_add_comment(): void { list($success, $message) = $this->add_comment(); json_response($message, $success); } public function ajax_update_comment(): void { list($success, $message) = $this->update_comment(); json_response($message, $success); } public function admin_edit_comment($admin): void { if (empty($_GET['id']) or !is_numeric($_GET['id'])) error( __("No ID Specified"), __("An ID is required to edit a comment.", "comments"), code:400 ); $comment = new Comment( $_GET['id'], array("filter" => false) ); if ($comment->no_results) Flash::warning( __("Comment not found.", "comments"), "manage_comments" ); if (!$comment->editable()) show_403( __("Access Denied"), __("You do not have sufficient privileges to edit this comment.", "comments") ); $admin->display( "pages".DIR."edit_comment", array("comment" => $comment) ); } public function admin_delete_comment($admin): void { if (empty($_GET['id']) or !is_numeric($_GET['id'])) error( __("No ID Specified"), __("An ID is required to delete a comment.", "comments"), code:400 ); $comment = new Comment($_GET['id']); if ($comment->no_results) Flash::warning( __("Comment not found.", "comments"), "manage_comments" ); if (!$comment->deletable()) show_403( __("Access Denied"), __("You do not have sufficient privileges to delete this comment.", "comments") ); $admin->display( "pages".DIR."delete_comment", array("comment" => $comment) ); } public function admin_destroy_comment()/*: never */{ if (!isset($_POST['hash']) or !Session::check_token($_POST['hash'])) show_403( __("Access Denied"), __("Invalid authentication token.") ); if (empty($_POST['id']) or !is_numeric($_POST['id'])) error( __("No ID Specified"), __("An ID is required to delete a comment.", "comments"), code:400 ); if (!isset($_POST['destroy']) or $_POST['destroy'] != "indubitably") redirect("manage_comments"); $comment = new Comment($_POST['id']); if ($comment->no_results) show_404( __("Not Found"), __("Comment not found.", "comments") ); if (!$comment->deletable()) show_403( __("Access Denied"), __("You do not have sufficient privileges to delete this comment.", "comments") ); Comment::delete($comment->id); $redirect = ($comment->status == Comment::STATUS_SPAM) ? "manage_spam" : "manage_comments" ; Flash::notice(__("Comment deleted.", "comments"), $redirect); } public function admin_manage_comments($admin): void { if (!Comment::any_editable() and !Comment::any_deletable()) show_403( __("Access Denied"), __("You do not have sufficient privileges to manage any comments.", "comments") ); # Redirect searches to a clean URL or dirty GET depending on configuration. if (isset($_POST['query'])) redirect( "manage_comments/query/". str_ireplace("%2F", "", urlencode($_POST['query'])). "/" ); fallback($_GET['query'], ""); list($where, $params, $order) = keywords( $_GET['query'], "body LIKE :query", "comments" ); $where[] = "status != '".Comment::STATUS_SPAM."'"; fallback($order, "post_id DESC, created_at ASC"); $visitor = Visitor::current(); if (!$visitor->group->can("edit_comment", "delete_comment", true)) $where["user_id"] = $visitor->id; $admin->display( "pages".DIR."manage_comments", array( "comments" => new Paginator( Comment::find( array( "placeholders" => true, "where" => $where, "params" => $params, "order" => $order ) ), $admin->post_limit ) ) ); } public function admin_manage_spam($admin): void { if (!Visitor::current()->group->can("edit_comment", "delete_comment", true)) show_403( __("Access Denied"), __("You do not have sufficient privileges to manage any comments.", "comments") ); # Redirect searches to a clean URL or dirty GET depending on configuration. if (isset($_POST['query'])) redirect( "manage_spam/query/". str_ireplace("%2F", "", urlencode($_POST['query'])). "/" ); fallback($_GET['query'], ""); list($where, $params, $order) = keywords( $_GET['query'], "body LIKE :query", "comments" ); $where[] = "status = '".Comment::STATUS_SPAM."'"; fallback($order, "post_id DESC, created_at ASC"); $admin->display( "pages".DIR."manage_spam", array( "comments" => new Paginator( Comment::find( array( "placeholders" => true, "where" => $where, "params" => $params, "order" => $order ) ), $admin->post_limit ) ) ); } public function admin_bulk_comments()/*: never */{ if (!isset($_POST['hash']) or !Session::check_token($_POST['hash'])) show_403( __("Access Denied"), __("Invalid authentication token.") ); if (!isset($_POST['comment'])) Flash::warning( __("No comments selected."), "manage_comments" ); $trigger = Trigger::current(); $false_positives = array(); $false_negatives = array(); $comments = array_keys($_POST['comment']); switch (fallback($_POST['task'])) { case "delete": $count_delete = 0; foreach ($comments as $comment) { $comment = new Comment( $comment, array("filter" => false) ); if (!$comment->deletable()) continue; Comment::delete($comment->id); $count_delete++; } if (!empty($count_delete)) Flash::notice( __("Selected comments deleted.", "comments") ); break; case "deny": $count_deny = 0; foreach ($comments as $comment) { $comment = new Comment( $comment, array("filter" => false) ); if (!$comment->editable()) continue; if ($comment->status == Comment::STATUS_PINGBACK) continue; if ($comment->status == Comment::STATUS_SPAM) $false_positives[] = $comment; $comment->update( body:$comment->body, author:$comment->author, author_url:$comment->author_url, author_email:$comment->author_email, status:Comment::STATUS_DENIED ); $count_deny++; } if (!empty($count_deny)) Flash::notice( __("Selected comments denied.", "comments") ); break; case "approve": $count_approve = 0; foreach ($comments as $comment) { $comment = new Comment( $comment, array("filter" => false) ); if (!$comment->editable()) continue; if ($comment->status == Comment::STATUS_PINGBACK) continue; if ($comment->status == Comment::STATUS_SPAM) $false_positives[] = $comment; $comment->update( body:$comment->body, author:$comment->author, author_url:$comment->author_url, author_email:$comment->author_email, status:Comment::STATUS_APPROVED ); $count_approve++; } if (!empty($count_approve)) Flash::notice( __("Selected comments approved.", "comments") ); break; case "spam": $count_spam = 0; foreach ($comments as $comment) { $comment = new Comment( $comment, array("filter" => false) ); if (!$comment->editable()) continue; if ($comment->status == Comment::STATUS_PINGBACK) continue; $comment->update( body:$comment->body, author:$comment->author, author_url:$comment->author_url, author_email:$comment->author_email, status:Comment::STATUS_SPAM ); $count_spam++; $false_negatives[] = $comment; } if (!empty($count_spam)) Flash::notice( __("Selected comments marked as spam.", "comments") ); break; } if (!empty($false_positives)) $trigger->call("comments_false_positives", $false_positives); if (!empty($false_negatives)) $trigger->call("comments_false_negatives", $false_negatives); redirect("manage_comments"); } public function admin_comment_settings($admin): void { if (!Visitor::current()->group->can("change_settings")) show_403( __("Access Denied"), __("You do not have sufficient privileges to change settings.") ); $config = Config::current(); $comments_html = implode( ", ", $config->module_comments["allowed_comment_html"] ); $comments_status = array( Comment::STATUS_APPROVED => __("Approved", "comments"), Comment::STATUS_DENIED => __("Denied", "comments"), Comment::STATUS_SPAM => __("Spam", "comments") ); if (empty($_POST)) { $admin->display( "pages".DIR."comment_settings", array( "comments_html" => $comments_html, "comments_status" => $comments_status ) ); return; } if (!isset($_POST['hash']) or !Session::check_token($_POST['hash'])) show_403( __("Access Denied"), __("Invalid authentication token.") ); fallback($_POST['default_comment_status'], Comment::STATUS_DENIED); fallback($_POST['allowed_comment_html'], ""); fallback($_POST['comments_per_page'], 25); fallback($_POST['auto_reload_comments'], 30); # Split at the comma. $allowed_comment_html = explode(",", $_POST['allowed_comment_html']); # Remove whitespace. $allowed_comment_html = array_map("trim", $allowed_comment_html); # Remove duplicates. $allowed_comment_html = array_unique($allowed_comment_html); # Remove empties. $allowed_comment_html = array_diff($allowed_comment_html, array("")); $config = Config::current(); $config->set( "module_comments", array( "notify_site_contact" => isset($_POST['notify_site_contact']), "notify_post_author" => isset($_POST['notify_post_author']), "default_comment_status" => $_POST['default_comment_status'], "allowed_comment_html" => $allowed_comment_html, "comments_per_page" => abs((int) $_POST['comments_per_page']), "enable_reload_comments" => isset($_POST['enable_reload_comments']), "auto_reload_comments" => (int) $_POST['auto_reload_comments'] ) ); Flash::notice( __("Settings updated."), "comment_settings" ); } public function admin_determine_action($action): ?string { if ( $action == "manage" and (Comment::any_editable() or Comment::any_deletable()) ) return "manage_comments"; return null; } public function settings_nav($navs): array { if (Visitor::current()->group->can("change_settings")) $navs["comment_settings"] = array( "title" => __("Comments", "comments") ); return $navs; } public function manage_nav($navs): array { if (!Comment::any_editable() and !Comment::any_deletable()) return $navs; $sql = SQL::current(); $comment_count = $sql->count( "comments", array("status not" => Comment::STATUS_SPAM) ); $spam_count = $sql->count( "comments", array("status" => Comment::STATUS_SPAM) ); $navs["manage_comments"] = array( "title" => _f("Comments (%d)", $comment_count, "comments"), "selected" => array("edit_comment", "delete_comment") ); if (Visitor::current()->group->can("edit_comment", "delete_comment")) $navs["manage_spam"] = array( "title" => _f("Spam (%d)", $spam_count, "comments") ); return $navs; } public function manage_posts_column_header(): string { return '