run:R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
23.8 KB
2026-03-11 16:18:51
R W Run
7.8 KB
2026-03-11 16:18:52
R W Run
36.1 KB
2026-03-11 16:18:51
R W Run
11.9 KB
2026-03-11 16:18:52
R W Run
18.94 KB
2026-03-11 16:18:52
R W Run
7.35 KB
2026-03-11 16:18:52
R W Run
28.6 KB
2026-03-11 16:18:51
R W Run
316 By
2026-03-11 16:18:51
R W Run
12.9 KB
2026-03-11 16:18:51
R W Run
61.02 KB
2026-03-11 16:18:52
R W Run
15 KB
2026-03-11 16:18:51
R W Run
112.05 KB
2026-03-11 16:18:51
R W Run
12.47 KB
2026-03-11 16:18:51
R W Run
15.07 KB
2026-03-11 16:18:52
R W Run
9.84 KB
2026-03-11 16:18:52
R W Run
13.17 KB
2026-03-11 16:18:52
R W Run
33.83 KB
2026-03-11 16:18:51
R W Run
42.63 KB
2026-03-11 16:18:51
R W Run
55.71 KB
2026-03-11 16:18:52
R W Run
12.53 KB
2026-03-11 16:18:51
R W Run
2.55 KB
2026-03-11 16:18:52
R W Run
28.92 KB
2026-03-11 16:18:52
R W Run
539 By
2026-03-11 16:18:51
R W Run
367 By
2026-03-11 16:18:52
R W Run
42.65 KB
2026-03-11 16:18:51
R W Run
401 By
2026-03-11 16:18:51
R W Run
6.61 KB
2026-03-11 16:18:51
R W Run
664 By
2026-03-11 16:18:52
R W Run
20.63 KB
2026-03-11 16:18:51
R W Run
2.18 KB
2026-03-11 16:18:52
R W Run
453 By
2026-03-11 16:18:52
R W Run
457 By
2026-03-11 16:18:51
R W Run
36.83 KB
2026-03-11 16:18:52
R W Run
2.41 KB
2026-03-11 16:18:52
R W Run
8.28 KB
2026-03-11 16:18:51
R W Run
13.89 KB
2026-03-11 16:18:51
R W Run
11.76 KB
2026-03-11 16:18:51
R W Run
2.65 KB
2026-03-11 16:18:51
R W Run
7.43 KB
2026-03-11 16:18:51
R W Run
17.46 KB
2026-03-11 16:18:51
R W Run
5.14 KB
2026-03-11 16:18:52
R W Run
16.7 KB
2026-03-11 16:18:51
R W Run
8.28 KB
2026-03-11 16:18:52
R W Run
2.92 KB
2026-03-11 16:18:52
R W Run
1.32 KB
2026-03-11 16:18:51
R W Run
4.6 KB
2026-03-11 16:18:52
R W Run
11.62 KB
2026-03-11 16:18:52
R W Run
2.5 KB
2026-03-11 16:18:51
R W Run
1.97 KB
2026-03-11 16:18:51
R W Run
11.25 KB
2026-03-11 16:18:52
R W Run
5.32 KB
2026-03-11 16:18:51
R W Run
10.99 KB
2026-03-11 16:18:52
R W Run
68.32 KB
2026-03-11 16:18:51
R W Run
6.34 KB
2026-03-11 16:18:51
R W Run
5.49 KB
2026-03-11 16:18:51
R W Run
1.99 KB
2026-03-11 16:18:52
R W Run
7.02 KB
2026-03-11 16:18:51
R W Run
4.91 KB
2026-03-11 16:18:52
R W Run
16.86 KB
2026-03-11 16:18:51
R W Run
24.23 KB
2026-03-11 16:18:51
R W Run
3.97 KB
2026-03-11 16:18:51
R W Run
47.66 KB
2026-03-11 16:18:51
R W Run
9.22 KB
2026-03-11 16:18:51
R W Run
25.51 KB
2026-03-11 16:18:51
R W Run
198.38 KB
2026-03-11 16:18:52
R W Run
56.65 KB
2026-03-11 16:18:51
R W Run
10.46 KB
2026-03-11 16:18:51
R W Run
10.95 KB
2026-03-11 16:18:52
R W Run
29.26 KB
2026-03-11 16:18:51
R W Run
70.91 KB
2026-03-11 16:18:52
R W Run
35.3 KB
2026-03-11 16:18:52
R W Run
16.61 KB
2026-03-11 16:18:52
R W Run
2.57 KB
2026-03-11 16:18:52
R W Run
39.83 KB
2026-03-11 16:18:51
R W Run
70.64 KB
2026-03-11 16:18:51
R W Run
15.56 KB
2026-03-11 16:18:52
R W Run
7.33 KB
2026-03-11 16:18:52
R W Run
253 By
2026-03-11 16:18:51
R W Run
7.96 KB
2026-03-11 16:18:52
R W Run
3.23 KB
2026-03-11 16:18:52
R W Run
969 By
2026-03-11 16:18:52
R W Run
16.28 KB
2026-03-11 16:18:51
R W Run
7.22 KB
2026-03-11 16:18:51
R W Run
12.95 KB
2026-03-11 16:18:51
R W Run
6.53 KB
2026-03-11 16:18:51
R W Run
3.42 KB
2026-03-11 16:18:52
R W Run
5.84 KB
2026-03-11 16:18:51
R W Run
1.97 KB
2026-03-11 16:18:51
R W Run
4.3 KB
2026-03-11 16:18:52
R W Run
2.91 KB
2026-03-11 16:18:51
R W Run
16.46 KB
2026-03-11 16:18:52
R W Run
40.6 KB
2026-03-11 16:18:51
R W Run
20.22 KB
2026-03-11 16:18:51
R W Run
36.11 KB
2026-03-11 16:18:52
R W Run
17.01 KB
2026-03-11 16:18:51
R W Run
7.27 KB
2026-03-11 16:18:52
R W Run
6.62 KB
2026-03-11 16:18:52
R W Run
16.49 KB
2026-03-11 16:18:52
R W Run
1.79 KB
2026-03-11 16:18:52
R W Run
29.82 KB
2026-03-11 16:18:51
R W Run
6.67 KB
2026-03-11 16:18:52
R W Run
8.98 KB
2026-03-11 16:18:52
R W Run
19.42 KB
2026-03-11 16:18:51
R W Run
12.01 KB
2026-03-11 16:18:51
R W Run
17.11 KB
2026-03-11 16:18:51
R W Run
6.74 KB
2026-03-11 16:18:52
R W Run
30.93 KB
2026-03-11 16:18:51
R W Run
4.99 KB
2026-03-11 16:18:51
R W Run
4.25 KB
2026-03-11 16:18:51
R W Run
24.72 KB
2026-03-11 16:18:51
R W Run
29.96 KB
2026-03-11 16:18:52
R W Run
6.41 KB
2026-03-11 16:18:51
R W Run
160 KB
2026-03-11 16:18:51
R W Run
6.72 KB
2026-03-11 16:18:52
R W Run
10.92 KB
2026-03-11 16:18:51
R W Run
4.77 KB
2026-03-11 16:18:51
R W Run
3.38 KB
2026-03-11 16:18:51
R W Run
11.18 KB
2026-03-11 16:18:51
R W Run
62.19 KB
2026-03-11 16:18:51
R W Run
2.46 KB
2026-03-11 16:18:51
R W Run
9.17 KB
2026-03-11 16:18:51
R W Run
32.15 KB
2026-03-11 16:18:51
R W Run
34.05 KB
2026-03-11 16:18:52
R W Run
7.15 KB
2026-03-11 16:18:51
R W Run
3.47 KB
2026-03-11 16:18:52
R W Run
1.87 KB
2026-03-11 16:18:52
R W Run
30.91 KB
2026-03-11 16:18:51
R W Run
7.29 KB
2026-03-11 16:18:52
R W Run
7.35 KB
2026-03-11 16:18:51
R W Run
12.54 KB
2026-03-11 16:18:51
R W Run
19.12 KB
2026-03-11 16:18:51
R W Run
18.12 KB
2026-03-11 16:18:52
R W Run
39.99 KB
2026-03-11 16:18:52
R W Run
5.17 KB
2026-03-11 16:18:52
R W Run
979 By
2026-03-11 16:18:51
R W Run
18.44 KB
2026-03-11 16:18:52
R W Run
10.24 KB
2026-03-11 16:18:51
R W Run
1.77 KB
2026-03-11 16:18:52
R W Run
34.9 KB
2026-03-11 16:18:51
R W Run
7.19 KB
2026-03-11 16:18:52
R W Run
160.5 KB
2026-03-11 16:18:51
R W Run
64.27 KB
2026-03-11 16:18:51
R W Run
27.95 KB
2026-03-11 16:18:51
R W Run
4.69 KB
2026-03-11 16:18:51
R W Run
2.94 KB
2026-03-11 16:18:51
R W Run
43.13 KB
2026-03-11 16:18:52
R W Run
2.25 KB
2026-03-11 16:18:52
R W Run
22.5 KB
2026-03-11 16:18:51
R W Run
13.01 KB
2026-03-11 16:18:52
R W Run
3.27 KB
2026-03-11 16:18:51
R W Run
18 KB
2026-03-11 16:18:51
R W Run
210.4 KB
2026-03-11 16:18:52
R W Run
25.86 KB
2026-03-11 16:18:52
R W Run
115.85 KB
2026-03-11 16:18:51
R W Run
373 By
2026-03-11 16:18:52
R W Run
343 By
2026-03-11 16:18:52
R W Run
338 By
2026-03-11 16:18:51
R W Run
100.73 KB
2026-03-11 16:18:52
R W Run
130.93 KB
2026-03-11 16:18:51
R W Run
19.1 KB
2026-03-11 16:18:51
R W Run
17.41 KB
2026-03-11 16:18:52
R W Run
41.98 KB
2026-03-11 16:18:52
R W Run
400 By
2026-03-11 16:18:52
R W Run
11.1 KB
2026-03-11 16:18:52
R W Run
37.02 KB
2026-03-11 16:18:51
R W Run
2.24 KB
2026-03-11 16:18:51
R W Run
188.13 KB
2026-03-11 16:18:51
R W Run
338 By
2026-03-11 16:18:51
R W Run
38 KB
2026-03-11 16:18:51
R W Run
4.02 KB
2026-03-11 16:18:52
R W Run
5.38 KB
2026-03-11 16:18:51
R W Run
3.05 KB
2026-03-11 16:18:52
R W Run
2.61 KB
2026-03-11 16:18:51
R W Run
1.16 KB
2026-03-11 16:18:52
R W Run
4.04 KB
2026-03-11 16:18:51
R W Run
3.71 KB
2026-03-11 16:18:51
R W Run
24.6 KB
2026-03-11 16:18:51
R W Run
9.56 KB
2026-03-11 16:18:51
R W Run
346.43 KB
2026-03-11 16:18:52
R W Run
281.84 KB
2026-03-11 16:18:52
R W Run
14.95 KB
2026-03-11 16:18:51
R W Run
8.44 KB
2026-03-11 16:18:52
R W Run
168.95 KB
2026-03-11 16:18:52
R W Run
20.71 KB
2026-03-11 16:18:52
R W Run
25.27 KB
2026-03-11 16:18:51
R W Run
5.72 KB
2026-03-11 16:18:51
R W Run
4.63 KB
2026-03-11 16:18:52
R W Run
81.73 KB
2026-03-11 16:18:51
R W Run
67.18 KB
2026-03-11 16:18:51
R W Run
156.36 KB
2026-03-11 16:18:52
R W Run
55.19 KB
2026-03-11 16:18:51
R W Run
162 By
2026-03-11 16:18:51
R W Run
61.72 KB
2026-03-11 16:18:51
R W Run
216.06 KB
2026-03-11 16:18:52
R W Run
65.09 KB
2026-03-11 16:18:51
R W Run
25.24 KB
2026-03-11 16:18:52
R W Run
4.81 KB
2026-03-11 16:18:51
R W Run
6.48 KB
2026-03-11 16:18:52
R W Run
21.25 KB
2026-03-11 16:18:51
R W Run
2.79 KB
2026-03-11 16:18:52
R W Run
89.69 KB
2026-03-11 16:18:52
R W Run
19.42 KB
2026-03-11 16:18:52
R W Run
3.69 KB
2026-03-11 16:18:52
R W Run
4.11 KB
2026-03-11 16:18:51
R W Run
40.74 KB
2026-03-11 16:18:51
R W Run
25.38 KB
2026-03-11 16:18:51
R W Run
43.31 KB
2026-03-11 16:18:52
R W Run
102.57 KB
2026-03-11 16:18:52
R W Run
6.18 KB
2026-03-11 16:18:51
R W Run
124.47 KB
2026-03-11 16:18:52
R W Run
35.65 KB
2026-03-11 16:18:52
R W Run
6.94 KB
2026-03-11 16:18:52
R W Run
67.04 KB
2026-03-11 16:18:52
R W Run
10.62 KB
2026-03-11 16:18:51
R W Run
289.35 KB
2026-03-11 16:18:52
R W Run
36.23 KB
2026-03-11 16:18:51
R W Run
200 By
2026-03-11 16:18:52
R W Run
200 By
2026-03-11 16:18:52
R W Run
98.29 KB
2026-03-11 16:18:52
R W Run
30.02 KB
2026-03-11 16:18:52
R W Run
19.03 KB
2026-03-11 16:18:52
R W Run
5.06 KB
2026-03-11 16:18:52
R W Run
255 By
2026-03-11 16:18:51
R W Run
22.66 KB
2026-03-11 16:18:52
R W Run
154.63 KB
2026-03-11 16:18:51
R W Run
9.68 KB
2026-03-11 16:18:51
R W Run
258 By
2026-03-11 16:18:51
R W Run
23.49 KB
2026-03-11 16:18:51
R W Run
3.16 KB
2026-03-11 16:18:51
R W Run
8.4 KB
2026-03-11 16:18:52
R W Run
441 By
2026-03-11 16:18:51
R W Run
7.39 KB
2026-03-11 16:18:51
R W Run
173 KB
2026-03-11 16:18:52
R W Run
544 By
2026-03-11 16:18:52
R W Run
4.17 KB
2026-03-11 16:18:51
R W Run
35.97 KB
2026-03-11 16:18:52
R W Run
1.69 KB
2026-03-11 16:18:51
R W Run
2.84 KB
2026-03-11 16:18:52
R W Run
6.09 KB
2026-03-11 16:18:51
R W Run
8.71 KB
2026-03-11 16:18:51
R W Run
131.84 KB
2026-03-11 16:18:51
R W Run
37.45 KB
2026-03-11 16:18:51
R W Run
173.89 KB
2026-03-11 16:18:51
R W Run
7.09 KB
2026-03-11 16:18:51
R W Run
6.41 KB
2026-03-11 16:18:51
R W Run
1.08 KB
2026-03-11 16:18:51
R W Run
69.46 KB
2026-03-11 16:18:52
R W Run
445 By
2026-03-11 16:18:51
R W Run
799 By
2026-03-11 16:18:52
R W Run
error_log
📄comment.php
1<?php
2/**
3 * Core Comment API
4 *
5 * @package WordPress
6 * @subpackage Comment
7 */
8
9/**
10 * Checks whether a comment passes internal checks to be allowed to add.
11 *
12 * If manual comment moderation is set in the administration, then all checks,
13 * regardless of their type and substance, will fail and the function will
14 * return false.
15 *
16 * If the number of links exceeds the amount in the administration, then the
17 * check fails. If any of the parameter contents contain any disallowed words,
18 * then the check fails.
19 *
20 * If the comment author was approved before, then the comment is automatically
21 * approved.
22 *
23 * If all checks pass, the function will return true.
24 *
25 * @since 1.2.0
26 *
27 * @global wpdb $wpdb WordPress database abstraction object.
28 *
29 * @param string $author Comment author name.
30 * @param string $email Comment author email.
31 * @param string $url Comment author URL.
32 * @param string $comment Content of the comment.
33 * @param string $user_ip Comment author IP address.
34 * @param string $user_agent Comment author User-Agent.
35 * @param string $comment_type Comment type, either user-submitted comment,
36 * trackback, or pingback.
37 * @return bool If all checks pass, true, otherwise false.
38 */
39function check_comment( $author, $email, $url, $comment, $user_ip, $user_agent, $comment_type ) {
40 global $wpdb;
41
42 // If manual moderation is enabled, skip all checks and return false.
43 if ( '1' === get_option( 'comment_moderation' ) ) {
44 return false;
45 }
46
47 /** This filter is documented in wp-includes/comment-template.php */
48 $comment = apply_filters( 'comment_text', $comment, null, array() );
49
50 // Check for the number of external links if a max allowed number is set.
51 $max_links = get_option( 'comment_max_links' );
52 if ( $max_links ) {
53 $num_links = preg_match_all( '/<a [^>]*href/i', $comment, $out );
54
55 /**
56 * Filters the number of links found in a comment.
57 *
58 * @since 3.0.0
59 * @since 4.7.0 Added the `$comment` parameter.
60 *
61 * @param int $num_links The number of links found.
62 * @param string $url Comment author's URL. Included in allowed links total.
63 * @param string $comment Content of the comment.
64 */
65 $num_links = apply_filters( 'comment_max_links_url', $num_links, $url, $comment );
66
67 /*
68 * If the number of links in the comment exceeds the allowed amount,
69 * fail the check by returning false.
70 */
71 if ( $num_links >= $max_links ) {
72 return false;
73 }
74 }
75
76 $mod_keys = trim( get_option( 'moderation_keys' ) );
77
78 // If moderation 'keys' (keywords) are set, process them.
79 if ( ! empty( $mod_keys ) ) {
80 $words = explode( "\n", $mod_keys );
81
82 foreach ( (array) $words as $word ) {
83 $word = trim( $word );
84
85 // Skip empty lines.
86 if ( empty( $word ) ) {
87 continue;
88 }
89
90 /*
91 * Do some escaping magic so that '#' (number of) characters in the spam
92 * words don't break things:
93 */
94 $word = preg_quote( $word, '#' );
95
96 /*
97 * Check the comment fields for moderation keywords. If any are found,
98 * fail the check for the given field by returning false.
99 */
100 $pattern = "#$word#iu";
101 if ( preg_match( $pattern, $author ) ) {
102 return false;
103 }
104 if ( preg_match( $pattern, $email ) ) {
105 return false;
106 }
107 if ( preg_match( $pattern, $url ) ) {
108 return false;
109 }
110 if ( preg_match( $pattern, $comment ) ) {
111 return false;
112 }
113 if ( preg_match( $pattern, $user_ip ) ) {
114 return false;
115 }
116 if ( preg_match( $pattern, $user_agent ) ) {
117 return false;
118 }
119 }
120 }
121
122 /*
123 * Check if the option to approve comments by previously-approved authors is enabled.
124 *
125 * If it is enabled, check whether the comment author has a previously-approved comment,
126 * as well as whether there are any moderation keywords (if set) present in the author
127 * email address. If both checks pass, return true. Otherwise, return false.
128 */
129 if ( '1' === get_option( 'comment_previously_approved' ) ) {
130 if ( 'trackback' !== $comment_type && 'pingback' !== $comment_type && '' !== $author && '' !== $email ) {
131 $comment_user = get_user_by( 'email', wp_unslash( $email ) );
132 if ( ! empty( $comment_user->ID ) ) {
133 $ok_to_comment = $wpdb->get_var(
134 $wpdb->prepare(
135 "SELECT comment_approved
136 FROM $wpdb->comments
137 WHERE user_id = %d
138 AND comment_approved = '1'
139 LIMIT 1",
140 $comment_user->ID
141 )
142 );
143 } else {
144 // expected_slashed ($author, $email)
145 $ok_to_comment = $wpdb->get_var(
146 $wpdb->prepare(
147 "SELECT comment_approved
148 FROM $wpdb->comments
149 WHERE comment_author = %s
150 AND comment_author_email = %s
151 AND comment_approved = '1'
152 LIMIT 1",
153 $author,
154 $email
155 )
156 );
157 }
158
159 if ( '1' === $ok_to_comment && ( empty( $mod_keys ) || ! str_contains( $email, $mod_keys ) ) ) {
160 return true;
161 } else {
162 return false;
163 }
164 } else {
165 return false;
166 }
167 }
168 return true;
169}
170
171/**
172 * Retrieves the approved comments for a post.
173 *
174 * @since 2.0.0
175 * @since 4.1.0 Refactored to leverage WP_Comment_Query over a direct query.
176 *
177 * @param int $post_id The ID of the post.
178 * @param array $args {
179 * Optional. See WP_Comment_Query::__construct() for information on accepted arguments.
180 *
181 * @type int $status Comment status to limit results by. Defaults to approved comments.
182 * @type int $post_id Limit results to those affiliated with a given post ID.
183 * @type string $order How to order retrieved comments. Default 'ASC'.
184 * }
185 * @return WP_Comment[]|int[]|int The approved comments, or number of comments if `$count`
186 * argument is true.
187 */
188function get_approved_comments( $post_id, $args = array() ) {
189 if ( ! $post_id ) {
190 return array();
191 }
192
193 $defaults = array(
194 'status' => 1,
195 'post_id' => $post_id,
196 'order' => 'ASC',
197 );
198 $parsed_args = wp_parse_args( $args, $defaults );
199
200 $query = new WP_Comment_Query();
201 return $query->query( $parsed_args );
202}
203
204/**
205 * Retrieves comment data given a comment ID or comment object.
206 *
207 * If an object is passed then the comment data will be cached and then returned
208 * after being passed through a filter. If the comment is empty, then the global
209 * comment variable will be used, if it is set.
210 *
211 * @since 2.0.0
212 *
213 * @global WP_Comment $comment Global comment object.
214 *
215 * @param WP_Comment|string|int $comment Comment to retrieve.
216 * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which
217 * correspond to a WP_Comment object, an associative array, or a numeric array,
218 * respectively. Default OBJECT.
219 * @return WP_Comment|array|null Depends on $output value.
220 */
221function get_comment( $comment = null, $output = OBJECT ) {
222 if ( empty( $comment ) && isset( $GLOBALS['comment'] ) ) {
223 $comment = $GLOBALS['comment'];
224 }
225
226 if ( $comment instanceof WP_Comment ) {
227 $_comment = $comment;
228 } elseif ( is_object( $comment ) ) {
229 $_comment = new WP_Comment( $comment );
230 } else {
231 $_comment = WP_Comment::get_instance( $comment );
232 }
233
234 if ( ! $_comment ) {
235 return null;
236 }
237
238 /**
239 * Fires after a comment is retrieved.
240 *
241 * @since 2.3.0
242 *
243 * @param WP_Comment $_comment Comment data.
244 */
245 $_comment = apply_filters( 'get_comment', $_comment );
246
247 if ( OBJECT === $output ) {
248 return $_comment;
249 } elseif ( ARRAY_A === $output ) {
250 return $_comment->to_array();
251 } elseif ( ARRAY_N === $output ) {
252 return array_values( $_comment->to_array() );
253 }
254 return $_comment;
255}
256
257/**
258 * Retrieves a list of comments.
259 *
260 * The comment list can be for the blog as a whole or for an individual post.
261 *
262 * @since 2.7.0
263 *
264 * @param string|array $args Optional. Array or string of arguments. See WP_Comment_Query::__construct()
265 * for information on accepted arguments. Default empty string.
266 * @return WP_Comment[]|int[]|int List of comments or number of found comments if `$count` argument is true.
267 */
268function get_comments( $args = '' ) {
269 $query = new WP_Comment_Query();
270 return $query->query( $args );
271}
272
273/**
274 * Retrieves all of the WordPress supported comment statuses.
275 *
276 * Comments have a limited set of valid status values, this provides the comment
277 * status values and descriptions.
278 *
279 * @since 2.7.0
280 *
281 * @return string[] List of comment status labels keyed by status.
282 */
283function get_comment_statuses() {
284 $status = array(
285 'hold' => __( 'Unapproved' ),
286 'approve' => _x( 'Approved', 'comment status' ),
287 'spam' => _x( 'Spam', 'comment status' ),
288 'trash' => _x( 'Trash', 'comment status' ),
289 );
290
291 return $status;
292}
293
294/**
295 * Gets the default comment status for a post type.
296 *
297 * @since 4.3.0
298 *
299 * @param string $post_type Optional. Post type. Default 'post'.
300 * @param string $comment_type Optional. Comment type. Default 'comment'.
301 * @return string Either 'open' or 'closed'.
302 */
303function get_default_comment_status( $post_type = 'post', $comment_type = 'comment' ) {
304 switch ( $comment_type ) {
305 case 'pingback':
306 case 'trackback':
307 $supports = 'trackbacks';
308 $option = 'ping';
309 break;
310 default:
311 $supports = 'comments';
312 $option = 'comment';
313 break;
314 }
315
316 // Set the status.
317 if ( 'page' === $post_type ) {
318 $status = 'closed';
319 } elseif ( post_type_supports( $post_type, $supports ) ) {
320 $status = get_option( "default_{$option}_status" );
321 } else {
322 $status = 'closed';
323 }
324
325 /**
326 * Filters the default comment status for the given post type.
327 *
328 * @since 4.3.0
329 *
330 * @param string $status Default status for the given post type,
331 * either 'open' or 'closed'.
332 * @param string $post_type Post type. Default is `post`.
333 * @param string $comment_type Type of comment. Default is `comment`.
334 */
335 return apply_filters( 'get_default_comment_status', $status, $post_type, $comment_type );
336}
337
338/**
339 * Retrieves the date the last comment was modified.
340 *
341 * @since 1.5.0
342 * @since 4.7.0 Replaced caching the modified date in a local static variable
343 * with the Object Cache API.
344 *
345 * @global wpdb $wpdb WordPress database abstraction object.
346 *
347 * @param string $timezone Which timezone to use in reference to 'gmt', 'blog', or 'server' locations.
348 * @return string|false Last comment modified date on success, false on failure.
349 */
350function get_lastcommentmodified( $timezone = 'server' ) {
351 global $wpdb;
352
353 $timezone = strtolower( $timezone );
354 $key = "lastcommentmodified:$timezone";
355
356 $comment_modified_date = wp_cache_get( $key, 'timeinfo' );
357 if ( false !== $comment_modified_date ) {
358 return $comment_modified_date;
359 }
360
361 switch ( $timezone ) {
362 case 'gmt':
363 $comment_modified_date = $wpdb->get_var( "SELECT comment_date_gmt FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1" );
364 break;
365 case 'blog':
366 $comment_modified_date = $wpdb->get_var( "SELECT comment_date FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1" );
367 break;
368 case 'server':
369 $add_seconds_server = gmdate( 'Z' );
370
371 $comment_modified_date = $wpdb->get_var( $wpdb->prepare( "SELECT DATE_ADD(comment_date_gmt, INTERVAL %s SECOND) FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1", $add_seconds_server ) );
372 break;
373 }
374
375 if ( $comment_modified_date ) {
376 wp_cache_set( $key, $comment_modified_date, 'timeinfo' );
377
378 return $comment_modified_date;
379 }
380
381 return false;
382}
383
384/**
385 * Retrieves the total comment counts for the whole site or a single post.
386 *
387 * @since 2.0.0
388 *
389 * @param int $post_id Optional. Restrict the comment counts to the given post. Default 0, which indicates that
390 * comment counts for the whole site will be retrieved.
391 * @return int[] {
392 * The number of comments keyed by their status.
393 *
394 * @type int $approved The number of approved comments.
395 * @type int $awaiting_moderation The number of comments awaiting moderation (a.k.a. pending).
396 * @type int $spam The number of spam comments.
397 * @type int $trash The number of trashed comments.
398 * @type int $post-trashed The number of comments for posts that are in the trash.
399 * @type int $total_comments The total number of non-trashed comments, including spam.
400 * @type int $all The total number of pending or approved comments.
401 * }
402 */
403function get_comment_count( $post_id = 0 ) {
404 $post_id = (int) $post_id;
405
406 $comment_count = array(
407 'approved' => 0,
408 'awaiting_moderation' => 0,
409 'spam' => 0,
410 'trash' => 0,
411 'post-trashed' => 0,
412 'total_comments' => 0,
413 'all' => 0,
414 );
415
416 $args = array(
417 'count' => true,
418 'update_comment_meta_cache' => false,
419 'orderby' => 'none',
420 );
421 if ( $post_id > 0 ) {
422 $args['post_id'] = $post_id;
423 }
424 $mapping = array(
425 'approved' => 'approve',
426 'awaiting_moderation' => 'hold',
427 'spam' => 'spam',
428 'trash' => 'trash',
429 'post-trashed' => 'post-trashed',
430 );
431 $comment_count = array();
432 foreach ( $mapping as $key => $value ) {
433 $comment_count[ $key ] = get_comments( array_merge( $args, array( 'status' => $value ) ) );
434 }
435
436 $comment_count['all'] = $comment_count['approved'] + $comment_count['awaiting_moderation'];
437 $comment_count['total_comments'] = $comment_count['all'] + $comment_count['spam'];
438
439 return array_map( 'intval', $comment_count );
440}
441
442//
443// Comment meta functions.
444//
445
446/**
447 * Adds meta data field to a comment.
448 *
449 * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
450 *
451 * @since 2.9.0
452 *
453 * @link https://developer.wordpress.org/reference/functions/add_comment_meta/
454 *
455 * @param int $comment_id Comment ID.
456 * @param string $meta_key Metadata name.
457 * @param mixed $meta_value Metadata value. Arrays and objects are stored as serialized data and
458 * will be returned as the same type when retrieved. Other data types will
459 * be stored as strings in the database:
460 * - false is stored and retrieved as an empty string ('')
461 * - true is stored and retrieved as '1'
462 * - numbers (both integer and float) are stored and retrieved as strings
463 * Must be serializable if non-scalar.
464 * @param bool $unique Optional. Whether the same key should not be added.
465 * Default false.
466 * @return int|false Meta ID on success, false on failure.
467 */
468function add_comment_meta( $comment_id, $meta_key, $meta_value, $unique = false ) {
469 return add_metadata( 'comment', $comment_id, $meta_key, $meta_value, $unique );
470}
471
472/**
473 * Removes metadata matching criteria from a comment.
474 *
475 * You can match based on the key, or key and value. Removing based on key and
476 * value, will keep from removing duplicate metadata with the same key. It also
477 * allows removing all metadata matching key, if needed.
478 *
479 * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
480 *
481 * @since 2.9.0
482 *
483 * @link https://developer.wordpress.org/reference/functions/delete_comment_meta/
484 *
485 * @param int $comment_id Comment ID.
486 * @param string $meta_key Metadata name.
487 * @param mixed $meta_value Optional. Metadata value. If provided,
488 * rows will only be removed that match the value.
489 * Must be serializable if non-scalar. Default empty string.
490 * @return bool True on success, false on failure.
491 */
492function delete_comment_meta( $comment_id, $meta_key, $meta_value = '' ) {
493 return delete_metadata( 'comment', $comment_id, $meta_key, $meta_value );
494}
495
496/**
497 * Retrieves comment meta field for a comment.
498 *
499 * @since 2.9.0
500 *
501 * @link https://developer.wordpress.org/reference/functions/get_comment_meta/
502 *
503 * @param int $comment_id Comment ID.
504 * @param string $key Optional. The meta key to retrieve. By default,
505 * returns data for all keys. Default empty string.
506 * @param bool $single Optional. Whether to return a single value.
507 * This parameter has no effect if `$key` is not specified.
508 * Default false.
509 * @return mixed An array of values if `$single` is false.
510 * The value of meta data field if `$single` is true.
511 * False for an invalid `$comment_id` (non-numeric, zero, or negative value).
512 * An empty array if a valid but non-existing comment ID is passed and `$single` is false.
513 * An empty string if a valid but non-existing comment ID is passed and `$single` is true.
514 * Note: Non-serialized values are returned as strings:
515 * - false values are returned as empty strings ('')
516 * - true values are returned as '1'
517 * - numbers are returned as strings
518 * Arrays and objects retain their original type.
519 */
520function get_comment_meta( $comment_id, $key = '', $single = false ) {
521 return get_metadata( 'comment', $comment_id, $key, $single );
522}
523
524/**
525 * Queue comment meta for lazy-loading.
526 *
527 * @since 6.3.0
528 *
529 * @param array $comment_ids List of comment IDs.
530 */
531function wp_lazyload_comment_meta( array $comment_ids ) {
532 if ( empty( $comment_ids ) ) {
533 return;
534 }
535 $lazyloader = wp_metadata_lazyloader();
536 $lazyloader->queue_objects( 'comment', $comment_ids );
537}
538
539/**
540 * Updates comment meta field based on comment ID.
541 *
542 * Use the $prev_value parameter to differentiate between meta fields with the
543 * same key and comment ID.
544 *
545 * If the meta field for the comment does not exist, it will be added.
546 *
547 * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input.
548 *
549 * @since 2.9.0
550 *
551 * @link https://developer.wordpress.org/reference/functions/update_comment_meta/
552 *
553 * @param int $comment_id Comment ID.
554 * @param string $meta_key Metadata key.
555 * @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
556 * @param mixed $prev_value Optional. Previous value to check before updating.
557 * If specified, only update existing metadata entries with
558 * this value. Otherwise, update all entries. Default empty string.
559 * @return int|bool Meta ID if the key didn't exist, true on successful update,
560 * false on failure or if the value passed to the function
561 * is the same as the one that is already in the database.
562 */
563function update_comment_meta( $comment_id, $meta_key, $meta_value, $prev_value = '' ) {
564 return update_metadata( 'comment', $comment_id, $meta_key, $meta_value, $prev_value );
565}
566
567/**
568 * Sets the cookies used to store an unauthenticated commentator's identity. Typically used
569 * to recall previous comments by this commentator that are still held in moderation.
570 *
571 * @since 3.4.0
572 * @since 4.9.6 The `$cookies_consent` parameter was added.
573 *
574 * @param WP_Comment $comment Comment object.
575 * @param WP_User $user Comment author's user object. The user may not exist.
576 * @param bool $cookies_consent Optional. Comment author's consent to store cookies. Default true.
577 */
578function wp_set_comment_cookies( $comment, $user, $cookies_consent = true ) {
579 // If the user already exists, or the user opted out of cookies, don't set cookies.
580 if ( $user->exists() ) {
581 return;
582 }
583
584 if ( false === $cookies_consent ) {
585 // Remove any existing cookies.
586 $past = time() - YEAR_IN_SECONDS;
587 setcookie( 'comment_author_' . COOKIEHASH, ' ', $past, COOKIEPATH, COOKIE_DOMAIN );
588 setcookie( 'comment_author_email_' . COOKIEHASH, ' ', $past, COOKIEPATH, COOKIE_DOMAIN );
589 setcookie( 'comment_author_url_' . COOKIEHASH, ' ', $past, COOKIEPATH, COOKIE_DOMAIN );
590
591 return;
592 }
593
594 /**
595 * Filters the lifetime of the comment cookie in seconds.
596 *
597 * @since 2.8.0
598 * @since 6.6.0 The default $seconds value changed from 30000000 to YEAR_IN_SECONDS.
599 *
600 * @param int $seconds Comment cookie lifetime. Default YEAR_IN_SECONDS.
601 */
602 $comment_cookie_lifetime = time() + apply_filters( 'comment_cookie_lifetime', YEAR_IN_SECONDS );
603
604 $secure = ( 'https' === parse_url( home_url(), PHP_URL_SCHEME ) );
605
606 setcookie( 'comment_author_' . COOKIEHASH, $comment->comment_author, $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN, $secure );
607 setcookie( 'comment_author_email_' . COOKIEHASH, $comment->comment_author_email, $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN, $secure );
608 setcookie( 'comment_author_url_' . COOKIEHASH, esc_url( $comment->comment_author_url ), $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN, $secure );
609}
610
611/**
612 * Sanitizes the cookies sent to the user already.
613 *
614 * Will only do anything if the cookies have already been created for the user.
615 * Mostly used after cookies had been sent to use elsewhere.
616 *
617 * @since 2.0.4
618 */
619function sanitize_comment_cookies() {
620 if ( isset( $_COOKIE[ 'comment_author_' . COOKIEHASH ] ) ) {
621 /**
622 * Filters the comment author's name cookie before it is set.
623 *
624 * When this filter hook is evaluated in wp_filter_comment(),
625 * the comment author's name string is passed.
626 *
627 * @since 1.5.0
628 *
629 * @param string $author_cookie The comment author name cookie.
630 */
631 $comment_author = apply_filters( 'pre_comment_author_name', $_COOKIE[ 'comment_author_' . COOKIEHASH ] );
632 $comment_author = wp_unslash( $comment_author );
633 $comment_author = esc_attr( $comment_author );
634
635 $_COOKIE[ 'comment_author_' . COOKIEHASH ] = $comment_author;
636 }
637
638 if ( isset( $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] ) ) {
639 /**
640 * Filters the comment author's email cookie before it is set.
641 *
642 * When this filter hook is evaluated in wp_filter_comment(),
643 * the comment author's email string is passed.
644 *
645 * @since 1.5.0
646 *
647 * @param string $author_email_cookie The comment author email cookie.
648 */
649 $comment_author_email = apply_filters( 'pre_comment_author_email', $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] );
650 $comment_author_email = wp_unslash( $comment_author_email );
651 $comment_author_email = esc_attr( $comment_author_email );
652
653 $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] = $comment_author_email;
654 }
655
656 if ( isset( $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] ) ) {
657 /**
658 * Filters the comment author's URL cookie before it is set.
659 *
660 * When this filter hook is evaluated in wp_filter_comment(),
661 * the comment author's URL string is passed.
662 *
663 * @since 1.5.0
664 *
665 * @param string $author_url_cookie The comment author URL cookie.
666 */
667 $comment_author_url = apply_filters( 'pre_comment_author_url', $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] );
668 $comment_author_url = wp_unslash( $comment_author_url );
669
670 $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] = $comment_author_url;
671 }
672}
673
674/**
675 * Validates whether this comment is allowed to be made.
676 *
677 * @since 2.0.0
678 * @since 4.7.0 The `$avoid_die` parameter was added, allowing the function
679 * to return a WP_Error object instead of dying.
680 * @since 5.5.0 The `$avoid_die` parameter was renamed to `$wp_error`.
681 *
682 * @global wpdb $wpdb WordPress database abstraction object.
683 *
684 * @param array $commentdata Contains information on the comment.
685 * @param bool $wp_error When true, a disallowed comment will result in the function
686 * returning a WP_Error object, rather than executing wp_die().
687 * Default false.
688 * @return int|string|WP_Error Allowed comments return the approval status (0|1|'spam'|'trash').
689 * If `$wp_error` is true, disallowed comments return a WP_Error.
690 */
691function wp_allow_comment( $commentdata, $wp_error = false ) {
692 global $wpdb;
693
694 /*
695 * Simple duplicate check.
696 * expected_slashed ($comment_post_ID, $comment_author, $comment_author_email, $comment_content)
697 */
698 $dupe = $wpdb->prepare(
699 "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_parent = %s AND comment_approved != 'trash' AND ( comment_author = %s ",
700 wp_unslash( $commentdata['comment_post_ID'] ),
701 wp_unslash( $commentdata['comment_parent'] ),
702 wp_unslash( $commentdata['comment_author'] )
703 );
704 if ( $commentdata['comment_author_email'] ) {
705 $dupe .= $wpdb->prepare(
706 'AND comment_author_email = %s ',
707 wp_unslash( $commentdata['comment_author_email'] )
708 );
709 }
710 $dupe .= $wpdb->prepare(
711 ') AND comment_content = %s LIMIT 1',
712 wp_unslash( $commentdata['comment_content'] )
713 );
714
715 $dupe_id = $wpdb->get_var( $dupe );
716
717 /**
718 * Filters the ID, if any, of the duplicate comment found when creating a new comment.
719 *
720 * Return an empty value from this filter to allow what WP considers a duplicate comment.
721 *
722 * @since 4.4.0
723 *
724 * @param int $dupe_id ID of the comment identified as a duplicate.
725 * @param array $commentdata Data for the comment being created.
726 */
727 $dupe_id = apply_filters( 'duplicate_comment_id', $dupe_id, $commentdata );
728
729 if ( $dupe_id ) {
730 /**
731 * Fires immediately after a duplicate comment is detected.
732 *
733 * @since 3.0.0
734 *
735 * @param array $commentdata Comment data.
736 */
737 do_action( 'comment_duplicate_trigger', $commentdata );
738
739 /**
740 * Filters duplicate comment error message.
741 *
742 * @since 5.2.0
743 *
744 * @param string $comment_duplicate_message Duplicate comment error message.
745 */
746 $comment_duplicate_message = apply_filters( 'comment_duplicate_message', __( 'Duplicate comment detected; it looks as though you&#8217;ve already said that!' ) );
747
748 if ( $wp_error ) {
749 return new WP_Error( 'comment_duplicate', $comment_duplicate_message, 409 );
750 } else {
751 if ( wp_doing_ajax() ) {
752 die( $comment_duplicate_message );
753 }
754
755 wp_die( $comment_duplicate_message, 409 );
756 }
757 }
758
759 /**
760 * Fires immediately before a comment is marked approved.
761 *
762 * Allows checking for comment flooding.
763 *
764 * @since 2.3.0
765 * @since 4.7.0 The `$avoid_die` parameter was added.
766 * @since 5.5.0 The `$avoid_die` parameter was renamed to `$wp_error`.
767 *
768 * @param string $comment_author_ip Comment author's IP address.
769 * @param string $comment_author_email Comment author's email.
770 * @param string $comment_date_gmt GMT date the comment was posted.
771 * @param bool $wp_error Whether to return a WP_Error object instead of executing
772 * wp_die() or die() if a comment flood is occurring.
773 */
774 do_action(
775 'check_comment_flood',
776 $commentdata['comment_author_IP'],
777 $commentdata['comment_author_email'],
778 $commentdata['comment_date_gmt'],
779 $wp_error
780 );
781
782 /**
783 * Filters whether a comment is part of a comment flood.
784 *
785 * The default check is wp_check_comment_flood(). See check_comment_flood_db().
786 *
787 * @since 4.7.0
788 * @since 5.5.0 The `$avoid_die` parameter was renamed to `$wp_error`.
789 *
790 * @param bool $is_flood Is a comment flooding occurring? Default false.
791 * @param string $comment_author_ip Comment author's IP address.
792 * @param string $comment_author_email Comment author's email.
793 * @param string $comment_date_gmt GMT date the comment was posted.
794 * @param bool $wp_error Whether to return a WP_Error object instead of executing
795 * wp_die() or die() if a comment flood is occurring.
796 */
797 $is_flood = apply_filters(
798 'wp_is_comment_flood',
799 false,
800 $commentdata['comment_author_IP'],
801 $commentdata['comment_author_email'],
802 $commentdata['comment_date_gmt'],
803 $wp_error
804 );
805
806 if ( $is_flood ) {
807 /** This filter is documented in wp-includes/comment-template.php */
808 $comment_flood_message = apply_filters( 'comment_flood_message', __( 'You are posting comments too quickly. Slow down.' ) );
809
810 return new WP_Error( 'comment_flood', $comment_flood_message, 429 );
811 }
812
813 return wp_check_comment_data( $commentdata );
814}
815
816/**
817 * Hooks WP's native database-based comment-flood check.
818 *
819 * This wrapper maintains backward compatibility with plugins that expect to
820 * be able to unhook the legacy check_comment_flood_db() function from
821 * 'check_comment_flood' using remove_action().
822 *
823 * @since 2.3.0
824 * @since 4.7.0 Converted to be an add_filter() wrapper.
825 */
826function check_comment_flood_db() {
827 add_filter( 'wp_is_comment_flood', 'wp_check_comment_flood', 10, 5 );
828}
829
830/**
831 * Checks whether comment flooding is occurring.
832 *
833 * Won't run, if current user can manage options, so to not block
834 * administrators.
835 *
836 * @since 4.7.0
837 *
838 * @global wpdb $wpdb WordPress database abstraction object.
839 *
840 * @param bool $is_flood Is a comment flooding occurring?
841 * @param string $ip Comment author's IP address.
842 * @param string $email Comment author's email address.
843 * @param string $date MySQL time string.
844 * @param bool $avoid_die When true, a disallowed comment will result in the function
845 * returning without executing wp_die() or die(). Default false.
846 * @return bool Whether comment flooding is occurring.
847 */
848function wp_check_comment_flood( $is_flood, $ip, $email, $date, $avoid_die = false ) {
849 global $wpdb;
850
851 // Another callback has declared a flood. Trust it.
852 if ( true === $is_flood ) {
853 return $is_flood;
854 }
855
856 // Don't throttle admins or moderators.
857 if ( current_user_can( 'manage_options' ) || current_user_can( 'moderate_comments' ) ) {
858 return false;
859 }
860
861 $hour_ago = gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS );
862
863 if ( is_user_logged_in() ) {
864 $user = get_current_user_id();
865 $check_column = '`user_id`';
866 } else {
867 $user = $ip;
868 $check_column = '`comment_author_IP`';
869 }
870
871 $sql = $wpdb->prepare(
872 "SELECT `comment_date_gmt` FROM `$wpdb->comments` WHERE `comment_date_gmt` >= %s AND ( $check_column = %s OR `comment_author_email` = %s ) ORDER BY `comment_date_gmt` DESC LIMIT 1",
873 $hour_ago,
874 $user,
875 $email
876 );
877
878 $lasttime = $wpdb->get_var( $sql );
879
880 if ( $lasttime ) {
881 $time_lastcomment = mysql2date( 'U', $lasttime, false );
882 $time_newcomment = mysql2date( 'U', $date, false );
883
884 /**
885 * Filters the comment flood status.
886 *
887 * @since 2.1.0
888 *
889 * @param bool $bool Whether a comment flood is occurring. Default false.
890 * @param int $time_lastcomment Timestamp of when the last comment was posted.
891 * @param int $time_newcomment Timestamp of when the new comment was posted.
892 */
893 $flood_die = apply_filters( 'comment_flood_filter', false, $time_lastcomment, $time_newcomment );
894
895 if ( $flood_die ) {
896 /**
897 * Fires before the comment flood message is triggered.
898 *
899 * @since 1.5.0
900 *
901 * @param int $time_lastcomment Timestamp of when the last comment was posted.
902 * @param int $time_newcomment Timestamp of when the new comment was posted.
903 */
904 do_action( 'comment_flood_trigger', $time_lastcomment, $time_newcomment );
905
906 if ( $avoid_die ) {
907 return true;
908 } else {
909 /**
910 * Filters the comment flood error message.
911 *
912 * @since 5.2.0
913 *
914 * @param string $comment_flood_message Comment flood error message.
915 */
916 $comment_flood_message = apply_filters( 'comment_flood_message', __( 'You are posting comments too quickly. Slow down.' ) );
917
918 if ( wp_doing_ajax() ) {
919 die( $comment_flood_message );
920 }
921
922 wp_die( $comment_flood_message, 429 );
923 }
924 }
925 }
926
927 return false;
928}
929
930/**
931 * Separates an array of comments into an array keyed by comment_type.
932 *
933 * @since 2.7.0
934 *
935 * @param WP_Comment[] $comments Array of comments.
936 * @return array<string, WP_Comment[]> Array of comments keyed by comment type.
937 */
938function separate_comments( &$comments ) {
939 $comments_by_type = array(
940 'comment' => array(),
941 'trackback' => array(),
942 'pingback' => array(),
943 'pings' => array(),
944 );
945
946 $count = count( $comments );
947
948 for ( $i = 0; $i < $count; $i++ ) {
949 $type = $comments[ $i ]->comment_type;
950
951 if ( empty( $type ) ) {
952 $type = 'comment';
953 }
954
955 $comments_by_type[ $type ][] = &$comments[ $i ];
956
957 if ( 'trackback' === $type || 'pingback' === $type ) {
958 $comments_by_type['pings'][] = &$comments[ $i ];
959 }
960 }
961
962 return $comments_by_type;
963}
964
965/**
966 * Calculates the total number of comment pages.
967 *
968 * @since 2.7.0
969 *
970 * @uses Walker_Comment
971 *
972 * @global WP_Query $wp_query WordPress Query object.
973 *
974 * @param WP_Comment[] $comments Optional. Array of WP_Comment objects. Defaults to `$wp_query->comments`.
975 * @param int $per_page Optional. Comments per page. Defaults to the value of `comments_per_page`
976 * query var, option of the same name, or 1 (in that order).
977 * @param bool $threaded Optional. Control over flat or threaded comments. Defaults to the value
978 * of `thread_comments` option.
979 * @return int Number of comment pages.
980 */
981function get_comment_pages_count( $comments = null, $per_page = null, $threaded = null ) {
982 global $wp_query;
983
984 if ( null === $comments && null === $per_page && null === $threaded && ! empty( $wp_query->max_num_comment_pages ) ) {
985 return $wp_query->max_num_comment_pages;
986 }
987
988 if ( ( ! $comments || ! is_array( $comments ) ) && ! empty( $wp_query->comments ) ) {
989 $comments = $wp_query->comments;
990 }
991
992 if ( empty( $comments ) ) {
993 return 0;
994 }
995
996 if ( ! get_option( 'page_comments' ) ) {
997 return 1;
998 }
999
1000 if ( ! isset( $per_page ) ) {
1001 $per_page = (int) get_query_var( 'comments_per_page' );
1002 }
1003 if ( 0 === $per_page ) {
1004 $per_page = (int) get_option( 'comments_per_page' );
1005 }
1006 if ( 0 === $per_page ) {
1007 return 1;
1008 }
1009
1010 if ( ! isset( $threaded ) ) {
1011 $threaded = get_option( 'thread_comments' );
1012 }
1013
1014 if ( $threaded ) {
1015 $walker = new Walker_Comment();
1016 $count = ceil( $walker->get_number_of_root_elements( $comments ) / $per_page );
1017 } else {
1018 $count = ceil( count( $comments ) / $per_page );
1019 }
1020
1021 return (int) $count;
1022}
1023
1024/**
1025 * Calculates what page number a comment will appear on for comment paging.
1026 *
1027 * @since 2.7.0
1028 *
1029 * @global wpdb $wpdb WordPress database abstraction object.
1030 *
1031 * @param int $comment_id Comment ID.
1032 * @param array $args {
1033 * Array of optional arguments.
1034 *
1035 * @type string $type Limit paginated comments to those matching a given type.
1036 * Accepts 'comment', 'trackback', 'pingback', 'pings'
1037 * (trackbacks and pingbacks), or 'all'. Default 'all'.
1038 * @type int $per_page Per-page count to use when calculating pagination.
1039 * Defaults to the value of the 'comments_per_page' option.
1040 * @type int|string $max_depth If greater than 1, comment page will be determined
1041 * for the top-level parent `$comment_id`.
1042 * Defaults to the value of the 'thread_comments_depth' option.
1043 * }
1044 * @return int|null Comment page number or null on error.
1045 */
1046function get_page_of_comment( $comment_id, $args = array() ) {
1047 global $wpdb;
1048
1049 $page = null;
1050
1051 $comment = get_comment( $comment_id );
1052 if ( ! $comment ) {
1053 return null;
1054 }
1055
1056 $defaults = array(
1057 'type' => 'all',
1058 'page' => '',
1059 'per_page' => '',
1060 'max_depth' => '',
1061 );
1062 $args = wp_parse_args( $args, $defaults );
1063 $original_args = $args;
1064
1065 // Order of precedence: 1. `$args['per_page']`, 2. 'comments_per_page' query_var, 3. 'comments_per_page' option.
1066 if ( get_option( 'page_comments' ) ) {
1067 if ( '' === $args['per_page'] ) {
1068 $args['per_page'] = get_query_var( 'comments_per_page' );
1069 }
1070
1071 if ( '' === $args['per_page'] ) {
1072 $args['per_page'] = get_option( 'comments_per_page' );
1073 }
1074 }
1075
1076 if ( empty( $args['per_page'] ) ) {
1077 $args['per_page'] = 0;
1078 $args['page'] = 0;
1079 }
1080
1081 if ( $args['per_page'] < 1 ) {
1082 $page = 1;
1083 }
1084
1085 if ( null === $page ) {
1086 if ( '' === $args['max_depth'] ) {
1087 if ( get_option( 'thread_comments' ) ) {
1088 $args['max_depth'] = get_option( 'thread_comments_depth' );
1089 } else {
1090 $args['max_depth'] = -1;
1091 }
1092 }
1093
1094 // Find this comment's top-level parent if threading is enabled.
1095 if ( $args['max_depth'] > 1 && '0' !== $comment->comment_parent ) {
1096 return get_page_of_comment( $comment->comment_parent, $args );
1097 }
1098
1099 $comment_args = array(
1100 'type' => $args['type'],
1101 'post_id' => $comment->comment_post_ID,
1102 'fields' => 'ids',
1103 'count' => true,
1104 'status' => 'approve',
1105 'orderby' => 'none',
1106 'parent' => 0,
1107 'date_query' => array(
1108 array(
1109 'column' => "$wpdb->comments.comment_date_gmt",
1110 'before' => $comment->comment_date_gmt,
1111 ),
1112 ),
1113 );
1114
1115 if ( is_user_logged_in() ) {
1116 $comment_args['include_unapproved'] = array( get_current_user_id() );
1117 } else {
1118 $unapproved_email = wp_get_unapproved_comment_author_email();
1119
1120 if ( $unapproved_email ) {
1121 $comment_args['include_unapproved'] = array( $unapproved_email );
1122 }
1123 }
1124
1125 /**
1126 * Filters the arguments used to query comments in get_page_of_comment().
1127 *
1128 * @since 5.5.0
1129 *
1130 * @see WP_Comment_Query::__construct()
1131 *
1132 * @param array $comment_args {
1133 * Array of WP_Comment_Query arguments.
1134 *
1135 * @type string $type Limit paginated comments to those matching a given type.
1136 * Accepts 'comment', 'trackback', 'pingback', 'pings'
1137 * (trackbacks and pingbacks), or 'all'. Default 'all'.
1138 * @type int $post_id ID of the post.
1139 * @type string $fields Comment fields to return.
1140 * @type bool $count Whether to return a comment count (true) or array
1141 * of comment objects (false).
1142 * @type string $status Comment status.
1143 * @type int $parent Parent ID of comment to retrieve children of.
1144 * @type array $date_query Date query clauses to limit comments by. See WP_Date_Query.
1145 * @type array $include_unapproved Array of IDs or email addresses whose unapproved comments
1146 * will be included in paginated comments.
1147 * }
1148 */
1149 $comment_args = apply_filters( 'get_page_of_comment_query_args', $comment_args );
1150
1151 $comment_query = new WP_Comment_Query();
1152 $older_comment_count = $comment_query->query( $comment_args );
1153
1154 // No older comments? Then it's page #1.
1155 if ( 0 === $older_comment_count ) {
1156 $page = 1;
1157
1158 // Divide comments older than this one by comments per page to get this comment's page number.
1159 } else {
1160 $page = (int) ceil( ( $older_comment_count + 1 ) / $args['per_page'] );
1161 }
1162 }
1163
1164 /**
1165 * Filters the calculated page on which a comment appears.
1166 *
1167 * @since 4.4.0
1168 * @since 4.7.0 Introduced the `$comment_id` parameter.
1169 *
1170 * @param int $page Comment page.
1171 * @param array $args {
1172 * Arguments used to calculate pagination. These include arguments auto-detected by the function,
1173 * based on query vars, system settings, etc. For pristine arguments passed to the function,
1174 * see `$original_args`.
1175 *
1176 * @type string $type Type of comments to count.
1177 * @type int $page Calculated current page.
1178 * @type int $per_page Calculated number of comments per page.
1179 * @type int $max_depth Maximum comment threading depth allowed.
1180 * }
1181 * @param array $original_args {
1182 * Array of arguments passed to the function. Some or all of these may not be set.
1183 *
1184 * @type string $type Type of comments to count.
1185 * @type int $page Current comment page.
1186 * @type int $per_page Number of comments per page.
1187 * @type int $max_depth Maximum comment threading depth allowed.
1188 * }
1189 * @param int $comment_id ID of the comment.
1190 */
1191 return apply_filters( 'get_page_of_comment', (int) $page, $args, $original_args, $comment_id );
1192}
1193
1194/**
1195 * Retrieves the maximum character lengths for the comment form fields.
1196 *
1197 * @since 4.5.0
1198 *
1199 * @global wpdb $wpdb WordPress database abstraction object.
1200 *
1201 * @return int[] Array of maximum lengths keyed by field name.
1202 */
1203function wp_get_comment_fields_max_lengths() {
1204 global $wpdb;
1205
1206 $lengths = array(
1207 'comment_author' => 245,
1208 'comment_author_email' => 100,
1209 'comment_author_url' => 200,
1210 'comment_content' => 65525,
1211 );
1212
1213 if ( $wpdb->is_mysql ) {
1214 foreach ( $lengths as $column => $length ) {
1215 $col_length = $wpdb->get_col_length( $wpdb->comments, $column );
1216 $max_length = 0;
1217
1218 // No point if we can't get the DB column lengths.
1219 if ( is_wp_error( $col_length ) ) {
1220 break;
1221 }
1222
1223 if ( ! is_array( $col_length ) && (int) $col_length > 0 ) {
1224 $max_length = (int) $col_length;
1225 } elseif ( is_array( $col_length ) && isset( $col_length['length'] ) && (int) $col_length['length'] > 0 ) {
1226 $max_length = (int) $col_length['length'];
1227
1228 if ( ! empty( $col_length['type'] ) && 'byte' === $col_length['type'] ) {
1229 $max_length = $max_length - 10;
1230 }
1231 }
1232
1233 if ( $max_length > 0 ) {
1234 $lengths[ $column ] = $max_length;
1235 }
1236 }
1237 }
1238
1239 /**
1240 * Filters the lengths for the comment form fields.
1241 *
1242 * @since 4.5.0
1243 *
1244 * @param int[] $lengths Array of maximum lengths keyed by field name.
1245 */
1246 return apply_filters( 'wp_get_comment_fields_max_lengths', $lengths );
1247}
1248
1249/**
1250 * Compares the lengths of comment data against the maximum character limits.
1251 *
1252 * @since 4.7.0
1253 *
1254 * @param array $comment_data Array of arguments for inserting a comment.
1255 * @return WP_Error|true WP_Error when a comment field exceeds the limit,
1256 * otherwise true.
1257 */
1258function wp_check_comment_data_max_lengths( $comment_data ) {
1259 $max_lengths = wp_get_comment_fields_max_lengths();
1260
1261 if ( isset( $comment_data['comment_author'] ) && mb_strlen( $comment_data['comment_author'], '8bit' ) > $max_lengths['comment_author'] ) {
1262 return new WP_Error( 'comment_author_column_length', __( '<strong>Error:</strong> Your name is too long.' ), 200 );
1263 }
1264
1265 if ( isset( $comment_data['comment_author_email'] ) && strlen( $comment_data['comment_author_email'] ) > $max_lengths['comment_author_email'] ) {
1266 return new WP_Error( 'comment_author_email_column_length', __( '<strong>Error:</strong> Your email address is too long.' ), 200 );
1267 }
1268
1269 if ( isset( $comment_data['comment_author_url'] ) && strlen( $comment_data['comment_author_url'] ) > $max_lengths['comment_author_url'] ) {
1270 return new WP_Error( 'comment_author_url_column_length', __( '<strong>Error:</strong> Your URL is too long.' ), 200 );
1271 }
1272
1273 if ( isset( $comment_data['comment_content'] ) && mb_strlen( $comment_data['comment_content'], '8bit' ) > $max_lengths['comment_content'] ) {
1274 return new WP_Error( 'comment_content_column_length', __( '<strong>Error:</strong> Your comment is too long.' ), 200 );
1275 }
1276
1277 return true;
1278}
1279
1280/**
1281 * Checks whether comment data passes internal checks or has disallowed content.
1282 *
1283 * @since 6.7.0
1284 *
1285 * @global wpdb $wpdb WordPress database abstraction object.
1286 *
1287 * @param array $comment_data Array of arguments for inserting a comment.
1288 * @return int|string|WP_Error The approval status on success (0|1|'spam'|'trash'),
1289 * WP_Error otherwise.
1290 */
1291function wp_check_comment_data( $comment_data ) {
1292 global $wpdb;
1293
1294 if ( ! empty( $comment_data['user_id'] ) ) {
1295 $user = get_userdata( $comment_data['user_id'] );
1296 $post_author = (int) $wpdb->get_var(
1297 $wpdb->prepare(
1298 "SELECT post_author FROM $wpdb->posts WHERE ID = %d LIMIT 1",
1299 $comment_data['comment_post_ID']
1300 )
1301 );
1302 }
1303
1304 if ( isset( $user ) && ( $comment_data['user_id'] === $post_author || $user->has_cap( 'moderate_comments' ) ) ) {
1305 // The author and the admins get respect.
1306 $approved = 1;
1307 } else {
1308 // Everyone else's comments will be checked.
1309 if ( check_comment(
1310 $comment_data['comment_author'],
1311 $comment_data['comment_author_email'],
1312 $comment_data['comment_author_url'],
1313 $comment_data['comment_content'],
1314 $comment_data['comment_author_IP'],
1315 $comment_data['comment_agent'],
1316 $comment_data['comment_type']
1317 ) ) {
1318 $approved = 1;
1319 } else {
1320 $approved = 0;
1321 }
1322
1323 if ( wp_check_comment_disallowed_list(
1324 $comment_data['comment_author'],
1325 $comment_data['comment_author_email'],
1326 $comment_data['comment_author_url'],
1327 $comment_data['comment_content'],
1328 $comment_data['comment_author_IP'],
1329 $comment_data['comment_agent']
1330 ) ) {
1331 $approved = EMPTY_TRASH_DAYS ? 'trash' : 'spam';
1332 }
1333 }
1334
1335 /**
1336 * Filters a comment's approval status before it is set.
1337 *
1338 * @since 2.1.0
1339 * @since 4.9.0 Returning a WP_Error value from the filter will short-circuit comment insertion
1340 * and allow skipping further processing.
1341 *
1342 * @param int|string|WP_Error $approved The approval status. Accepts 1, 0, 'spam', 'trash',
1343 * or WP_Error.
1344 * @param array $commentdata Comment data.
1345 */
1346 return apply_filters( 'pre_comment_approved', $approved, $comment_data );
1347}
1348
1349/**
1350 * Checks if a comment contains disallowed characters or words.
1351 *
1352 * @since 5.5.0
1353 *
1354 * @param string $author The author of the comment.
1355 * @param string $email The email of the comment.
1356 * @param string $url The url used in the comment.
1357 * @param string $comment The comment content.
1358 * @param string $user_ip The comment author's IP address.
1359 * @param string $user_agent The author's browser user agent.
1360 * @return bool True if the comment contains disallowed content, false otherwise.
1361 */
1362function wp_check_comment_disallowed_list( $author, $email, $url, $comment, $user_ip, $user_agent ) {
1363 /**
1364 * Fires before the comment is tested for disallowed characters or words.
1365 *
1366 * @since 1.5.0
1367 * @deprecated 5.5.0 Use {@see 'wp_check_comment_disallowed_list'} instead.
1368 *
1369 * @param string $author Comment author.
1370 * @param string $email Comment author's email.
1371 * @param string $url Comment author's URL.
1372 * @param string $comment Comment content.
1373 * @param string $user_ip Comment author's IP address.
1374 * @param string $user_agent Comment author's browser user agent.
1375 */
1376 do_action_deprecated(
1377 'wp_blacklist_check',
1378 array( $author, $email, $url, $comment, $user_ip, $user_agent ),
1379 '5.5.0',
1380 'wp_check_comment_disallowed_list',
1381 __( 'Please consider writing more inclusive code.' )
1382 );
1383
1384 /**
1385 * Fires before the comment is tested for disallowed characters or words.
1386 *
1387 * @since 5.5.0
1388 *
1389 * @param string $author Comment author.
1390 * @param string $email Comment author's email.
1391 * @param string $url Comment author's URL.
1392 * @param string $comment Comment content.
1393 * @param string $user_ip Comment author's IP address.
1394 * @param string $user_agent Comment author's browser user agent.
1395 */
1396 do_action( 'wp_check_comment_disallowed_list', $author, $email, $url, $comment, $user_ip, $user_agent );
1397
1398 $mod_keys = trim( get_option( 'disallowed_keys' ) );
1399 if ( '' === $mod_keys ) {
1400 return false; // If moderation keys are empty.
1401 }
1402
1403 // Ensure HTML tags are not being used to bypass the list of disallowed characters and words.
1404 $comment_without_html = wp_strip_all_tags( $comment );
1405
1406 $words = explode( "\n", $mod_keys );
1407
1408 foreach ( (array) $words as $word ) {
1409 $word = trim( $word );
1410
1411 // Skip empty lines.
1412 if ( empty( $word ) ) {
1413 continue; }
1414
1415 // Do some escaping magic so that '#' chars in the spam words don't break things:
1416 $word = preg_quote( $word, '#' );
1417
1418 $pattern = "#$word#iu";
1419 if ( preg_match( $pattern, $author )
1420 || preg_match( $pattern, $email )
1421 || preg_match( $pattern, $url )
1422 || preg_match( $pattern, $comment )
1423 || preg_match( $pattern, $comment_without_html )
1424 || preg_match( $pattern, $user_ip )
1425 || preg_match( $pattern, $user_agent )
1426 ) {
1427 return true;
1428 }
1429 }
1430 return false;
1431}
1432
1433/**
1434 * Retrieves the total comment counts for the whole site or a single post.
1435 *
1436 * The comment stats are cached and then retrieved, if they already exist in the
1437 * cache.
1438 *
1439 * @see get_comment_count() Which handles fetching the live comment counts.
1440 *
1441 * @since 2.5.0
1442 *
1443 * @param int $post_id Optional. Restrict the comment counts to the given post. Default 0, which indicates that
1444 * comment counts for the whole site will be retrieved.
1445 * @return stdClass {
1446 * The number of comments keyed by their status.
1447 *
1448 * @type int $approved The number of approved comments.
1449 * @type int $moderated The number of comments awaiting moderation (a.k.a. pending).
1450 * @type int $spam The number of spam comments.
1451 * @type int $trash The number of trashed comments.
1452 * @type int $post-trashed The number of comments for posts that are in the trash.
1453 * @type int $total_comments The total number of non-trashed comments, including spam.
1454 * @type int $all The total number of pending or approved comments.
1455 * }
1456 */
1457function wp_count_comments( $post_id = 0 ) {
1458 $post_id = (int) $post_id;
1459
1460 /**
1461 * Filters the comments count for a given post or the whole site.
1462 *
1463 * @since 2.7.0
1464 *
1465 * @param array|stdClass $count An empty array or an object containing comment counts.
1466 * @param int $post_id The post ID. Can be 0 to represent the whole site.
1467 */
1468 $filtered = apply_filters( 'wp_count_comments', array(), $post_id );
1469 if ( ! empty( $filtered ) ) {
1470 return $filtered;
1471 }
1472
1473 $count = wp_cache_get( "comments-{$post_id}", 'counts' );
1474 if ( false !== $count ) {
1475 return $count;
1476 }
1477
1478 $stats = get_comment_count( $post_id );
1479 $stats['moderated'] = $stats['awaiting_moderation'];
1480 unset( $stats['awaiting_moderation'] );
1481
1482 $stats_object = (object) $stats;
1483 wp_cache_set( "comments-{$post_id}", $stats_object, 'counts' );
1484
1485 return $stats_object;
1486}
1487
1488/**
1489 * Trashes or deletes a comment.
1490 *
1491 * The comment is moved to Trash instead of permanently deleted unless Trash is
1492 * disabled, item is already in the Trash, or $force_delete is true.
1493 *
1494 * The post comment count will be updated if the comment was approved and has a
1495 * post ID available.
1496 *
1497 * @since 2.0.0
1498 *
1499 * @global wpdb $wpdb WordPress database abstraction object.
1500 *
1501 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object.
1502 * @param bool $force_delete Whether to bypass Trash and force deletion. Default false.
1503 * @return bool True on success, false on failure.
1504 */
1505function wp_delete_comment( $comment_id, $force_delete = false ) {
1506 global $wpdb;
1507
1508 $comment = get_comment( $comment_id );
1509 if ( ! $comment ) {
1510 return false;
1511 }
1512
1513 if ( ! $force_delete && EMPTY_TRASH_DAYS && ! in_array( wp_get_comment_status( $comment ), array( 'trash', 'spam' ), true ) ) {
1514 return wp_trash_comment( $comment_id );
1515 }
1516
1517 /**
1518 * Fires immediately before a comment is deleted from the database.
1519 *
1520 * @since 1.2.0
1521 * @since 4.9.0 Added the `$comment` parameter.
1522 *
1523 * @param string $comment_id The comment ID as a numeric string.
1524 * @param WP_Comment $comment The comment to be deleted.
1525 */
1526 do_action( 'delete_comment', $comment->comment_ID, $comment );
1527
1528 // Move children up a level.
1529 $children = $wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_parent = %d", $comment->comment_ID ) );
1530 if ( ! empty( $children ) ) {
1531 $wpdb->update( $wpdb->comments, array( 'comment_parent' => $comment->comment_parent ), array( 'comment_parent' => $comment->comment_ID ) );
1532 clean_comment_cache( $children );
1533 }
1534
1535 // Delete metadata.
1536 $meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->commentmeta WHERE comment_id = %d", $comment->comment_ID ) );
1537 foreach ( $meta_ids as $mid ) {
1538 delete_metadata_by_mid( 'comment', $mid );
1539 }
1540
1541 if ( ! $wpdb->delete( $wpdb->comments, array( 'comment_ID' => $comment->comment_ID ) ) ) {
1542 return false;
1543 }
1544
1545 /**
1546 * Fires immediately after a comment is deleted from the database.
1547 *
1548 * @since 2.9.0
1549 * @since 4.9.0 Added the `$comment` parameter.
1550 *
1551 * @param string $comment_id The comment ID as a numeric string.
1552 * @param WP_Comment $comment The deleted comment.
1553 */
1554 do_action( 'deleted_comment', $comment->comment_ID, $comment );
1555
1556 $post_id = $comment->comment_post_ID;
1557 if ( $post_id && '1' === $comment->comment_approved ) {
1558 wp_update_comment_count( $post_id );
1559 }
1560
1561 clean_comment_cache( $comment->comment_ID );
1562
1563 /** This action is documented in wp-includes/comment.php */
1564 do_action( 'wp_set_comment_status', $comment->comment_ID, 'delete' );
1565
1566 wp_transition_comment_status( 'delete', $comment->comment_approved, $comment );
1567
1568 return true;
1569}
1570
1571/**
1572 * Moves a comment to the Trash
1573 *
1574 * If Trash is disabled, comment is permanently deleted.
1575 *
1576 * @since 2.9.0
1577 * @since 6.9.0 Any child notes are deleted when deleting a note.
1578 *
1579 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object.
1580 * @return bool True on success, false on failure.
1581 */
1582function wp_trash_comment( $comment_id ) {
1583 if ( ! EMPTY_TRASH_DAYS ) {
1584 $comment = get_comment( $comment_id );
1585 $success = wp_delete_comment( $comment_id, true );
1586
1587 if ( ! $success ) {
1588 return false;
1589 }
1590
1591 // Also delete children of top level 'note' type comments.
1592 if ( $comment && 'note' === $comment->comment_type && 0 === (int) $comment->comment_parent ) {
1593 $children = $comment->get_children(
1594 array(
1595 'fields' => 'ids',
1596 'status' => 'all',
1597 'type' => 'note',
1598 )
1599 );
1600
1601 foreach ( $children as $child_id ) {
1602 if ( ! wp_delete_comment( $child_id, true ) ) {
1603 $success = false;
1604 }
1605 }
1606 }
1607
1608 return $success;
1609 }
1610
1611 $comment = get_comment( $comment_id );
1612 if ( ! $comment ) {
1613 return false;
1614 }
1615
1616 /**
1617 * Fires immediately before a comment is sent to the Trash.
1618 *
1619 * @since 2.9.0
1620 * @since 4.9.0 Added the `$comment` parameter.
1621 *
1622 * @param string $comment_id The comment ID as a numeric string.
1623 * @param WP_Comment $comment The comment to be trashed.
1624 */
1625 do_action( 'trash_comment', $comment->comment_ID, $comment );
1626
1627 if ( wp_set_comment_status( $comment, 'trash' ) ) {
1628 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_status' );
1629 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_time' );
1630 add_comment_meta( $comment->comment_ID, '_wp_trash_meta_status', $comment->comment_approved );
1631 add_comment_meta( $comment->comment_ID, '_wp_trash_meta_time', time() );
1632
1633 /**
1634 * Fires immediately after a comment is sent to Trash.
1635 *
1636 * @since 2.9.0
1637 * @since 4.9.0 Added the `$comment` parameter.
1638 *
1639 * @param string $comment_id The comment ID as a numeric string.
1640 * @param WP_Comment $comment The trashed comment.
1641 */
1642 do_action( 'trashed_comment', $comment->comment_ID, $comment );
1643
1644 // For top level 'note' type comments, also trash children.
1645 if ( 'note' === $comment->comment_type && 0 === (int) $comment->comment_parent ) {
1646 $children = $comment->get_children(
1647 array(
1648 'fields' => 'ids',
1649 'status' => 'all',
1650 'type' => 'note',
1651 )
1652 );
1653
1654 $success = true;
1655 foreach ( $children as $child_id ) {
1656 if ( ! wp_trash_comment( $child_id ) ) {
1657 $success = false;
1658 }
1659 }
1660 return $success;
1661 }
1662
1663 return true;
1664 }
1665
1666 return false;
1667}
1668
1669/**
1670 * Removes a comment from the Trash
1671 *
1672 * @since 2.9.0
1673 *
1674 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object.
1675 * @return bool True on success, false on failure.
1676 */
1677function wp_untrash_comment( $comment_id ) {
1678 $comment = get_comment( $comment_id );
1679 if ( ! $comment ) {
1680 return false;
1681 }
1682
1683 /**
1684 * Fires immediately before a comment is restored from the Trash.
1685 *
1686 * @since 2.9.0
1687 * @since 4.9.0 Added the `$comment` parameter.
1688 *
1689 * @param string $comment_id The comment ID as a numeric string.
1690 * @param WP_Comment $comment The comment to be untrashed.
1691 */
1692 do_action( 'untrash_comment', $comment->comment_ID, $comment );
1693
1694 $status = (string) get_comment_meta( $comment->comment_ID, '_wp_trash_meta_status', true );
1695 if ( empty( $status ) ) {
1696 $status = '0';
1697 }
1698
1699 if ( wp_set_comment_status( $comment, $status ) ) {
1700 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_time' );
1701 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_status' );
1702
1703 /**
1704 * Fires immediately after a comment is restored from the Trash.
1705 *
1706 * @since 2.9.0
1707 * @since 4.9.0 Added the `$comment` parameter.
1708 *
1709 * @param string $comment_id The comment ID as a numeric string.
1710 * @param WP_Comment $comment The untrashed comment.
1711 */
1712 do_action( 'untrashed_comment', $comment->comment_ID, $comment );
1713
1714 return true;
1715 }
1716
1717 return false;
1718}
1719
1720/**
1721 * Marks a comment as Spam.
1722 *
1723 * @since 2.9.0
1724 *
1725 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object.
1726 * @return bool True on success, false on failure.
1727 */
1728function wp_spam_comment( $comment_id ) {
1729 $comment = get_comment( $comment_id );
1730 if ( ! $comment ) {
1731 return false;
1732 }
1733
1734 /**
1735 * Fires immediately before a comment is marked as Spam.
1736 *
1737 * @since 2.9.0
1738 * @since 4.9.0 Added the `$comment` parameter.
1739 *
1740 * @param int $comment_id The comment ID.
1741 * @param WP_Comment $comment The comment to be marked as spam.
1742 */
1743 do_action( 'spam_comment', $comment->comment_ID, $comment );
1744
1745 if ( wp_set_comment_status( $comment, 'spam' ) ) {
1746 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_status' );
1747 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_time' );
1748 add_comment_meta( $comment->comment_ID, '_wp_trash_meta_status', $comment->comment_approved );
1749 add_comment_meta( $comment->comment_ID, '_wp_trash_meta_time', time() );
1750
1751 /**
1752 * Fires immediately after a comment is marked as Spam.
1753 *
1754 * @since 2.9.0
1755 * @since 4.9.0 Added the `$comment` parameter.
1756 *
1757 * @param int $comment_id The comment ID.
1758 * @param WP_Comment $comment The comment marked as spam.
1759 */
1760 do_action( 'spammed_comment', $comment->comment_ID, $comment );
1761
1762 return true;
1763 }
1764
1765 return false;
1766}
1767
1768/**
1769 * Removes a comment from the Spam.
1770 *
1771 * @since 2.9.0
1772 *
1773 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object.
1774 * @return bool True on success, false on failure.
1775 */
1776function wp_unspam_comment( $comment_id ) {
1777 $comment = get_comment( $comment_id );
1778 if ( ! $comment ) {
1779 return false;
1780 }
1781
1782 /**
1783 * Fires immediately before a comment is unmarked as Spam.
1784 *
1785 * @since 2.9.0
1786 * @since 4.9.0 Added the `$comment` parameter.
1787 *
1788 * @param string $comment_id The comment ID as a numeric string.
1789 * @param WP_Comment $comment The comment to be unmarked as spam.
1790 */
1791 do_action( 'unspam_comment', $comment->comment_ID, $comment );
1792
1793 $status = (string) get_comment_meta( $comment->comment_ID, '_wp_trash_meta_status', true );
1794 if ( empty( $status ) ) {
1795 $status = '0';
1796 }
1797
1798 if ( wp_set_comment_status( $comment, $status ) ) {
1799 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_status' );
1800 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_time' );
1801
1802 /**
1803 * Fires immediately after a comment is unmarked as Spam.
1804 *
1805 * @since 2.9.0
1806 * @since 4.9.0 Added the `$comment` parameter.
1807 *
1808 * @param string $comment_id The comment ID as a numeric string.
1809 * @param WP_Comment $comment The comment unmarked as spam.
1810 */
1811 do_action( 'unspammed_comment', $comment->comment_ID, $comment );
1812
1813 return true;
1814 }
1815
1816 return false;
1817}
1818
1819/**
1820 * Retrieves the status of a comment by comment ID.
1821 *
1822 * @since 1.0.0
1823 *
1824 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object
1825 * @return string|false Status might be 'trash', 'approved', 'unapproved', 'spam'. False on failure.
1826 */
1827function wp_get_comment_status( $comment_id ) {
1828 $comment = get_comment( $comment_id );
1829 if ( ! $comment ) {
1830 return false;
1831 }
1832
1833 $approved = $comment->comment_approved;
1834
1835 if ( null === $approved ) {
1836 return false;
1837 } elseif ( '1' === $approved ) {
1838 return 'approved';
1839 } elseif ( '0' === $approved ) {
1840 return 'unapproved';
1841 } elseif ( 'spam' === $approved ) {
1842 return 'spam';
1843 } elseif ( 'trash' === $approved ) {
1844 return 'trash';
1845 } else {
1846 return false;
1847 }
1848}
1849
1850/**
1851 * Calls hooks for when a comment status transition occurs.
1852 *
1853 * Calls hooks for comment status transitions. If the new comment status is not the same
1854 * as the previous comment status, then two hooks will be ran, the first is
1855 * {@see 'transition_comment_status'} with new status, old status, and comment data.
1856 * The next action called is {@see 'comment_$old_status_to_$new_status'}. It has
1857 * the comment data.
1858 *
1859 * The final action will run whether or not the comment statuses are the same.
1860 * The action is named {@see 'comment_$new_status_$comment->comment_type'}.
1861 *
1862 * @since 2.7.0
1863 *
1864 * @param string $new_status New comment status.
1865 * @param string $old_status Previous comment status.
1866 * @param WP_Comment $comment Comment object.
1867 */
1868function wp_transition_comment_status( $new_status, $old_status, $comment ) {
1869 /*
1870 * Translate raw statuses to human-readable formats for the hooks.
1871 * This is not a complete list of comment status, it's only the ones
1872 * that need to be renamed.
1873 */
1874 $comment_statuses = array(
1875 0 => 'unapproved',
1876 'hold' => 'unapproved', // wp_set_comment_status() uses "hold".
1877 1 => 'approved',
1878 'approve' => 'approved', // wp_set_comment_status() uses "approve".
1879 );
1880 if ( isset( $comment_statuses[ $new_status ] ) ) {
1881 $new_status = $comment_statuses[ $new_status ];
1882 }
1883 if ( isset( $comment_statuses[ $old_status ] ) ) {
1884 $old_status = $comment_statuses[ $old_status ];
1885 }
1886
1887 // Call the hooks.
1888 if ( $new_status !== $old_status ) {
1889 /**
1890 * Fires when the comment status is in transition.
1891 *
1892 * @since 2.7.0
1893 *
1894 * @param string $new_status The new comment status.
1895 * @param string $old_status The old comment status.
1896 * @param WP_Comment $comment Comment object.
1897 */
1898 do_action( 'transition_comment_status', $new_status, $old_status, $comment );
1899
1900 /**
1901 * Fires when the comment status is in transition from one specific status to another.
1902 *
1903 * The dynamic portions of the hook name, `$old_status`, and `$new_status`,
1904 * refer to the old and new comment statuses, respectively.
1905 *
1906 * Possible hook names include:
1907 *
1908 * - `comment_unapproved_to_approved`
1909 * - `comment_spam_to_approved`
1910 * - `comment_approved_to_unapproved`
1911 * - `comment_spam_to_unapproved`
1912 * - `comment_unapproved_to_spam`
1913 * - `comment_approved_to_spam`
1914 *
1915 * @since 2.7.0
1916 *
1917 * @param WP_Comment $comment Comment object.
1918 */
1919 do_action( "comment_{$old_status}_to_{$new_status}", $comment );
1920 }
1921 /**
1922 * Fires when the status of a specific comment type is in transition.
1923 *
1924 * The dynamic portions of the hook name, `$new_status`, and `$comment->comment_type`,
1925 * refer to the new comment status, and the type of comment, respectively.
1926 *
1927 * Typical comment types include 'comment', 'pingback', or 'trackback'.
1928 *
1929 * Possible hook names include:
1930 *
1931 * - `comment_approved_comment`
1932 * - `comment_approved_pingback`
1933 * - `comment_approved_trackback`
1934 * - `comment_unapproved_comment`
1935 * - `comment_unapproved_pingback`
1936 * - `comment_unapproved_trackback`
1937 * - `comment_spam_comment`
1938 * - `comment_spam_pingback`
1939 * - `comment_spam_trackback`
1940 *
1941 * @since 2.7.0
1942 *
1943 * @param string $comment_id The comment ID as a numeric string.
1944 * @param WP_Comment $comment Comment object.
1945 */
1946 do_action( "comment_{$new_status}_{$comment->comment_type}", $comment->comment_ID, $comment );
1947}
1948
1949/**
1950 * Clears the lastcommentmodified cached value when a comment status is changed.
1951 *
1952 * Deletes the lastcommentmodified cache key when a comment enters or leaves
1953 * 'approved' status.
1954 *
1955 * @since 4.7.0
1956 * @access private
1957 *
1958 * @param string $new_status The new comment status.
1959 * @param string $old_status The old comment status.
1960 */
1961function _clear_modified_cache_on_transition_comment_status( $new_status, $old_status ) {
1962 if ( 'approved' === $new_status || 'approved' === $old_status ) {
1963 $data = array();
1964 foreach ( array( 'server', 'gmt', 'blog' ) as $timezone ) {
1965 $data[] = "lastcommentmodified:$timezone";
1966 }
1967 wp_cache_delete_multiple( $data, 'timeinfo' );
1968 }
1969}
1970
1971/**
1972 * Gets current commenter's name, email, and URL.
1973 *
1974 * Expects cookies content to already be sanitized. User of this function might
1975 * wish to recheck the returned array for validity.
1976 *
1977 * @see sanitize_comment_cookies() Use to sanitize cookies
1978 *
1979 * @since 2.0.4
1980 *
1981 * @return array {
1982 * An array of current commenter variables.
1983 *
1984 * @type string $comment_author The name of the current commenter, or an empty string.
1985 * @type string $comment_author_email The email address of the current commenter, or an empty string.
1986 * @type string $comment_author_url The URL address of the current commenter, or an empty string.
1987 * }
1988 */
1989function wp_get_current_commenter() {
1990 // Cookies should already be sanitized.
1991
1992 $comment_author = '';
1993 if ( isset( $_COOKIE[ 'comment_author_' . COOKIEHASH ] ) ) {
1994 $comment_author = $_COOKIE[ 'comment_author_' . COOKIEHASH ];
1995 }
1996
1997 $comment_author_email = '';
1998 if ( isset( $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] ) ) {
1999 $comment_author_email = $_COOKIE[ 'comment_author_email_' . COOKIEHASH ];
2000 }
2001
2002 $comment_author_url = '';
2003 if ( isset( $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] ) ) {
2004 $comment_author_url = $_COOKIE[ 'comment_author_url_' . COOKIEHASH ];
2005 }
2006
2007 /**
2008 * Filters the current commenter's name, email, and URL.
2009 *
2010 * @since 3.1.0
2011 *
2012 * @param array $comment_author_data {
2013 * An array of current commenter variables.
2014 *
2015 * @type string $comment_author The name of the current commenter, or an empty string.
2016 * @type string $comment_author_email The email address of the current commenter, or an empty string.
2017 * @type string $comment_author_url The URL address of the current commenter, or an empty string.
2018 * }
2019 */
2020 return apply_filters( 'wp_get_current_commenter', compact( 'comment_author', 'comment_author_email', 'comment_author_url' ) );
2021}
2022
2023/**
2024 * Gets unapproved comment author's email.
2025 *
2026 * Used to allow the commenter to see their pending comment.
2027 *
2028 * @since 5.1.0
2029 * @since 5.7.0 The window within which the author email for an unapproved comment
2030 * can be retrieved was extended to 10 minutes.
2031 *
2032 * @return string The unapproved comment author's email (when supplied).
2033 */
2034function wp_get_unapproved_comment_author_email() {
2035 $commenter_email = '';
2036
2037 if ( ! empty( $_GET['unapproved'] ) && ! empty( $_GET['moderation-hash'] ) ) {
2038 $comment_id = (int) $_GET['unapproved'];
2039 $comment = get_comment( $comment_id );
2040
2041 if ( $comment && hash_equals( $_GET['moderation-hash'], wp_hash( $comment->comment_date_gmt ) ) ) {
2042 // The comment will only be viewable by the comment author for 10 minutes.
2043 $comment_preview_expires = strtotime( $comment->comment_date_gmt . '+10 minutes' );
2044
2045 if ( time() < $comment_preview_expires ) {
2046 $commenter_email = $comment->comment_author_email;
2047 }
2048 }
2049 }
2050
2051 if ( ! $commenter_email ) {
2052 $commenter = wp_get_current_commenter();
2053 $commenter_email = $commenter['comment_author_email'];
2054 }
2055
2056 return $commenter_email;
2057}
2058
2059/**
2060 * Inserts a comment into the database.
2061 *
2062 * @since 2.0.0
2063 * @since 4.4.0 Introduced the `$comment_meta` argument.
2064 * @since 5.5.0 Default value for `$comment_type` argument changed to `comment`.
2065 *
2066 * @global wpdb $wpdb WordPress database abstraction object.
2067 *
2068 * @param array $commentdata {
2069 * Array of arguments for inserting a new comment.
2070 *
2071 * @type string $comment_agent The HTTP user agent of the `$comment_author` when
2072 * the comment was submitted. Default empty.
2073 * @type int|string $comment_approved Whether the comment has been approved. Default 1.
2074 * @type string $comment_author The name of the author of the comment. Default empty.
2075 * @type string $comment_author_email The email address of the `$comment_author`. Default empty.
2076 * @type string $comment_author_IP The IP address of the `$comment_author`. Default empty.
2077 * @type string $comment_author_url The URL address of the `$comment_author`. Default empty.
2078 * @type string $comment_content The content of the comment. Default empty.
2079 * @type string $comment_date The date the comment was submitted. To set the date
2080 * manually, `$comment_date_gmt` must also be specified.
2081 * Default is the current time.
2082 * @type string $comment_date_gmt The date the comment was submitted in the GMT timezone.
2083 * Default is `$comment_date` in the site's GMT timezone.
2084 * @type int $comment_karma The karma of the comment. Default 0.
2085 * @type int $comment_parent ID of this comment's parent, if any. Default 0.
2086 * @type int $comment_post_ID ID of the post that relates to the comment, if any.
2087 * Default 0.
2088 * @type string $comment_type Comment type. Default 'comment'.
2089 * @type array $comment_meta Optional. Array of key/value pairs to be stored in commentmeta for the
2090 * new comment.
2091 * @type int $user_id ID of the user who submitted the comment. Default 0.
2092 * }
2093 * @return int|false The new comment's ID on success, false on failure.
2094 */
2095function wp_insert_comment( $commentdata ) {
2096 global $wpdb;
2097
2098 $data = wp_unslash( $commentdata );
2099
2100 $comment_author = ! isset( $data['comment_author'] ) ? '' : $data['comment_author'];
2101 $comment_author_email = ! isset( $data['comment_author_email'] ) ? '' : $data['comment_author_email'];
2102 $comment_author_url = ! isset( $data['comment_author_url'] ) ? '' : $data['comment_author_url'];
2103 $comment_author_ip = ! isset( $data['comment_author_IP'] ) ? '' : $data['comment_author_IP'];
2104
2105 $comment_date = ! isset( $data['comment_date'] ) ? current_time( 'mysql' ) : $data['comment_date'];
2106 $comment_date_gmt = ! isset( $data['comment_date_gmt'] ) ? get_gmt_from_date( $comment_date ) : $data['comment_date_gmt'];
2107
2108 $comment_post_id = ! isset( $data['comment_post_ID'] ) ? 0 : $data['comment_post_ID'];
2109 $comment_content = ! isset( $data['comment_content'] ) ? '' : $data['comment_content'];
2110 $comment_karma = ! isset( $data['comment_karma'] ) ? 0 : $data['comment_karma'];
2111 $comment_approved = ! isset( $data['comment_approved'] ) ? 1 : $data['comment_approved'];
2112 $comment_agent = ! isset( $data['comment_agent'] ) ? '' : $data['comment_agent'];
2113 $comment_type = empty( $data['comment_type'] ) ? 'comment' : $data['comment_type'];
2114 $comment_parent = ! isset( $data['comment_parent'] ) ? 0 : $data['comment_parent'];
2115
2116 $user_id = ! isset( $data['user_id'] ) ? 0 : $data['user_id'];
2117
2118 $compacted = array(
2119 'comment_post_ID' => $comment_post_id,
2120 'comment_author_IP' => $comment_author_ip,
2121 );
2122
2123 $compacted += compact(
2124 'comment_author',
2125 'comment_author_email',
2126 'comment_author_url',
2127 'comment_date',
2128 'comment_date_gmt',
2129 'comment_content',
2130 'comment_karma',
2131 'comment_approved',
2132 'comment_agent',
2133 'comment_type',
2134 'comment_parent',
2135 'user_id'
2136 );
2137
2138 if ( ! $wpdb->insert( $wpdb->comments, $compacted ) ) {
2139 return false;
2140 }
2141
2142 $id = (int) $wpdb->insert_id;
2143
2144 if ( 1 === (int) $comment_approved ) {
2145 wp_update_comment_count( $comment_post_id );
2146
2147 $data = array();
2148 foreach ( array( 'server', 'gmt', 'blog' ) as $timezone ) {
2149 $data[] = "lastcommentmodified:$timezone";
2150 }
2151 wp_cache_delete_multiple( $data, 'timeinfo' );
2152 }
2153
2154 clean_comment_cache( $id );
2155
2156 $comment = get_comment( $id );
2157
2158 // If metadata is provided, store it.
2159 if ( isset( $commentdata['comment_meta'] ) && is_array( $commentdata['comment_meta'] ) ) {
2160 foreach ( $commentdata['comment_meta'] as $meta_key => $meta_value ) {
2161 add_comment_meta( $comment->comment_ID, $meta_key, $meta_value, true );
2162 }
2163 }
2164
2165 /**
2166 * Fires immediately after a comment is inserted into the database.
2167 *
2168 * @since 2.8.0
2169 *
2170 * @param int $id The comment ID.
2171 * @param WP_Comment $comment Comment object.
2172 */
2173 do_action( 'wp_insert_comment', $id, $comment );
2174
2175 return $id;
2176}
2177
2178/**
2179 * Filters and sanitizes comment data.
2180 *
2181 * Sets the comment data 'filtered' field to true when finished. This can be
2182 * checked as to whether the comment should be filtered and to keep from
2183 * filtering the same comment more than once.
2184 *
2185 * @since 2.0.0
2186 *
2187 * @param array $commentdata Contains information on the comment.
2188 * @return array Parsed comment information.
2189 */
2190function wp_filter_comment( $commentdata ) {
2191 if ( isset( $commentdata['user_ID'] ) ) {
2192 /**
2193 * Filters the comment author's user ID before it is set.
2194 *
2195 * The first time this filter is evaluated, `user_ID` is checked
2196 * (for back-compat), followed by the standard `user_id` value.
2197 *
2198 * @since 1.5.0
2199 *
2200 * @param int $user_id The comment author's user ID.
2201 */
2202 $commentdata['user_id'] = apply_filters( 'pre_user_id', $commentdata['user_ID'] );
2203 } elseif ( isset( $commentdata['user_id'] ) ) {
2204 /** This filter is documented in wp-includes/comment.php */
2205 $commentdata['user_id'] = apply_filters( 'pre_user_id', $commentdata['user_id'] );
2206 }
2207
2208 /**
2209 * Filters the comment author's browser user agent before it is set.
2210 *
2211 * @since 1.5.0
2212 *
2213 * @param string $comment_agent The comment author's browser user agent.
2214 */
2215 $commentdata['comment_agent'] = apply_filters( 'pre_comment_user_agent', ( isset( $commentdata['comment_agent'] ) ? $commentdata['comment_agent'] : '' ) );
2216 /** This filter is documented in wp-includes/comment.php */
2217 $commentdata['comment_author'] = apply_filters( 'pre_comment_author_name', $commentdata['comment_author'] );
2218 /**
2219 * Filters the comment content before it is set.
2220 *
2221 * @since 1.5.0
2222 *
2223 * @param string $comment_content The comment content.
2224 */
2225 $commentdata['comment_content'] = apply_filters( 'pre_comment_content', $commentdata['comment_content'] );
2226 /**
2227 * Filters the comment author's IP address before it is set.
2228 *
2229 * @since 1.5.0
2230 *
2231 * @param string $comment_author_ip The comment author's IP address.
2232 */
2233 $commentdata['comment_author_IP'] = apply_filters( 'pre_comment_user_ip', $commentdata['comment_author_IP'] );
2234 /** This filter is documented in wp-includes/comment.php */
2235 $commentdata['comment_author_url'] = apply_filters( 'pre_comment_author_url', $commentdata['comment_author_url'] );
2236 /** This filter is documented in wp-includes/comment.php */
2237 $commentdata['comment_author_email'] = apply_filters( 'pre_comment_author_email', $commentdata['comment_author_email'] );
2238
2239 $commentdata['filtered'] = true;
2240
2241 return $commentdata;
2242}
2243
2244/**
2245 * Determines whether a comment should be blocked because of comment flood.
2246 *
2247 * @since 2.1.0
2248 *
2249 * @param bool $block Whether plugin has already blocked comment.
2250 * @param int $time_lastcomment Timestamp for last comment.
2251 * @param int $time_newcomment Timestamp for new comment.
2252 * @return bool Whether comment should be blocked.
2253 */
2254function wp_throttle_comment_flood( $block, $time_lastcomment, $time_newcomment ) {
2255 if ( $block ) { // A plugin has already blocked... we'll let that decision stand.
2256 return $block;
2257 }
2258 if ( ( $time_newcomment - $time_lastcomment ) < 15 ) {
2259 return true;
2260 }
2261 return false;
2262}
2263
2264/**
2265 * Adds a new comment to the database.
2266 *
2267 * Filters new comment to ensure that the fields are sanitized and valid before
2268 * inserting comment into database. Calls {@see 'comment_post'} action with comment ID
2269 * and whether comment is approved by WordPress. Also has {@see 'preprocess_comment'}
2270 * filter for processing the comment data before the function handles it.
2271 *
2272 * We use `REMOTE_ADDR` here directly. If you are behind a proxy, you should ensure
2273 * that it is properly set, such as in wp-config.php, for your environment.
2274 *
2275 * See {@link https://core.trac.wordpress.org/ticket/9235}
2276 *
2277 * @since 1.5.0
2278 * @since 4.3.0 Introduced the `comment_agent` and `comment_author_IP` arguments.
2279 * @since 4.7.0 The `$avoid_die` parameter was added, allowing the function
2280 * to return a WP_Error object instead of dying.
2281 * @since 5.5.0 The `$avoid_die` parameter was renamed to `$wp_error`.
2282 * @since 5.5.0 Introduced the `comment_type` argument.
2283 *
2284 * @see wp_insert_comment()
2285 * @global wpdb $wpdb WordPress database abstraction object.
2286 *
2287 * @param array $commentdata {
2288 * Comment data.
2289 *
2290 * @type string $comment_author The name of the comment author.
2291 * @type string $comment_author_email The comment author email address.
2292 * @type string $comment_author_url The comment author URL.
2293 * @type string $comment_content The content of the comment.
2294 * @type string $comment_date The date the comment was submitted. Default is the current time.
2295 * @type string $comment_date_gmt The date the comment was submitted in the GMT timezone.
2296 * Default is `$comment_date` in the GMT timezone.
2297 * @type string $comment_type Comment type. Default 'comment'.
2298 * @type int $comment_parent The ID of this comment's parent, if any. Default 0.
2299 * @type int $comment_post_ID The ID of the post that relates to the comment.
2300 * @type int $user_id The ID of the user who submitted the comment. Default 0.
2301 * @type int $user_ID Kept for backward-compatibility. Use `$user_id` instead.
2302 * @type string $comment_agent Comment author user agent. Default is the value of 'HTTP_USER_AGENT'
2303 * in the `$_SERVER` superglobal sent in the original request.
2304 * @type string $comment_author_IP Comment author IP address in IPv4 format. Default is the value of
2305 * 'REMOTE_ADDR' in the `$_SERVER` superglobal sent in the original request.
2306 * }
2307 * @param bool $wp_error Should errors be returned as WP_Error objects instead of
2308 * executing wp_die()? Default false.
2309 * @return int|false|WP_Error The ID of the comment on success, false or WP_Error on failure.
2310 */
2311function wp_new_comment( $commentdata, $wp_error = false ) {
2312 global $wpdb;
2313
2314 /*
2315 * Normalize `user_ID` to `user_id`, but pass the old key
2316 * to the `preprocess_comment` filter for backward compatibility.
2317 */
2318 if ( isset( $commentdata['user_ID'] ) ) {
2319 $commentdata['user_ID'] = (int) $commentdata['user_ID'];
2320 $commentdata['user_id'] = $commentdata['user_ID'];
2321 } elseif ( isset( $commentdata['user_id'] ) ) {
2322 $commentdata['user_id'] = (int) $commentdata['user_id'];
2323 $commentdata['user_ID'] = $commentdata['user_id'];
2324 }
2325
2326 $prefiltered_user_id = ( isset( $commentdata['user_id'] ) ) ? (int) $commentdata['user_id'] : 0;
2327
2328 if ( ! isset( $commentdata['comment_author_IP'] ) ) {
2329 $commentdata['comment_author_IP'] = $_SERVER['REMOTE_ADDR'];
2330 }
2331
2332 if ( ! isset( $commentdata['comment_agent'] ) ) {
2333 $commentdata['comment_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
2334 }
2335
2336 /**
2337 * Filters a comment's data before it is sanitized and inserted into the database.
2338 *
2339 * @since 1.5.0
2340 * @since 5.6.0 Comment data includes the `comment_agent` and `comment_author_IP` values.
2341 *
2342 * @param array $commentdata Comment data.
2343 */
2344 $commentdata = apply_filters( 'preprocess_comment', $commentdata );
2345
2346 $commentdata['comment_post_ID'] = (int) $commentdata['comment_post_ID'];
2347
2348 // Normalize `user_ID` to `user_id` again, after the filter.
2349 if ( isset( $commentdata['user_ID'] ) && $prefiltered_user_id !== (int) $commentdata['user_ID'] ) {
2350 $commentdata['user_ID'] = (int) $commentdata['user_ID'];
2351 $commentdata['user_id'] = $commentdata['user_ID'];
2352 } elseif ( isset( $commentdata['user_id'] ) ) {
2353 $commentdata['user_id'] = (int) $commentdata['user_id'];
2354 $commentdata['user_ID'] = $commentdata['user_id'];
2355 }
2356
2357 $commentdata['comment_parent'] = isset( $commentdata['comment_parent'] ) ? absint( $commentdata['comment_parent'] ) : 0;
2358
2359 $parent_status = ( $commentdata['comment_parent'] > 0 ) ? wp_get_comment_status( $commentdata['comment_parent'] ) : '';
2360
2361 $commentdata['comment_parent'] = ( 'approved' === $parent_status || 'unapproved' === $parent_status ) ? $commentdata['comment_parent'] : 0;
2362
2363 $commentdata['comment_author_IP'] = preg_replace( '/[^0-9a-fA-F:., ]/', '', $commentdata['comment_author_IP'] );
2364
2365 $commentdata['comment_agent'] = substr( $commentdata['comment_agent'], 0, 254 );
2366
2367 if ( empty( $commentdata['comment_date'] ) ) {
2368 $commentdata['comment_date'] = current_time( 'mysql' );
2369 }
2370
2371 if ( empty( $commentdata['comment_date_gmt'] ) ) {
2372 $commentdata['comment_date_gmt'] = current_time( 'mysql', true );
2373 }
2374
2375 if ( empty( $commentdata['comment_type'] ) ) {
2376 $commentdata['comment_type'] = 'comment';
2377 }
2378
2379 $commentdata['comment_approved'] = wp_allow_comment( $commentdata, $wp_error );
2380
2381 if ( is_wp_error( $commentdata['comment_approved'] ) ) {
2382 return $commentdata['comment_approved'];
2383 }
2384
2385 $commentdata = wp_filter_comment( $commentdata );
2386
2387 if ( ! in_array( $commentdata['comment_approved'], array( 'trash', 'spam' ), true ) ) {
2388 // Validate the comment again after filters are applied to comment data.
2389 $commentdata['comment_approved'] = wp_check_comment_data( $commentdata );
2390 }
2391
2392 if ( is_wp_error( $commentdata['comment_approved'] ) ) {
2393 return $commentdata['comment_approved'];
2394 }
2395
2396 $comment_id = wp_insert_comment( $commentdata );
2397
2398 if ( ! $comment_id ) {
2399 $fields = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_content' );
2400
2401 foreach ( $fields as $field ) {
2402 if ( isset( $commentdata[ $field ] ) ) {
2403 $commentdata[ $field ] = $wpdb->strip_invalid_text_for_column( $wpdb->comments, $field, $commentdata[ $field ] );
2404 }
2405 }
2406
2407 $commentdata = wp_filter_comment( $commentdata );
2408
2409 $commentdata['comment_approved'] = wp_allow_comment( $commentdata, $wp_error );
2410 if ( is_wp_error( $commentdata['comment_approved'] ) ) {
2411 return $commentdata['comment_approved'];
2412 }
2413
2414 $comment_id = wp_insert_comment( $commentdata );
2415 if ( ! $comment_id ) {
2416 return false;
2417 }
2418 }
2419
2420 /**
2421 * Fires immediately after a comment is inserted into the database.
2422 *
2423 * @since 1.2.0
2424 * @since 4.5.0 The `$commentdata` parameter was added.
2425 *
2426 * @param int $comment_id The comment ID.
2427 * @param int|string $comment_approved 1 if the comment is approved, 0 if not, 'spam' if spam.
2428 * @param array $commentdata Comment data.
2429 */
2430 do_action( 'comment_post', $comment_id, $commentdata['comment_approved'], $commentdata );
2431
2432 return $comment_id;
2433}
2434
2435/**
2436 * Sends a comment moderation notification to the comment moderator.
2437 *
2438 * @since 4.4.0
2439 *
2440 * @param int $comment_id ID of the comment.
2441 * @return bool True on success, false on failure.
2442 */
2443function wp_new_comment_notify_moderator( $comment_id ) {
2444 $comment = get_comment( $comment_id );
2445
2446 // Only send notifications for pending comments.
2447 $maybe_notify = ( '0' === $comment->comment_approved );
2448
2449 /** This filter is documented in wp-includes/pluggable.php */
2450 $maybe_notify = apply_filters( 'notify_moderator', $maybe_notify, $comment_id );
2451
2452 if ( ! $maybe_notify ) {
2453 return false;
2454 }
2455
2456 return wp_notify_moderator( $comment_id );
2457}
2458
2459/**
2460 * Sends a notification of a new comment to the post author.
2461 *
2462 * @since 4.4.0
2463 *
2464 * Uses the {@see 'notify_post_author'} filter to determine whether the post author
2465 * should be notified when a new comment is added, overriding site setting.
2466 *
2467 * @param int $comment_id Comment ID.
2468 * @return bool True on success, false on failure.
2469 */
2470function wp_new_comment_notify_postauthor( $comment_id ) {
2471 $comment = get_comment( $comment_id );
2472 $is_note = ( $comment && 'note' === $comment->comment_type );
2473
2474 $maybe_notify = $is_note ? get_option( 'wp_notes_notify', 1 ) : get_option( 'comments_notify' );
2475
2476 /**
2477 * Filters whether to send the post author new comment notification emails,
2478 * overriding the site setting.
2479 *
2480 * @since 4.4.0
2481 *
2482 * @param bool $maybe_notify Whether to notify the post author about the new comment.
2483 * @param int $comment_id The ID of the comment for the notification.
2484 */
2485 $maybe_notify = apply_filters( 'notify_post_author', $maybe_notify, $comment_id );
2486
2487 /*
2488 * wp_notify_postauthor() checks if notifying the author of their own comment.
2489 * By default, it won't, but filters can override this.
2490 */
2491 if ( ! $maybe_notify ) {
2492 return false;
2493 }
2494
2495 // Send notifications for approved comments and all notes.
2496 if (
2497 ! isset( $comment->comment_approved ) ||
2498 ( '1' !== $comment->comment_approved && ! $is_note ) ) {
2499 return false;
2500 }
2501
2502 return wp_notify_postauthor( $comment_id );
2503}
2504
2505/**
2506 * Send a notification to the post author when a new note is added via the REST API.
2507 *
2508 * @since 6.9.0
2509 *
2510 * @param WP_Comment $comment The comment object.
2511 */
2512function wp_new_comment_via_rest_notify_postauthor( $comment ) {
2513 if ( $comment instanceof WP_Comment && 'note' === $comment->comment_type ) {
2514 wp_new_comment_notify_postauthor( (int) $comment->comment_ID );
2515 }
2516}
2517
2518/**
2519 * Sets the status of a comment.
2520 *
2521 * The {@see 'wp_set_comment_status'} action is called after the comment is handled.
2522 * If the comment status is not in the list, then false is returned.
2523 *
2524 * @since 1.0.0
2525 *
2526 * @global wpdb $wpdb WordPress database abstraction object.
2527 *
2528 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object.
2529 * @param string $comment_status New comment status, either 'hold', 'approve', 'spam', or 'trash'.
2530 * @param bool $wp_error Whether to return a WP_Error object if there is a failure. Default false.
2531 * @return bool|WP_Error True on success, false or WP_Error on failure.
2532 */
2533function wp_set_comment_status( $comment_id, $comment_status, $wp_error = false ) {
2534 global $wpdb;
2535
2536 switch ( $comment_status ) {
2537 case 'hold':
2538 case '0':
2539 $status = '0';
2540 break;
2541 case 'approve':
2542 case '1':
2543 $status = '1';
2544 add_action( 'wp_set_comment_status', 'wp_new_comment_notify_postauthor' );
2545 break;
2546 case 'spam':
2547 $status = 'spam';
2548 break;
2549 case 'trash':
2550 $status = 'trash';
2551 break;
2552 default:
2553 return false;
2554 }
2555
2556 $comment_old = clone get_comment( $comment_id );
2557
2558 if ( ! $wpdb->update( $wpdb->comments, array( 'comment_approved' => $status ), array( 'comment_ID' => $comment_old->comment_ID ) ) ) {
2559 if ( $wp_error ) {
2560 return new WP_Error( 'db_update_error', __( 'Could not update comment status.' ), $wpdb->last_error );
2561 } else {
2562 return false;
2563 }
2564 }
2565
2566 clean_comment_cache( $comment_old->comment_ID );
2567
2568 $comment = get_comment( $comment_old->comment_ID );
2569
2570 /**
2571 * Fires immediately after transitioning a comment's status from one to another in the database
2572 * and removing the comment from the object cache, but prior to all status transition hooks.
2573 *
2574 * @since 1.5.0
2575 *
2576 * @param string $comment_id Comment ID as a numeric string.
2577 * @param string $comment_status Current comment status. Possible values include
2578 * 'hold', '0', 'approve', '1', 'spam', and 'trash'.
2579 */
2580 do_action( 'wp_set_comment_status', $comment->comment_ID, $comment_status );
2581
2582 wp_transition_comment_status( $comment_status, $comment_old->comment_approved, $comment );
2583
2584 wp_update_comment_count( $comment->comment_post_ID );
2585
2586 return true;
2587}
2588
2589/**
2590 * Updates an existing comment in the database.
2591 *
2592 * Filters the comment and makes sure certain fields are valid before updating.
2593 *
2594 * @since 2.0.0
2595 * @since 4.9.0 Add updating comment meta during comment update.
2596 * @since 5.5.0 The `$wp_error` parameter was added.
2597 * @since 5.5.0 The return values for an invalid comment or post ID
2598 * were changed to false instead of 0.
2599 *
2600 * @global wpdb $wpdb WordPress database abstraction object.
2601 *
2602 * @param array $commentarr Contains information on the comment.
2603 * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false.
2604 * @return int|false|WP_Error The value 1 if the comment was updated, 0 if not updated.
2605 * False or a WP_Error object on failure.
2606 */
2607function wp_update_comment( $commentarr, $wp_error = false ) {
2608 global $wpdb;
2609
2610 // First, get all of the original fields.
2611 $comment = get_comment( $commentarr['comment_ID'], ARRAY_A );
2612
2613 if ( empty( $comment ) ) {
2614 if ( $wp_error ) {
2615 return new WP_Error( 'invalid_comment_id', __( 'Invalid comment ID.' ) );
2616 } else {
2617 return false;
2618 }
2619 }
2620
2621 // Make sure that the comment post ID is valid (if specified).
2622 if ( ! empty( $commentarr['comment_post_ID'] ) && ! get_post( $commentarr['comment_post_ID'] ) ) {
2623 if ( $wp_error ) {
2624 return new WP_Error( 'invalid_post_id', __( 'Invalid post ID.' ) );
2625 } else {
2626 return false;
2627 }
2628 }
2629
2630 $filter_comment = false;
2631 if ( ! has_filter( 'pre_comment_content', 'wp_filter_kses' ) ) {
2632 $filter_comment = ! user_can( isset( $comment['user_id'] ) ? $comment['user_id'] : 0, 'unfiltered_html' );
2633 }
2634
2635 if ( $filter_comment ) {
2636 add_filter( 'pre_comment_content', 'wp_filter_kses' );
2637 }
2638
2639 // Escape data pulled from DB.
2640 $comment = wp_slash( $comment );
2641
2642 $old_status = $comment['comment_approved'];
2643
2644 // Merge old and new fields with new fields overwriting old ones.
2645 $commentarr = array_merge( $comment, $commentarr );
2646
2647 $commentarr = wp_filter_comment( $commentarr );
2648
2649 if ( $filter_comment ) {
2650 remove_filter( 'pre_comment_content', 'wp_filter_kses' );
2651 }
2652
2653 // Now extract the merged array.
2654 $data = wp_unslash( $commentarr );
2655
2656 /**
2657 * Filters the comment content before it is updated in the database.
2658 *
2659 * @since 1.5.0
2660 *
2661 * @param string $comment_content The comment data.
2662 */
2663 $data['comment_content'] = apply_filters( 'comment_save_pre', $data['comment_content'] );
2664
2665 $data['comment_date_gmt'] = get_gmt_from_date( $data['comment_date'] );
2666
2667 if ( ! isset( $data['comment_approved'] ) ) {
2668 $data['comment_approved'] = 1;
2669 } elseif ( 'hold' === $data['comment_approved'] ) {
2670 $data['comment_approved'] = 0;
2671 } elseif ( 'approve' === $data['comment_approved'] ) {
2672 $data['comment_approved'] = 1;
2673 }
2674
2675 $comment_id = $data['comment_ID'];
2676 $comment_post_id = $data['comment_post_ID'];
2677
2678 /**
2679 * Filters the comment data immediately before it is updated in the database.
2680 *
2681 * Note: data being passed to the filter is already unslashed.
2682 *
2683 * @since 4.7.0
2684 * @since 5.5.0 Returning a WP_Error value from the filter will short-circuit comment update
2685 * and allow skipping further processing.
2686 *
2687 * @param array|WP_Error $data The new, processed comment data, or WP_Error.
2688 * @param array $comment The old, unslashed comment data.
2689 * @param array $commentarr The new, raw comment data.
2690 */
2691 $data = apply_filters( 'wp_update_comment_data', $data, $comment, $commentarr );
2692
2693 // Do not carry on on failure.
2694 if ( is_wp_error( $data ) ) {
2695 if ( $wp_error ) {
2696 return $data;
2697 } else {
2698 return false;
2699 }
2700 }
2701
2702 $keys = array(
2703 'comment_post_ID',
2704 'comment_author',
2705 'comment_author_email',
2706 'comment_author_url',
2707 'comment_author_IP',
2708 'comment_date',
2709 'comment_date_gmt',
2710 'comment_content',
2711 'comment_karma',
2712 'comment_approved',
2713 'comment_agent',
2714 'comment_type',
2715 'comment_parent',
2716 'user_id',
2717 );
2718
2719 $data = wp_array_slice_assoc( $data, $keys );
2720
2721 $result = $wpdb->update( $wpdb->comments, $data, array( 'comment_ID' => $comment_id ) );
2722
2723 if ( false === $result ) {
2724 if ( $wp_error ) {
2725 return new WP_Error( 'db_update_error', __( 'Could not update comment in the database.' ), $wpdb->last_error );
2726 } else {
2727 return false;
2728 }
2729 }
2730
2731 // If metadata is provided, store it.
2732 if ( isset( $commentarr['comment_meta'] ) && is_array( $commentarr['comment_meta'] ) ) {
2733 foreach ( $commentarr['comment_meta'] as $meta_key => $meta_value ) {
2734 update_comment_meta( $comment_id, $meta_key, $meta_value );
2735 }
2736 }
2737
2738 clean_comment_cache( $comment_id );
2739 wp_update_comment_count( $comment_post_id );
2740
2741 /**
2742 * Fires immediately after a comment is updated in the database.
2743 *
2744 * The hook also fires immediately before comment status transition hooks are fired.
2745 *
2746 * @since 1.2.0
2747 * @since 4.6.0 Added the `$data` parameter.
2748 *
2749 * @param int $comment_id The comment ID.
2750 * @param array $data Comment data.
2751 */
2752 do_action( 'edit_comment', $comment_id, $data );
2753
2754 $comment = get_comment( $comment_id );
2755
2756 wp_transition_comment_status( $comment->comment_approved, $old_status, $comment );
2757
2758 return $result;
2759}
2760
2761/**
2762 * Determines whether to defer comment counting.
2763 *
2764 * When setting $defer to true, all post comment counts will not be updated
2765 * until $defer is set to false. When $defer is set to false, then all
2766 * previously deferred updated post comment counts will then be automatically
2767 * updated without having to call wp_update_comment_count() after.
2768 *
2769 * @since 2.5.0
2770 *
2771 * @param bool $defer
2772 * @return bool
2773 */
2774function wp_defer_comment_counting( $defer = null ) {
2775 static $_defer = false;
2776
2777 if ( is_bool( $defer ) ) {
2778 $_defer = $defer;
2779 // Flush any deferred counts.
2780 if ( ! $defer ) {
2781 wp_update_comment_count( null, true );
2782 }
2783 }
2784
2785 return $_defer;
2786}
2787
2788/**
2789 * Updates the comment count for post(s).
2790 *
2791 * When $do_deferred is false (is by default) and the comments have been set to
2792 * be deferred, the post_id will be added to a queue, which will be updated at a
2793 * later date and only updated once per post ID.
2794 *
2795 * If the comments have not be set up to be deferred, then the post will be
2796 * updated. When $do_deferred is set to true, then all previous deferred post
2797 * IDs will be updated along with the current $post_id.
2798 *
2799 * @since 2.1.0
2800 *
2801 * @see wp_update_comment_count_now() For what could cause a false return value
2802 *
2803 * @param int|null $post_id Post ID.
2804 * @param bool $do_deferred Optional. Whether to process previously deferred
2805 * post comment counts. Default false.
2806 * @return bool|void True on success, false on failure or if post with ID does
2807 * not exist.
2808 */
2809function wp_update_comment_count( $post_id, $do_deferred = false ) {
2810 static $_deferred = array();
2811
2812 if ( empty( $post_id ) && ! $do_deferred ) {
2813 return false;
2814 }
2815
2816 if ( $do_deferred ) {
2817 $_deferred = array_unique( $_deferred );
2818 foreach ( $_deferred as $i => $_post_id ) {
2819 wp_update_comment_count_now( $_post_id );
2820 unset( $_deferred[ $i ] );
2821 /** @todo Move this outside of the foreach and reset $_deferred to an array instead */
2822 }
2823 }
2824
2825 if ( wp_defer_comment_counting() ) {
2826 $_deferred[] = $post_id;
2827 return true;
2828 } elseif ( $post_id ) {
2829 return wp_update_comment_count_now( $post_id );
2830 }
2831}
2832
2833/**
2834 * Updates the comment count for the post.
2835 *
2836 * @since 2.5.0
2837 *
2838 * @global wpdb $wpdb WordPress database abstraction object.
2839 *
2840 * @param int $post_id Post ID
2841 * @return bool True on success, false if the post does not exist.
2842 */
2843function wp_update_comment_count_now( $post_id ) {
2844 global $wpdb;
2845
2846 $post_id = (int) $post_id;
2847
2848 if ( ! $post_id ) {
2849 return false;
2850 }
2851
2852 wp_cache_delete( 'comments-0', 'counts' );
2853 wp_cache_delete( "comments-{$post_id}", 'counts' );
2854
2855 $post = get_post( $post_id );
2856
2857 if ( ! $post ) {
2858 return false;
2859 }
2860
2861 $old = (int) $post->comment_count;
2862
2863 /**
2864 * Filters a post's comment count before it is updated in the database.
2865 *
2866 * @since 4.5.0
2867 *
2868 * @param int|null $new The new comment count. Default null.
2869 * @param int $old The old comment count.
2870 * @param int $post_id Post ID.
2871 */
2872 $new = apply_filters( 'pre_wp_update_comment_count_now', null, $old, $post_id );
2873
2874 if ( is_null( $new ) ) {
2875 $new = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type != 'note'", $post_id ) );
2876 } else {
2877 $new = (int) $new;
2878 }
2879
2880 $wpdb->update( $wpdb->posts, array( 'comment_count' => $new ), array( 'ID' => $post_id ) );
2881
2882 clean_post_cache( $post );
2883
2884 /**
2885 * Fires immediately after a post's comment count is updated in the database.
2886 *
2887 * @since 2.3.0
2888 *
2889 * @param int $post_id Post ID.
2890 * @param int $new The new comment count.
2891 * @param int $old The old comment count.
2892 */
2893 do_action( 'wp_update_comment_count', $post_id, $new, $old );
2894
2895 /** This action is documented in wp-includes/post.php */
2896 do_action( "edit_post_{$post->post_type}", $post_id, $post );
2897
2898 /** This action is documented in wp-includes/post.php */
2899 do_action( 'edit_post', $post_id, $post );
2900
2901 return true;
2902}
2903
2904//
2905// Ping and trackback functions.
2906//
2907
2908/**
2909 * Finds a pingback server URI based on the given URL.
2910 *
2911 * Checks the HTML for the rel="pingback" link and X-Pingback headers. It does
2912 * a check for the X-Pingback headers first and returns that, if available.
2913 * The check for the rel="pingback" has more overhead than just the header.
2914 *
2915 * @since 1.5.0
2916 *
2917 * @param string $url URL to ping.
2918 * @param string $deprecated Not Used.
2919 * @return string|false String containing URI on success, false on failure.
2920 */
2921function discover_pingback_server_uri( $url, $deprecated = '' ) {
2922 if ( ! empty( $deprecated ) ) {
2923 _deprecated_argument( __FUNCTION__, '2.7.0' );
2924 }
2925
2926 $pingback_str_dquote = 'rel="pingback"';
2927 $pingback_str_squote = 'rel=\'pingback\'';
2928
2929 /** @todo Should use Filter Extension or custom preg_match instead. */
2930 $parsed_url = parse_url( $url );
2931
2932 if ( ! isset( $parsed_url['host'] ) ) { // Not a URL. This should never happen.
2933 return false;
2934 }
2935
2936 // Do not search for a pingback server on our own uploads.
2937 $uploads_dir = wp_get_upload_dir();
2938 if ( str_starts_with( $url, $uploads_dir['baseurl'] ) ) {
2939 return false;
2940 }
2941
2942 $response = wp_safe_remote_head(
2943 $url,
2944 array(
2945 'timeout' => 2,
2946 'httpversion' => '1.0',
2947 )
2948 );
2949
2950 if ( is_wp_error( $response ) ) {
2951 return false;
2952 }
2953
2954 if ( wp_remote_retrieve_header( $response, 'X-Pingback' ) ) {
2955 return wp_remote_retrieve_header( $response, 'X-Pingback' );
2956 }
2957
2958 // Not an (x)html, sgml, or xml page, no use going further.
2959 if ( preg_match( '#(image|audio|video|model)/#is', wp_remote_retrieve_header( $response, 'Content-Type' ) ) ) {
2960 return false;
2961 }
2962
2963 // Now do a GET since we're going to look in the HTML headers (and we're sure it's not a binary file).
2964 $response = wp_safe_remote_get(
2965 $url,
2966 array(
2967 'timeout' => 2,
2968 'httpversion' => '1.0',
2969 )
2970 );
2971
2972 if ( is_wp_error( $response ) ) {
2973 return false;
2974 }
2975
2976 $contents = wp_remote_retrieve_body( $response );
2977
2978 $pingback_link_offset_dquote = strpos( $contents, $pingback_str_dquote );
2979 $pingback_link_offset_squote = strpos( $contents, $pingback_str_squote );
2980
2981 if ( $pingback_link_offset_dquote || $pingback_link_offset_squote ) {
2982 $quote = ( $pingback_link_offset_dquote ) ? '"' : '\'';
2983 $pingback_link_offset = ( '"' === $quote ) ? $pingback_link_offset_dquote : $pingback_link_offset_squote;
2984 $pingback_href_pos = strpos( $contents, 'href=', $pingback_link_offset );
2985 $pingback_href_start = $pingback_href_pos + 6;
2986 $pingback_href_end = strpos( $contents, $quote, $pingback_href_start );
2987 $pingback_server_url_len = $pingback_href_end - $pingback_href_start;
2988 $pingback_server_url = substr( $contents, $pingback_href_start, $pingback_server_url_len );
2989
2990 // We may find rel="pingback" but an incomplete pingback URL.
2991 if ( $pingback_server_url_len > 0 ) { // We got it!
2992 return $pingback_server_url;
2993 }
2994 }
2995
2996 return false;
2997}
2998
2999/**
3000 * Performs all pingbacks, enclosures, trackbacks, and sends to pingback services.
3001 *
3002 * @since 2.1.0
3003 * @since 5.6.0 Introduced `do_all_pings` action hook for individual services.
3004 */
3005function do_all_pings() {
3006 /**
3007 * Fires immediately after the `do_pings` event to hook services individually.
3008 *
3009 * @since 5.6.0
3010 */
3011 do_action( 'do_all_pings' );
3012}
3013
3014/**
3015 * Performs all pingbacks.
3016 *
3017 * @since 5.6.0
3018 */
3019function do_all_pingbacks() {
3020 $pings = get_posts(
3021 array(
3022 'post_type' => get_post_types(),
3023 'suppress_filters' => false,
3024 'nopaging' => true,
3025 'meta_key' => '_pingme',
3026 'fields' => 'ids',
3027 )
3028 );
3029
3030 foreach ( $pings as $ping ) {
3031 delete_post_meta( $ping, '_pingme' );
3032 pingback( null, $ping );
3033 }
3034}
3035
3036/**
3037 * Performs all enclosures.
3038 *
3039 * @since 5.6.0
3040 */
3041function do_all_enclosures() {
3042 $enclosures = get_posts(
3043 array(
3044 'post_type' => get_post_types(),
3045 'suppress_filters' => false,
3046 'nopaging' => true,
3047 'meta_key' => '_encloseme',
3048 'fields' => 'ids',
3049 )
3050 );
3051
3052 foreach ( $enclosures as $enclosure ) {
3053 delete_post_meta( $enclosure, '_encloseme' );
3054 do_enclose( null, $enclosure );
3055 }
3056}
3057
3058/**
3059 * Performs all trackbacks.
3060 *
3061 * @since 5.6.0
3062 */
3063function do_all_trackbacks() {
3064 $trackbacks = get_posts(
3065 array(
3066 'post_type' => get_post_types(),
3067 'suppress_filters' => false,
3068 'nopaging' => true,
3069 'meta_key' => '_trackbackme',
3070 'fields' => 'ids',
3071 )
3072 );
3073
3074 foreach ( $trackbacks as $trackback ) {
3075 delete_post_meta( $trackback, '_trackbackme' );
3076 do_trackbacks( $trackback );
3077 }
3078}
3079
3080/**
3081 * Performs trackbacks.
3082 *
3083 * @since 1.5.0
3084 * @since 4.7.0 `$post` can be a WP_Post object.
3085 *
3086 * @global wpdb $wpdb WordPress database abstraction object.
3087 *
3088 * @param int|WP_Post $post Post ID or object to do trackbacks on.
3089 * @return void|false Returns false on failure.
3090 */
3091function do_trackbacks( $post ) {
3092 global $wpdb;
3093
3094 $post = get_post( $post );
3095
3096 if ( ! $post ) {
3097 return false;
3098 }
3099
3100 $to_ping = get_to_ping( $post );
3101 $pinged = get_pung( $post );
3102
3103 if ( empty( $to_ping ) ) {
3104 $wpdb->update( $wpdb->posts, array( 'to_ping' => '' ), array( 'ID' => $post->ID ) );
3105 return;
3106 }
3107
3108 if ( empty( $post->post_excerpt ) ) {
3109 /** This filter is documented in wp-includes/post-template.php */
3110 $excerpt = apply_filters( 'the_content', $post->post_content, $post->ID );
3111 } else {
3112 /** This filter is documented in wp-includes/post-template.php */
3113 $excerpt = apply_filters( 'the_excerpt', $post->post_excerpt );
3114 }
3115
3116 $excerpt = str_replace( ']]>', ']]&gt;', $excerpt );
3117 $excerpt = wp_html_excerpt( $excerpt, 252, '&#8230;' );
3118
3119 /** This filter is documented in wp-includes/post-template.php */
3120 $post_title = apply_filters( 'the_title', $post->post_title, $post->ID );
3121 $post_title = strip_tags( $post_title );
3122
3123 foreach ( (array) $to_ping as $tb_ping ) {
3124 $tb_ping = trim( $tb_ping );
3125 if ( ! in_array( $tb_ping, $pinged, true ) ) {
3126 trackback( $tb_ping, $post_title, $excerpt, $post->ID );
3127 $pinged[] = $tb_ping;
3128 } else {
3129 $wpdb->query(
3130 $wpdb->prepare(
3131 "UPDATE $wpdb->posts SET to_ping = TRIM(REPLACE(to_ping, %s, '')) WHERE ID = %d",
3132 $tb_ping,
3133 $post->ID
3134 )
3135 );
3136 }
3137 }
3138}
3139
3140/**
3141 * Sends pings to all of the ping site services.
3142 *
3143 * @since 1.2.0
3144 *
3145 * @param int $post_id Post ID.
3146 * @return int Same post ID as provided.
3147 */
3148function generic_ping( $post_id = 0 ) {
3149 $services = get_option( 'ping_sites' );
3150
3151 $services = explode( "\n", $services );
3152 foreach ( (array) $services as $service ) {
3153 $service = trim( $service );
3154 if ( '' !== $service ) {
3155 weblog_ping( $service );
3156 }
3157 }
3158
3159 return $post_id;
3160}
3161
3162/**
3163 * Pings back the links found in a post.
3164 *
3165 * @since 0.71
3166 * @since 4.7.0 `$post` can be a WP_Post object.
3167 * @since 6.8.0 Returns an array of pingback statuses indexed by link.
3168 *
3169 * @param string $content Post content to check for links. If empty will retrieve from post.
3170 * @param int|WP_Post $post Post ID or object.
3171 * @return array<string, bool> An array of pingback statuses indexed by link.
3172 */
3173function pingback( $content, $post ) {
3174 require_once ABSPATH . WPINC . '/class-IXR.php';
3175 require_once ABSPATH . WPINC . '/class-wp-http-ixr-client.php';
3176
3177 // Original code by Mort (http://mort.mine.nu:8080).
3178 $post_links = array();
3179
3180 $post = get_post( $post );
3181
3182 if ( ! $post ) {
3183 return array();
3184 }
3185
3186 $pung = get_pung( $post );
3187
3188 if ( empty( $content ) ) {
3189 $content = $post->post_content;
3190 }
3191
3192 /*
3193 * Step 1.
3194 * Parsing the post, external links (if any) are stored in the $post_links array.
3195 */
3196 $post_links_temp = wp_extract_urls( $content );
3197
3198 $ping_status = array();
3199 /*
3200 * Step 2.
3201 * Walking through the links array.
3202 * First we get rid of links pointing to sites, not to specific files.
3203 * Example:
3204 * http://dummy-weblog.org
3205 * http://dummy-weblog.org/
3206 * http://dummy-weblog.org/post.php
3207 * We don't wanna ping first and second types, even if they have a valid <link/>.
3208 */
3209 foreach ( (array) $post_links_temp as $link_test ) {
3210 // If we haven't pung it already and it isn't a link to itself.
3211 if ( ! in_array( $link_test, $pung, true ) && ( url_to_postid( $link_test ) !== $post->ID )
3212 // Also, let's never ping local attachments.
3213 && ! is_local_attachment( $link_test )
3214 ) {
3215 $test = parse_url( $link_test );
3216 if ( $test ) {
3217 if ( isset( $test['query'] ) ) {
3218 $post_links[] = $link_test;
3219 } elseif ( isset( $test['path'] ) && ( '/' !== $test['path'] ) && ( '' !== $test['path'] ) ) {
3220 $post_links[] = $link_test;
3221 }
3222 }
3223 }
3224 }
3225
3226 $post_links = array_unique( $post_links );
3227
3228 /**
3229 * Fires just before pinging back links found in a post.
3230 *
3231 * @since 2.0.0
3232 *
3233 * @param string[] $post_links Array of link URLs to be checked (passed by reference).
3234 * @param string[] $pung Array of link URLs already pinged (passed by reference).
3235 * @param int $post_id The post ID.
3236 */
3237 do_action_ref_array( 'pre_ping', array( &$post_links, &$pung, $post->ID ) );
3238
3239 foreach ( (array) $post_links as $pagelinkedto ) {
3240 $pingback_server_url = discover_pingback_server_uri( $pagelinkedto );
3241
3242 if ( $pingback_server_url ) {
3243 // Allow an additional 60 seconds for each pingback to complete.
3244 if ( function_exists( 'set_time_limit' ) ) {
3245 set_time_limit( 60 );
3246 }
3247
3248 // Now, the RPC call.
3249 $pagelinkedfrom = get_permalink( $post );
3250
3251 // Using a timeout of 3 seconds should be enough to cover slow servers.
3252 $client = new WP_HTTP_IXR_Client( $pingback_server_url );
3253 $client->timeout = 3;
3254 /**
3255 * Filters the user agent sent when pinging-back a URL.
3256 *
3257 * @since 2.9.0
3258 *
3259 * @param string $concat_useragent The user agent concatenated with ' -- WordPress/'
3260 * and the WordPress version.
3261 * @param string $useragent The useragent.
3262 * @param string $pingback_server_url The server URL being linked to.
3263 * @param string $pagelinkedto URL of page linked to.
3264 * @param string $pagelinkedfrom URL of page linked from.
3265 */
3266 $client->useragent = apply_filters( 'pingback_useragent', $client->useragent . ' -- WordPress/' . get_bloginfo( 'version' ), $client->useragent, $pingback_server_url, $pagelinkedto, $pagelinkedfrom );
3267 // When set to true, this outputs debug messages by itself.
3268 $client->debug = false;
3269
3270 $status = $client->query( 'pingback.ping', $pagelinkedfrom, $pagelinkedto );
3271
3272 if ( $status // Ping registered.
3273 || ( isset( $client->error->code ) && 48 === $client->error->code ) // Already registered.
3274 ) {
3275 add_ping( $post, $pagelinkedto );
3276 }
3277 $ping_status[ $pagelinkedto ] = $status;
3278 }
3279 }
3280
3281 return $ping_status;
3282}
3283
3284/**
3285 * Checks whether blog is public before returning sites.
3286 *
3287 * @since 2.1.0
3288 *
3289 * @param mixed $sites Will return if blog is public, will not return if not public.
3290 * @return mixed Empty string if blog is not public, returns $sites, if site is public.
3291 */
3292function privacy_ping_filter( $sites ) {
3293 if ( '0' !== get_option( 'blog_public' ) ) {
3294 return $sites;
3295 } else {
3296 return '';
3297 }
3298}
3299
3300/**
3301 * Sends a Trackback.
3302 *
3303 * Updates database when sending trackback to prevent duplicates.
3304 *
3305 * @since 0.71
3306 *
3307 * @global wpdb $wpdb WordPress database abstraction object.
3308 *
3309 * @param string $trackback_url URL to send trackbacks.
3310 * @param string $title Title of post.
3311 * @param string $excerpt Excerpt of post.
3312 * @param int $post_id Post ID.
3313 * @return int|false|void Database query from update.
3314 */
3315function trackback( $trackback_url, $title, $excerpt, $post_id ) {
3316 global $wpdb;
3317
3318 if ( empty( $trackback_url ) ) {
3319 return;
3320 }
3321
3322 $options = array();
3323 $options['timeout'] = 10;
3324 $options['body'] = array(
3325 'title' => $title,
3326 'url' => get_permalink( $post_id ),
3327 'blog_name' => get_option( 'blogname' ),
3328 'excerpt' => $excerpt,
3329 );
3330
3331 $response = wp_safe_remote_post( $trackback_url, $options );
3332
3333 if ( is_wp_error( $response ) ) {
3334 return;
3335 }
3336
3337 $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET pinged = CONCAT(pinged, '\n', %s) WHERE ID = %d", $trackback_url, $post_id ) );
3338 return $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET to_ping = TRIM(REPLACE(to_ping, %s, '')) WHERE ID = %d", $trackback_url, $post_id ) );
3339}
3340
3341/**
3342 * Sends a pingback.
3343 *
3344 * @since 1.2.0
3345 *
3346 * @param string $server Host of blog to connect to.
3347 * @param string $path Path to send the ping.
3348 */
3349function weblog_ping( $server = '', $path = '' ) {
3350 require_once ABSPATH . WPINC . '/class-IXR.php';
3351 require_once ABSPATH . WPINC . '/class-wp-http-ixr-client.php';
3352
3353 // Using a timeout of 3 seconds should be enough to cover slow servers.
3354 $client = new WP_HTTP_IXR_Client( $server, ( ( ! strlen( trim( $path ) ) || ( '/' === $path ) ) ? false : $path ) );
3355 $client->timeout = 3;
3356 $client->useragent .= ' -- WordPress/' . get_bloginfo( 'version' );
3357
3358 // When set to true, this outputs debug messages by itself.
3359 $client->debug = false;
3360 $home = trailingslashit( home_url() );
3361 if ( ! $client->query( 'weblogUpdates.extendedPing', get_option( 'blogname' ), $home, get_bloginfo( 'rss2_url' ) ) ) { // Then try a normal ping.
3362 $client->query( 'weblogUpdates.ping', get_option( 'blogname' ), $home );
3363 }
3364}
3365
3366/**
3367 * Default filter attached to pingback_ping_source_uri to validate the pingback's Source URI.
3368 *
3369 * @since 3.5.1
3370 *
3371 * @see wp_http_validate_url()
3372 *
3373 * @param string $source_uri
3374 * @return string
3375 */
3376function pingback_ping_source_uri( $source_uri ) {
3377 return (string) wp_http_validate_url( $source_uri );
3378}
3379
3380/**
3381 * Default filter attached to xmlrpc_pingback_error.
3382 *
3383 * Returns a generic pingback error code unless the error code is 48,
3384 * which reports that the pingback is already registered.
3385 *
3386 * @since 3.5.1
3387 *
3388 * @link https://www.hixie.ch/specs/pingback/pingback#TOC3
3389 *
3390 * @param IXR_Error $ixr_error
3391 * @return IXR_Error
3392 */
3393function xmlrpc_pingback_error( $ixr_error ) {
3394 if ( 48 === $ixr_error->code ) {
3395 return $ixr_error;
3396 }
3397 return new IXR_Error( 0, '' );
3398}
3399
3400//
3401// Cache.
3402//
3403
3404/**
3405 * Removes a comment from the object cache.
3406 *
3407 * @since 2.3.0
3408 *
3409 * @param int|array $ids Comment ID or an array of comment IDs to remove from cache.
3410 */
3411function clean_comment_cache( $ids ) {
3412 $comment_ids = (array) $ids;
3413 wp_cache_delete_multiple( $comment_ids, 'comment' );
3414 foreach ( $comment_ids as $id ) {
3415 /**
3416 * Fires immediately after a comment has been removed from the object cache.
3417 *
3418 * @since 4.5.0
3419 *
3420 * @param int $id Comment ID.
3421 */
3422 do_action( 'clean_comment_cache', $id );
3423 }
3424
3425 wp_cache_set_comments_last_changed();
3426}
3427
3428/**
3429 * Updates the comment cache of given comments.
3430 *
3431 * Will add the comments in $comments to the cache. If comment ID already exists
3432 * in the comment cache then it will not be updated. The comment is added to the
3433 * cache using the comment group with the key using the ID of the comments.
3434 *
3435 * @since 2.3.0
3436 * @since 4.4.0 Introduced the `$update_meta_cache` parameter.
3437 *
3438 * @param WP_Comment[] $comments Array of comment objects
3439 * @param bool $update_meta_cache Whether to update commentmeta cache. Default true.
3440 */
3441function update_comment_cache( $comments, $update_meta_cache = true ) {
3442 $data = array();
3443 foreach ( (array) $comments as $comment ) {
3444 $data[ $comment->comment_ID ] = $comment;
3445 }
3446 wp_cache_add_multiple( $data, 'comment' );
3447
3448 if ( $update_meta_cache ) {
3449 // Avoid `wp_list_pluck()` in case `$comments` is passed by reference.
3450 $comment_ids = array();
3451 foreach ( $comments as $comment ) {
3452 $comment_ids[] = $comment->comment_ID;
3453 }
3454 update_meta_cache( 'comment', $comment_ids );
3455 }
3456}
3457
3458/**
3459 * Adds any comments from the given IDs to the cache that do not already exist in cache.
3460 *
3461 * @since 4.4.0
3462 * @since 6.1.0 This function is no longer marked as "private".
3463 * @since 6.3.0 Use wp_lazyload_comment_meta() for lazy-loading of comment meta.
3464 *
3465 * @see update_comment_cache()
3466 * @global wpdb $wpdb WordPress database abstraction object.
3467 *
3468 * @param int[] $comment_ids Array of comment IDs.
3469 * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true.
3470 */
3471function _prime_comment_caches( $comment_ids, $update_meta_cache = true ) {
3472 global $wpdb;
3473
3474 $non_cached_ids = _get_non_cached_ids( $comment_ids, 'comment' );
3475 if ( ! empty( $non_cached_ids ) ) {
3476 $fresh_comments = $wpdb->get_results( sprintf( "SELECT $wpdb->comments.* FROM $wpdb->comments WHERE comment_ID IN (%s)", implode( ',', array_map( 'intval', $non_cached_ids ) ) ) );
3477
3478 update_comment_cache( $fresh_comments, false );
3479 }
3480
3481 if ( $update_meta_cache ) {
3482 wp_lazyload_comment_meta( $comment_ids );
3483 }
3484}
3485
3486//
3487// Internal.
3488//
3489
3490/**
3491 * Closes comments on old posts on the fly, without any extra DB queries. Hooked to the_posts.
3492 *
3493 * @since 2.7.0
3494 * @access private
3495 *
3496 * @param WP_Post[] $posts Array of post objects.
3497 * @param WP_Query $query Query object.
3498 * @return WP_Post[]
3499 */
3500function _close_comments_for_old_posts( $posts, $query ) {
3501 if ( empty( $posts ) || ! $query->is_singular() || ! get_option( 'close_comments_for_old_posts' ) ) {
3502 return $posts;
3503 }
3504
3505 /**
3506 * Filters the list of post types to automatically close comments for.
3507 *
3508 * @since 3.2.0
3509 *
3510 * @param string[] $post_types An array of post type names.
3511 */
3512 $post_types = apply_filters( 'close_comments_for_post_types', array( 'post' ) );
3513 if ( ! in_array( $posts[0]->post_type, $post_types, true ) ) {
3514 return $posts;
3515 }
3516
3517 $days_old = (int) get_option( 'close_comments_days_old' );
3518 if ( ! $days_old ) {
3519 return $posts;
3520 }
3521
3522 if ( time() - strtotime( $posts[0]->post_date_gmt ) > ( $days_old * DAY_IN_SECONDS ) ) {
3523 $posts[0]->comment_status = 'closed';
3524 $posts[0]->ping_status = 'closed';
3525 }
3526
3527 return $posts;
3528}
3529
3530/**
3531 * Closes comments on an old post. Hooked to comments_open and pings_open.
3532 *
3533 * @since 2.7.0
3534 * @access private
3535 *
3536 * @param bool $open Comments open or closed.
3537 * @param int $post_id Post ID.
3538 * @return bool $open
3539 */
3540function _close_comments_for_old_post( $open, $post_id ) {
3541 if ( ! $open ) {
3542 return $open;
3543 }
3544
3545 if ( ! get_option( 'close_comments_for_old_posts' ) ) {
3546 return $open;
3547 }
3548
3549 $days_old = (int) get_option( 'close_comments_days_old' );
3550 if ( ! $days_old ) {
3551 return $open;
3552 }
3553
3554 $post = get_post( $post_id );
3555
3556 /** This filter is documented in wp-includes/comment.php */
3557 $post_types = apply_filters( 'close_comments_for_post_types', array( 'post' ) );
3558 if ( ! in_array( $post->post_type, $post_types, true ) ) {
3559 return $open;
3560 }
3561
3562 // Undated drafts should not show up as comments closed.
3563 if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) {
3564 return $open;
3565 }
3566
3567 if ( time() - strtotime( $post->post_date_gmt ) > ( $days_old * DAY_IN_SECONDS ) ) {
3568 return false;
3569 }
3570
3571 return $open;
3572}
3573
3574/**
3575 * Handles the submission of a comment, usually posted to wp-comments-post.php via a comment form.
3576 *
3577 * This function expects unslashed data, as opposed to functions such as `wp_new_comment()` which
3578 * expect slashed data.
3579 *
3580 * @since 4.4.0
3581 *
3582 * @param array $comment_data {
3583 * Comment data.
3584 *
3585 * @type string|int $comment_post_ID The ID of the post that relates to the comment.
3586 * @type string $author The name of the comment author.
3587 * @type string $email The comment author email address.
3588 * @type string $url The comment author URL.
3589 * @type string $comment The content of the comment.
3590 * @type string|int $comment_parent The ID of this comment's parent, if any. Default 0.
3591 * @type string $_wp_unfiltered_html_comment The nonce value for allowing unfiltered HTML.
3592 * }
3593 * @return WP_Comment|WP_Error A WP_Comment object on success, a WP_Error object on failure.
3594 */
3595function wp_handle_comment_submission( $comment_data ) {
3596 $comment_post_id = 0;
3597 $comment_author = '';
3598 $comment_author_email = '';
3599 $comment_author_url = '';
3600 $comment_content = '';
3601 $comment_parent = 0;
3602 $user_id = 0;
3603
3604 if ( isset( $comment_data['comment_post_ID'] ) ) {
3605 $comment_post_id = (int) $comment_data['comment_post_ID'];
3606 }
3607 if ( isset( $comment_data['author'] ) && is_string( $comment_data['author'] ) ) {
3608 $comment_author = trim( strip_tags( $comment_data['author'] ) );
3609 }
3610 if ( isset( $comment_data['email'] ) && is_string( $comment_data['email'] ) ) {
3611 $comment_author_email = trim( $comment_data['email'] );
3612 }
3613 if ( isset( $comment_data['url'] ) && is_string( $comment_data['url'] ) ) {
3614 $comment_author_url = trim( $comment_data['url'] );
3615 }
3616 if ( isset( $comment_data['comment'] ) && is_string( $comment_data['comment'] ) ) {
3617 $comment_content = trim( $comment_data['comment'] );
3618 }
3619 if ( isset( $comment_data['comment_parent'] ) ) {
3620 $comment_parent = absint( $comment_data['comment_parent'] );
3621 $comment_parent_object = get_comment( $comment_parent );
3622
3623 if (
3624 0 !== $comment_parent &&
3625 (
3626 ! $comment_parent_object instanceof WP_Comment ||
3627 0 === (int) $comment_parent_object->comment_approved
3628 )
3629 ) {
3630 /**
3631 * Fires when a comment reply is attempted to an unapproved comment.
3632 *
3633 * @since 6.2.0
3634 *
3635 * @param int $comment_post_id Post ID.
3636 * @param int $comment_parent Parent comment ID.
3637 */
3638 do_action( 'comment_reply_to_unapproved_comment', $comment_post_id, $comment_parent );
3639
3640 return new WP_Error( 'comment_reply_to_unapproved_comment', __( 'Sorry, replies to unapproved comments are not allowed.' ), 403 );
3641 }
3642 }
3643
3644 $post = get_post( $comment_post_id );
3645
3646 if ( empty( $post->comment_status ) ) {
3647
3648 /**
3649 * Fires when a comment is attempted on a post that does not exist.
3650 *
3651 * @since 1.5.0
3652 *
3653 * @param int $comment_post_id Post ID.
3654 */
3655 do_action( 'comment_id_not_found', $comment_post_id );
3656
3657 return new WP_Error( 'comment_id_not_found' );
3658
3659 }
3660
3661 // get_post_status() will get the parent status for attachments.
3662 $status = get_post_status( $post );
3663
3664 if ( ( 'private' === $status ) && ! current_user_can( 'read_post', $comment_post_id ) ) {
3665 return new WP_Error( 'comment_id_not_found' );
3666 }
3667
3668 $status_obj = get_post_status_object( $status );
3669
3670 if ( ! comments_open( $comment_post_id ) ) {
3671
3672 /**
3673 * Fires when a comment is attempted on a post that has comments closed.
3674 *
3675 * @since 1.5.0
3676 *
3677 * @param int $comment_post_id Post ID.
3678 */
3679 do_action( 'comment_closed', $comment_post_id );
3680
3681 return new WP_Error( 'comment_closed', __( 'Sorry, comments are closed for this item.' ), 403 );
3682
3683 } elseif ( 'trash' === $status ) {
3684
3685 /**
3686 * Fires when a comment is attempted on a trashed post.
3687 *
3688 * @since 2.9.0
3689 *
3690 * @param int $comment_post_id Post ID.
3691 */
3692 do_action( 'comment_on_trash', $comment_post_id );
3693
3694 return new WP_Error( 'comment_on_trash' );
3695
3696 } elseif ( ! $status_obj->public && ! $status_obj->private ) {
3697
3698 /**
3699 * Fires when a comment is attempted on a post in draft mode.
3700 *
3701 * @since 1.5.1
3702 *
3703 * @param int $comment_post_id Post ID.
3704 */
3705 do_action( 'comment_on_draft', $comment_post_id );
3706
3707 if ( current_user_can( 'read_post', $comment_post_id ) ) {
3708 return new WP_Error( 'comment_on_draft', __( 'Sorry, comments are not allowed for this item.' ), 403 );
3709 } else {
3710 return new WP_Error( 'comment_on_draft' );
3711 }
3712 } elseif ( post_password_required( $comment_post_id ) ) {
3713
3714 /**
3715 * Fires when a comment is attempted on a password-protected post.
3716 *
3717 * @since 2.9.0
3718 *
3719 * @param int $comment_post_id Post ID.
3720 */
3721 do_action( 'comment_on_password_protected', $comment_post_id );
3722
3723 return new WP_Error( 'comment_on_password_protected' );
3724
3725 } else {
3726 /**
3727 * Fires before a comment is posted.
3728 *
3729 * @since 2.8.0
3730 *
3731 * @param int $comment_post_id Post ID.
3732 */
3733 do_action( 'pre_comment_on_post', $comment_post_id );
3734 }
3735
3736 // If the user is logged in.
3737 $user = wp_get_current_user();
3738 if ( $user->exists() ) {
3739 if ( empty( $user->display_name ) ) {
3740 $user->display_name = $user->user_login;
3741 }
3742
3743 $comment_author = $user->display_name;
3744 $comment_author_email = $user->user_email;
3745 $comment_author_url = $user->user_url;
3746 $user_id = $user->ID;
3747
3748 if ( current_user_can( 'unfiltered_html' ) ) {
3749 if ( ! isset( $comment_data['_wp_unfiltered_html_comment'] )
3750 || ! wp_verify_nonce( $comment_data['_wp_unfiltered_html_comment'], 'unfiltered-html-comment_' . $comment_post_id )
3751 ) {
3752 kses_remove_filters(); // Start with a clean slate.
3753 kses_init_filters(); // Set up the filters.
3754 remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
3755 add_filter( 'pre_comment_content', 'wp_filter_kses' );
3756 }
3757 }
3758 } else {
3759 if ( get_option( 'comment_registration' ) ) {
3760 return new WP_Error( 'not_logged_in', __( 'Sorry, you must be logged in to comment.' ), 403 );
3761 }
3762 }
3763
3764 $comment_type = 'comment';
3765
3766 if ( get_option( 'require_name_email' ) && ! $user->exists() ) {
3767 if ( '' === $comment_author_email || '' === $comment_author ) {
3768 return new WP_Error( 'require_name_email', __( '<strong>Error:</strong> Please fill the required fields.' ), 200 );
3769 } elseif ( ! is_email( $comment_author_email ) ) {
3770 return new WP_Error( 'require_valid_email', __( '<strong>Error:</strong> Please enter a valid email address.' ), 200 );
3771 }
3772 }
3773
3774 $commentdata = array(
3775 'comment_post_ID' => $comment_post_id,
3776 );
3777
3778 $commentdata += compact(
3779 'comment_author',
3780 'comment_author_email',
3781 'comment_author_url',
3782 'comment_content',
3783 'comment_type',
3784 'comment_parent',
3785 'user_id'
3786 );
3787
3788 /**
3789 * Filters whether an empty comment should be allowed.
3790 *
3791 * @since 5.1.0
3792 *
3793 * @param bool $allow_empty_comment Whether to allow empty comments. Default false.
3794 * @param array $commentdata Array of comment data to be sent to wp_insert_comment().
3795 */
3796 $allow_empty_comment = apply_filters( 'allow_empty_comment', false, $commentdata );
3797 if ( '' === $comment_content && ! $allow_empty_comment ) {
3798 return new WP_Error( 'require_valid_comment', __( '<strong>Error:</strong> Please type your comment text.' ), 200 );
3799 }
3800
3801 $check_max_lengths = wp_check_comment_data_max_lengths( $commentdata );
3802 if ( is_wp_error( $check_max_lengths ) ) {
3803 return $check_max_lengths;
3804 }
3805
3806 $comment_id = wp_new_comment( wp_slash( $commentdata ), true );
3807 if ( is_wp_error( $comment_id ) ) {
3808 return $comment_id;
3809 }
3810
3811 if ( ! $comment_id ) {
3812 return new WP_Error( 'comment_save_error', __( '<strong>Error:</strong> The comment could not be saved. Please try again later.' ), 500 );
3813 }
3814
3815 return get_comment( $comment_id );
3816}
3817
3818/**
3819 * Registers the personal data exporter for comments.
3820 *
3821 * @since 4.9.6
3822 *
3823 * @param array[] $exporters An array of personal data exporters.
3824 * @return array[] An array of personal data exporters.
3825 */
3826function wp_register_comment_personal_data_exporter( $exporters ) {
3827 $exporters['wordpress-comments'] = array(
3828 'exporter_friendly_name' => __( 'WordPress Comments' ),
3829 'callback' => 'wp_comments_personal_data_exporter',
3830 );
3831
3832 return $exporters;
3833}
3834
3835/**
3836 * Finds and exports personal data associated with an email address from the comments table.
3837 *
3838 * @since 4.9.6
3839 *
3840 * @param string $email_address The comment author email address.
3841 * @param int $page Comment page number.
3842 * @return array {
3843 * An array of personal data.
3844 *
3845 * @type array[] $data An array of personal data arrays.
3846 * @type bool $done Whether the exporter is finished.
3847 * }
3848 */
3849function wp_comments_personal_data_exporter( $email_address, $page = 1 ) {
3850 // Limit us to 500 comments at a time to avoid timing out.
3851 $number = 500;
3852 $page = (int) $page;
3853
3854 $data_to_export = array();
3855
3856 $comments = get_comments(
3857 array(
3858 'author_email' => $email_address,
3859 'number' => $number,
3860 'paged' => $page,
3861 'orderby' => 'comment_ID',
3862 'order' => 'ASC',
3863 'update_comment_meta_cache' => false,
3864 )
3865 );
3866
3867 $comment_prop_to_export = array(
3868 'comment_author' => __( 'Comment Author' ),
3869 'comment_author_email' => __( 'Comment Author Email' ),
3870 'comment_author_url' => __( 'Comment Author URL' ),
3871 'comment_author_IP' => __( 'Comment Author IP' ),
3872 'comment_agent' => __( 'Comment Author User Agent' ),
3873 'comment_date' => __( 'Comment Date' ),
3874 'comment_content' => __( 'Comment Content' ),
3875 'comment_link' => __( 'Comment URL' ),
3876 );
3877
3878 foreach ( (array) $comments as $comment ) {
3879 $comment_data_to_export = array();
3880
3881 foreach ( $comment_prop_to_export as $key => $name ) {
3882 $value = '';
3883
3884 switch ( $key ) {
3885 case 'comment_author':
3886 case 'comment_author_email':
3887 case 'comment_author_url':
3888 case 'comment_author_IP':
3889 case 'comment_agent':
3890 case 'comment_date':
3891 $value = $comment->{$key};
3892 break;
3893
3894 case 'comment_content':
3895 $value = get_comment_text( $comment->comment_ID );
3896 break;
3897
3898 case 'comment_link':
3899 $value = get_comment_link( $comment->comment_ID );
3900 $value = sprintf(
3901 '<a href="%s" target="_blank">%s</a>',
3902 esc_url( $value ),
3903 esc_html( $value )
3904 );
3905 break;
3906 }
3907
3908 if ( ! empty( $value ) ) {
3909 $comment_data_to_export[] = array(
3910 'name' => $name,
3911 'value' => $value,
3912 );
3913 }
3914 }
3915
3916 $data_to_export[] = array(
3917 'group_id' => 'comments',
3918 'group_label' => __( 'Comments' ),
3919 'group_description' => __( 'User&#8217;s comment data.' ),
3920 'item_id' => "comment-{$comment->comment_ID}",
3921 'data' => $comment_data_to_export,
3922 );
3923 }
3924
3925 $done = count( $comments ) < $number;
3926
3927 return array(
3928 'data' => $data_to_export,
3929 'done' => $done,
3930 );
3931}
3932
3933/**
3934 * Registers the personal data eraser for comments.
3935 *
3936 * @since 4.9.6
3937 *
3938 * @param array $erasers An array of personal data erasers.
3939 * @return array An array of personal data erasers.
3940 */
3941function wp_register_comment_personal_data_eraser( $erasers ) {
3942 $erasers['wordpress-comments'] = array(
3943 'eraser_friendly_name' => __( 'WordPress Comments' ),
3944 'callback' => 'wp_comments_personal_data_eraser',
3945 );
3946
3947 return $erasers;
3948}
3949
3950/**
3951 * Erases personal data associated with an email address from the comments table.
3952 *
3953 * @since 4.9.6
3954 *
3955 * @global wpdb $wpdb WordPress database abstraction object.
3956 *
3957 * @param string $email_address The comment author email address.
3958 * @param int $page Comment page number.
3959 * @return array {
3960 * Data removal results.
3961 *
3962 * @type bool $items_removed Whether items were actually removed.
3963 * @type bool $items_retained Whether items were retained.
3964 * @type string[] $messages An array of messages to add to the personal data export file.
3965 * @type bool $done Whether the eraser is finished.
3966 * }
3967 */
3968function wp_comments_personal_data_eraser( $email_address, $page = 1 ) {
3969 global $wpdb;
3970
3971 if ( empty( $email_address ) ) {
3972 return array(
3973 'items_removed' => false,
3974 'items_retained' => false,
3975 'messages' => array(),
3976 'done' => true,
3977 );
3978 }
3979
3980 // Limit us to 500 comments at a time to avoid timing out.
3981 $number = 500;
3982 $page = (int) $page;
3983 $items_removed = false;
3984 $items_retained = false;
3985
3986 $comments = get_comments(
3987 array(
3988 'author_email' => $email_address,
3989 'number' => $number,
3990 'paged' => $page,
3991 'orderby' => 'comment_ID',
3992 'order' => 'ASC',
3993 'include_unapproved' => true,
3994 )
3995 );
3996
3997 /* translators: Name of a comment's author after being anonymized. */
3998 $anon_author = __( 'Anonymous' );
3999 $messages = array();
4000
4001 foreach ( (array) $comments as $comment ) {
4002 $anonymized_comment = array();
4003 $anonymized_comment['comment_agent'] = '';
4004 $anonymized_comment['comment_author'] = $anon_author;
4005 $anonymized_comment['comment_author_email'] = '';
4006 $anonymized_comment['comment_author_IP'] = wp_privacy_anonymize_data( 'ip', $comment->comment_author_IP );
4007 $anonymized_comment['comment_author_url'] = '';
4008 $anonymized_comment['user_id'] = 0;
4009
4010 $comment_id = (int) $comment->comment_ID;
4011
4012 /**
4013 * Filters whether to anonymize the comment.
4014 *
4015 * @since 4.9.6
4016 *
4017 * @param bool|string $anon_message Whether to apply the comment anonymization (bool) or a custom
4018 * message (string). Default true.
4019 * @param WP_Comment $comment WP_Comment object.
4020 * @param array $anonymized_comment Anonymized comment data.
4021 */
4022 $anon_message = apply_filters( 'wp_anonymize_comment', true, $comment, $anonymized_comment );
4023
4024 if ( true !== $anon_message ) {
4025 if ( $anon_message && is_string( $anon_message ) ) {
4026 $messages[] = esc_html( $anon_message );
4027 } else {
4028 /* translators: %d: Comment ID. */
4029 $messages[] = sprintf( __( 'Comment %d contains personal data but could not be anonymized.' ), $comment_id );
4030 }
4031
4032 $items_retained = true;
4033
4034 continue;
4035 }
4036
4037 $args = array(
4038 'comment_ID' => $comment_id,
4039 );
4040
4041 $updated = $wpdb->update( $wpdb->comments, $anonymized_comment, $args );
4042
4043 if ( $updated ) {
4044 $items_removed = true;
4045 clean_comment_cache( $comment_id );
4046 } else {
4047 $items_retained = true;
4048 }
4049 }
4050
4051 $done = count( $comments ) < $number;
4052
4053 return array(
4054 'items_removed' => $items_removed,
4055 'items_retained' => $items_retained,
4056 'messages' => $messages,
4057 'done' => $done,
4058 );
4059}
4060
4061/**
4062 * Sets the last changed time for the 'comment' cache group.
4063 *
4064 * @since 5.0.0
4065 */
4066function wp_cache_set_comments_last_changed() {
4067 wp_cache_set_last_changed( 'comment' );
4068}
4069
4070/**
4071 * Updates the comment type for a batch of comments.
4072 *
4073 * @since 5.5.0
4074 *
4075 * @global wpdb $wpdb WordPress database abstraction object.
4076 */
4077function _wp_batch_update_comment_type() {
4078 global $wpdb;
4079
4080 $lock_name = 'update_comment_type.lock';
4081
4082 // Try to lock.
4083 $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_name, time() ) );
4084
4085 if ( ! $lock_result ) {
4086 $lock_result = get_option( $lock_name );
4087
4088 // Bail if we were unable to create a lock, or if the existing lock is still valid.
4089 if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) {
4090 wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_update_comment_type_batch' );
4091 return;
4092 }
4093 }
4094
4095 // Update the lock, as by this point we've definitely got a lock, just need to fire the actions.
4096 update_option( $lock_name, time() );
4097
4098 // Check if there's still an empty comment type.
4099 $empty_comment_type = $wpdb->get_var(
4100 "SELECT comment_ID FROM $wpdb->comments
4101 WHERE comment_type = ''
4102 LIMIT 1"
4103 );
4104
4105 // No empty comment type, we're done here.
4106 if ( ! $empty_comment_type ) {
4107 update_option( 'finished_updating_comment_type', true );
4108 delete_option( $lock_name );
4109 return;
4110 }
4111
4112 // Empty comment type found? We'll need to run this script again.
4113 wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_update_comment_type_batch' );
4114
4115 /**
4116 * Filters the comment batch size for updating the comment type.
4117 *
4118 * @since 5.5.0
4119 *
4120 * @param int $comment_batch_size The comment batch size. Default 100.
4121 */
4122 $comment_batch_size = (int) apply_filters( 'wp_update_comment_type_batch_size', 100 );
4123
4124 // Get the IDs of the comments to update.
4125 $comment_ids = $wpdb->get_col(
4126 $wpdb->prepare(
4127 "SELECT comment_ID
4128 FROM {$wpdb->comments}
4129 WHERE comment_type = ''
4130 ORDER BY comment_ID DESC
4131 LIMIT %d",
4132 $comment_batch_size
4133 )
4134 );
4135
4136 if ( $comment_ids ) {
4137 $comment_id_list = implode( ',', $comment_ids );
4138
4139 // Update the `comment_type` field value to be `comment` for the next batch of comments.
4140 $wpdb->query(
4141 "UPDATE {$wpdb->comments}
4142 SET comment_type = 'comment'
4143 WHERE comment_type = ''
4144 AND comment_ID IN ({$comment_id_list})" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
4145 );
4146
4147 // Make sure to clean the comment cache.
4148 clean_comment_cache( $comment_ids );
4149 }
4150
4151 delete_option( $lock_name );
4152}
4153
4154/**
4155 * In order to avoid the _wp_batch_update_comment_type() job being accidentally removed,
4156 * check that it's still scheduled while we haven't finished updating comment types.
4157 *
4158 * @ignore
4159 * @since 5.5.0
4160 */
4161function _wp_check_for_scheduled_update_comment_type() {
4162 if ( ! get_option( 'finished_updating_comment_type' ) && ! wp_next_scheduled( 'wp_update_comment_type_batch' ) ) {
4163 wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_update_comment_type_batch' );
4164 }
4165}
4166
4167/**
4168 * Register initial note status meta.
4169 *
4170 * @since 6.9.0
4171 */
4172function wp_create_initial_comment_meta() {
4173 register_meta(
4174 'comment',
4175 '_wp_note_status',
4176 array(
4177 'type' => 'string',
4178 'description' => __( 'Note resolution status' ),
4179 'single' => true,
4180 'show_in_rest' => array(
4181 'schema' => array(
4182 'type' => 'string',
4183 'enum' => array( 'resolved', 'reopen' ),
4184 ),
4185 ),
4186 'auth_callback' => function ( $allowed, $meta_key, $object_id ) {
4187 return current_user_can( 'edit_comment', $object_id );
4188 },
4189 )
4190 );
4191}
4192