651 lines
21 KiB
PHP
651 lines
21 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* Class: Comment
|
||
|
* The model for the Comments SQL table.
|
||
|
*
|
||
|
* See Also:
|
||
|
* <Model>
|
||
|
*/
|
||
|
class Comment extends Model {
|
||
|
const STATUS_APPROVED = "approved";
|
||
|
const STATUS_DENIED = "denied";
|
||
|
const STATUS_SPAM = "spam";
|
||
|
const STATUS_PINGBACK = "pingback";
|
||
|
const OPTION_OPEN = "open";
|
||
|
const OPTION_CLOSED = "closed";
|
||
|
const OPTION_PRIVATE = "private";
|
||
|
const OPTION_REG_ONLY = "registered_only";
|
||
|
|
||
|
public $belongs_to = array("post", "user", "parent" => array("model" => "comment"));
|
||
|
|
||
|
public $has_many = array("children" => array("model" => "comment", "by" => "parent"));
|
||
|
|
||
|
/**
|
||
|
* Function: __construct
|
||
|
*
|
||
|
* See Also:
|
||
|
* <Model::grab>
|
||
|
*/
|
||
|
public function __construct($comment_id, $options = array()) {
|
||
|
$skip_where = (
|
||
|
ADMIN or
|
||
|
(isset($options["skip_where"]) and $options["skip_where"])
|
||
|
);
|
||
|
|
||
|
if (!$skip_where)
|
||
|
$options["where"][] = self::redactions();
|
||
|
|
||
|
parent::grab($this, $comment_id, $options);
|
||
|
|
||
|
if ($this->no_results)
|
||
|
return;
|
||
|
|
||
|
$this->filtered = !isset($options["filter"]) or $options["filter"];
|
||
|
|
||
|
Trigger::current()->filter($this, "comment");
|
||
|
|
||
|
if ($this->filtered)
|
||
|
$this->filter();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: find
|
||
|
*
|
||
|
* See Also:
|
||
|
* <Model::search>
|
||
|
*/
|
||
|
public static function find(
|
||
|
$options = array(),
|
||
|
$options_for_object = array()
|
||
|
): array {
|
||
|
$skip_where = (
|
||
|
ADMIN or
|
||
|
(isset($options["skip_where"]) and $options["skip_where"])
|
||
|
);
|
||
|
|
||
|
if (!$skip_where)
|
||
|
$options["where"][] = self::redactions();
|
||
|
|
||
|
fallback($options["order"], "created_at ASC");
|
||
|
return parent::search(
|
||
|
self::class,
|
||
|
$options,
|
||
|
$options_for_object
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: create
|
||
|
* Attempts to create a new comment that will be attributed to the current visitor.
|
||
|
*
|
||
|
* Parameters:
|
||
|
* $body - The comment.
|
||
|
* $author - The name of the commenter.
|
||
|
* $author_url - The commenter's website.
|
||
|
* $author_email - The commenter's email.
|
||
|
* $post - The <Post> they're commenting on.
|
||
|
* $parent - The id of the <Comment> they're replying to.
|
||
|
* $notify - Send correspondence if additional comments are added?
|
||
|
* $status - A string describing the comment status (optional).
|
||
|
*/
|
||
|
public static function create(
|
||
|
$body,
|
||
|
$author,
|
||
|
$author_url,
|
||
|
$author_email,
|
||
|
$post,
|
||
|
$parent,
|
||
|
$notify,
|
||
|
$status = null
|
||
|
): self {
|
||
|
$config = Config::current();
|
||
|
$visitor = Visitor::current();
|
||
|
$trigger = Trigger::current();
|
||
|
$values = array(
|
||
|
"body" => $body,
|
||
|
"author" => $author,
|
||
|
"author_url" => $author_url,
|
||
|
"author_email" => $author_email
|
||
|
);
|
||
|
|
||
|
fallback($_SESSION['comments'], array());
|
||
|
fallback($status,
|
||
|
($post->user_id == $visitor->id) ?
|
||
|
self::STATUS_APPROVED :
|
||
|
$config->module_comments["default_comment_status"]
|
||
|
);
|
||
|
|
||
|
$spam = ($status == self::STATUS_SPAM);
|
||
|
$trigger->filter($spam, "comment_is_spam", $values);
|
||
|
|
||
|
$agent = "";
|
||
|
|
||
|
if (isset($_SERVER['HTTP_USER_AGENT']))
|
||
|
$agent = $_SERVER['HTTP_USER_AGENT'];
|
||
|
|
||
|
if (isset($_SERVER['HTTP_SEC_CH_UA']))
|
||
|
$agent = $_SERVER['HTTP_SEC_CH_UA'];
|
||
|
|
||
|
if ($spam)
|
||
|
$status = self::STATUS_SPAM;
|
||
|
|
||
|
if (!logged_in() or !$config->email_correspondence)
|
||
|
$notify = false;
|
||
|
|
||
|
$comment = self::add(
|
||
|
body:$body,
|
||
|
author:$author,
|
||
|
author_url:$author_url,
|
||
|
author_email:$author_email,
|
||
|
ip:crc24($_SERVER['REMOTE_ADDR']),
|
||
|
agent:$agent,
|
||
|
status:$status,
|
||
|
post_id:$post->id,
|
||
|
user_id:$visitor->id,
|
||
|
parent:$parent,
|
||
|
notify:$notify
|
||
|
);
|
||
|
|
||
|
$_SESSION['comments'][] = $comment->id;
|
||
|
return $comment;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: add
|
||
|
* Adds a comment to the database.
|
||
|
*
|
||
|
* Parameters:
|
||
|
* $body - The comment.
|
||
|
* $author - The name of the commenter.
|
||
|
* $author_url - The commenter's website.
|
||
|
* $author_email - The commenter's email.
|
||
|
* $ip - Hash value of the commenter's IP address.
|
||
|
* $agent - The commenter's user agent.
|
||
|
* $status - The new comment's status.
|
||
|
* $post_id - The ID of the <Post> they're commenting on.
|
||
|
* $user_id - The ID of the <User> this comment was made by.
|
||
|
* $parent - The <Comment> they're replying to.
|
||
|
* $notify - Notification on follow-up comments.
|
||
|
* $created_at - The new comment's @created_at@ timestamp.
|
||
|
* $updated_at - The new comment's @updated_at@ timestamp.
|
||
|
*
|
||
|
* Returns:
|
||
|
* The newly created <Comment>.
|
||
|
*
|
||
|
* See Also:
|
||
|
* <update>
|
||
|
*/
|
||
|
public static function add(
|
||
|
$body,
|
||
|
$author,
|
||
|
$author_url,
|
||
|
$author_email,
|
||
|
$ip,
|
||
|
$agent,
|
||
|
$status,
|
||
|
$post_id,
|
||
|
$user_id,
|
||
|
$parent = null,
|
||
|
$notify = null,
|
||
|
$created_at = null,
|
||
|
$updated_at = null
|
||
|
): self {
|
||
|
$sql = SQL::current();
|
||
|
$config = Config::current();
|
||
|
$trigger = Trigger::current();
|
||
|
|
||
|
$sql->insert(
|
||
|
table:"comments",
|
||
|
data:array(
|
||
|
"body" => $body,
|
||
|
"author" => strip_tags($author),
|
||
|
"author_url" => strip_tags($author_url),
|
||
|
"author_email" => strip_tags($author_email),
|
||
|
"author_ip" => $ip,
|
||
|
"author_agent" => $agent,
|
||
|
"status" => $status,
|
||
|
"post_id" => $post_id,
|
||
|
"user_id" => $user_id,
|
||
|
"parent_id" => fallback($parent, 0),
|
||
|
"notify" => fallback($notify, false),
|
||
|
"created_at" => fallback($created_at, datetime()),
|
||
|
"updated_at" => fallback($updated_at, SQL_DATETIME_ZERO)
|
||
|
)
|
||
|
);
|
||
|
|
||
|
$comment = new self(
|
||
|
$sql->latest("comments"),
|
||
|
array("skip_where" => true)
|
||
|
);
|
||
|
|
||
|
# Notify site contact, post author, and commenters of a new comment.
|
||
|
if (
|
||
|
$config->email_correspondence and
|
||
|
$comment->status != self::STATUS_PINGBACK
|
||
|
) {
|
||
|
$done = array($comment->author_email);
|
||
|
|
||
|
if ($config->module_comments["notify_site_contact"]) {
|
||
|
if (!in_array($config->email, $done)) {
|
||
|
Modules::$instances["comments"]::email_site_new_comment(
|
||
|
$comment
|
||
|
);
|
||
|
$done[] = $config->email;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($config->module_comments["notify_post_author"]) {
|
||
|
$post = new Post(
|
||
|
$comment->post_id,
|
||
|
array("skip_where" => true)
|
||
|
);
|
||
|
|
||
|
if (
|
||
|
!$post->no_results and
|
||
|
!$post->user->no_results and
|
||
|
!in_array($post->user->email, $done)
|
||
|
) {
|
||
|
Modules::$instances["comments"]::email_user_new_comment(
|
||
|
$comment,
|
||
|
$comment->post->user
|
||
|
);
|
||
|
$done[] = $comment->post->user->email;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($comment->status == self::STATUS_APPROVED) {
|
||
|
$peers = self::find(
|
||
|
array(
|
||
|
"skip_where" => true,
|
||
|
"where" => array(
|
||
|
"post_id" => $comment->post_id,
|
||
|
"user_id !=" => $comment->user_id,
|
||
|
"status" => self::STATUS_APPROVED,
|
||
|
"notify" => true
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
|
||
|
foreach ($peers as $peer) {
|
||
|
if (!in_array($peer->author_email, $done)) {
|
||
|
Modules::$instances["comments"]::email_peer_new_comment(
|
||
|
$comment,
|
||
|
$peer
|
||
|
);
|
||
|
$done[] = $peer->author_email;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$trigger->call("add_comment", $comment);
|
||
|
return $comment;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: update
|
||
|
* Updates a comment with the given attributes.
|
||
|
*
|
||
|
* Parameters:
|
||
|
* $body - The comment.
|
||
|
* $author - The name of the commenter.
|
||
|
* $author_url - The commenter's website.
|
||
|
* $author_email - The commenter's email.
|
||
|
* $status - The comment's status.
|
||
|
* $notify - Notification on follow-up comments.
|
||
|
* $created_at - New @created_at@ timestamp for the comment.
|
||
|
* $updated_at - New @updated_at@ timestamp for the comment.
|
||
|
*
|
||
|
* Returns:
|
||
|
* The updated <Comment>.
|
||
|
*/
|
||
|
public function update(
|
||
|
$body,
|
||
|
$author,
|
||
|
$author_url,
|
||
|
$author_email,
|
||
|
$status = null,
|
||
|
$notify = null,
|
||
|
$created_at = null,
|
||
|
$updated_at = null
|
||
|
): self|false {
|
||
|
if ($this->no_results)
|
||
|
return false;
|
||
|
|
||
|
if ($this->status == self::STATUS_PINGBACK)
|
||
|
$status = $this->status;
|
||
|
|
||
|
$sql = SQL::current();
|
||
|
$config = Config::current();
|
||
|
$trigger = Trigger::current();
|
||
|
|
||
|
$new_values = array(
|
||
|
"body" => $body,
|
||
|
"author" => strip_tags($author),
|
||
|
"author_url" => strip_tags($author_url),
|
||
|
"author_email" => strip_tags($author_email),
|
||
|
"status" => fallback($status, $this->status),
|
||
|
"notify" => fallback($notify, $this->notify),
|
||
|
"created_at" => fallback($created_at, $this->created_at),
|
||
|
"updated_at" => fallback($updated_at, datetime())
|
||
|
);
|
||
|
|
||
|
$sql->update(
|
||
|
table:"comments",
|
||
|
conds:array("id" => $this->id),
|
||
|
data:$new_values
|
||
|
);
|
||
|
|
||
|
$comment = new self(
|
||
|
null,
|
||
|
array(
|
||
|
"read_from" => array_merge(
|
||
|
$new_values,
|
||
|
array(
|
||
|
"id" => $this->id,
|
||
|
"author_ip" => $this->author_ip,
|
||
|
"author_agent" => $this->author_agent,
|
||
|
"post_id" => $this->post_id,
|
||
|
"user_id" => $this->user_id,
|
||
|
"parent_id" => $this->parent_id
|
||
|
)
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
|
||
|
# Notify commenters of a newly approved comment.
|
||
|
if (
|
||
|
$config->email_correspondence and
|
||
|
$this->status != self::STATUS_APPROVED and
|
||
|
$comment->status == self::STATUS_APPROVED
|
||
|
) {
|
||
|
$done = array($comment->author_email);
|
||
|
|
||
|
$peers = self::find(
|
||
|
array(
|
||
|
"skip_where" => true,
|
||
|
"where" => array(
|
||
|
"post_id" => $comment->post_id,
|
||
|
"user_id !=" => $comment->user_id,
|
||
|
"status" => self::STATUS_APPROVED,
|
||
|
"notify" => true
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
|
||
|
foreach ($peers as $peer) {
|
||
|
if (!in_array($peer->author_email, $done)) {
|
||
|
Modules::$instances["comments"]::email_peer_new_comment(
|
||
|
$comment,
|
||
|
$peer
|
||
|
);
|
||
|
$done[] = $peer->author_email;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$trigger->call("update_comment", $comment, $this);
|
||
|
return $comment;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: delete
|
||
|
* Deletes a comment from the database.
|
||
|
*
|
||
|
* See Also:
|
||
|
* <Model::destroy>
|
||
|
*/
|
||
|
public static function delete($comment_id): void {
|
||
|
parent::destroy(
|
||
|
self::class,
|
||
|
$comment_id,
|
||
|
array("skip_where" => true)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: editable
|
||
|
* Checks if the <User> can edit the comment.
|
||
|
*/
|
||
|
public function editable($user = null): bool {
|
||
|
if ($this->no_results)
|
||
|
return false;
|
||
|
|
||
|
fallback($user, Visitor::current());
|
||
|
return (
|
||
|
$user->group->can("edit_comment") or
|
||
|
(
|
||
|
logged_in() and
|
||
|
$user->group->can("edit_own_comment") and
|
||
|
$user->id == $this->user_id
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: deletable
|
||
|
* Checks if the <User> can delete the comment.
|
||
|
*/
|
||
|
public function deletable($user = null): bool {
|
||
|
if ($this->no_results)
|
||
|
return false;
|
||
|
|
||
|
fallback($user, Visitor::current());
|
||
|
return (
|
||
|
$user->group->can("delete_comment") or
|
||
|
(
|
||
|
logged_in() and
|
||
|
$user->group->can("delete_own_comment") and
|
||
|
$user->id == $this->user_id
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: any_editable
|
||
|
* Checks if the <Visitor> can edit any comments.
|
||
|
*/
|
||
|
public static function any_editable(): bool {
|
||
|
$visitor = Visitor::current();
|
||
|
|
||
|
# Can they edit comments?
|
||
|
if ($visitor->group->can("edit_comment"))
|
||
|
return true;
|
||
|
|
||
|
# Can they edit their own comments, and do they have any?
|
||
|
if (
|
||
|
$visitor->group->can("edit_own_comment") and
|
||
|
SQL::current()->count(
|
||
|
tables:"comments",
|
||
|
conds:array("user_id" => $visitor->id)
|
||
|
)
|
||
|
)
|
||
|
return true;
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: any_deletable
|
||
|
* Checks if the <Visitor> can delete any comments.
|
||
|
*/
|
||
|
public static function any_deletable(): bool {
|
||
|
$visitor = Visitor::current();
|
||
|
|
||
|
# Can they delete comments?
|
||
|
if ($visitor->group->can("delete_comment"))
|
||
|
return true;
|
||
|
|
||
|
# Can they delete their own comments, and do they have any?
|
||
|
if (
|
||
|
$visitor->group->can("delete_own_comment") and
|
||
|
SQL::current()->count(
|
||
|
tables:"comments",
|
||
|
conds:array("user_id" => $visitor->id)
|
||
|
)
|
||
|
)
|
||
|
return true;
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: creatable
|
||
|
* Checks if the <Visitor> can comment on a post.
|
||
|
*/
|
||
|
public static function creatable($post): bool {
|
||
|
$visitor = Visitor::current();
|
||
|
|
||
|
if (!$visitor->group->can("add_comment"))
|
||
|
return false;
|
||
|
|
||
|
# Assume allowed comments by default.
|
||
|
return (
|
||
|
empty($post->comment_status) or
|
||
|
!(
|
||
|
$post->comment_status == self::OPTION_CLOSED or
|
||
|
(
|
||
|
$post->comment_status == self::OPTION_REG_ONLY and
|
||
|
!logged_in()
|
||
|
) or
|
||
|
(
|
||
|
$post->comment_status == self::OPTION_PRIVATE and
|
||
|
!$visitor->group->can("add_comment_private")
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: redactions
|
||
|
* Returns a SQL query "chunk" that hides some comments from the <Visitor>.
|
||
|
*/
|
||
|
public static function redactions(): string {
|
||
|
$user_id = (int) Visitor::current()->id;
|
||
|
$id_list = "(0)";
|
||
|
|
||
|
if (!logged_in() and !empty($_SESSION['comments']))
|
||
|
$id_list = QueryBuilder::build_list(
|
||
|
SQL::current(),
|
||
|
$_SESSION['comments']
|
||
|
);
|
||
|
|
||
|
return "(".
|
||
|
"status != '".self::STATUS_SPAM."'".
|
||
|
" AND ".
|
||
|
"status != '".self::STATUS_DENIED."'".
|
||
|
")".
|
||
|
" OR ".
|
||
|
"(".
|
||
|
"(user_id != 0 AND user_id = ".$user_id.")".
|
||
|
" OR ".
|
||
|
"(id IN ".$id_list.")".
|
||
|
")";
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: url
|
||
|
* Returns a comment's URL.
|
||
|
*/
|
||
|
public function url(): string|false {
|
||
|
if ($this->no_results)
|
||
|
return false;
|
||
|
|
||
|
return url(
|
||
|
"comment/".$this->id, MainController::current()
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: author_link
|
||
|
* Returns the commenter's name enclosed in a hyperlink to their website.
|
||
|
*/
|
||
|
public function author_link(): string|false {
|
||
|
if ($this->no_results)
|
||
|
return false;
|
||
|
|
||
|
if (empty($this->author))
|
||
|
return __("Anon", "comments");
|
||
|
|
||
|
if (is_url($this->author_url))
|
||
|
return '<a href="'.
|
||
|
fix($this->author_url, true).
|
||
|
'">'.
|
||
|
$this->author.
|
||
|
'</a>';
|
||
|
else
|
||
|
return $this->author;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: filter
|
||
|
* Filters the comment through filter_comment and markup filters.
|
||
|
*/
|
||
|
private function filter(): void {
|
||
|
$trigger = Trigger::current();
|
||
|
$trigger->filter($this, "filter_comment");
|
||
|
|
||
|
$this->body_unfiltered = $this->body;
|
||
|
$trigger->filter($this->body, array("markup_comment_text", "markup_text"));
|
||
|
|
||
|
$config = Config::current();
|
||
|
|
||
|
$group = (!empty($this->user_id) and !$this->user->no_results) ?
|
||
|
$this->user->group :
|
||
|
new Group($config->guest_group) ;
|
||
|
|
||
|
if (
|
||
|
$this->status != "pingback" and
|
||
|
!$group->can("code_in_comments")
|
||
|
) {
|
||
|
$allowed = $config->module_comments["allowed_comment_html"];
|
||
|
$this->body = strip_tags(
|
||
|
$this->body,
|
||
|
"<".implode("><", $allowed).">"
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->body = sanitize_html($this->body);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: install
|
||
|
* Creates the database table.
|
||
|
*/
|
||
|
public static function install(): void {
|
||
|
SQL::current()->create(
|
||
|
table:"comments",
|
||
|
cols:array(
|
||
|
"id INTEGER PRIMARY KEY AUTO_INCREMENT",
|
||
|
"body LONGTEXT",
|
||
|
"author VARCHAR(250) DEFAULT ''",
|
||
|
"author_url VARCHAR(2048) DEFAULT ''",
|
||
|
"author_email VARCHAR(128) DEFAULT ''",
|
||
|
"author_ip INTEGER DEFAULT 0",
|
||
|
"author_agent VARCHAR(255) DEFAULT ''",
|
||
|
"status VARCHAR(32) default '".self::STATUS_DENIED."'",
|
||
|
"post_id INTEGER DEFAULT 0",
|
||
|
"user_id INTEGER DEFAULT 0",
|
||
|
"parent_id INTEGER DEFAULT 0",
|
||
|
"notify BOOLEAN DEFAULT FALSE",
|
||
|
"created_at DATETIME DEFAULT NULL",
|
||
|
"updated_at DATETIME DEFAULT NULL"
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Function: uninstall
|
||
|
* Drops the database table.
|
||
|
*/
|
||
|
public static function uninstall(): void {
|
||
|
$sql = SQL::current();
|
||
|
|
||
|
$sql->drop("comments");
|
||
|
$sql->delete(
|
||
|
table:"post_attributes",
|
||
|
conds:array("name" => "comment_status")
|
||
|
);
|
||
|
}
|
||
|
}
|