PluginProbe ʕ •ᴥ•ʔ
Akismet Anti-spam: Spam Protection / 4.1.4
Akismet Anti-spam: Spam Protection v4.1.4
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 6 years ago views 6 years ago .htaccess 10 years ago LICENSE.txt 10 years ago akismet.php 6 years ago class.akismet-admin.php 6 years ago class.akismet-cli.php 7 years ago class.akismet-rest-api.php 7 years ago class.akismet-widget.php 8 years ago class.akismet.php 6 years ago index.php 12 years ago readme.txt 6 years ago wrapper.php 7 years ago
class.akismet.php
1497 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 private static $last_comment = '';
9 private static $initiated = false;
10 private static $prevent_moderation_email_for_these_comments = array();
11 private static $last_comment_result = null;
12 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' => '' );
13 private static $is_rest_api_call = false;
14
15 public static function init() {
16 if ( ! self::$initiated ) {
17 self::init_hooks();
18 }
19 }
20
21 /**
22 * Initializes WordPress hooks
23 */
24 private static function init_hooks() {
25 self::$initiated = true;
26
27 add_action( 'wp_insert_comment', array( 'Akismet', 'auto_check_update_meta' ), 10, 2 );
28 add_filter( 'preprocess_comment', array( 'Akismet', 'auto_check_comment' ), 1 );
29 add_filter( 'rest_pre_insert_comment', array( 'Akismet', 'rest_auto_check_comment' ), 1 );
30
31 add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments' ) );
32 add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments_meta' ) );
33 add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_orphaned_commentmeta' ) );
34 add_action( 'akismet_schedule_cron_recheck', array( 'Akismet', 'cron_recheck' ) );
35
36 add_action( 'comment_form', array( 'Akismet', 'add_comment_nonce' ), 1 );
37
38 add_action( 'admin_head-edit-comments.php', array( 'Akismet', 'load_form_js' ) );
39 add_action( 'comment_form', array( 'Akismet', 'load_form_js' ) );
40 add_action( 'comment_form', array( 'Akismet', 'inject_ak_js' ) );
41 add_filter( 'script_loader_tag', array( 'Akismet', 'set_form_js_async' ), 10, 3 );
42
43 add_filter( 'comment_moderation_recipients', array( 'Akismet', 'disable_moderation_emails_if_unreachable' ), 1000, 2 );
44 add_filter( 'pre_comment_approved', array( 'Akismet', 'last_comment_status' ), 10, 2 );
45
46 add_action( 'transition_comment_status', array( 'Akismet', 'transition_comment_status' ), 10, 3 );
47
48 // Run this early in the pingback call, before doing a remote fetch of the source uri
49 add_action( 'xmlrpc_call', array( 'Akismet', 'pre_check_pingback' ) );
50
51 // Jetpack compatibility
52 add_filter( 'jetpack_options_whitelist', array( 'Akismet', 'add_to_jetpack_options_whitelist' ) );
53 add_action( 'update_option_wordpress_api_key', array( 'Akismet', 'updated_option' ), 10, 2 );
54 add_action( 'add_option_wordpress_api_key', array( 'Akismet', 'added_option' ), 10, 2 );
55
56 add_action( 'comment_form_after', array( 'Akismet', 'display_comment_form_privacy_notice' ) );
57 }
58
59 public static function get_api_key() {
60 return apply_filters( 'akismet_get_api_key', defined('WPCOM_API_KEY') ? constant('WPCOM_API_KEY') : get_option('wordpress_api_key') );
61 }
62
63 public static function check_key_status( $key, $ip = null ) {
64 return self::http_post( Akismet::build_query( array( 'key' => $key, 'blog' => get_option( 'home' ) ) ), 'verify-key', $ip );
65 }
66
67 public static function verify_key( $key, $ip = null ) {
68 // Shortcut for obviously invalid keys.
69 if ( strlen( $key ) != 12 ) {
70 return 'invalid';
71 }
72
73 $response = self::check_key_status( $key, $ip );
74
75 if ( $response[1] != 'valid' && $response[1] != 'invalid' )
76 return 'failed';
77
78 return $response[1];
79 }
80
81 public static function deactivate_key( $key ) {
82 $response = self::http_post( Akismet::build_query( array( 'key' => $key, 'blog' => get_option( 'home' ) ) ), 'deactivate' );
83
84 if ( $response[1] != 'deactivated' )
85 return 'failed';
86
87 return $response[1];
88 }
89
90 /**
91 * Add the akismet option to the Jetpack options management whitelist.
92 *
93 * @param array $options The list of whitelisted option names.
94 * @return array The updated whitelist
95 */
96 public static function add_to_jetpack_options_whitelist( $options ) {
97 $options[] = 'wordpress_api_key';
98 return $options;
99 }
100
101 /**
102 * When the akismet option is updated, run the registration call.
103 *
104 * This should only be run when the option is updated from the Jetpack/WP.com
105 * API call, and only if the new key is different than the old key.
106 *
107 * @param mixed $old_value The old option value.
108 * @param mixed $value The new option value.
109 */
110 public static function updated_option( $old_value, $value ) {
111 // Not an API call
112 if ( ! class_exists( 'WPCOM_JSON_API_Update_Option_Endpoint' ) ) {
113 return;
114 }
115 // Only run the registration if the old key is different.
116 if ( $old_value !== $value ) {
117 self::verify_key( $value );
118 }
119 }
120
121 /**
122 * Treat the creation of an API key the same as updating the API key to a new value.
123 *
124 * @param mixed $option_name Will always be "wordpress_api_key", until something else hooks in here.
125 * @param mixed $value The option value.
126 */
127 public static function added_option( $option_name, $value ) {
128 if ( 'wordpress_api_key' === $option_name ) {
129 return self::updated_option( '', $value );
130 }
131 }
132
133 public static function rest_auto_check_comment( $commentdata ) {
134 self::$is_rest_api_call = true;
135
136 return self::auto_check_comment( $commentdata );
137 }
138
139 public static function auto_check_comment( $commentdata ) {
140 // If no key is configured, then there's no point in doing any of this.
141 if ( ! self::get_api_key() ) {
142 return $commentdata;
143 }
144
145 self::$last_comment_result = null;
146
147 $comment = $commentdata;
148
149 $comment['user_ip'] = self::get_ip_address();
150 $comment['user_agent'] = self::get_user_agent();
151 $comment['referrer'] = self::get_referer();
152 $comment['blog'] = get_option( 'home' );
153 $comment['blog_lang'] = get_locale();
154 $comment['blog_charset'] = get_option('blog_charset');
155 $comment['permalink'] = get_permalink( $comment['comment_post_ID'] );
156
157 if ( ! empty( $comment['user_ID'] ) ) {
158 $comment['user_role'] = Akismet::get_user_roles( $comment['user_ID'] );
159 }
160
161 /** See filter documentation in init_hooks(). */
162 $akismet_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) );
163 $comment['akismet_comment_nonce'] = 'inactive';
164 if ( $akismet_nonce_option == 'true' || $akismet_nonce_option == '' ) {
165 $comment['akismet_comment_nonce'] = 'failed';
166 if ( isset( $_POST['akismet_comment_nonce'] ) && wp_verify_nonce( $_POST['akismet_comment_nonce'], 'akismet_comment_nonce_' . $comment['comment_post_ID'] ) )
167 $comment['akismet_comment_nonce'] = 'passed';
168
169 // comment reply in wp-admin
170 if ( isset( $_POST['_ajax_nonce-replyto-comment'] ) && check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' ) )
171 $comment['akismet_comment_nonce'] = 'passed';
172
173 }
174
175 if ( self::is_test_mode() )
176 $comment['is_test'] = 'true';
177
178 foreach( $_POST as $key => $value ) {
179 if ( is_string( $value ) )
180 $comment["POST_{$key}"] = $value;
181 }
182
183 foreach ( $_SERVER as $key => $value ) {
184 if ( ! is_string( $value ) ) {
185 continue;
186 }
187
188 if ( preg_match( "/^HTTP_COOKIE/", $key ) ) {
189 continue;
190 }
191
192 // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need.
193 if ( preg_match( "/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/", $key ) ) {
194 $comment[ "$key" ] = $value;
195 }
196 }
197
198 $post = get_post( $comment['comment_post_ID'] );
199
200 if ( ! is_null( $post ) ) {
201 // $post can technically be null, although in the past, it's always been an indicator of another plugin interfering.
202 $comment[ 'comment_post_modified_gmt' ] = $post->post_modified_gmt;
203 }
204
205 $response = self::http_post( Akismet::build_query( $comment ), 'comment-check' );
206
207 do_action( 'akismet_comment_check_response', $response );
208
209 $commentdata['comment_as_submitted'] = array_intersect_key( $comment, self::$comment_as_submitted_allowed_keys );
210 $commentdata['akismet_result'] = $response[1];
211
212 if ( isset( $response[0]['x-akismet-pro-tip'] ) )
213 $commentdata['akismet_pro_tip'] = $response[0]['x-akismet-pro-tip'];
214
215 if ( isset( $response[0]['x-akismet-error'] ) ) {
216 // An error occurred that we anticipated (like a suspended key) and want the user to act on.
217 // Send to moderation.
218 self::$last_comment_result = '0';
219 }
220 else if ( 'true' == $response[1] ) {
221 // akismet_spam_count will be incremented later by comment_is_spam()
222 self::$last_comment_result = 'spam';
223
224 $discard = ( isset( $commentdata['akismet_pro_tip'] ) && $commentdata['akismet_pro_tip'] === 'discard' && self::allow_discard() );
225
226 do_action( 'akismet_spam_caught', $discard );
227
228 if ( $discard ) {
229 // The spam is obvious, so we're bailing out early.
230 // akismet_result_spam() won't be called so bump the counter here
231 if ( $incr = apply_filters( 'akismet_spam_count_incr', 1 ) ) {
232 update_option( 'akismet_spam_count', get_option( 'akismet_spam_count' ) + $incr );
233 }
234
235 if ( self::$is_rest_api_call ) {
236 return new WP_Error( 'akismet_rest_comment_discarded', __( 'Comment discarded.', 'akismet' ) );
237 }
238 else {
239 // Redirect back to the previous page, or failing that, the post permalink, or failing that, the homepage of the blog.
240 $redirect_to = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : ( $post ? get_permalink( $post ) : home_url() );
241 wp_safe_redirect( esc_url_raw( $redirect_to ) );
242 die();
243 }
244 }
245 else if ( self::$is_rest_api_call ) {
246 // The way the REST API structures its calls, we can set the comment_approved value right away.
247 $commentdata['comment_approved'] = 'spam';
248 }
249 }
250
251 // if the response is neither true nor false, hold the comment for moderation and schedule a recheck
252 if ( 'true' != $response[1] && 'false' != $response[1] ) {
253 if ( !current_user_can('moderate_comments') ) {
254 // Comment status should be moderated
255 self::$last_comment_result = '0';
256 }
257
258 if ( ! wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) {
259 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
260 do_action( 'akismet_scheduled_recheck', 'invalid-response-' . $response[1] );
261 }
262
263 self::$prevent_moderation_email_for_these_comments[] = $commentdata;
264 }
265
266 // Delete old comments daily
267 if ( ! wp_next_scheduled( 'akismet_scheduled_delete' ) ) {
268 wp_schedule_event( time(), 'daily', 'akismet_scheduled_delete' );
269 }
270
271 self::set_last_comment( $commentdata );
272 self::fix_scheduled_recheck();
273
274 return $commentdata;
275 }
276
277 public static function get_last_comment() {
278 return self::$last_comment;
279 }
280
281 public static function set_last_comment( $comment ) {
282 if ( is_null( $comment ) ) {
283 self::$last_comment = null;
284 }
285 else {
286 // We filter it here so that it matches the filtered comment data that we'll have to compare against later.
287 // wp_filter_comment expects comment_author_IP
288 self::$last_comment = wp_filter_comment(
289 array_merge(
290 array( 'comment_author_IP' => self::get_ip_address() ),
291 $comment
292 )
293 );
294 }
295 }
296
297 // this fires on wp_insert_comment. we can't update comment_meta when auto_check_comment() runs
298 // because we don't know the comment ID at that point.
299 public static function auto_check_update_meta( $id, $comment ) {
300 // wp_insert_comment() might be called in other contexts, so make sure this is the same comment
301 // as was checked by auto_check_comment
302 if ( is_object( $comment ) && !empty( self::$last_comment ) && is_array( self::$last_comment ) ) {
303 if ( self::matches_last_comment( $comment ) ) {
304
305 load_plugin_textdomain( 'akismet' );
306
307 // normal result: true or false
308 if ( self::$last_comment['akismet_result'] == 'true' ) {
309 update_comment_meta( $comment->comment_ID, 'akismet_result', 'true' );
310 self::update_comment_history( $comment->comment_ID, '', 'check-spam' );
311 if ( $comment->comment_approved != 'spam' )
312 self::update_comment_history(
313 $comment->comment_ID,
314 '',
315 'status-changed-'.$comment->comment_approved
316 );
317 }
318 elseif ( self::$last_comment['akismet_result'] == 'false' ) {
319 update_comment_meta( $comment->comment_ID, 'akismet_result', 'false' );
320 self::update_comment_history( $comment->comment_ID, '', 'check-ham' );
321 // Status could be spam or trash, depending on the WP version and whether this change applies:
322 // https://core.trac.wordpress.org/changeset/34726
323 if ( $comment->comment_approved == 'spam' || $comment->comment_approved == 'trash' ) {
324 if ( wp_blacklist_check($comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent) )
325 self::update_comment_history( $comment->comment_ID, '', 'wp-blacklisted' );
326 else
327 self::update_comment_history( $comment->comment_ID, '', 'status-changed-'.$comment->comment_approved );
328 }
329 } // abnormal result: error
330 else {
331 update_comment_meta( $comment->comment_ID, 'akismet_error', time() );
332 self::update_comment_history(
333 $comment->comment_ID,
334 '',
335 'check-error',
336 array( 'response' => substr( self::$last_comment['akismet_result'], 0, 50 ) )
337 );
338 }
339
340 // record the complete original data as submitted for checking
341 if ( isset( self::$last_comment['comment_as_submitted'] ) )
342 update_comment_meta( $comment->comment_ID, 'akismet_as_submitted', self::$last_comment['comment_as_submitted'] );
343
344 if ( isset( self::$last_comment['akismet_pro_tip'] ) )
345 update_comment_meta( $comment->comment_ID, 'akismet_pro_tip', self::$last_comment['akismet_pro_tip'] );
346 }
347 }
348 }
349
350 public static function delete_old_comments() {
351 global $wpdb;
352
353 /**
354 * Determines how many comments will be deleted in each batch.
355 *
356 * @param int The default, as defined by AKISMET_DELETE_LIMIT.
357 */
358 $delete_limit = apply_filters( 'akismet_delete_comment_limit', defined( 'AKISMET_DELETE_LIMIT' ) ? AKISMET_DELETE_LIMIT : 10000 );
359 $delete_limit = max( 1, intval( $delete_limit ) );
360
361 /**
362 * Determines how many days a comment will be left in the Spam queue before being deleted.
363 *
364 * @param int The default number of days.
365 */
366 $delete_interval = apply_filters( 'akismet_delete_comment_interval', 15 );
367 $delete_interval = max( 1, intval( $delete_interval ) );
368
369 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 ) ) ) {
370 if ( empty( $comment_ids ) )
371 return;
372
373 $wpdb->queries = array();
374
375 foreach ( $comment_ids as $comment_id ) {
376 do_action( 'delete_comment', $comment_id );
377 do_action( 'akismet_batch_delete_count', __FUNCTION__ );
378 }
379
380 // Prepared as strings since comment_id is an unsigned BIGINT, and using %d will constrain the value to the maximum signed BIGINT.
381 $format_string = implode( ", ", array_fill( 0, count( $comment_ids ), '%s' ) );
382
383 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->comments} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) );
384 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) );
385
386 clean_comment_cache( $comment_ids );
387 do_action( 'akismet_delete_comment_batch', count( $comment_ids ) );
388 }
389
390 if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->comments ) ) // lucky number
391 $wpdb->query("OPTIMIZE TABLE {$wpdb->comments}");
392 }
393
394 public static function delete_old_comments_meta() {
395 global $wpdb;
396
397 $interval = apply_filters( 'akismet_delete_commentmeta_interval', 15 );
398
399 # enforce a minimum of 1 day
400 $interval = absint( $interval );
401 if ( $interval < 1 )
402 $interval = 1;
403
404 // akismet_as_submitted meta values are large, so expire them
405 // after $interval days regardless of the comment status
406 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 ) ) ) {
407 if ( empty( $comment_ids ) )
408 return;
409
410 $wpdb->queries = array();
411
412 foreach ( $comment_ids as $comment_id ) {
413 delete_comment_meta( $comment_id, 'akismet_as_submitted' );
414 do_action( 'akismet_batch_delete_count', __FUNCTION__ );
415 }
416
417 do_action( 'akismet_delete_commentmeta_batch', count( $comment_ids ) );
418 }
419
420 if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->commentmeta ) ) // lucky number
421 $wpdb->query("OPTIMIZE TABLE {$wpdb->commentmeta}");
422 }
423
424 // Clear out comments meta that no longer have corresponding comments in the database
425 public static function delete_orphaned_commentmeta() {
426 global $wpdb;
427
428 $last_meta_id = 0;
429 $start_time = isset( $_SERVER['REQUEST_TIME_FLOAT'] ) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true );
430 $max_exec_time = max( ini_get('max_execution_time') - 5, 3 );
431
432 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 ) ) ) {
433 if ( empty( $commentmeta_results ) )
434 return;
435
436 $wpdb->queries = array();
437
438 $commentmeta_deleted = 0;
439
440 foreach ( $commentmeta_results as $commentmeta ) {
441 if ( 'akismet_' == substr( $commentmeta->meta_key, 0, 8 ) ) {
442 delete_comment_meta( $commentmeta->comment_id, $commentmeta->meta_key );
443 do_action( 'akismet_batch_delete_count', __FUNCTION__ );
444 $commentmeta_deleted++;
445 }
446
447 $last_meta_id = $commentmeta->meta_id;
448 }
449
450 do_action( 'akismet_delete_commentmeta_batch', $commentmeta_deleted );
451
452 // If we're getting close to max_execution_time, quit for this round.
453 if ( microtime(true) - $start_time > $max_exec_time )
454 return;
455 }
456
457 if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->commentmeta ) ) // lucky number
458 $wpdb->query("OPTIMIZE TABLE {$wpdb->commentmeta}");
459 }
460
461 // how many approved comments does this author have?
462 public static function get_user_comments_approved( $user_id, $comment_author_email, $comment_author, $comment_author_url ) {
463 global $wpdb;
464
465 if ( !empty( $user_id ) )
466 return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE user_id = %d AND comment_approved = 1", $user_id ) );
467
468 if ( !empty( $comment_author_email ) )
469 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_author_email, $comment_author, $comment_author_url ) );
470
471 return 0;
472 }
473
474 // get the full comment history for a given comment, as an array in reverse chronological order
475 public static function get_comment_history( $comment_id ) {
476 $history = get_comment_meta( $comment_id, 'akismet_history', false );
477 if ( empty( $history ) || empty( $history[ 0 ] ) ) {
478 return false;
479 }
480
481 /*
482 // To see all variants when testing.
483 $history[] = array( 'time' => 445856401, 'message' => 'Old versions of Akismet stored the message as a literal string in the commentmeta.', 'event' => null );
484 $history[] = array( 'time' => 445856402, 'event' => 'recheck-spam' );
485 $history[] = array( 'time' => 445856403, 'event' => 'check-spam' );
486 $history[] = array( 'time' => 445856404, 'event' => 'recheck-ham' );
487 $history[] = array( 'time' => 445856405, 'event' => 'check-ham' );
488 $history[] = array( 'time' => 445856406, 'event' => 'wp-blacklisted' );
489 $history[] = array( 'time' => 445856407, 'event' => 'report-spam' );
490 $history[] = array( 'time' => 445856408, 'event' => 'report-spam', 'user' => 'sam' );
491 $history[] = array( 'message' => 'sam reported this comment as spam (hardcoded message).', 'time' => 445856400, 'event' => 'report-spam', 'user' => 'sam' );
492 $history[] = array( 'time' => 445856409, 'event' => 'report-ham', 'user' => 'sam' );
493 $history[] = array( 'message' => 'sam reported this comment as ham (hardcoded message).', 'time' => 445856400, 'event' => 'report-ham', 'user' => 'sam' ); //
494 $history[] = array( 'time' => 445856410, 'event' => 'cron-retry-spam' );
495 $history[] = array( 'time' => 445856411, 'event' => 'cron-retry-ham' );
496 $history[] = array( 'time' => 445856412, 'event' => 'check-error' ); //
497 $history[] = array( 'time' => 445856413, 'event' => 'check-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) );
498 $history[] = array( 'time' => 445856414, 'event' => 'recheck-error' ); // Should not generate a message.
499 $history[] = array( 'time' => 445856415, 'event' => 'recheck-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) );
500 $history[] = array( 'time' => 445856416, 'event' => 'status-changedtrash' );
501 $history[] = array( 'time' => 445856417, 'event' => 'status-changedspam' );
502 $history[] = array( 'time' => 445856418, 'event' => 'status-changedhold' );
503 $history[] = array( 'time' => 445856419, 'event' => 'status-changedapprove' );
504 $history[] = array( 'time' => 445856420, 'event' => 'status-changed-trash' );
505 $history[] = array( 'time' => 445856421, 'event' => 'status-changed-spam' );
506 $history[] = array( 'time' => 445856422, 'event' => 'status-changed-hold' );
507 $history[] = array( 'time' => 445856423, 'event' => 'status-changed-approve' );
508 $history[] = array( 'time' => 445856424, 'event' => 'status-trash', 'user' => 'sam' );
509 $history[] = array( 'time' => 445856425, 'event' => 'status-spam', 'user' => 'sam' );
510 $history[] = array( 'time' => 445856426, 'event' => 'status-hold', 'user' => 'sam' );
511 $history[] = array( 'time' => 445856427, 'event' => 'status-approve', 'user' => 'sam' );
512 */
513
514 usort( $history, array( 'Akismet', '_cmp_time' ) );
515 return $history;
516 }
517
518 /**
519 * Log an event for a given comment, storing it in comment_meta.
520 *
521 * @param int $comment_id The ID of the relevant comment.
522 * @param string $message The string description of the event. No longer used.
523 * @param string $event The event code.
524 * @param array $meta Metadata about the history entry. e.g., the user that reported or changed the status of a given comment.
525 */
526 public static function update_comment_history( $comment_id, $message, $event=null, $meta=null ) {
527 global $current_user;
528
529 $user = '';
530
531 $event = array(
532 'time' => self::_get_microtime(),
533 'event' => $event,
534 );
535
536 if ( is_object( $current_user ) && isset( $current_user->user_login ) ) {
537 $event['user'] = $current_user->user_login;
538 }
539
540 if ( ! empty( $meta ) ) {
541 $event['meta'] = $meta;
542 }
543
544 // $unique = false so as to allow multiple values per comment
545 $r = add_comment_meta( $comment_id, 'akismet_history', $event, false );
546 }
547
548 public static function check_db_comment( $id, $recheck_reason = 'recheck_queue' ) {
549 global $wpdb;
550
551 if ( ! self::get_api_key() ) {
552 return new WP_Error( 'akismet-not-configured', __( 'Akismet is not configured. Please enter an API key.', 'akismet' ) );
553 }
554
555 $c = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $id ), ARRAY_A );
556
557 if ( ! $c ) {
558 return new WP_Error( 'invalid-comment-id', __( 'Comment not found.', 'akismet' ) );
559 }
560
561 $c['user_ip'] = $c['comment_author_IP'];
562 $c['user_agent'] = $c['comment_agent'];
563 $c['referrer'] = '';
564 $c['blog'] = get_option( 'home' );
565 $c['blog_lang'] = get_locale();
566 $c['blog_charset'] = get_option('blog_charset');
567 $c['permalink'] = get_permalink($c['comment_post_ID']);
568 $c['recheck_reason'] = $recheck_reason;
569
570 $c['user_role'] = '';
571 if ( ! empty( $c['user_ID'] ) ) {
572 $c['user_role'] = Akismet::get_user_roles( $c['user_ID'] );
573 }
574
575 if ( self::is_test_mode() )
576 $c['is_test'] = 'true';
577
578 $response = self::http_post( Akismet::build_query( $c ), 'comment-check' );
579
580 if ( ! empty( $response[1] ) ) {
581 return $response[1];
582 }
583
584 return false;
585 }
586
587 public static function recheck_comment( $id, $recheck_reason = 'recheck_queue' ) {
588 add_comment_meta( $id, 'akismet_rechecking', true );
589
590 $api_response = self::check_db_comment( $id, $recheck_reason );
591
592 delete_comment_meta( $id, 'akismet_rechecking' );
593
594 if ( is_wp_error( $api_response ) ) {
595 // Invalid comment ID.
596 }
597 else if ( 'true' === $api_response ) {
598 wp_set_comment_status( $id, 'spam' );
599 update_comment_meta( $id, 'akismet_result', 'true' );
600 delete_comment_meta( $id, 'akismet_error' );
601 delete_comment_meta( $id, 'akismet_delayed_moderation_email' );
602 Akismet::update_comment_history( $id, '', 'recheck-spam' );
603 }
604 elseif ( 'false' === $api_response ) {
605 update_comment_meta( $id, 'akismet_result', 'false' );
606 delete_comment_meta( $id, 'akismet_error' );
607 delete_comment_meta( $id, 'akismet_delayed_moderation_email' );
608 Akismet::update_comment_history( $id, '', 'recheck-ham' );
609 }
610 else {
611 // abnormal result: error
612 update_comment_meta( $id, 'akismet_result', 'error' );
613 Akismet::update_comment_history(
614 $id,
615 '',
616 'recheck-error',
617 array( 'response' => substr( $api_response, 0, 50 ) )
618 );
619 }
620
621 return $api_response;
622 }
623
624 public static function transition_comment_status( $new_status, $old_status, $comment ) {
625
626 if ( $new_status == $old_status )
627 return;
628
629 if ( 'spam' === $new_status || 'spam' === $old_status ) {
630 // Clear the cache of the "X comments in your spam queue" count on the dashboard.
631 wp_cache_delete( 'akismet_spam_count', 'widget' );
632 }
633
634 # we don't need to record a history item for deleted comments
635 if ( $new_status == 'delete' )
636 return;
637
638 if ( !current_user_can( 'edit_post', $comment->comment_post_ID ) && !current_user_can( 'moderate_comments' ) )
639 return;
640
641 if ( defined('WP_IMPORTING') && WP_IMPORTING == true )
642 return;
643
644 // if this is present, it means the status has been changed by a re-check, not an explicit user action
645 if ( get_comment_meta( $comment->comment_ID, 'akismet_rechecking' ) )
646 return;
647
648 // Assumption alert:
649 // We want to submit comments to Akismet only when a moderator explicitly spams or approves it - not if the status
650 // is changed automatically by another plugin. Unfortunately WordPress doesn't provide an unambiguous way to
651 // determine why the transition_comment_status action was triggered. And there are several different ways by which
652 // to spam and unspam comments: bulk actions, ajax, links in moderation emails, the dashboard, and perhaps others.
653 // We'll assume that this is an explicit user action if certain POST/GET variables exist.
654 if (
655 // status=spam: Marking as spam via the REST API or...
656 // 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?...
657 // status=approved: Unspamming via the REST API (Calypso) or...
658 ( isset( $_POST['status'] ) && in_array( $_POST['status'], array( 'spam', 'unspam', 'approved', ) ) )
659 // spam=1: Clicking "Spam" underneath a comment in wp-admin and allowing the AJAX request to happen.
660 || ( isset( $_POST['spam'] ) && (int) $_POST['spam'] == 1 )
661 // 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.
662 || ( isset( $_POST['unspam'] ) && (int) $_POST['unspam'] == 1 )
663 // comment_status=spam/unspam: It's unclear where this is happening.
664 || ( isset( $_POST['comment_status'] ) && in_array( $_POST['comment_status'], array( 'spam', 'unspam' ) ) )
665 // action=spam: Choosing "Mark as Spam" from the Bulk Actions dropdown in wp-admin (or the "Spam it" link in notification emails).
666 // action=unspam: Choosing "Not Spam" from the Bulk Actions dropdown in wp-admin.
667 // action=spamcomment: Following the "Spam" link below a comment in wp-admin (not allowing AJAX request to happen).
668 // action=unspamcomment: Following the "Not Spam" link below a comment in wp-admin (not allowing AJAX request to happen).
669 || ( isset( $_GET['action'] ) && in_array( $_GET['action'], array( 'spam', 'unspam', 'spamcomment', 'unspamcomment', ) ) )
670 // action=editedcomment: Editing a comment via wp-admin (and possibly changing its status).
671 || ( isset( $_POST['action'] ) && in_array( $_POST['action'], array( 'editedcomment' ) ) )
672 // for=jetpack: Moderation via the WordPress app, Calypso, anything powered by the Jetpack connection.
673 || ( isset( $_GET['for'] ) && ( 'jetpack' == $_GET['for'] ) && ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) )
674 // Certain WordPress.com API requests
675 || ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST )
676 // WordPress.org REST API requests
677 || ( defined( 'REST_REQUEST' ) && REST_REQUEST )
678 ) {
679 if ( $new_status == 'spam' && ( $old_status == 'approved' || $old_status == 'unapproved' || !$old_status ) ) {
680 return self::submit_spam_comment( $comment->comment_ID );
681 } elseif ( $old_status == 'spam' && ( $new_status == 'approved' || $new_status == 'unapproved' ) ) {
682 return self::submit_nonspam_comment( $comment->comment_ID );
683 }
684 }
685
686 self::update_comment_history( $comment->comment_ID, '', 'status-' . $new_status );
687 }
688
689 public static function submit_spam_comment( $comment_id ) {
690 global $wpdb, $current_user, $current_site;
691
692 $comment_id = (int) $comment_id;
693
694 $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ) );
695
696 if ( !$comment ) // it was deleted
697 return;
698
699 if ( 'spam' != $comment->comment_approved )
700 return;
701
702 self::update_comment_history( $comment_id, '', 'report-spam' );
703
704 // If the user hasn't configured Akismet, there's nothing else to do at this point.
705 if ( ! self::get_api_key() ) {
706 return;
707 }
708
709 // use the original version stored in comment_meta if available
710 $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) );
711
712 if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) )
713 $comment = (object) array_merge( (array)$comment, $as_submitted );
714
715 $comment->blog = get_option( 'home' );
716 $comment->blog_lang = get_locale();
717 $comment->blog_charset = get_option('blog_charset');
718 $comment->permalink = get_permalink($comment->comment_post_ID);
719
720 if ( is_object($current_user) )
721 $comment->reporter = $current_user->user_login;
722
723 if ( is_object($current_site) )
724 $comment->site_domain = $current_site->domain;
725
726 $comment->user_role = '';
727 if ( ! empty( $comment->user_ID ) ) {
728 $comment->user_role = Akismet::get_user_roles( $comment->user_ID );
729 }
730
731 if ( self::is_test_mode() )
732 $comment->is_test = 'true';
733
734 $post = get_post( $comment->comment_post_ID );
735
736 if ( ! is_null( $post ) ) {
737 $comment->comment_post_modified_gmt = $post->post_modified_gmt;
738 }
739
740 $response = Akismet::http_post( Akismet::build_query( $comment ), 'submit-spam' );
741
742 update_comment_meta( $comment_id, 'akismet_user_result', 'true' );
743
744 if ( $comment->reporter ) {
745 update_comment_meta( $comment_id, 'akismet_user', $comment->reporter );
746 }
747
748 do_action('akismet_submit_spam_comment', $comment_id, $response[1]);
749 }
750
751 public static function submit_nonspam_comment( $comment_id ) {
752 global $wpdb, $current_user, $current_site;
753
754 $comment_id = (int) $comment_id;
755
756 $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ) );
757 if ( !$comment ) // it was deleted
758 return;
759
760 self::update_comment_history( $comment_id, '', 'report-ham' );
761
762 // If the user hasn't configured Akismet, there's nothing else to do at this point.
763 if ( ! self::get_api_key() ) {
764 return;
765 }
766
767 // use the original version stored in comment_meta if available
768 $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) );
769
770 if ( $as_submitted && is_array($as_submitted) && isset($as_submitted['comment_content']) )
771 $comment = (object) array_merge( (array)$comment, $as_submitted );
772
773 $comment->blog = get_option( 'home' );
774 $comment->blog_lang = get_locale();
775 $comment->blog_charset = get_option('blog_charset');
776 $comment->permalink = get_permalink( $comment->comment_post_ID );
777 $comment->user_role = '';
778
779 if ( is_object($current_user) )
780 $comment->reporter = $current_user->user_login;
781
782 if ( is_object($current_site) )
783 $comment->site_domain = $current_site->domain;
784
785 if ( ! empty( $comment->user_ID ) ) {
786 $comment->user_role = Akismet::get_user_roles( $comment->user_ID );
787 }
788
789 if ( Akismet::is_test_mode() )
790 $comment->is_test = 'true';
791
792 $post = get_post( $comment->comment_post_ID );
793
794 if ( ! is_null( $post ) ) {
795 $comment->comment_post_modified_gmt = $post->post_modified_gmt;
796 }
797
798 $response = self::http_post( Akismet::build_query( $comment ), 'submit-ham' );
799
800 update_comment_meta( $comment_id, 'akismet_user_result', 'false' );
801
802 if ( $comment->reporter ) {
803 update_comment_meta( $comment_id, 'akismet_user', $comment->reporter );
804 }
805
806 do_action('akismet_submit_nonspam_comment', $comment_id, $response[1]);
807 }
808
809 public static function cron_recheck() {
810 global $wpdb;
811
812 $api_key = self::get_api_key();
813
814 $status = self::verify_key( $api_key );
815 if ( get_option( 'akismet_alert_code' ) || $status == 'invalid' ) {
816 // since there is currently a problem with the key, reschedule a check for 6 hours hence
817 wp_schedule_single_event( time() + 21600, 'akismet_schedule_cron_recheck' );
818 do_action( 'akismet_scheduled_recheck', 'key-problem-' . get_option( 'akismet_alert_code' ) . '-' . $status );
819 return false;
820 }
821
822 delete_option('akismet_available_servers');
823
824 $comment_errors = $wpdb->get_col( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error' LIMIT 100" );
825
826 load_plugin_textdomain( 'akismet' );
827
828 foreach ( (array) $comment_errors as $comment_id ) {
829 // if the comment no longer exists, or is too old, remove the meta entry from the queue to avoid getting stuck
830 $comment = get_comment( $comment_id );
831
832 if (
833 ! $comment // Comment has been deleted
834 || strtotime( $comment->comment_date_gmt ) < strtotime( "-15 days" ) // Comment is too old.
835 || $comment->comment_approved !== "0" // Comment is no longer in the Pending queue
836 ) {
837 delete_comment_meta( $comment_id, 'akismet_error' );
838 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
839 continue;
840 }
841
842 add_comment_meta( $comment_id, 'akismet_rechecking', true );
843 $status = self::check_db_comment( $comment_id, 'retry' );
844
845 $event = '';
846 if ( $status == 'true' ) {
847 $event = 'cron-retry-spam';
848 } elseif ( $status == 'false' ) {
849 $event = 'cron-retry-ham';
850 }
851
852 // If we got back a legit response then update the comment history
853 // other wise just bail now and try again later. No point in
854 // re-trying all the comments once we hit one failure.
855 if ( !empty( $event ) ) {
856 delete_comment_meta( $comment_id, 'akismet_error' );
857 self::update_comment_history( $comment_id, '', $event );
858 update_comment_meta( $comment_id, 'akismet_result', $status );
859 // make sure the comment status is still pending. if it isn't, that means the user has already moved it elsewhere.
860 $comment = get_comment( $comment_id );
861 if ( $comment && 'unapproved' == wp_get_comment_status( $comment_id ) ) {
862 if ( $status == 'true' ) {
863 wp_spam_comment( $comment_id );
864 } elseif ( $status == 'false' ) {
865 // comment is good, but it's still in the pending queue. depending on the moderation settings
866 // we may need to change it to approved.
867 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) )
868 wp_set_comment_status( $comment_id, 1 );
869 else if ( get_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ) )
870 wp_notify_moderator( $comment_id );
871 }
872 }
873
874 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
875 } else {
876 // If this comment has been pending moderation for longer than MAX_DELAY_BEFORE_MODERATION_EMAIL,
877 // send a moderation email now.
878 if ( ( intval( gmdate( 'U' ) ) - strtotime( $comment->comment_date_gmt ) ) < self::MAX_DELAY_BEFORE_MODERATION_EMAIL ) {
879 delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
880 wp_notify_moderator( $comment_id );
881 }
882
883 delete_comment_meta( $comment_id, 'akismet_rechecking' );
884 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
885 do_action( 'akismet_scheduled_recheck', 'check-db-comment-' . $status );
886 return;
887 }
888 delete_comment_meta( $comment_id, 'akismet_rechecking' );
889 }
890
891 $remaining = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error'" );
892 if ( $remaining && !wp_next_scheduled('akismet_schedule_cron_recheck') ) {
893 wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
894 do_action( 'akismet_scheduled_recheck', 'remaining' );
895 }
896 }
897
898 public static function fix_scheduled_recheck() {
899 $future_check = wp_next_scheduled( 'akismet_schedule_cron_recheck' );
900 if ( !$future_check ) {
901 return;
902 }
903
904 if ( get_option( 'akismet_alert_code' ) > 0 ) {
905 return;
906 }
907
908 $check_range = time() + 1200;
909 if ( $future_check > $check_range ) {
910 wp_clear_scheduled_hook( 'akismet_schedule_cron_recheck' );
911 wp_schedule_single_event( time() + 300, 'akismet_schedule_cron_recheck' );
912 do_action( 'akismet_scheduled_recheck', 'fix-scheduled-recheck' );
913 }
914 }
915
916 public static function add_comment_nonce( $post_id ) {
917 /**
918 * To disable the Akismet comment nonce, add a filter for the 'akismet_comment_nonce' tag
919 * and return any string value that is not 'true' or '' (empty string).
920 *
921 * Don't return boolean false, because that implies that the 'akismet_comment_nonce' option
922 * has not been set and that Akismet should just choose the default behavior for that
923 * situation.
924 */
925
926 if ( ! self::get_api_key() ) {
927 return;
928 }
929
930 $akismet_comment_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) );
931
932 if ( $akismet_comment_nonce_option == 'true' || $akismet_comment_nonce_option == '' ) {
933 echo '<p style="display: none;">';
934 wp_nonce_field( 'akismet_comment_nonce_' . $post_id, 'akismet_comment_nonce', FALSE );
935 echo '</p>';
936 }
937 }
938
939 public static function is_test_mode() {
940 return defined('AKISMET_TEST_MODE') && AKISMET_TEST_MODE;
941 }
942
943 public static function allow_discard() {
944 if ( defined( 'DOING_AJAX' ) && DOING_AJAX )
945 return false;
946 if ( is_user_logged_in() )
947 return false;
948
949 return ( get_option( 'akismet_strictness' ) === '1' );
950 }
951
952 public static function get_ip_address() {
953 return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
954 }
955
956 /**
957 * Do these two comments, without checking the comment_ID, "match"?
958 *
959 * @param mixed $comment1 A comment object or array.
960 * @param mixed $comment2 A comment object or array.
961 * @return bool Whether the two comments should be treated as the same comment.
962 */
963 private static function comments_match( $comment1, $comment2 ) {
964 $comment1 = (array) $comment1;
965 $comment2 = (array) $comment2;
966
967 // Set default values for these strings that we check in order to simplify
968 // the checks and avoid PHP warnings.
969 if ( ! isset( $comment1['comment_author'] ) ) {
970 $comment1['comment_author'] = '';
971 }
972
973 if ( ! isset( $comment2['comment_author'] ) ) {
974 $comment2['comment_author'] = '';
975 }
976
977 if ( ! isset( $comment1['comment_author_email'] ) ) {
978 $comment1['comment_author_email'] = '';
979 }
980
981 if ( ! isset( $comment2['comment_author_email'] ) ) {
982 $comment2['comment_author_email'] = '';
983 }
984
985 $comments_match = (
986 isset( $comment1['comment_post_ID'], $comment2['comment_post_ID'] )
987 && intval( $comment1['comment_post_ID'] ) == intval( $comment2['comment_post_ID'] )
988 && (
989 // The comment author length max is 255 characters, limited by the TINYTEXT column type.
990 // If the comment author includes multibyte characters right around the 255-byte mark, they
991 // may be stripped when the author is saved in the DB, so a 300+ char author may turn into
992 // a 253-char author when it's saved, not 255 exactly. The longest possible character is
993 // theoretically 6 bytes, so we'll only look at the first 248 bytes to be safe.
994 substr( $comment1['comment_author'], 0, 248 ) == substr( $comment2['comment_author'], 0, 248 )
995 || substr( stripslashes( $comment1['comment_author'] ), 0, 248 ) == substr( $comment2['comment_author'], 0, 248 )
996 || substr( $comment1['comment_author'], 0, 248 ) == substr( stripslashes( $comment2['comment_author'] ), 0, 248 )
997 // Certain long comment author names will be truncated to nothing, depending on their encoding.
998 || ( ! $comment1['comment_author'] && strlen( $comment2['comment_author'] ) > 248 )
999 || ( ! $comment2['comment_author'] && strlen( $comment1['comment_author'] ) > 248 )
1000 )
1001 && (
1002 // The email max length is 100 characters, limited by the VARCHAR(100) column type.
1003 // Same argument as above for only looking at the first 93 characters.
1004 substr( $comment1['comment_author_email'], 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 )
1005 || substr( stripslashes( $comment1['comment_author_email'] ), 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 )
1006 || substr( $comment1['comment_author_email'], 0, 93 ) == substr( stripslashes( $comment2['comment_author_email'] ), 0, 93 )
1007 // Very long emails can be truncated and then stripped if the [0:100] substring isn't a valid address.
1008 || ( ! $comment1['comment_author_email'] && strlen( $comment2['comment_author_email'] ) > 100 )
1009 || ( ! $comment2['comment_author_email'] && strlen( $comment1['comment_author_email'] ) > 100 )
1010 )
1011 );
1012
1013 return $comments_match;
1014 }
1015
1016 // Does the supplied comment match the details of the one most recently stored in self::$last_comment?
1017 public static function matches_last_comment( $comment ) {
1018 return self::comments_match( self::$last_comment, $comment );
1019 }
1020
1021 private static function get_user_agent() {
1022 return isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null;
1023 }
1024
1025 private static function get_referer() {
1026 return isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : null;
1027 }
1028
1029 // return a comma-separated list of role names for the given user
1030 public static function get_user_roles( $user_id ) {
1031 $roles = false;
1032
1033 if ( !class_exists('WP_User') )
1034 return false;
1035
1036 if ( $user_id > 0 ) {
1037 $comment_user = new WP_User( $user_id );
1038 if ( isset( $comment_user->roles ) )
1039 $roles = join( ',', $comment_user->roles );
1040 }
1041
1042 if ( is_multisite() && is_super_admin( $user_id ) ) {
1043 if ( empty( $roles ) ) {
1044 $roles = 'super_admin';
1045 } else {
1046 $comment_user->roles[] = 'super_admin';
1047 $roles = join( ',', $comment_user->roles );
1048 }
1049 }
1050
1051 return $roles;
1052 }
1053
1054 // filter handler used to return a spam result to pre_comment_approved
1055 public static function last_comment_status( $approved, $comment ) {
1056 if ( is_null( self::$last_comment_result ) ) {
1057 // We didn't have reason to store the result of the last check.
1058 return $approved;
1059 }
1060
1061 // Only do this if it's the correct comment
1062 if ( ! self::matches_last_comment( $comment ) ) {
1063 self::log( "comment_is_spam mismatched comment, returning unaltered $approved" );
1064 return $approved;
1065 }
1066
1067 if ( 'trash' === $approved ) {
1068 // If the last comment we checked has had its approval set to 'trash',
1069 // then it failed the comment blacklist check. Let that blacklist override
1070 // the spam check, since users have the (valid) expectation that when
1071 // they fill out their blacklists, comments that match it will always
1072 // end up in the trash.
1073 return $approved;
1074 }
1075
1076 // bump the counter here instead of when the filter is added to reduce the possibility of overcounting
1077 if ( $incr = apply_filters('akismet_spam_count_incr', 1) )
1078 update_option( 'akismet_spam_count', get_option('akismet_spam_count') + $incr );
1079
1080 return self::$last_comment_result;
1081 }
1082
1083 /**
1084 * If Akismet is temporarily unreachable, we don't want to "spam" the blogger with
1085 * moderation emails for comments that will be automatically cleared or spammed on
1086 * the next retry.
1087 *
1088 * For comments that will be rechecked later, empty the list of email addresses that
1089 * the moderation email would be sent to.
1090 *
1091 * @param array $emails An array of email addresses that the moderation email will be sent to.
1092 * @param int $comment_id The ID of the relevant comment.
1093 * @return array An array of email addresses that the moderation email will be sent to.
1094 */
1095 public static function disable_moderation_emails_if_unreachable( $emails, $comment_id ) {
1096 if ( ! empty( self::$prevent_moderation_email_for_these_comments ) && ! empty( $emails ) ) {
1097 $comment = get_comment( $comment_id );
1098
1099 foreach ( self::$prevent_moderation_email_for_these_comments as $possible_match ) {
1100 if ( self::comments_match( $possible_match, $comment ) ) {
1101 update_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true );
1102 return array();
1103 }
1104 }
1105 }
1106
1107 return $emails;
1108 }
1109
1110 public static function _cmp_time( $a, $b ) {
1111 return $a['time'] > $b['time'] ? -1 : 1;
1112 }
1113
1114 public static function _get_microtime() {
1115 $mtime = explode( ' ', microtime() );
1116 return $mtime[1] + $mtime[0];
1117 }
1118
1119 /**
1120 * Make a POST request to the Akismet API.
1121 *
1122 * @param string $request The body of the request.
1123 * @param string $path The path for the request.
1124 * @param string $ip The specific IP address to hit.
1125 * @return array A two-member array consisting of the headers and the response body, both empty in the case of a failure.
1126 */
1127 public static function http_post( $request, $path, $ip=null ) {
1128
1129 $akismet_ua = sprintf( 'WordPress/%s | Akismet/%s', $GLOBALS['wp_version'], constant( 'AKISMET_VERSION' ) );
1130 $akismet_ua = apply_filters( 'akismet_ua', $akismet_ua );
1131
1132 $content_length = strlen( $request );
1133
1134 $api_key = self::get_api_key();
1135 $host = self::API_HOST;
1136
1137 if ( !empty( $api_key ) )
1138 $host = $api_key.'.'.$host;
1139
1140 $http_host = $host;
1141 // use a specific IP if provided
1142 // needed by Akismet_Admin::check_server_connectivity()
1143 if ( $ip && long2ip( ip2long( $ip ) ) ) {
1144 $http_host = $ip;
1145 }
1146
1147 $http_args = array(
1148 'body' => $request,
1149 'headers' => array(
1150 'Content-Type' => 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' ),
1151 'Host' => $host,
1152 'User-Agent' => $akismet_ua,
1153 ),
1154 'httpversion' => '1.0',
1155 'timeout' => 15
1156 );
1157
1158 $akismet_url = $http_akismet_url = "http://{$http_host}/1.1/{$path}";
1159
1160 /**
1161 * Try SSL first; if that fails, try without it and don't try it again for a while.
1162 */
1163
1164 $ssl = $ssl_failed = false;
1165
1166 // Check if SSL requests were disabled fewer than X hours ago.
1167 $ssl_disabled = get_option( 'akismet_ssl_disabled' );
1168
1169 if ( $ssl_disabled && $ssl_disabled < ( time() - 60 * 60 * 24 ) ) { // 24 hours
1170 $ssl_disabled = false;
1171 delete_option( 'akismet_ssl_disabled' );
1172 }
1173 else if ( $ssl_disabled ) {
1174 do_action( 'akismet_ssl_disabled' );
1175 }
1176
1177 if ( ! $ssl_disabled && ( $ssl = wp_http_supports( array( 'ssl' ) ) ) ) {
1178 $akismet_url = set_url_scheme( $akismet_url, 'https' );
1179
1180 do_action( 'akismet_https_request_pre' );
1181 }
1182
1183 $response = wp_remote_post( $akismet_url, $http_args );
1184
1185 Akismet::log( compact( 'akismet_url', 'http_args', 'response' ) );
1186
1187 if ( $ssl && is_wp_error( $response ) ) {
1188 do_action( 'akismet_https_request_failure', $response );
1189
1190 // Intermittent connection problems may cause the first HTTPS
1191 // request to fail and subsequent HTTP requests to succeed randomly.
1192 // Retry the HTTPS request once before disabling SSL for a time.
1193 $response = wp_remote_post( $akismet_url, $http_args );
1194
1195 Akismet::log( compact( 'akismet_url', 'http_args', 'response' ) );
1196
1197 if ( is_wp_error( $response ) ) {
1198 $ssl_failed = true;
1199
1200 do_action( 'akismet_https_request_failure', $response );
1201
1202 do_action( 'akismet_http_request_pre' );
1203
1204 // Try the request again without SSL.
1205 $response = wp_remote_post( $http_akismet_url, $http_args );
1206
1207 Akismet::log( compact( 'http_akismet_url', 'http_args', 'response' ) );
1208 }
1209 }
1210
1211 if ( is_wp_error( $response ) ) {
1212 do_action( 'akismet_request_failure', $response );
1213
1214 return array( '', '' );
1215 }
1216
1217 if ( $ssl_failed ) {
1218 // The request failed when using SSL but succeeded without it. Disable SSL for future requests.
1219 update_option( 'akismet_ssl_disabled', time() );
1220
1221 do_action( 'akismet_https_disabled' );
1222 }
1223
1224 $simplified_response = array( $response['headers'], $response['body'] );
1225
1226 self::update_alert( $simplified_response );
1227
1228 return $simplified_response;
1229 }
1230
1231 // given a response from an API call like check_key_status(), update the alert code options if an alert is present.
1232 public static function update_alert( $response ) {
1233 $code = $msg = null;
1234 if ( isset( $response[0]['x-akismet-alert-code'] ) ) {
1235 $code = $response[0]['x-akismet-alert-code'];
1236 $msg = $response[0]['x-akismet-alert-msg'];
1237 }
1238
1239 // only call update_option() if the value has changed
1240 if ( $code != get_option( 'akismet_alert_code' ) ) {
1241 if ( ! $code ) {
1242 delete_option( 'akismet_alert_code' );
1243 delete_option( 'akismet_alert_msg' );
1244 }
1245 else {
1246 update_option( 'akismet_alert_code', $code );
1247 update_option( 'akismet_alert_msg', $msg );
1248 }
1249 }
1250 }
1251
1252 public static function load_form_js() {
1253 if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) {
1254 return;
1255 }
1256
1257 if ( ! self::get_api_key() ) {
1258 return;
1259 }
1260
1261 wp_register_script( 'akismet-form', plugin_dir_url( __FILE__ ) . '_inc/form.js', array(), AKISMET_VERSION, true );
1262 wp_enqueue_script( 'akismet-form' );
1263 }
1264
1265 /**
1266 * Mark form.js as async. Because nothing depends on it, it can run at any time
1267 * after it's loaded, and the browser won't have to wait for it to load to continue
1268 * parsing the rest of the page.
1269 */
1270 public static function set_form_js_async( $tag, $handle, $src ) {
1271 if ( 'akismet-form' !== $handle ) {
1272 return $tag;
1273 }
1274
1275 return preg_replace( '/^<script /i', '<script async="async" ', $tag );
1276 }
1277
1278 public static function inject_ak_js( $fields ) {
1279 echo '<p style="display: none;">';
1280 echo '<input type="hidden" id="ak_js" name="ak_js" value="' . mt_rand( 0, 250 ) . '"/>';
1281 echo '</p>';
1282 }
1283
1284 private static function bail_on_activation( $message, $deactivate = true ) {
1285 ?>
1286 <!doctype html>
1287 <html>
1288 <head>
1289 <meta charset="<?php bloginfo( 'charset' ); ?>" />
1290 <style>
1291 * {
1292 text-align: center;
1293 margin: 0;
1294 padding: 0;
1295 font-family: "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
1296 }
1297 p {
1298 margin-top: 1em;
1299 font-size: 18px;
1300 }
1301 </style>
1302 </head>
1303 <body>
1304 <p><?php echo esc_html( $message ); ?></p>
1305 </body>
1306 </html>
1307 <?php
1308 if ( $deactivate ) {
1309 $plugins = get_option( 'active_plugins' );
1310 $akismet = plugin_basename( AKISMET__PLUGIN_DIR . 'akismet.php' );
1311 $update = false;
1312 foreach ( $plugins as $i => $plugin ) {
1313 if ( $plugin === $akismet ) {
1314 $plugins[$i] = false;
1315 $update = true;
1316 }
1317 }
1318
1319 if ( $update ) {
1320 update_option( 'active_plugins', array_filter( $plugins ) );
1321 }
1322 }
1323 exit;
1324 }
1325
1326 public static function view( $name, array $args = array() ) {
1327 $args = apply_filters( 'akismet_view_arguments', $args, $name );
1328
1329 foreach ( $args AS $key => $val ) {
1330 $$key = $val;
1331 }
1332
1333 load_plugin_textdomain( 'akismet' );
1334
1335 $file = AKISMET__PLUGIN_DIR . 'views/'. $name . '.php';
1336
1337 include( $file );
1338 }
1339
1340 /**
1341 * Attached to activate_{ plugin_basename( __FILES__ ) } by register_activation_hook()
1342 * @static
1343 */
1344 public static function plugin_activation() {
1345 if ( version_compare( $GLOBALS['wp_version'], AKISMET__MINIMUM_WP_VERSION, '<' ) ) {
1346 load_plugin_textdomain( 'akismet' );
1347
1348 $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/');
1349
1350 Akismet::bail_on_activation( $message );
1351 } elseif ( ! empty( $_SERVER['SCRIPT_NAME'] ) && false !== strpos( $_SERVER['SCRIPT_NAME'], '/wp-admin/plugins.php' ) ) {
1352 add_option( 'Activated_Akismet', true );
1353 }
1354 }
1355
1356 /**
1357 * Removes all connection options
1358 * @static
1359 */
1360 public static function plugin_deactivation( ) {
1361 self::deactivate_key( self::get_api_key() );
1362
1363 // Remove any scheduled cron jobs.
1364 $akismet_cron_events = array(
1365 'akismet_schedule_cron_recheck',
1366 'akismet_scheduled_delete',
1367 );
1368
1369 foreach ( $akismet_cron_events as $akismet_cron_event ) {
1370 $timestamp = wp_next_scheduled( $akismet_cron_event );
1371
1372 if ( $timestamp ) {
1373 wp_unschedule_event( $timestamp, $akismet_cron_event );
1374 }
1375 }
1376 }
1377
1378 /**
1379 * Essentially a copy of WP's build_query but one that doesn't expect pre-urlencoded values.
1380 *
1381 * @param array $args An array of key => value pairs
1382 * @return string A string ready for use as a URL query string.
1383 */
1384 public static function build_query( $args ) {
1385 return _http_build_query( $args, '', '&' );
1386 }
1387
1388 /**
1389 * Log debugging info to the error log.
1390 *
1391 * Enabled when WP_DEBUG_LOG is enabled (and WP_DEBUG, since according to
1392 * core, "WP_DEBUG_DISPLAY and WP_DEBUG_LOG perform no function unless
1393 * WP_DEBUG is true), but can be disabled via the akismet_debug_log filter.
1394 *
1395 * @param mixed $akismet_debug The data to log.
1396 */
1397 public static function log( $akismet_debug ) {
1398 if ( apply_filters( 'akismet_debug_log', defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG && defined( 'AKISMET_DEBUG' ) && AKISMET_DEBUG ) ) {
1399 error_log( print_r( compact( 'akismet_debug' ), true ) );
1400 }
1401 }
1402
1403 public static function pre_check_pingback( $method ) {
1404 if ( $method !== 'pingback.ping' )
1405 return;
1406
1407 global $wp_xmlrpc_server;
1408
1409 if ( !is_object( $wp_xmlrpc_server ) )
1410 return false;
1411
1412 // Lame: tightly coupled with the IXR class.
1413 $args = $wp_xmlrpc_server->message->params;
1414
1415 if ( !empty( $args[1] ) ) {
1416 $post_id = url_to_postid( $args[1] );
1417
1418 // If pingbacks aren't open on this post, we'll still check whether this request is part of a potential DDOS,
1419 // but indicate to the server that pingbacks are indeed closed so we don't include this request in the user's stats,
1420 // since the user has already done their part by disabling pingbacks.
1421 $pingbacks_closed = false;
1422
1423 $post = get_post( $post_id );
1424
1425 if ( ! $post || ! pings_open( $post ) ) {
1426 $pingbacks_closed = true;
1427 }
1428
1429 $comment = array(
1430 'comment_author_url' => $args[0],
1431 'comment_post_ID' => $post_id,
1432 'comment_author' => '',
1433 'comment_author_email' => '',
1434 'comment_content' => '',
1435 'comment_type' => 'pingback',
1436 'akismet_pre_check' => '1',
1437 'comment_pingback_target' => $args[1],
1438 'pingbacks_closed' => $pingbacks_closed ? '1' : '0',
1439 );
1440
1441 $comment = Akismet::auto_check_comment( $comment );
1442
1443 if ( isset( $comment['akismet_result'] ) && 'true' == $comment['akismet_result'] ) {
1444 // Lame: tightly coupled with the IXR classes. Unfortunately the action provides no context and no way to return anything.
1445 $wp_xmlrpc_server->error( new IXR_Error( 0, 'Invalid discovery target' ) );
1446 }
1447 }
1448 }
1449
1450 /**
1451 * Ensure that we are loading expected scalar values from akismet_as_submitted commentmeta.
1452 *
1453 * @param mixed $meta_value
1454 * @return mixed
1455 */
1456 private static function sanitize_comment_as_submitted( $meta_value ) {
1457 if ( empty( $meta_value ) ) {
1458 return $meta_value;
1459 }
1460
1461 $meta_value = (array) $meta_value;
1462
1463 foreach ( $meta_value as $key => $value ) {
1464 if ( ! isset( self::$comment_as_submitted_allowed_keys[$key] ) || ! is_scalar( $value ) ) {
1465 unset( $meta_value[$key] );
1466 }
1467 }
1468
1469 return $meta_value;
1470 }
1471
1472 public static function predefined_api_key() {
1473 if ( defined( 'WPCOM_API_KEY' ) ) {
1474 return true;
1475 }
1476
1477 return apply_filters( 'akismet_predefined_api_key', false );
1478 }
1479
1480 /**
1481 * Controls the display of a privacy related notice underneath the comment form using the `akismet_comment_form_privacy_notice` option and filter respectively.
1482 * Default is top not display the notice, leaving the choice to site admins, or integrators.
1483 */
1484 public static function display_comment_form_privacy_notice() {
1485 if ( 'display' !== apply_filters( 'akismet_comment_form_privacy_notice', get_option( 'akismet_comment_form_privacy_notice', 'hide' ) ) ) {
1486 return;
1487 }
1488 echo apply_filters(
1489 'akismet_comment_form_privacy_notice_markup',
1490 '<p class="akismet_comment_form_privacy_notice">' . sprintf(
1491 __( 'This site uses Akismet to reduce spam. <a href="%s" target="_blank" rel="nofollow noopener">Learn how your comment data is processed</a>.', 'akismet' ),
1492 'https://akismet.com/privacy/'
1493 ) . '</p>'
1494 );
1495 }
1496 }
1497