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