PluginProbe ʕ •ᴥ•ʔ
Akismet Anti-spam: Spam Protection / 5.3.3
Akismet Anti-spam: Spam Protection v5.3.3
5.7 3.0.4 3.0.5 3.1 3.1.1 3.1.10 3.1.11 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2 3.3 3.3.1 3.3.2 3.3.3 3.3.4 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1 4.1.1 4.1.10 4.1.11 4.1.12 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 5.0 5.0.1 5.0.2 5.1 5.2 5.3 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4 5.5 5.6 trunk 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 2.4.0 2.4.1 2.5.0 2.5.1 2.5.10 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 3.0.0 3.0.0-RC1 3.0.1 3.0.2 3.0.3
akismet / class.akismet.php
akismet Last commit date
_inc 2 years ago views 1 year ago .htaccess 2 years ago LICENSE.txt 10 years ago akismet.php 1 year ago changelog.txt 2 years ago class.akismet-admin.php 1 year ago class.akismet-cli.php 2 years ago class.akismet-rest-api.php 2 years ago class.akismet-widget.php 2 years ago class.akismet.php 1 year ago index.php 12 years ago readme.txt 1 year ago wrapper.php 7 years ago
class.akismet.php
1922 lines
1 <?php
2
3 class Akismet {
4 const API_HOST = 'rest.akismet.com';
5 const API_PORT = 80;
6 const MAX_DELAY_BEFORE_MODERATION_EMAIL = 86400; // One day in seconds
7
8 public static $limit_notices = array(
9 10501 => 'FIRST_MONTH_OVER_LIMIT',
10 10502 => 'SECOND_MONTH_OVER_LIMIT',
11 10504 => 'THIRD_MONTH_APPROACHING_LIMIT',
12 10508 => 'THIRD_MONTH_OVER_LIMIT',
13 10516 => 'FOUR_PLUS_MONTHS_OVER_LIMIT',
14 );
15
16 private static $last_comment = '';
17 private static $initiated = false;
18 private static $prevent_moderation_email_for_these_comments = array();
19 private static $last_comment_result = null;
20 private static $comment_as_submitted_allowed_keys = array( 'blog' => '', 'blog_charset' => '', 'blog_lang' => '', 'blog_ua' => '', 'comment_agent' => '', 'comment_author' => '', 'comment_author_IP' => '', 'comment_author_email' => '', 'comment_author_url' => '', 'comment_content' => '', 'comment_date_gmt' => '', 'comment_tags' => '', 'comment_type' => '', 'guid' => '', 'is_test' => '', 'permalink' => '', 'reporter' => '', 'site_domain' => '', 'submit_referer' => '', 'submit_uri' => '', 'user_ID' => '', 'user_agent' => '', 'user_id' => '', 'user_ip' => '' );
21
22 public static function init() {
23 if ( ! self::$initiated ) {
24 self::init_hooks();
25 }
26 }
27
28 /**
29 * Initializes WordPress hooks
30 */
31 private static function init_hooks() {
32 self::$initiated = true;
33
34 add_action( 'wp_insert_comment', array( 'Akismet', 'auto_check_update_meta' ), 10, 2 );
35 add_filter( 'preprocess_comment', array( 'Akismet', 'auto_check_comment' ), 1 );
36 add_filter( 'rest_pre_insert_comment', array( 'Akismet', 'rest_auto_check_comment' ), 1 );
37
38 add_action( 'comment_form', array( 'Akismet', 'load_form_js' ) );
39 add_action( 'do_shortcode_tag', array( 'Akismet', 'load_form_js_via_filter' ), 10, 4 );
40
41 add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments' ) );
42 add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments_meta' ) );
43 add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_orphaned_commentmeta' ) );
44 add_action( 'akismet_schedule_cron_recheck', array( 'Akismet', 'cron_recheck' ) );
45
46 add_action( 'comment_form', array( 'Akismet', 'add_comment_nonce' ), 1 );
47 add_action( 'comment_form', array( 'Akismet', 'output_custom_form_fields' ) );
48 add_filter( 'script_loader_tag', array( 'Akismet', 'set_form_js_async' ), 10, 3 );
49
50 add_filter( 'comment_moderation_recipients', array( 'Akismet', 'disable_moderation_emails_if_unreachable' ), 1000, 2 );
51 add_filter( 'pre_comment_approved', array( 'Akismet', 'last_comment_status' ), 10, 2 );
52
53 add_action( 'transition_comment_status', array( 'Akismet', 'transition_comment_status' ), 10, 3 );
54
55 // Run this early in the pingback call, before doing a remote fetch of the source uri
56 add_action( 'xmlrpc_call', array( 'Akismet', 'pre_check_pingback' ), 10, 3 );
57
58 // Jetpack compatibility
59 add_filter( 'jetpack_options_whitelist', array( 'Akismet', 'add_to_jetpack_options_whitelist' ) );
60 add_filter( 'jetpack_contact_form_html', array( 'Akismet', 'inject_custom_form_fields' ) );
61 add_filter( 'jetpack_contact_form_akismet_values', array( 'Akismet', 'prepare_custom_form_values' ) );
62
63 // Gravity Forms
64 add_filter( 'gform_get_form_filter', array( 'Akismet', 'inject_custom_form_fields' ) );
65 add_filter( 'gform_akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ) );
66
67 // Contact Form 7
68 add_filter( 'wpcf7_form_elements', array( 'Akismet', 'append_custom_form_fields' ) );
69 add_filter( 'wpcf7_akismet_parameters', array( 'Akismet', 'prepare_custom_form_values' ) );
70
71 // Formidable Forms
72 add_filter( 'frm_filter_final_form', array( 'Akismet', 'inject_custom_form_fields' ) );
73 add_filter( 'frm_akismet_values', array( 'Akismet', 'prepare_custom_form_values' ) );
74
75 // Fluent Forms
76 /*
77 * The Fluent Forms hook names were updated in version 5.0.0. The last version that supported
78 * the original hook names was 4.3.25, and version 4.3.25 was tested up to WordPress version 6.1.
79 *
80 * The legacy hooks are fired before the new hooks. See
81 * https://github.com/fluentform/fluentform/commit/cc45341afcae400f217470a7bbfb15efdd80454f
82 *
83 * The legacy Fluent Forms hooks will be removed when Akismet no longer supports WordPress version 6.1.
84 * This will provide compatibility with previous versions of Fluent Forms for a reasonable amount of time.
85 */
86 add_filter( 'fluentform_form_element_start', array( 'Akismet', 'output_custom_form_fields' ) );
87 add_filter( 'fluentform_akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ), 10, 2 );
88 // Current Fluent Form hooks.
89 add_filter( 'fluentform/form_element_start', array( 'Akismet', 'output_custom_form_fields' ) );
90 add_filter( 'fluentform/akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ), 10, 2 );
91
92 add_action( 'update_option_wordpress_api_key', array( 'Akismet', 'updated_option' ), 10, 2 );
93 add_action( 'add_option_wordpress_api_key', array( 'Akismet', 'added_option' ), 10, 2 );
94
95 add_action( 'comment_form_after', array( 'Akismet', 'display_comment_form_privacy_notice' ) );
96 }
97
98 public static function get_api_key() {
99 return apply_filters( 'akismet_get_api_key', defined('WPCOM_API_KEY') ? constant('WPCOM_API_KEY') : get_option('wordpress_api_key') );
100 }
101
102 /**
103 * Exchange the API key for a token that can only be used to access stats pages.
104 *
105 * @return string
106 */
107 public static function get_access_token() {
108 static $access_token = null;
109
110 if ( is_null( $access_token ) ) {
111 $request_args = array( 'api_key' => self::get_api_key() );
112
113 $request_args = apply_filters( 'akismet_request_args', $request_args, 'token' );
114
115 $response = self::http_post( self::build_query( $request_args ), 'token' );
116
117 $access_token = $response[1];
118 }
119
120 return $access_token;
121 }
122
123 public static function check_key_status( $key, $ip = null ) {
124 $request_args = array(
125 'key' => $key,
126 'blog' => get_option( 'home' ),
127 );
128
129 $request_args = apply_filters( 'akismet_request_args', $request_args, 'verify-key' );
130
131 return self::http_post( self::build_query( $request_args ), 'verify-key', $ip );
132 }
133
134 public static function verify_key( $key, $ip = null ) {
135 // Shortcut for obviously invalid keys.
136 if ( strlen( $key ) != 12 ) {
137 return 'invalid';
138 }
139
140 $response = self::check_key_status( $key, $ip );
141
142 if ( $response[1] != 'valid' && $response[1] != 'invalid' )
143 return 'failed';
144
145 return $response[1];
146 }
147
148 public static function deactivate_key( $key ) {
149 $request_args = array(
150 'key' => $key,
151 'blog' => get_option( 'home' ),
152 );
153
154 $request_args = apply_filters( 'akismet_request_args', $request_args, 'deactivate' );
155
156 $response = self::http_post( self::build_query( $request_args ), 'deactivate' );
157
158 if ( $response[1] != 'deactivated' )
159 return 'failed';
160
161 return $response[1];
162 }
163
164 /**
165 * Add the akismet option to the Jetpack options management whitelist.
166 *
167 * @param array $options The list of whitelisted option names.
168 * @return array The updated whitelist
169 */
170 public static function add_to_jetpack_options_whitelist( $options ) {
171 $options[] = 'wordpress_api_key';
172 return $options;
173 }
174
175 /**
176 * When the akismet option is updated, run the registration call.
177 *
178 * This should only be run when the option is updated from the Jetpack/WP.com
179 * API call, and only if the new key is different than the old key.
180 *
181 * @param mixed $old_value The old option value.
182 * @param mixed $value The new option value.
183 */
184 public static function updated_option( $old_value, $value ) {
185 // Not an API call
186 if ( ! class_exists( 'WPCOM_JSON_API_Update_Option_Endpoint' ) ) {
187 return;
188 }
189 // Only run the registration if the old key is different.
190 if ( $old_value !== $value ) {
191 self::verify_key( $value );
192 }
193 }
194
195 /**
196 * Treat the creation of an API key the same as updating the API key to a new value.
197 *
198 * @param mixed $option_name Will always be "wordpress_api_key", until something else hooks in here.
199 * @param mixed $value The option value.
200 */
201 public static function added_option( $option_name, $value ) {
202 if ( 'wordpress_api_key' === $option_name ) {
203 return self::updated_option( '', $value );
204 }
205 }
206
207 public static function rest_auto_check_comment( $commentdata ) {
208 return self::auto_check_comment( $commentdata, 'rest_api' );
209 }
210
211 /**
212 * Check a comment for spam.
213 *
214 * @param array $commentdata
215 * @param string $context What kind of request triggered this comment check? Possible values are 'default', 'rest_api', and 'xml-rpc'.
216 * @return array|WP_Error Either the $commentdata array with additional entries related to its spam status
217 * or a WP_Error, if it's a REST API request and the comment should be discarded.
218 */
219 public static function auto_check_comment( $commentdata, $context = 'default' ) {
220 // If no key is configured, then there's no point in doing any of this.
221 if ( ! self::get_api_key() ) {
222 return $commentdata;
223 }
224
225 self::$last_comment_result = null;
226
227 // Skip the Akismet check if the comment matches the Disallowed Keys list.
228 if ( function_exists( 'wp_check_comment_disallowed_list' ) ) {
229 $comment_author = isset( $commentdata['comment_author'] ) ? $commentdata['comment_author'] : '';
230 $comment_author_email = isset( $commentdata['comment_author_email'] ) ? $commentdata['comment_author_email'] : '';
231 $comment_author_url = isset( $commentdata['comment_author_url'] ) ? $commentdata['comment_author_url'] : '';
232 $comment_content = isset( $commentdata['comment_content'] ) ? $commentdata['comment_content'] : '';
233 $comment_author_ip = isset( $commentdata['comment_author_IP'] ) ? $commentdata['comment_author_IP'] : '';
234 $comment_agent = isset( $commentdata['comment_agent'] ) ? $commentdata['comment_agent'] : '';
235
236 if ( wp_check_comment_disallowed_list( $comment_author, $comment_author_email, $comment_author_url, $comment_content, $comment_author_ip, $comment_agent ) ) {
237 self::set_last_comment( $commentdata );
238 return $commentdata;
239 }
240 }
241
242 $comment = $commentdata;
243
244 $comment['user_ip'] = self::get_ip_address();
245 $comment['user_agent'] = self::get_user_agent();
246 $comment['referrer'] = self::get_referer();
247 $comment['blog'] = get_option( 'home' );
248 $comment['blog_lang'] = get_locale();
249 $comment['blog_charset'] = get_option('blog_charset');
250 $comment['permalink'] = get_permalink( $comment['comment_post_ID'] );
251
252 if ( ! empty( $comment['user_ID'] ) ) {
253 $comment['user_role'] = Akismet::get_user_roles( $comment['user_ID'] );
254 }
255
256 /** See filter documentation in init_hooks(). */
257 $akismet_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) );
258 $comment['akismet_comment_nonce'] = 'inactive';
259 if ( $akismet_nonce_option == 'true' || $akismet_nonce_option == '' ) {
260 $comment['akismet_comment_nonce'] = 'failed';
261 if ( isset( $_POST['akismet_comment_nonce'] ) && wp_verify_nonce( $_POST['akismet_comment_nonce'], 'akismet_comment_nonce_' . $comment['comment_post_ID'] ) )
262 $comment['akismet_comment_nonce'] = 'passed';
263
264 // comment reply in wp-admin
265 if ( isset( $_POST['_ajax_nonce-replyto-comment'] ) && check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' ) )
266 $comment['akismet_comment_nonce'] = 'passed';
267
268 }
269
270 if ( self::is_test_mode() )
271 $comment['is_test'] = 'true';
272
273 foreach( $_POST as $key => $value ) {
274 if ( is_string( $value ) )
275 $comment["POST_{$key}"] = $value;
276 }
277
278 foreach ( $_SERVER as $key => $value ) {
279 if ( ! is_string( $value ) ) {
280 continue;
281 }
282
283 if ( preg_match( "/^HTTP_COOKIE/", $key ) ) {
284 continue;
285 }
286
287 // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need.
288 if ( preg_match( "/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/", $key ) ) {
289 $comment[ "$key" ] = $value;
290 }
291 }
292
293 $post = get_post( $comment['comment_post_ID'] );
294
295 if ( ! is_null( $post ) ) {
296 // $post can technically be null, although in the past, it's always been an indicator of another plugin interfering.
297 $comment[ 'comment_post_modified_gmt' ] = $post->post_modified_gmt;
298
299 // Tags and categories are important context in which to consider the comment.
300 $comment['comment_context'] = array();
301
302 $tag_names = wp_get_post_tags( $post->ID, array( 'fields' => 'names' ) );
303
304 if ( $tag_names && ! is_wp_error( $tag_names ) ) {
305 foreach ( $tag_names as $tag_name ) {
306 $comment['comment_context'][] = $tag_name;
307 }
308 }
309
310 $category_names = wp_get_post_categories( $post->ID, array( 'fields' => 'names' ) );
311
312 if ( $category_names && ! is_wp_error( $category_names ) ) {
313 foreach ( $category_names as $category_name ) {
314 $comment['comment_context'][] = $category_name;
315 }
316 }
317 }
318
319 /**
320 * Filter the data that is used to generate the request body for the API call.
321 *
322 * @since 5.3.1
323 *
324 * @param array $comment An array of request data.
325 * @param string $endpoint The API endpoint being requested.
326 */
327 $comment = apply_filters( 'akismet_request_args', $comment, 'comment-check' );
328
329 $response = self::http_post( self::build_query( $comment ), 'comment-check' );
330
331 do_action( 'akismet_comment_check_response', $response );
332
333 $commentdata['comment_as_submitted'] = array_intersect_key( $comment, self::$comment_as_submitted_allowed_keys );
334
335 // Also include any form fields we inject into the comment form, like ak_js
336 foreach ( $_POST as $key => $value ) {
337 if ( is_string( $value ) && strpos( $key, 'ak_' ) === 0 ) {
338 $commentdata['comment_as_submitted'][ 'POST_' . $key ] = $value;
339 }
340 }
341
342 $commentdata['akismet_result'] = $response[1];
343
344 if ( isset( $response[0]['x-akismet-pro-tip'] ) )
345 $commentdata['akismet_pro_tip'] = $response[0]['x-akismet-pro-tip'];
346
347 if ( isset( $response[0]['x-akismet-guid'] ) ) {
348 $commentdata['akismet_guid'] = $response[0]['x-akismet-guid'];
349 }
350
351 if ( isset( $response[0]['x-akismet-error'] ) ) {
352 // An error occurred that we anticipated (like a suspended key) and want the user to act on.
353 // Send to moderation.
354 self::$last_comment_result = '0';
355 }
356 else if ( 'true' == $response[1] ) {
357 // akismet_spam_count will be incremented later by comment_is_spam()
358 self::$last_comment_result = 'spam';
359
360 $discard = ( isset( $commentdata['akismet_pro_tip'] ) && $commentdata['akismet_pro_tip'] === 'discard' && self::allow_discard() );
361
362 do_action( 'akismet_spam_caught', $discard );
363
364 if ( $discard ) {
365 // The spam is obvious, so we're bailing out early.
366 // akismet_result_spam() won't be called so bump the counter here
367 if ( $incr = apply_filters( 'akismet_spam_count_incr', 1 ) ) {
368 update_option( 'akismet_spam_count', get_option( 'akismet_spam_count' ) + $incr );
369 }
370
371 if ( 'rest_api' === $context ) {
372 return new WP_Error( 'akismet_rest_comment_discarded', __( 'Comment discarded.', 'akismet' ) );
373 } else if ( 'xml-rpc' === $context ) {
374 // If this is a pingback that we're pre-checking, the discard behavior is the same as the normal spam response behavior.
375 return $commentdata;
376 } else {
377 // Redirect back to the previous page, or failing that, the post permalink, or failing that, the homepage of the blog.
378 $redirect_to = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : ( $post ? get_permalink( $post ) : home_url() );
379 wp_safe_redirect( esc_url_raw( $redirect_to ) );
380 die();
381 }
382 }
383 else if ( 'rest_api' === $context ) {
384 // The way the REST API structures its calls, we can set the comment_approved value right away.
385 $commentdata['comment_approved'] = 'spam';
386 }
387 }
388
389 // if the response is neither true nor false, hold the comment for moderation and schedule a recheck
390 if ( 'true' != $response[1] && 'false' != $response[1] ) {
391 if ( !current_user_can('moderate_comments') ) {
392 // Comment status should be moderated
393 self::$last_comment_result = '0';
394 }
395
396 if ( ! wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) {
397 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
398 do_action( 'akismet_scheduled_recheck', 'invalid-response-' . $response[1] );
399 }
400
401 self::$prevent_moderation_email_for_these_comments[] = $commentdata;
402 }
403
404 // Delete old comments daily
405 if ( ! wp_next_scheduled( 'akismet_scheduled_delete' ) ) {
406 wp_schedule_event( time(), 'daily', 'akismet_scheduled_delete' );
407 }
408
409 self::set_last_comment( $commentdata );
410 self::fix_scheduled_recheck();
411
412 return $commentdata;
413 }
414
415 public static function get_last_comment() {
416 return self::$last_comment;
417 }
418
419 public static function set_last_comment( $comment ) {
420 if ( is_null( $comment ) ) {
421 self::$last_comment = null;
422 }
423 else {
424 // We filter it here so that it matches the filtered comment data that we'll have to compare against later.
425 // wp_filter_comment expects comment_author_IP
426 self::$last_comment = wp_filter_comment(
427 array_merge(
428 array( 'comment_author_IP' => self::get_ip_address() ),
429 $comment
430 )
431 );
432 }
433 }
434
435 // this fires on wp_insert_comment. we can't update comment_meta when auto_check_comment() runs
436 // because we don't know the comment ID at that point.
437 public static function auto_check_update_meta( $id, $comment ) {
438 // wp_insert_comment() might be called in other contexts, so make sure this is the same comment
439 // as was checked by auto_check_comment
440 if ( is_object( $comment ) && !empty( self::$last_comment ) && is_array( self::$last_comment ) ) {
441 if ( self::matches_last_comment( $comment ) ) {
442 load_plugin_textdomain( 'akismet' );
443
444 // normal result: true or false
445 if ( isset( self::$last_comment['akismet_result'] ) && self::$last_comment['akismet_result'] == 'true' ) {
446 update_comment_meta( $comment->comment_ID, 'akismet_result', 'true' );
447 self::update_comment_history( $comment->comment_ID, '', 'check-spam' );
448 if ( $comment->comment_approved != 'spam' ) {
449 self::update_comment_history(
450 $comment->comment_ID,
451 '',
452 'status-changed-' . $comment->comment_approved
453 );
454 }
455 } elseif ( isset( self::$last_comment['akismet_result'] ) && self::$last_comment['akismet_result'] == 'false' ) {
456 update_comment_meta( $comment->comment_ID, 'akismet_result', 'false' );
457 self::update_comment_history( $comment->comment_ID, '', 'check-ham' );
458 // Status could be spam or trash, depending on the WP version and whether this change applies:
459 // https://core.trac.wordpress.org/changeset/34726
460 if ( $comment->comment_approved == 'spam' || $comment->comment_approved == 'trash' ) {
461 if ( function_exists( 'wp_check_comment_disallowed_list' ) ) {
462 if ( wp_check_comment_disallowed_list( $comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent ) ) {
463 self::update_comment_history( $comment->comment_ID, '', 'wp-disallowed' );
464 } else {
465 self::update_comment_history( $comment->comment_ID, '', 'status-changed-' . $comment->comment_approved );
466 }
467 } else {
468 self::update_comment_history( $comment->comment_ID, '', 'status-changed-' . $comment->comment_approved );
469 }
470 }
471 } elseif ( ! isset( self::$last_comment['akismet_result'] ) ) {
472 // akismet_result isn't set, so the comment wasn't sent to Akismet.
473 update_comment_meta( $comment->comment_ID, 'akismet_skipped', 'true' );
474 $caught_by_disallowed_list = false;
475
476 if ( function_exists( 'wp_check_comment_disallowed_list' ) ) {
477 $caught_by_disallowed_list = wp_check_comment_disallowed_list( $comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent );
478 }
479
480 if ( $caught_by_disallowed_list ) {
481 self::update_comment_history( $comment->comment_ID, '', 'wp-disallowed' );
482 self::update_comment_history( $comment->comment_ID, '', 'akismet-skipped-disallowed' );
483 } else {
484 // Add a generic skipped history item.
485 self::update_comment_history( $comment->comment_ID, '', 'akismet-skipped' );
486 }
487 } else {
488 // abnormal result: error
489 update_comment_meta( $comment->comment_ID, 'akismet_error', time() );
490 self::update_comment_history(
491 $comment->comment_ID,
492 '',
493 'check-error',
494 array( 'response' => substr( self::$last_comment['akismet_result'], 0, 50 ) )
495 );
496 }
497
498 // record the complete original data as submitted for checking
499 if ( isset( self::$last_comment['comment_as_submitted'] ) ) {
500 update_comment_meta( $comment->comment_ID, 'akismet_as_submitted', self::$last_comment['comment_as_submitted'] );
501 }
502
503 if ( isset( self::$last_comment['akismet_pro_tip'] ) ) {
504 update_comment_meta( $comment->comment_ID, 'akismet_pro_tip', self::$last_comment['akismet_pro_tip'] );
505 }
506
507 if ( isset( self::$last_comment['akismet_guid'] ) ) {
508 update_comment_meta( $comment->comment_ID, 'akismet_guid', self::$last_comment['akismet_guid'] );
509 }
510 }
511 }
512 }
513
514 public static function delete_old_comments() {
515 global $wpdb;
516
517 /**
518 * Determines how many comments will be deleted in each batch.
519 *
520 * @param int The default, as defined by AKISMET_DELETE_LIMIT.
521 */
522 $delete_limit = apply_filters( 'akismet_delete_comment_limit', defined( 'AKISMET_DELETE_LIMIT' ) ? AKISMET_DELETE_LIMIT : 10000 );
523 $delete_limit = max( 1, intval( $delete_limit ) );
524
525 /**
526 * Determines how many days a comment will be left in the Spam queue before being deleted.
527 *
528 * @param int The default number of days.
529 */
530 $delete_interval = apply_filters( 'akismet_delete_comment_interval', 15 );
531 $delete_interval = max( 1, intval( $delete_interval ) );
532
533 while ( $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_id FROM {$wpdb->comments} WHERE DATE_SUB(NOW(), INTERVAL %d DAY) > comment_date_gmt AND comment_approved = 'spam' LIMIT %d", $delete_interval, $delete_limit ) ) ) {
534 if ( empty( $comment_ids ) )
535 return;
536
537 $wpdb->queries = array();
538
539 $comments = array();
540
541 foreach ( $comment_ids as $comment_id ) {
542 $comments[ $comment_id ] = get_comment( $comment_id );
543
544 do_action( 'delete_comment', $comment_id, $comments[ $comment_id ] );
545 do_action( 'akismet_batch_delete_count', __FUNCTION__ );
546 }
547
548 // Prepared as strings since comment_id is an unsigned BIGINT, and using %d will constrain the value to the maximum signed BIGINT.
549 $format_string = implode( ', ', array_fill( 0, is_countable( $comment_ids ) ? count( $comment_ids ) : 0, '%s' ) );
550
551 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->comments} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) );
552 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) );
553
554 foreach ( $comment_ids as $comment_id ) {
555 do_action( 'deleted_comment', $comment_id, $comments[ $comment_id ] );
556 unset( $comments[ $comment_id ] );
557 }
558
559 clean_comment_cache( $comment_ids );
560 do_action( 'akismet_delete_comment_batch', is_countable( $comment_ids ) ? count( $comment_ids ) : 0 );
561 }
562
563 if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->comments ) ) // lucky number
564 $wpdb->query("OPTIMIZE TABLE {$wpdb->comments}");
565 }
566
567 public static function delete_old_comments_meta() {
568 global $wpdb;
569
570 $interval = apply_filters( 'akismet_delete_commentmeta_interval', 15 );
571
572 # enforce a minimum of 1 day
573 $interval = absint( $interval );
574 if ( $interval < 1 )
575 $interval = 1;
576
577 // akismet_as_submitted meta values are large, so expire them
578 // after $interval days regardless of the comment status
579 while ( $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT m.comment_id FROM {$wpdb->commentmeta} as m INNER JOIN {$wpdb->comments} as c USING(comment_id) WHERE m.meta_key = 'akismet_as_submitted' AND DATE_SUB(NOW(), INTERVAL %d DAY) > c.comment_date_gmt LIMIT 10000", $interval ) ) ) {
580 if ( empty( $comment_ids ) )
581 return;
582
583 $wpdb->queries = array();
584
585 foreach ( $comment_ids as $comment_id ) {
586 delete_comment_meta( $comment_id, 'akismet_as_submitted' );
587 do_action( 'akismet_batch_delete_count', __FUNCTION__ );
588 }
589
590 do_action( 'akismet_delete_commentmeta_batch', is_countable( $comment_ids ) ? count( $comment_ids ) : 0 );
591 }
592
593 if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->commentmeta ) ) // lucky number
594 $wpdb->query("OPTIMIZE TABLE {$wpdb->commentmeta}");
595 }
596
597 // Clear out comments meta that no longer have corresponding comments in the database
598 public static function delete_orphaned_commentmeta() {
599 global $wpdb;
600
601 $last_meta_id = 0;
602 $start_time = isset( $_SERVER['REQUEST_TIME_FLOAT'] ) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true );
603 $max_exec_time = max( ini_get('max_execution_time') - 5, 3 );
604
605 while ( $commentmeta_results = $wpdb->get_results( $wpdb->prepare( "SELECT m.meta_id, m.comment_id, m.meta_key FROM {$wpdb->commentmeta} as m LEFT JOIN {$wpdb->comments} as c USING(comment_id) WHERE c.comment_id IS NULL AND m.meta_id > %d ORDER BY m.meta_id LIMIT 1000", $last_meta_id ) ) ) {
606 if ( empty( $commentmeta_results ) )
607 return;
608
609 $wpdb->queries = array();
610
611 $commentmeta_deleted = 0;
612
613 foreach ( $commentmeta_results as $commentmeta ) {
614 if ( 'akismet_' == substr( $commentmeta->meta_key, 0, 8 ) ) {
615 delete_comment_meta( $commentmeta->comment_id, $commentmeta->meta_key );
616 do_action( 'akismet_batch_delete_count', __FUNCTION__ );
617 $commentmeta_deleted++;
618 }
619
620 $last_meta_id = $commentmeta->meta_id;
621 }
622
623 do_action( 'akismet_delete_commentmeta_batch', $commentmeta_deleted );
624
625 // If we're getting close to max_execution_time, quit for this round.
626 if ( microtime(true) - $start_time > $max_exec_time )
627 return;
628 }
629
630 if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->commentmeta ) ) // lucky number
631 $wpdb->query("OPTIMIZE TABLE {$wpdb->commentmeta}");
632 }
633
634 // how many approved comments does this author have?
635 public static function get_user_comments_approved( $user_id, $comment_author_email, $comment_author, $comment_author_url ) {
636 global $wpdb;
637
638 /**
639 * Which comment types should be ignored when counting a user's approved comments?
640 *
641 * Some plugins add entries to the comments table that are not actual
642 * comments that could have been checked by Akismet. Allow these comments
643 * to be excluded from the "approved comment count" query in order to
644 * avoid artificially inflating the approved comment count.
645 *
646 * @param array $comment_types An array of comment types that won't be considered
647 * when counting a user's approved comments.
648 *
649 * @since 4.2.2
650 */
651 $excluded_comment_types = apply_filters( 'akismet_excluded_comment_types', array() );
652
653 $comment_type_where = '';
654
655 if ( is_array( $excluded_comment_types ) && ! empty( $excluded_comment_types ) ) {
656 $excluded_comment_types = array_unique( $excluded_comment_types );
657
658 foreach ( $excluded_comment_types as $excluded_comment_type ) {
659 $comment_type_where .= $wpdb->prepare( ' AND comment_type <> %s ', $excluded_comment_type );
660 }
661 }
662
663 if ( ! empty( $user_id ) ) {
664 return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE user_id = %d AND comment_approved = 1" . $comment_type_where, $user_id ) );
665 }
666
667 if ( ! empty( $comment_author_email ) ) {
668 return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_author_email = %s AND comment_author = %s AND comment_author_url = %s AND comment_approved = 1" . $comment_type_where, $comment_author_email, $comment_author, $comment_author_url ) );
669 }
670
671 return 0;
672 }
673
674 /**
675 * Get the full comment history for a given comment, as an array in reverse chronological order.
676 * Each entry will have an 'event', a 'time', and possible a 'message' member (if the entry is old enough).
677 * Some entries will also have a 'user' or 'meta' member.
678 *
679 * @param int $comment_id The relevant comment ID.
680 * @return array|bool An array of history events, or false if there is no history.
681 */
682 public static function get_comment_history( $comment_id ) {
683 $history = get_comment_meta( $comment_id, 'akismet_history', false );
684 if ( empty( $history ) || empty( $history[ 0 ] ) ) {
685 return false;
686 }
687
688 /*
689 // To see all variants when testing.
690 $history[] = array( 'time' => 445856401, 'message' => 'Old versions of Akismet stored the message as a literal string in the commentmeta.', 'event' => null );
691 $history[] = array( 'time' => 445856402, 'event' => 'recheck-spam' );
692 $history[] = array( 'time' => 445856403, 'event' => 'check-spam' );
693 $history[] = array( 'time' => 445856404, 'event' => 'recheck-ham' );
694 $history[] = array( 'time' => 445856405, 'event' => 'check-ham' );
695 $history[] = array( 'time' => 445856406, 'event' => 'wp-blacklisted' );
696 $history[] = array( 'time' => 445856406, 'event' => 'wp-disallowed' );
697 $history[] = array( 'time' => 445856407, 'event' => 'report-spam' );
698 $history[] = array( 'time' => 445856408, 'event' => 'report-spam', 'user' => 'sam' );
699 $history[] = array( 'message' => 'sam reported this comment as spam (hardcoded message).', 'time' => 445856400, 'event' => 'report-spam', 'user' => 'sam' );
700 $history[] = array( 'time' => 445856409, 'event' => 'report-ham', 'user' => 'sam' );
701 $history[] = array( 'message' => 'sam reported this comment as ham (hardcoded message).', 'time' => 445856400, 'event' => 'report-ham', 'user' => 'sam' ); //
702 $history[] = array( 'time' => 445856410, 'event' => 'cron-retry-spam' );
703 $history[] = array( 'time' => 445856411, 'event' => 'cron-retry-ham' );
704 $history[] = array( 'time' => 445856412, 'event' => 'check-error' ); //
705 $history[] = array( 'time' => 445856413, 'event' => 'check-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) );
706 $history[] = array( 'time' => 445856414, 'event' => 'recheck-error' ); // Should not generate a message.
707 $history[] = array( 'time' => 445856415, 'event' => 'recheck-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) );
708 $history[] = array( 'time' => 445856416, 'event' => 'status-changedtrash' );
709 $history[] = array( 'time' => 445856417, 'event' => 'status-changedspam' );
710 $history[] = array( 'time' => 445856418, 'event' => 'status-changedhold' );
711 $history[] = array( 'time' => 445856419, 'event' => 'status-changedapprove' );
712 $history[] = array( 'time' => 445856420, 'event' => 'status-changed-trash' );
713 $history[] = array( 'time' => 445856421, 'event' => 'status-changed-spam' );
714 $history[] = array( 'time' => 445856422, 'event' => 'status-changed-hold' );
715 $history[] = array( 'time' => 445856423, 'event' => 'status-changed-approve' );
716 $history[] = array( 'time' => 445856424, 'event' => 'status-trash', 'user' => 'sam' );
717 $history[] = array( 'time' => 445856425, 'event' => 'status-spam', 'user' => 'sam' );
718 $history[] = array( 'time' => 445856426, 'event' => 'status-hold', 'user' => 'sam' );
719 $history[] = array( 'time' => 445856427, 'event' => 'status-approve', 'user' => 'sam' );
720 $history[] = array( 'time' => 445856427, 'event' => 'webhook-spam' );
721 $history[] = array( 'time' => 445856427, 'event' => 'webhook-ham' );
722 $history[] = array( 'time' => 445856427, 'event' => 'webhook-spam-noaction' );
723 $history[] = array( 'time' => 445856427, 'event' => 'webhook-ham-noaction' );
724 */
725
726 usort( $history, array( 'Akismet', '_cmp_time' ) );
727 return $history;
728 }
729
730 /**
731 * Log an event for a given comment, storing it in comment_meta.
732 *
733 * @param int $comment_id The ID of the relevant comment.
734 * @param string $message The string description of the event. No longer used.
735 * @param string $event The event code.
736 * @param array $meta Metadata about the history entry. e.g., the user that reported or changed the status of a given comment.
737 */
738 public static function update_comment_history( $comment_id, $message, $event=null, $meta=null ) {
739 global $current_user;
740
741 $user = '';
742
743 $event = array(
744 'time' => self::_get_microtime(),
745 'event' => $event,
746 );
747
748 if ( is_object( $current_user ) && isset( $current_user->user_login ) ) {
749 $event['user'] = $current_user->user_login;
750 }
751
752 if ( ! empty( $meta ) ) {
753 $event['meta'] = $meta;
754 }
755
756 // $unique = false so as to allow multiple values per comment
757 $r = add_comment_meta( $comment_id, 'akismet_history', $event, false );
758 }
759
760 public static function check_db_comment( $id, $recheck_reason = 'recheck_queue' ) {
761 global $wpdb;
762
763 if ( ! self::get_api_key() ) {
764 return new WP_Error( 'akismet-not-configured', __( 'Akismet is not configured. Please enter an API key.', 'akismet' ) );
765 }
766
767 $c = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $id ), ARRAY_A );
768
769 if ( ! $c ) {
770 return new WP_Error( 'invalid-comment-id', __( 'Comment not found.', 'akismet' ) );
771 }
772
773 $c['user_ip'] = $c['comment_author_IP'];
774 $c['user_agent'] = $c['comment_agent'];
775 $c['referrer'] = '';
776 $c['blog'] = get_option( 'home' );
777 $c['blog_lang'] = get_locale();
778 $c['blog_charset'] = get_option('blog_charset');
779 $c['permalink'] = get_permalink($c['comment_post_ID']);
780 $c['recheck_reason'] = $recheck_reason;
781
782 $c['user_role'] = '';
783 if ( ! empty( $c['user_ID'] ) ) {
784 $c['user_role'] = Akismet::get_user_roles( $c['user_ID'] );
785 }
786
787 if ( self::is_test_mode() )
788 $c['is_test'] = 'true';
789
790 $c = apply_filters( 'akismet_request_args', $c, 'comment-check' );
791
792 $response = self::http_post( self::build_query( $c ), 'comment-check' );
793
794 if ( ! empty( $response[1] ) ) {
795 return $response[1];
796 }
797
798 return false;
799 }
800
801 public static function recheck_comment( $id, $recheck_reason = 'recheck_queue' ) {
802 add_comment_meta( $id, 'akismet_rechecking', true );
803
804 $api_response = self::check_db_comment( $id, $recheck_reason );
805
806 if ( is_wp_error( $api_response ) ) {
807 // Invalid comment ID.
808 }
809 else if ( 'true' === $api_response ) {
810 wp_set_comment_status( $id, 'spam' );
811 update_comment_meta( $id, 'akismet_result', 'true' );
812 delete_comment_meta( $id, 'akismet_error' );
813 delete_comment_meta( $id, 'akismet_delayed_moderation_email' );
814 Akismet::update_comment_history( $id, '', 'recheck-spam' );
815 }
816 elseif ( 'false' === $api_response ) {
817 update_comment_meta( $id, 'akismet_result', 'false' );
818 delete_comment_meta( $id, 'akismet_error' );
819 delete_comment_meta( $id, 'akismet_delayed_moderation_email' );
820 Akismet::update_comment_history( $id, '', 'recheck-ham' );
821 }
822 else {
823 // abnormal result: error
824 update_comment_meta( $id, 'akismet_result', 'error' );
825 Akismet::update_comment_history(
826 $id,
827 '',
828 'recheck-error',
829 array( 'response' => substr( $api_response, 0, 50 ) )
830 );
831 }
832
833 delete_comment_meta( $id, 'akismet_rechecking' );
834
835 return $api_response;
836 }
837
838 public static function transition_comment_status( $new_status, $old_status, $comment ) {
839
840 if ( $new_status == $old_status )
841 return;
842
843 if ( 'spam' === $new_status || 'spam' === $old_status ) {
844 // Clear the cache of the "X comments in your spam queue" count on the dashboard.
845 wp_cache_delete( 'akismet_spam_count', 'widget' );
846 }
847
848 # we don't need to record a history item for deleted comments
849 if ( $new_status == 'delete' )
850 return;
851
852 if ( !current_user_can( 'edit_post', $comment->comment_post_ID ) && !current_user_can( 'moderate_comments' ) )
853 return;
854
855 if ( defined('WP_IMPORTING') && WP_IMPORTING == true )
856 return;
857
858 // if this is present, it means the status has been changed by a re-check, not an explicit user action
859 if ( get_comment_meta( $comment->comment_ID, 'akismet_rechecking' ) )
860 return;
861
862 if ( function_exists( 'getallheaders' ) ) {
863 $request_headers = getallheaders();
864
865 foreach ( $request_headers as $header => $value ) {
866 if ( strtolower( $header ) == 'x-akismet-webhook' ) {
867 // This change is due to a webhook request.
868 return;
869 }
870 }
871 }
872
873 // Assumption alert:
874 // We want to submit comments to Akismet only when a moderator explicitly spams or approves it - not if the status
875 // is changed automatically by another plugin. Unfortunately WordPress doesn't provide an unambiguous way to
876 // determine why the transition_comment_status action was triggered. And there are several different ways by which
877 // to spam and unspam comments: bulk actions, ajax, links in moderation emails, the dashboard, and perhaps others.
878 // We'll assume that this is an explicit user action if certain POST/GET variables exist.
879 if (
880 // status=spam: Marking as spam via the REST API or...
881 // status=unspam: I'm not sure. Maybe this used to be used instead of status=approved? Or the UI for removing from spam but not approving has been since removed?...
882 // status=approved: Unspamming via the REST API (Calypso) or...
883 ( isset( $_POST['status'] ) && in_array( $_POST['status'], array( 'spam', 'unspam', 'approved', ) ) )
884 // spam=1: Clicking "Spam" underneath a comment in wp-admin and allowing the AJAX request to happen.
885 || ( isset( $_POST['spam'] ) && (int) $_POST['spam'] == 1 )
886 // unspam=1: Clicking "Not Spam" underneath a comment in wp-admin and allowing the AJAX request to happen. Or, clicking "Undo" after marking something as spam.
887 || ( isset( $_POST['unspam'] ) && (int) $_POST['unspam'] == 1 )
888 // comment_status=spam/unspam: It's unclear where this is happening.
889 || ( isset( $_POST['comment_status'] ) && in_array( $_POST['comment_status'], array( 'spam', 'unspam' ) ) )
890 // action=spam: Choosing "Mark as Spam" from the Bulk Actions dropdown in wp-admin (or the "Spam it" link in notification emails).
891 // action=unspam: Choosing "Not Spam" from the Bulk Actions dropdown in wp-admin.
892 // action=spamcomment: Following the "Spam" link below a comment in wp-admin (not allowing AJAX request to happen).
893 // action=unspamcomment: Following the "Not Spam" link below a comment in wp-admin (not allowing AJAX request to happen).
894 || ( isset( $_GET['action'] ) && in_array( $_GET['action'], array( 'spam', 'unspam', 'spamcomment', 'unspamcomment', ) ) )
895 // action=editedcomment: Editing a comment via wp-admin (and possibly changing its status).
896 || ( isset( $_POST['action'] ) && in_array( $_POST['action'], array( 'editedcomment' ) ) )
897 // for=jetpack: Moderation via the WordPress app, Calypso, anything powered by the Jetpack connection.
898 || ( isset( $_GET['for'] ) && ( 'jetpack' == $_GET['for'] ) && ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) )
899 // Certain WordPress.com API requests
900 || ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST )
901 // WordPress.org REST API requests
902 || ( defined( 'REST_REQUEST' ) && REST_REQUEST )
903 ) {
904 if ( $new_status == 'spam' && ( $old_status == 'approved' || $old_status == 'unapproved' || !$old_status ) ) {
905 return self::submit_spam_comment( $comment->comment_ID );
906 } elseif ( $old_status == 'spam' && ( $new_status == 'approved' || $new_status == 'unapproved' ) ) {
907 return self::submit_nonspam_comment( $comment->comment_ID );
908 }
909 }
910
911 self::update_comment_history( $comment->comment_ID, '', 'status-' . $new_status );
912 }
913
914 public static function submit_spam_comment( $comment_id ) {
915 global $wpdb, $current_user, $current_site;
916
917 $comment_id = (int) $comment_id;
918
919 $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ), ARRAY_A );
920
921 if ( ! $comment ) {
922 // it was deleted
923 return;
924 }
925
926 if ( 'spam' != $comment['comment_approved'] ) {
927 return;
928 }
929
930 self::update_comment_history( $comment_id, '', 'report-spam' );
931
932 // If the user hasn't configured Akismet, there's nothing else to do at this point.
933 if ( ! self::get_api_key() ) {
934 return;
935 }
936
937 // use the original version stored in comment_meta if available
938 $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) );
939
940 if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) ) {
941 $comment = array_merge( $comment, $as_submitted );
942 }
943
944 $comment['blog'] = get_option( 'home' );
945 $comment['blog_lang'] = get_locale();
946 $comment['blog_charset'] = get_option( 'blog_charset' );
947 $comment['permalink'] = get_permalink( $comment['comment_post_ID'] );
948
949 if ( is_object( $current_user ) ) {
950 $comment['reporter'] = $current_user->user_login;
951 }
952
953 if ( is_object( $current_site ) ) {
954 $comment['site_domain'] = $current_site->domain;
955 }
956
957 $comment['user_role'] = '';
958 if ( ! empty( $comment['user_ID'] ) ) {
959 $comment['user_role'] = self::get_user_roles( $comment['user_ID'] );
960 }
961
962 if ( self::is_test_mode() ) {
963 $comment['is_test'] = 'true';
964 }
965
966 $post = get_post( $comment['comment_post_ID'] );
967
968 if ( ! is_null( $post ) ) {
969 $comment['comment_post_modified_gmt'] = $post->post_modified_gmt;
970 }
971
972 $comment = apply_filters( 'akismet_request_args', $comment, 'submit-spam' );
973
974 $response = self::http_post( self::build_query( $comment ), 'submit-spam' );
975
976 update_comment_meta( $comment_id, 'akismet_user_result', 'true' );
977
978 if ( $comment['reporter'] ) {
979 update_comment_meta( $comment_id, 'akismet_user', $comment['reporter'] );
980 }
981
982 do_action('akismet_submit_spam_comment', $comment_id, $response[1]);
983 }
984
985 public static function submit_nonspam_comment( $comment_id ) {
986 global $wpdb, $current_user, $current_site;
987
988 $comment_id = (int) $comment_id;
989
990 $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ), ARRAY_A );
991
992 if ( ! $comment ) {
993 // it was deleted
994 return;
995 }
996
997 self::update_comment_history( $comment_id, '', 'report-ham' );
998
999 // If the user hasn't configured Akismet, there's nothing else to do at this point.
1000 if ( ! self::get_api_key() ) {
1001 return;
1002 }
1003
1004 // use the original version stored in comment_meta if available
1005 $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) );
1006
1007 if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) ) {
1008 $comment = array_merge( $comment, $as_submitted );
1009 }
1010
1011 $comment['blog'] = get_option( 'home' );
1012 $comment['blog_lang'] = get_locale();
1013 $comment['blog_charset'] = get_option( 'blog_charset' );
1014 $comment['permalink'] = get_permalink( $comment['comment_post_ID'] );
1015 $comment['user_role'] = '';
1016
1017 if ( is_object( $current_user ) ) {
1018 $comment['reporter'] = $current_user->user_login;
1019 }
1020
1021 if ( is_object( $current_site ) ) {
1022 $comment['site_domain'] = $current_site->domain;
1023 }
1024
1025 if ( ! empty( $comment['user_ID'] ) ) {
1026 $comment['user_role'] = self::get_user_roles( $comment['user_ID'] );
1027 }
1028
1029 if ( self::is_test_mode() ) {
1030 $comment['is_test'] = 'true';
1031 }
1032
1033 $post = get_post( $comment['comment_post_ID'] );
1034
1035 if ( ! is_null( $post ) ) {
1036 $comment['comment_post_modified_gmt'] = $post->post_modified_gmt;
1037 }
1038
1039 $comment = apply_filters( 'akismet_request_args', $comment, 'submit-ham' );
1040
1041 $response = self::http_post( self::build_query( $comment ), 'submit-ham' );
1042
1043 update_comment_meta( $comment_id, 'akismet_user_result', 'false' );
1044
1045 if ( $comment['reporter'] ) {
1046 update_comment_meta( $comment_id, 'akismet_user', $comment['reporter'] );
1047 }
1048
1049 do_action('akismet_submit_nonspam_comment', $comment_id, $response[1]);
1050 }
1051
1052 public static function cron_recheck() {
1053 global $wpdb;
1054
1055 $api_key = self::get_api_key();
1056
1057 $status = self::verify_key( $api_key );
1058 if ( get_option( 'akismet_alert_code' ) || $status == 'invalid' ) {
1059 // since there is currently a problem with the key, reschedule a check for 6 hours hence
1060 wp_schedule_single_event( time() + 21600, 'akismet_schedule_cron_recheck' );
1061 do_action( 'akismet_scheduled_recheck', 'key-problem-' . get_option( 'akismet_alert_code' ) . '-' . $status );
1062 return false;
1063 }
1064
1065 delete_option('akismet_available_servers');
1066
1067 $comment_errors = $wpdb->get_col( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error' LIMIT 100" );
1068
1069 load_plugin_textdomain( 'akismet' );
1070
1071 foreach ( (array) $comment_errors as $comment_id ) {
1072 // if the comment no longer exists, or is too old, remove the meta entry from the queue to avoid getting stuck
1073 $comment = get_comment( $comment_id );
1074
1075 if (
1076 ! $comment // Comment has been deleted
1077 || strtotime( $comment->comment_date_gmt ) < strtotime( "-15 days" ) // Comment is too old.
1078 || $comment->comment_approved !== "0" // Comment is no longer in the Pending queue
1079 ) {
1080 delete_comment_meta( $comment_id, 'akismet_error' );
1081 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
1082 continue;
1083 }
1084
1085 add_comment_meta( $comment_id, 'akismet_rechecking', true );
1086 $status = self::check_db_comment( $comment_id, 'retry' );
1087
1088 $event = '';
1089 if ( $status == 'true' ) {
1090 $event = 'cron-retry-spam';
1091 } elseif ( $status == 'false' ) {
1092 $event = 'cron-retry-ham';
1093 }
1094
1095 // If we got back a legit response then update the comment history
1096 // other wise just bail now and try again later. No point in
1097 // re-trying all the comments once we hit one failure.
1098 if ( !empty( $event ) ) {
1099 delete_comment_meta( $comment_id, 'akismet_error' );
1100 self::update_comment_history( $comment_id, '', $event );
1101 update_comment_meta( $comment_id, 'akismet_result', $status );
1102 // make sure the comment status is still pending. if it isn't, that means the user has already moved it elsewhere.
1103 $comment = get_comment( $comment_id );
1104 if ( $comment && 'unapproved' == wp_get_comment_status( $comment_id ) ) {
1105 if ( $status == 'true' ) {
1106 wp_spam_comment( $comment_id );
1107 } elseif ( $status == 'false' ) {
1108 // comment is good, but it's still in the pending queue. depending on the moderation settings
1109 // we may need to change it to approved.
1110 if ( check_comment($comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent, $comment->comment_type) )
1111 wp_set_comment_status( $comment_id, 1 );
1112 else if ( get_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ) )
1113 wp_notify_moderator( $comment_id );
1114 }
1115 }
1116
1117 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
1118 } else {
1119 // If this comment has been pending moderation for longer than MAX_DELAY_BEFORE_MODERATION_EMAIL,
1120 // send a moderation email now.
1121 if ( ( intval( gmdate( 'U' ) ) - strtotime( $comment->comment_date_gmt ) ) < self::MAX_DELAY_BEFORE_MODERATION_EMAIL ) {
1122 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
1123 wp_notify_moderator( $comment_id );
1124 }
1125
1126 delete_comment_meta( $comment_id, 'akismet_rechecking' );
1127 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
1128 do_action( 'akismet_scheduled_recheck', 'check-db-comment-' . $status );
1129 return;
1130 }
1131 delete_comment_meta( $comment_id, 'akismet_rechecking' );
1132 }
1133
1134 $remaining = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error'" );
1135 if ( $remaining && !wp_next_scheduled('akismet_schedule_cron_recheck') ) {
1136 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
1137 do_action( 'akismet_scheduled_recheck', 'remaining' );
1138 }
1139 }
1140
1141 public static function fix_scheduled_recheck() {
1142 $future_check = wp_next_scheduled( 'akismet_schedule_cron_recheck' );
1143 if ( !$future_check ) {
1144 return;
1145 }
1146
1147 if ( get_option( 'akismet_alert_code' ) > 0 ) {
1148 return;
1149 }
1150
1151 $check_range = time() + 1200;
1152 if ( $future_check > $check_range ) {
1153 wp_clear_scheduled_hook( 'akismet_schedule_cron_recheck' );
1154 wp_schedule_single_event( time() + 300, 'akismet_schedule_cron_recheck' );
1155 do_action( 'akismet_scheduled_recheck', 'fix-scheduled-recheck' );
1156 }
1157 }
1158
1159 public static function add_comment_nonce( $post_id ) {
1160 /**
1161 * To disable the Akismet comment nonce, add a filter for the 'akismet_comment_nonce' tag
1162 * and return any string value that is not 'true' or '' (empty string).
1163 *
1164 * Don't return boolean false, because that implies that the 'akismet_comment_nonce' option
1165 * has not been set and that Akismet should just choose the default behavior for that
1166 * situation.
1167 */
1168
1169 if ( ! self::get_api_key() ) {
1170 return;
1171 }
1172
1173 $akismet_comment_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) );
1174
1175 if ( $akismet_comment_nonce_option == 'true' || $akismet_comment_nonce_option == '' ) {
1176 echo '<p style="display: none;">';
1177 wp_nonce_field( 'akismet_comment_nonce_' . $post_id, 'akismet_comment_nonce', FALSE );
1178 echo '</p>';
1179 }
1180 }
1181
1182 public static function is_test_mode() {
1183 return defined('AKISMET_TEST_MODE') && AKISMET_TEST_MODE;
1184 }
1185
1186 public static function allow_discard() {
1187 if ( defined( 'DOING_AJAX' ) && DOING_AJAX )
1188 return false;
1189 if ( is_user_logged_in() )
1190 return false;
1191
1192 return ( get_option( 'akismet_strictness' ) === '1' );
1193 }
1194
1195 public static function get_ip_address() {
1196 return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
1197 }
1198
1199 /**
1200 * Do these two comments, without checking the comment_ID, "match"?
1201 *
1202 * @param mixed $comment1 A comment object or array.
1203 * @param mixed $comment2 A comment object or array.
1204 * @return bool Whether the two comments should be treated as the same comment.
1205 */
1206 private static function comments_match( $comment1, $comment2 ) {
1207 $comment1 = (array) $comment1;
1208 $comment2 = (array) $comment2;
1209
1210 // Set default values for these strings that we check in order to simplify
1211 // the checks and avoid PHP warnings.
1212 if ( ! isset( $comment1['comment_author'] ) ) {
1213 $comment1['comment_author'] = '';
1214 }
1215
1216 if ( ! isset( $comment2['comment_author'] ) ) {
1217 $comment2['comment_author'] = '';
1218 }
1219
1220 if ( ! isset( $comment1['comment_author_email'] ) ) {
1221 $comment1['comment_author_email'] = '';
1222 }
1223
1224 if ( ! isset( $comment2['comment_author_email'] ) ) {
1225 $comment2['comment_author_email'] = '';
1226 }
1227
1228 $comments_match = (
1229 isset( $comment1['comment_post_ID'], $comment2['comment_post_ID'] )
1230 && intval( $comment1['comment_post_ID'] ) == intval( $comment2['comment_post_ID'] )
1231 && (
1232 // The comment author length max is 255 characters, limited by the TINYTEXT column type.
1233 // If the comment author includes multibyte characters right around the 255-byte mark, they
1234 // may be stripped when the author is saved in the DB, so a 300+ char author may turn into
1235 // a 253-char author when it's saved, not 255 exactly. The longest possible character is
1236 // theoretically 6 bytes, so we'll only look at the first 248 bytes to be safe.
1237 substr( $comment1['comment_author'], 0, 248 ) == substr( $comment2['comment_author'], 0, 248 )
1238 || substr( stripslashes( $comment1['comment_author'] ), 0, 248 ) == substr( $comment2['comment_author'], 0, 248 )
1239 || substr( $comment1['comment_author'], 0, 248 ) == substr( stripslashes( $comment2['comment_author'] ), 0, 248 )
1240 // Certain long comment author names will be truncated to nothing, depending on their encoding.
1241 || ( ! $comment1['comment_author'] && strlen( $comment2['comment_author'] ) > 248 )
1242 || ( ! $comment2['comment_author'] && strlen( $comment1['comment_author'] ) > 248 )
1243 )
1244 && (
1245 // The email max length is 100 characters, limited by the VARCHAR(100) column type.
1246 // Same argument as above for only looking at the first 93 characters.
1247 substr( $comment1['comment_author_email'], 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 )
1248 || substr( stripslashes( $comment1['comment_author_email'] ), 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 )
1249 || substr( $comment1['comment_author_email'], 0, 93 ) == substr( stripslashes( $comment2['comment_author_email'] ), 0, 93 )
1250 // Very long emails can be truncated and then stripped if the [0:100] substring isn't a valid address.
1251 || ( ! $comment1['comment_author_email'] && strlen( $comment2['comment_author_email'] ) > 100 )
1252 || ( ! $comment2['comment_author_email'] && strlen( $comment1['comment_author_email'] ) > 100 )
1253 )
1254 );
1255
1256 return $comments_match;
1257 }
1258
1259 // Does the supplied comment match the details of the one most recently stored in self::$last_comment?
1260 public static function matches_last_comment( $comment ) {
1261 return self::comments_match( self::$last_comment, $comment );
1262 }
1263
1264 private static function get_user_agent() {
1265 return isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null;
1266 }
1267
1268 private static function get_referer() {
1269 return isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : null;
1270 }
1271
1272 // return a comma-separated list of role names for the given user
1273 public static function get_user_roles( $user_id ) {
1274 $comment_user = null;
1275 $roles = false;
1276
1277 if ( !class_exists('WP_User') )
1278 return false;
1279
1280 if ( $user_id > 0 ) {
1281 $comment_user = new WP_User( $user_id );
1282 if ( isset( $comment_user->roles ) )
1283 $roles = implode( ',', $comment_user->roles );
1284 }
1285
1286 if ( is_multisite() && is_super_admin( $user_id ) ) {
1287 if ( empty( $roles ) ) {
1288 $roles = 'super_admin';
1289 } else {
1290 $comment_user->roles[] = 'super_admin';
1291 $roles = implode( ',', $comment_user->roles );
1292 }
1293 }
1294
1295 return $roles;
1296 }
1297
1298 // filter handler used to return a spam result to pre_comment_approved
1299 public static function last_comment_status( $approved, $comment ) {
1300 if ( is_null( self::$last_comment_result ) ) {
1301 // We didn't have reason to store the result of the last check.
1302 return $approved;
1303 }
1304
1305 // Only do this if it's the correct comment
1306 if ( ! self::matches_last_comment( $comment ) ) {
1307 self::log( "comment_is_spam mismatched comment, returning unaltered $approved" );
1308 return $approved;
1309 }
1310
1311 if ( 'trash' === $approved ) {
1312 // If the last comment we checked has had its approval set to 'trash',
1313 // then it failed the comment blacklist check. Let that blacklist override
1314 // the spam check, since users have the (valid) expectation that when
1315 // they fill out their blacklists, comments that match it will always
1316 // end up in the trash.
1317 return $approved;
1318 }
1319
1320 // bump the counter here instead of when the filter is added to reduce the possibility of overcounting
1321 if ( $incr = apply_filters('akismet_spam_count_incr', 1) )
1322 update_option( 'akismet_spam_count', get_option('akismet_spam_count') + $incr );
1323
1324 return self::$last_comment_result;
1325 }
1326
1327 /**
1328 * If Akismet is temporarily unreachable, we don't want to "spam" the blogger with
1329 * moderation emails for comments that will be automatically cleared or spammed on
1330 * the next retry.
1331 *
1332 * For comments that will be rechecked later, empty the list of email addresses that
1333 * the moderation email would be sent to.
1334 *
1335 * @param array $emails An array of email addresses that the moderation email will be sent to.
1336 * @param int $comment_id The ID of the relevant comment.
1337 * @return array An array of email addresses that the moderation email will be sent to.
1338 */
1339 public static function disable_moderation_emails_if_unreachable( $emails, $comment_id ) {
1340 if ( ! empty( self::$prevent_moderation_email_for_these_comments ) && ! empty( $emails ) ) {
1341 $comment = get_comment( $comment_id );
1342
1343 if ( $comment ) {
1344 foreach ( self::$prevent_moderation_email_for_these_comments as $possible_match ) {
1345 if ( self::comments_match( $possible_match, $comment ) ) {
1346 update_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true );
1347 return array();
1348 }
1349 }
1350 }
1351 }
1352
1353 return $emails;
1354 }
1355
1356 public static function _cmp_time( $a, $b ) {
1357 return $a['time'] > $b['time'] ? -1 : 1;
1358 }
1359
1360 public static function _get_microtime() {
1361 $mtime = explode( ' ', microtime() );
1362 return $mtime[1] + $mtime[0];
1363 }
1364
1365 /**
1366 * Make a POST request to the Akismet API.
1367 *
1368 * @param string $request The body of the request.
1369 * @param string $path The path for the request.
1370 * @param string $ip The specific IP address to hit.
1371 * @return array A two-member array consisting of the headers and the response body, both empty in the case of a failure.
1372 */
1373 public static function http_post( $request, $path, $ip=null ) {
1374
1375 $akismet_ua = sprintf( 'WordPress/%s | Akismet/%s', $GLOBALS['wp_version'], constant( 'AKISMET_VERSION' ) );
1376 $akismet_ua = apply_filters( 'akismet_ua', $akismet_ua );
1377
1378 $host = self::API_HOST;
1379 $api_key = self::get_api_key();
1380
1381 if ( $api_key ) {
1382 $request = add_query_arg( 'api_key', $api_key, $request );
1383 }
1384
1385 $http_host = $host;
1386 // use a specific IP if provided
1387 // needed by Akismet_Admin::check_server_connectivity()
1388 if ( $ip && long2ip( ip2long( $ip ) ) ) {
1389 $http_host = $ip;
1390 }
1391
1392 $http_args = array(
1393 'body' => $request,
1394 'headers' => array(
1395 'Content-Type' => 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' ),
1396 'Host' => $host,
1397 'User-Agent' => $akismet_ua,
1398 ),
1399 'httpversion' => '1.0',
1400 'timeout' => 15
1401 );
1402
1403 $akismet_url = $http_akismet_url = "http://{$http_host}/1.1/{$path}";
1404
1405 /**
1406 * Try SSL first; if that fails, try without it and don't try it again for a while.
1407 */
1408
1409 $ssl = $ssl_failed = false;
1410
1411 // Check if SSL requests were disabled fewer than X hours ago.
1412 $ssl_disabled = get_option( 'akismet_ssl_disabled' );
1413
1414 if ( $ssl_disabled && $ssl_disabled < ( time() - 60 * 60 * 24 ) ) { // 24 hours
1415 $ssl_disabled = false;
1416 delete_option( 'akismet_ssl_disabled' );
1417 }
1418 else if ( $ssl_disabled ) {
1419 do_action( 'akismet_ssl_disabled' );
1420 }
1421
1422 if ( ! $ssl_disabled && ( $ssl = wp_http_supports( array( 'ssl' ) ) ) ) {
1423 $akismet_url = set_url_scheme( $akismet_url, 'https' );
1424
1425 do_action( 'akismet_https_request_pre' );
1426 }
1427
1428 $response = wp_remote_post( $akismet_url, $http_args );
1429
1430 Akismet::log( compact( 'akismet_url', 'http_args', 'response' ) );
1431
1432 if ( $ssl && is_wp_error( $response ) ) {
1433 do_action( 'akismet_https_request_failure', $response );
1434
1435 // Intermittent connection problems may cause the first HTTPS
1436 // request to fail and subsequent HTTP requests to succeed randomly.
1437 // Retry the HTTPS request once before disabling SSL for a time.
1438 $response = wp_remote_post( $akismet_url, $http_args );
1439
1440 Akismet::log( compact( 'akismet_url', 'http_args', 'response' ) );
1441
1442 if ( is_wp_error( $response ) ) {
1443 $ssl_failed = true;
1444
1445 do_action( 'akismet_https_request_failure', $response );
1446
1447 do_action( 'akismet_http_request_pre' );
1448
1449 // Try the request again without SSL.
1450 $response = wp_remote_post( $http_akismet_url, $http_args );
1451
1452 Akismet::log( compact( 'http_akismet_url', 'http_args', 'response' ) );
1453 }
1454 }
1455
1456 if ( is_wp_error( $response ) ) {
1457 do_action( 'akismet_request_failure', $response );
1458
1459 return array( '', '' );
1460 }
1461
1462 if ( $ssl_failed ) {
1463 // The request failed when using SSL but succeeded without it. Disable SSL for future requests.
1464 update_option( 'akismet_ssl_disabled', time() );
1465
1466 do_action( 'akismet_https_disabled' );
1467 }
1468
1469 $simplified_response = array( $response['headers'], $response['body'] );
1470
1471 $alert_code_check_paths = array(
1472 'verify-key',
1473 'comment-check',
1474 'get-stats',
1475 );
1476
1477 if ( in_array( $path, $alert_code_check_paths ) ) {
1478 self::update_alert( $simplified_response );
1479 }
1480
1481 return $simplified_response;
1482 }
1483
1484 // given a response from an API call like check_key_status(), update the alert code options if an alert is present.
1485 public static function update_alert( $response ) {
1486 $alert_option_prefix = 'akismet_alert_';
1487 $alert_header_prefix = 'x-akismet-alert-';
1488 $alert_header_names = array(
1489 'code',
1490 'msg',
1491 'api-calls',
1492 'usage-limit',
1493 'upgrade-plan',
1494 'upgrade-url',
1495 'upgrade-type',
1496 'upgrade-via-support',
1497 );
1498
1499 foreach ( $alert_header_names as $alert_header_name ) {
1500 $value = null;
1501 if ( isset( $response[0][ $alert_header_prefix . $alert_header_name ] ) ) {
1502 $value = $response[0][ $alert_header_prefix . $alert_header_name ];
1503 }
1504
1505 $option_name = $alert_option_prefix . str_replace( '-', '_', $alert_header_name );
1506 if ( $value != get_option( $option_name ) ) {
1507 if ( ! $value ) {
1508 delete_option( $option_name );
1509 } else {
1510 update_option( $option_name, $value );
1511 }
1512 }
1513 }
1514 }
1515
1516 /**
1517 * Mark akismet-frontend.js as deferred. Because nothing depends on it, it can run at any time
1518 * after it's loaded, and the browser won't have to wait for it to load to continue
1519 * parsing the rest of the page.
1520 */
1521 public static function set_form_js_async( $tag, $handle, $src ) {
1522 if ( 'akismet-frontend' !== $handle ) {
1523 return $tag;
1524 }
1525
1526 return preg_replace( '/^<script /i', '<script defer ', $tag );
1527 }
1528
1529 public static function get_akismet_form_fields() {
1530 $fields = '';
1531
1532 $prefix = 'ak_';
1533
1534 // Contact Form 7 uses _wpcf7 as a prefix to know which fields to exclude from comment_content.
1535 if ( 'wpcf7_form_elements' === current_filter() ) {
1536 $prefix = '_wpcf7_ak_';
1537 }
1538
1539 $fields .= '<p style="display: none !important;" class="akismet-fields-container" data-prefix="' . esc_attr( $prefix ) . '">';
1540 $fields .= '<label>&#916;<textarea name="' . $prefix . 'hp_textarea" cols="45" rows="8" maxlength="100"></textarea></label>';
1541
1542 if ( ! function_exists( 'amp_is_request' ) || ! amp_is_request() ) {
1543 // Keep track of how many ak_js fields are in this page so that we don't re-use
1544 // the same ID.
1545 static $field_count = 0;
1546
1547 $field_count++;
1548
1549 $fields .= '<input type="hidden" id="ak_js_' . $field_count . '" name="' . $prefix . 'js" value="' . mt_rand( 0, 250 ) . '"/>';
1550 $fields .= '<script>document.getElementById( "ak_js_' . $field_count . '" ).setAttribute( "value", ( new Date() ).getTime() );</script>';
1551 }
1552
1553 $fields .= '</p>';
1554
1555 return $fields;
1556 }
1557
1558 public static function output_custom_form_fields( $post_id ) {
1559 if ( 'fluentform/form_element_start' === current_filter() && did_action( 'fluentform_form_element_start' ) ) {
1560 // Already did this via the legacy filter.
1561 return;
1562 }
1563
1564 // phpcs:ignore WordPress.Security.EscapeOutput
1565 echo self::get_akismet_form_fields();
1566 }
1567
1568 public static function inject_custom_form_fields( $html ) {
1569 $html = str_replace( '</form>', self::get_akismet_form_fields() . '</form>', $html );
1570
1571 return $html;
1572 }
1573
1574 public static function append_custom_form_fields( $html ) {
1575 $html .= self::get_akismet_form_fields();
1576
1577 return $html;
1578 }
1579
1580 /**
1581 * Ensure that any Akismet-added form fields are included in the comment-check call.
1582 *
1583 * @param array $form
1584 * @param array $data Some plugins will supply the POST data via the filter, since they don't
1585 * read it directly from $_POST.
1586 * @return array $form
1587 */
1588 public static function prepare_custom_form_values( $form, $data = null ) {
1589 if ( 'fluentform/akismet_fields' === current_filter() && did_filter( 'fluentform_akismet_fields' ) ) {
1590 // Already updated the form fields via the legacy filter.
1591 return $form;
1592 }
1593
1594 if ( is_null( $data ) ) {
1595 // phpcs:ignore WordPress.Security.NonceVerification.Missing
1596 $data = $_POST;
1597 }
1598
1599 $prefix = 'ak_';
1600
1601 // Contact Form 7 uses _wpcf7 as a prefix to know which fields to exclude from comment_content.
1602 if ( 'wpcf7_akismet_parameters' === current_filter() ) {
1603 $prefix = '_wpcf7_ak_';
1604 }
1605
1606 foreach ( $data as $key => $val ) {
1607 if ( 0 === strpos( $key, $prefix ) ) {
1608 $form[ 'POST_ak_' . substr( $key, strlen( $prefix ) ) ] = $val;
1609 }
1610 }
1611
1612 return $form;
1613 }
1614
1615 private static function bail_on_activation( $message, $deactivate = true ) {
1616 ?>
1617 <!doctype html>
1618 <html>
1619 <head>
1620 <meta charset="<?php bloginfo( 'charset' ); ?>" />
1621 <style>
1622 * {
1623 text-align: center;
1624 margin: 0;
1625 padding: 0;
1626 font-family: "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
1627 }
1628 p {
1629 margin-top: 1em;
1630 font-size: 18px;
1631 }
1632 </style>
1633 </head>
1634 <body>
1635 <p><?php echo esc_html( $message ); ?></p>
1636 </body>
1637 </html>
1638 <?php
1639 if ( $deactivate ) {
1640 $plugins = get_option( 'active_plugins' );
1641 $akismet = plugin_basename( AKISMET__PLUGIN_DIR . 'akismet.php' );
1642 $update = false;
1643 foreach ( $plugins as $i => $plugin ) {
1644 if ( $plugin === $akismet ) {
1645 $plugins[$i] = false;
1646 $update = true;
1647 }
1648 }
1649
1650 if ( $update ) {
1651 update_option( 'active_plugins', array_filter( $plugins ) );
1652 }
1653 }
1654 exit;
1655 }
1656
1657 public static function view( $name, array $args = array() ) {
1658 $args = apply_filters( 'akismet_view_arguments', $args, $name );
1659
1660 foreach ( $args as $key => $val ) {
1661 $$key = $val;
1662 }
1663
1664 load_plugin_textdomain( 'akismet' );
1665
1666 $file = AKISMET__PLUGIN_DIR . 'views/'. $name . '.php';
1667
1668 include( $file );
1669 }
1670
1671 /**
1672 * Attached to activate_{ plugin_basename( __FILES__ ) } by register_activation_hook()
1673 * @static
1674 */
1675 public static function plugin_activation() {
1676 if ( version_compare( $GLOBALS['wp_version'], AKISMET__MINIMUM_WP_VERSION, '<' ) ) {
1677 load_plugin_textdomain( 'akismet' );
1678
1679 $message = '<strong>'.sprintf(esc_html__( 'Akismet %s requires WordPress %s or higher.' , 'akismet'), AKISMET_VERSION, AKISMET__MINIMUM_WP_VERSION ).'</strong> '.sprintf(__('Please <a href="%1$s">upgrade WordPress</a> to a current version, or <a href="%2$s">downgrade to version 2.4 of the Akismet plugin</a>.', 'akismet'), 'https://codex.wordpress.org/Upgrading_WordPress', 'https://wordpress.org/extend/plugins/akismet/download/');
1680
1681 Akismet::bail_on_activation( $message );
1682 } elseif ( ! empty( $_SERVER['SCRIPT_NAME'] ) && false !== strpos( $_SERVER['SCRIPT_NAME'], '/wp-admin/plugins.php' ) ) {
1683 add_option( 'Activated_Akismet', true );
1684 }
1685 }
1686
1687 /**
1688 * Removes all connection options
1689 * @static
1690 */
1691 public static function plugin_deactivation( ) {
1692 self::deactivate_key( self::get_api_key() );
1693
1694 // Remove any scheduled cron jobs.
1695 $akismet_cron_events = array(
1696 'akismet_schedule_cron_recheck',
1697 'akismet_scheduled_delete',
1698 );
1699
1700 foreach ( $akismet_cron_events as $akismet_cron_event ) {
1701 $timestamp = wp_next_scheduled( $akismet_cron_event );
1702
1703 if ( $timestamp ) {
1704 wp_unschedule_event( $timestamp, $akismet_cron_event );
1705 }
1706 }
1707 }
1708
1709 /**
1710 * Essentially a copy of WP's build_query but one that doesn't expect pre-urlencoded values.
1711 *
1712 * @param array $args An array of key => value pairs
1713 * @return string A string ready for use as a URL query string.
1714 */
1715 public static function build_query( $args ) {
1716 return _http_build_query( $args, '', '&' );
1717 }
1718
1719 /**
1720 * Log debugging info to the error log.
1721 *
1722 * Enabled when WP_DEBUG_LOG is enabled (and WP_DEBUG, since according to
1723 * core, "WP_DEBUG_DISPLAY and WP_DEBUG_LOG perform no function unless
1724 * WP_DEBUG is true), but can be disabled via the akismet_debug_log filter.
1725 *
1726 * @param mixed $akismet_debug The data to log.
1727 */
1728 public static function log( $akismet_debug ) {
1729 if ( apply_filters( 'akismet_debug_log', defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG && defined( 'AKISMET_DEBUG' ) && AKISMET_DEBUG ) ) {
1730 error_log( print_r( compact( 'akismet_debug' ), true ) );
1731 }
1732 }
1733
1734 /**
1735 * Check pingbacks for spam before they're saved to the DB.
1736 *
1737 * @param string $method The XML-RPC method that was called.
1738 * @param array $args This and the $server arg are marked as optional since plugins might still be
1739 * calling do_action( 'xmlrpc_action', [...] ) without the arguments that were added in WP 5.7.
1740 * @param wp_xmlrpc_server $server
1741 */
1742 public static function pre_check_pingback( $method, $args = array(), $server = null ) {
1743 if ( $method !== 'pingback.ping' ) {
1744 return;
1745 }
1746
1747 /*
1748 * $args looks like this:
1749 *
1750 * Array
1751 * (
1752 * [0] => http://www.example.net/?p=1 // Site that created the pingback.
1753 * [1] => https://www.example.com/?p=2 // Post being pingback'd on this site.
1754 * )
1755 */
1756
1757 if ( ! is_null( $server ) && ! empty( $args[1] ) ) {
1758 $is_multicall = false;
1759 $multicall_count = 0;
1760
1761 if ( 'system.multicall' === $server->message->methodName ) {
1762 $is_multicall = true;
1763 $multicall_count = is_countable( $server->message->params ) ? count( $server->message->params ) : 0;
1764 }
1765
1766 $post_id = url_to_postid( $args[1] );
1767
1768 // If pingbacks aren't open on this post, we'll still check whether this request is part of a potential DDOS,
1769 // but indicate to the server that pingbacks are indeed closed so we don't include this request in the user's stats,
1770 // since the user has already done their part by disabling pingbacks.
1771 $pingbacks_closed = false;
1772
1773 $post = get_post( $post_id );
1774
1775 if ( ! $post || ! pings_open( $post ) ) {
1776 $pingbacks_closed = true;
1777 }
1778
1779 $comment = array(
1780 'comment_author_url' => $args[0],
1781 'comment_post_ID' => $post_id,
1782 'comment_author' => '',
1783 'comment_author_email' => '',
1784 'comment_content' => '',
1785 'comment_type' => 'pingback',
1786 'akismet_pre_check' => '1',
1787 'comment_pingback_target' => $args[1],
1788 'pingbacks_closed' => $pingbacks_closed ? '1' : '0',
1789 'is_multicall' => $is_multicall,
1790 'multicall_count' => $multicall_count,
1791 );
1792
1793 $comment = self::auto_check_comment( $comment, 'xml-rpc' );
1794
1795 if ( isset( $comment['akismet_result'] ) && 'true' == $comment['akismet_result'] ) {
1796 // Sad: tightly coupled with the IXR classes. Unfortunately the action provides no context and no way to return anything.
1797 $server->error( new IXR_Error( 0, 'Invalid discovery target' ) );
1798
1799 // Also note that if this was part of a multicall, a spam result will prevent the subsequent calls from being executed.
1800 // This is probably fine, but it raises the bar for what should be acceptable as a false positive.
1801 }
1802 }
1803 }
1804
1805 /**
1806 * Ensure that we are loading expected scalar values from akismet_as_submitted commentmeta.
1807 *
1808 * @param mixed $meta_value
1809 * @return mixed
1810 */
1811 private static function sanitize_comment_as_submitted( $meta_value ) {
1812 if ( empty( $meta_value ) ) {
1813 return $meta_value;
1814 }
1815
1816 $meta_value = (array) $meta_value;
1817
1818 foreach ( $meta_value as $key => $value ) {
1819 if ( ! is_scalar( $value ) ) {
1820 unset( $meta_value[ $key ] );
1821 } else {
1822 // These can change, so they're not explicitly listed in comment_as_submitted_allowed_keys.
1823 if ( strpos( $key, 'POST_ak_' ) === 0 ) {
1824 continue;
1825 }
1826
1827 if ( ! isset( self::$comment_as_submitted_allowed_keys[ $key ] ) ) {
1828 unset( $meta_value[ $key ] );
1829 }
1830 }
1831 }
1832
1833 return $meta_value;
1834 }
1835
1836 public static function predefined_api_key() {
1837 if ( defined( 'WPCOM_API_KEY' ) ) {
1838 return true;
1839 }
1840
1841 return apply_filters( 'akismet_predefined_api_key', false );
1842 }
1843
1844 /**
1845 * Controls the display of a privacy related notice underneath the comment form using the `akismet_comment_form_privacy_notice` option and filter respectively.
1846 * Default is top not display the notice, leaving the choice to site admins, or integrators.
1847 */
1848 public static function display_comment_form_privacy_notice() {
1849 if ( 'display' !== apply_filters( 'akismet_comment_form_privacy_notice', get_option( 'akismet_comment_form_privacy_notice', 'hide' ) ) ) {
1850 return;
1851 }
1852 echo apply_filters(
1853 'akismet_comment_form_privacy_notice_markup',
1854 '<p class="akismet_comment_form_privacy_notice">' . sprintf(
1855 __( 'This site uses Akismet to reduce spam. <a href="%s" target="_blank" rel="nofollow noopener">Learn how your comment data is processed</a>.', 'akismet' ),
1856 'https://akismet.com/privacy/'
1857 ) . '</p>'
1858 );
1859 }
1860
1861 public static function load_form_js() {
1862 if (
1863 ! is_admin()
1864 && ( ! function_exists( 'amp_is_request' ) || ! amp_is_request() )
1865 && self::get_api_key()
1866 ) {
1867 wp_register_script( 'akismet-frontend', plugin_dir_url( __FILE__ ) . '_inc/akismet-frontend.js', array(), filemtime( plugin_dir_path( __FILE__ ) . '_inc/akismet-frontend.js' ), true );
1868 wp_enqueue_script( 'akismet-frontend' );
1869 }
1870 }
1871
1872 /**
1873 * Add the form JavaScript when we detect that a supported form shortcode is being parsed.
1874 */
1875 public static function load_form_js_via_filter( $return_value, $tag, $attr, $m ) {
1876 if ( in_array( $tag, array( 'contact-form', 'gravityform', 'contact-form-7', 'formidable', 'fluentform' ) ) ) {
1877 self::load_form_js();
1878 }
1879
1880 return $return_value;
1881 }
1882
1883 /**
1884 * Was the last entry in the comment history created by Akismet?
1885 *
1886 * @param int $comment_id The ID of the comment.
1887 * @return bool
1888 */
1889 public static function last_comment_status_change_came_from_akismet( $comment_id ) {
1890 $history = self::get_comment_history( $comment_id );
1891
1892 if ( empty( $history ) ) {
1893 return false;
1894 }
1895
1896 $most_recent_history_event = $history[0];
1897
1898 if ( ! isset( $most_recent_history_event['event'] ) ) {
1899 return false;
1900 }
1901
1902 $akismet_history_events = array(
1903 'check-error',
1904 'cron-retry-ham',
1905 'cron-retry-spam',
1906 'check-ham',
1907 'check-spam',
1908 'recheck-error',
1909 'recheck-ham',
1910 'recheck-spam',
1911 'webhook-ham',
1912 'webhook-spam',
1913 );
1914
1915 if ( in_array( $most_recent_history_event['event'], $akismet_history_events ) ) {
1916 return true;
1917 }
1918
1919 return false;
1920 }
1921 }
1922