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