index.php 0000644 00000000034 15043671570 0006371 0 ustar 00 <?php
// Silence is golden.
akismet/class.akismet.php 0000644 00000225512 15043671570 0011472 0 ustar 00 <?php
// We plan to gradually remove all of the disabled lint rules below.
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// phpcs:disable WordPress.Security.NonceVerification.Missing
// phpcs:disable Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
class Akismet {
const API_HOST = 'rest.akismet.com';
const API_PORT = 80;
const MAX_DELAY_BEFORE_MODERATION_EMAIL = 86400; // One day in seconds
const ALERT_CODE_COMMERCIAL = 30001;
public static $limit_notices = array(
10501 => 'FIRST_MONTH_OVER_LIMIT',
10502 => 'SECOND_MONTH_OVER_LIMIT',
10504 => 'THIRD_MONTH_APPROACHING_LIMIT',
10508 => 'THIRD_MONTH_OVER_LIMIT',
10516 => 'FOUR_PLUS_MONTHS_OVER_LIMIT',
);
private static $last_comment = '';
private static $initiated = false;
private static $prevent_moderation_email_for_these_comments = array();
private static $last_comment_result = null;
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' => '',
);
public static function init() {
if ( ! self::$initiated ) {
self::init_hooks();
}
}
/**
* Initializes WordPress hooks
*/
private static function init_hooks() {
self::$initiated = true;
add_action( 'wp_insert_comment', array( 'Akismet', 'auto_check_update_meta' ), 10, 2 );
add_filter( 'preprocess_comment', array( 'Akismet', 'auto_check_comment' ), 1 );
add_filter( 'rest_pre_insert_comment', array( 'Akismet', 'rest_auto_check_comment' ), 1 );
add_action( 'comment_form', array( 'Akismet', 'load_form_js' ) );
add_action( 'do_shortcode_tag', array( 'Akismet', 'load_form_js_via_filter' ), 10, 4 );
add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments' ) );
add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments_meta' ) );
add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_orphaned_commentmeta' ) );
add_action( 'akismet_schedule_cron_recheck', array( 'Akismet', 'cron_recheck' ) );
add_action( 'comment_form', array( 'Akismet', 'add_comment_nonce' ), 1 );
add_action( 'comment_form', array( 'Akismet', 'output_custom_form_fields' ) );
add_filter( 'script_loader_tag', array( 'Akismet', 'set_form_js_async' ), 10, 3 );
add_filter( 'comment_moderation_recipients', array( 'Akismet', 'disable_moderation_emails_if_unreachable' ), 1000, 2 );
add_filter( 'pre_comment_approved', array( 'Akismet', 'last_comment_status' ), 10, 2 );
add_action( 'transition_comment_status', array( 'Akismet', 'transition_comment_status' ), 10, 3 );
// Run this early in the pingback call, before doing a remote fetch of the source uri
add_action( 'xmlrpc_call', array( 'Akismet', 'pre_check_pingback' ), 10, 3 );
// Jetpack compatibility
add_filter( 'jetpack_options_whitelist', array( 'Akismet', 'add_to_jetpack_options_whitelist' ) );
add_filter( 'jetpack_contact_form_html', array( 'Akismet', 'inject_custom_form_fields' ) );
add_filter( 'jetpack_contact_form_akismet_values', array( 'Akismet', 'prepare_custom_form_values' ) );
// Gravity Forms
add_filter( 'gform_get_form_filter', array( 'Akismet', 'inject_custom_form_fields' ) );
add_filter( 'gform_akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ) );
// Contact Form 7
add_filter( 'wpcf7_form_elements', array( 'Akismet', 'append_custom_form_fields' ) );
add_filter( 'wpcf7_akismet_parameters', array( 'Akismet', 'prepare_custom_form_values' ) );
// Formidable Forms
add_filter( 'frm_filter_final_form', array( 'Akismet', 'inject_custom_form_fields' ) );
add_filter( 'frm_akismet_values', array( 'Akismet', 'prepare_custom_form_values' ) );
// Fluent Forms
/*
* The Fluent Forms hook names were updated in version 5.0.0. The last version that supported
* the original hook names was 4.3.25, and version 4.3.25 was tested up to WordPress version 6.1.
*
* The legacy hooks are fired before the new hooks. See
* https://github.com/fluentform/fluentform/commit/cc45341afcae400f217470a7bbfb15efdd80454f
*
* The legacy Fluent Forms hooks will be removed when Akismet no longer supports WordPress version 6.1.
* This will provide compatibility with previous versions of Fluent Forms for a reasonable amount of time.
*/
add_filter( 'fluentform_form_element_start', array( 'Akismet', 'output_custom_form_fields' ) );
add_filter( 'fluentform_akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ), 10, 2 );
// Current Fluent Form hooks.
add_filter( 'fluentform/form_element_start', array( 'Akismet', 'output_custom_form_fields' ) );
add_filter( 'fluentform/akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ), 10, 2 );
add_action( 'update_option_wordpress_api_key', array( 'Akismet', 'updated_option' ), 10, 2 );
add_action( 'add_option_wordpress_api_key', array( 'Akismet', 'added_option' ), 10, 2 );
add_action( 'comment_form_after', array( 'Akismet', 'display_comment_form_privacy_notice' ) );
}
public static function get_api_key() {
return apply_filters( 'akismet_get_api_key', defined( 'WPCOM_API_KEY' ) ? constant( 'WPCOM_API_KEY' ) : get_option( 'wordpress_api_key' ) );
}
/**
* Exchange the API key for a token that can only be used to access stats pages.
*
* @return string
*/
public static function get_access_token() {
static $access_token = null;
if ( is_null( $access_token ) ) {
$request_args = array( 'api_key' => self::get_api_key() );
$request_args = apply_filters( 'akismet_request_args', $request_args, 'token' );
$response = self::http_post( self::build_query( $request_args ), 'token' );
$access_token = $response[1];
}
return $access_token;
}
public static function check_key_status( $key, $ip = null ) {
$request_args = array(
'key' => $key,
'blog' => get_option( 'home' ),
);
$request_args = apply_filters( 'akismet_request_args', $request_args, 'verify-key' );
return self::http_post( self::build_query( $request_args ), 'verify-key', $ip );
}
public static function verify_key( $key, $ip = null ) {
// Shortcut for obviously invalid keys.
if ( strlen( $key ) != 12 ) {
return 'invalid';
}
$response = self::check_key_status( $key, $ip );
if ( $response[1] != 'valid' && $response[1] != 'invalid' ) {
return 'failed';
}
return $response[1];
}
public static function deactivate_key( $key ) {
$request_args = array(
'key' => $key,
'blog' => get_option( 'home' ),
);
$request_args = apply_filters( 'akismet_request_args', $request_args, 'deactivate' );
$response = self::http_post( self::build_query( $request_args ), 'deactivate' );
if ( $response[1] != 'deactivated' ) {
return 'failed';
}
return $response[1];
}
/**
* Add the akismet option to the Jetpack options management whitelist.
*
* @param array $options The list of whitelisted option names.
* @return array The updated whitelist
*/
public static function add_to_jetpack_options_whitelist( $options ) {
$options[] = 'wordpress_api_key';
return $options;
}
/**
* When the akismet option is updated, run the registration call.
*
* This should only be run when the option is updated from the Jetpack/WP.com
* API call, and only if the new key is different than the old key.
*
* @param mixed $old_value The old option value.
* @param mixed $value The new option value.
*/
public static function updated_option( $old_value, $value ) {
// Not an API call
if ( ! class_exists( 'WPCOM_JSON_API_Update_Option_Endpoint' ) ) {
return;
}
// Only run the registration if the old key is different.
if ( $old_value !== $value ) {
self::verify_key( $value );
}
}
/**
* Treat the creation of an API key the same as updating the API key to a new value.
*
* @param mixed $option_name Will always be "wordpress_api_key", until something else hooks in here.
* @param mixed $value The option value.
*/
public static function added_option( $option_name, $value ) {
if ( 'wordpress_api_key' === $option_name ) {
return self::updated_option( '', $value );
}
}
public static function rest_auto_check_comment( $commentdata ) {
return self::auto_check_comment( $commentdata, 'rest_api' );
}
/**
* Check a comment for spam.
*
* @param array $commentdata
* @param string $context What kind of request triggered this comment check? Possible values are 'default', 'rest_api', and 'xml-rpc'.
* @return array|WP_Error Either the $commentdata array with additional entries related to its spam status
* or a WP_Error, if it's a REST API request and the comment should be discarded.
*/
public static function auto_check_comment( $commentdata, $context = 'default' ) {
// If no key is configured, then there's no point in doing any of this.
if ( ! self::get_api_key() ) {
return $commentdata;
}
if ( ! isset( $commentdata['comment_meta'] ) ) {
$commentdata['comment_meta'] = array();
}
self::$last_comment_result = null;
// Skip the Akismet check if the comment matches the Disallowed Keys list.
if ( function_exists( 'wp_check_comment_disallowed_list' ) ) {
$comment_author = isset( $commentdata['comment_author'] ) ? $commentdata['comment_author'] : '';
$comment_author_email = isset( $commentdata['comment_author_email'] ) ? $commentdata['comment_author_email'] : '';
$comment_author_url = isset( $commentdata['comment_author_url'] ) ? $commentdata['comment_author_url'] : '';
$comment_content = isset( $commentdata['comment_content'] ) ? $commentdata['comment_content'] : '';
$comment_author_ip = isset( $commentdata['comment_author_IP'] ) ? $commentdata['comment_author_IP'] : '';
$comment_agent = isset( $commentdata['comment_agent'] ) ? $commentdata['comment_agent'] : '';
if ( wp_check_comment_disallowed_list( $comment_author, $comment_author_email, $comment_author_url, $comment_content, $comment_author_ip, $comment_agent ) ) {
$commentdata['akismet_result'] = 'skipped';
$commentdata['comment_meta']['akismet_result'] = 'skipped';
$commentdata['akismet_skipped_microtime'] = microtime( true );
$commentdata['comment_meta']['akismet_skipped_microtime'] = $commentdata['akismet_skipped_microtime'];
self::set_last_comment( $commentdata );
return $commentdata;
}
}
$comment = $commentdata;
$comment['user_ip'] = self::get_ip_address();
$comment['user_agent'] = self::get_user_agent();
$comment['referrer'] = self::get_referer();
$comment['blog'] = get_option( 'home' );
$comment['blog_lang'] = get_locale();
$comment['blog_charset'] = get_option( 'blog_charset' );
$comment['permalink'] = get_permalink( $comment['comment_post_ID'] );
if ( ! empty( $comment['user_ID'] ) ) {
$comment['user_role'] = self::get_user_roles( $comment['user_ID'] );
}
/** See filter documentation in init_hooks(). */
$akismet_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) );
$comment['akismet_comment_nonce'] = 'inactive';
if ( $akismet_nonce_option == 'true' || $akismet_nonce_option == '' ) {
$comment['akismet_comment_nonce'] = 'failed';
if ( isset( $_POST['akismet_comment_nonce'] ) && wp_verify_nonce( $_POST['akismet_comment_nonce'], 'akismet_comment_nonce_' . $comment['comment_post_ID'] ) ) {
$comment['akismet_comment_nonce'] = 'passed';
}
// comment reply in wp-admin
if ( isset( $_POST['_ajax_nonce-replyto-comment'] ) && check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' ) ) {
$comment['akismet_comment_nonce'] = 'passed';
}
}
if ( self::is_test_mode() ) {
$comment['is_test'] = 'true';
}
foreach ( $_POST as $key => $value ) {
if ( is_string( $value ) ) {
$comment[ "POST_{$key}" ] = $value;
}
}
foreach ( $_SERVER as $key => $value ) {
if ( ! is_string( $value ) ) {
continue;
}
if ( preg_match( '/^HTTP_COOKIE/', $key ) ) {
continue;
}
// Send any potentially useful $_SERVER vars, but avoid sending junk we don't need.
if ( preg_match( '/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/', $key ) ) {
$comment[ "$key" ] = $value;
}
}
$post = get_post( $comment['comment_post_ID'] );
if ( ! is_null( $post ) ) {
// $post can technically be null, although in the past, it's always been an indicator of another plugin interfering.
$comment['comment_post_modified_gmt'] = $post->post_modified_gmt;
// Tags and categories are important context in which to consider the comment.
$comment['comment_context'] = array();
$tag_names = wp_get_post_tags( $post->ID, array( 'fields' => 'names' ) );
if ( $tag_names && ! is_wp_error( $tag_names ) ) {
foreach ( $tag_names as $tag_name ) {
$comment['comment_context'][] = $tag_name;
}
}
$category_names = wp_get_post_categories( $post->ID, array( 'fields' => 'names' ) );
if ( $category_names && ! is_wp_error( $category_names ) ) {
foreach ( $category_names as $category_name ) {
$comment['comment_context'][] = $category_name;
}
}
}
/**
* Filter the data that is used to generate the request body for the API call.
*
* @since 5.3.1
*
* @param array $comment An array of request data.
* @param string $endpoint The API endpoint being requested.
*/
$comment = apply_filters( 'akismet_request_args', $comment, 'comment-check' );
$response = self::http_post( self::build_query( $comment ), 'comment-check' );
do_action( 'akismet_comment_check_response', $response );
$commentdata['comment_as_submitted'] = array_intersect_key( $comment, self::$comment_as_submitted_allowed_keys );
// Also include any form fields we inject into the comment form, like ak_js
foreach ( $_POST as $key => $value ) {
if ( is_string( $value ) && strpos( $key, 'ak_' ) === 0 ) {
$commentdata['comment_as_submitted'][ 'POST_' . $key ] = $value;
}
}
$commentdata['akismet_result'] = $response[1];
if ( 'true' === $response[1] || 'false' === $response[1] ) {
$commentdata['comment_meta']['akismet_result'] = $response[1];
} else {
$commentdata['comment_meta']['akismet_error'] = time();
}
if ( isset( $response[0]['x-akismet-pro-tip'] ) ) {
$commentdata['akismet_pro_tip'] = $response[0]['x-akismet-pro-tip'];
$commentdata['comment_meta']['akismet_pro_tip'] = $response[0]['x-akismet-pro-tip'];
}
if ( isset( $response[0]['x-akismet-guid'] ) ) {
$commentdata['akismet_guid'] = $response[0]['x-akismet-guid'];
$commentdata['comment_meta']['akismet_guid'] = $response[0]['x-akismet-guid'];
}
$commentdata['comment_meta']['akismet_as_submitted'] = $commentdata['comment_as_submitted'];
if ( isset( $response[0]['x-akismet-error'] ) ) {
// An error occurred that we anticipated (like a suspended key) and want the user to act on.
// Send to moderation.
self::$last_comment_result = '0';
} elseif ( 'true' == $response[1] ) {
// akismet_spam_count will be incremented later by comment_is_spam()
self::$last_comment_result = 'spam';
$discard = ( isset( $commentdata['akismet_pro_tip'] ) && $commentdata['akismet_pro_tip'] === 'discard' && self::allow_discard() );
do_action( 'akismet_spam_caught', $discard );
if ( $discard ) {
// The spam is obvious, so we're bailing out early.
// akismet_result_spam() won't be called so bump the counter here
if ( $incr = apply_filters( 'akismet_spam_count_incr', 1 ) ) {
update_option( 'akismet_spam_count', get_option( 'akismet_spam_count' ) + $incr );
}
if ( 'rest_api' === $context ) {
return new WP_Error( 'akismet_rest_comment_discarded', __( 'Comment discarded.', 'akismet' ) );
} elseif ( 'xml-rpc' === $context ) {
// If this is a pingback that we're pre-checking, the discard behavior is the same as the normal spam response behavior.
return $commentdata;
} else {
// Redirect back to the previous page, or failing that, the post permalink, or failing that, the homepage of the blog.
$redirect_to = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : ( $post ? get_permalink( $post ) : home_url() );
wp_safe_redirect( esc_url_raw( $redirect_to ) );
die();
}
} elseif ( 'rest_api' === $context ) {
// The way the REST API structures its calls, we can set the comment_approved value right away.
$commentdata['comment_approved'] = 'spam';
}
}
// if the response is neither true nor false, hold the comment for moderation and schedule a recheck
if ( 'true' != $response[1] && 'false' != $response[1] ) {
if ( ! current_user_can( 'moderate_comments' ) ) {
// Comment status should be moderated
self::$last_comment_result = '0';
}
if ( ! wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) {
wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
do_action( 'akismet_scheduled_recheck', 'invalid-response-' . $response[1] );
}
self::$prevent_moderation_email_for_these_comments[] = $commentdata;
}
// Delete old comments daily
if ( ! wp_next_scheduled( 'akismet_scheduled_delete' ) ) {
wp_schedule_event( time(), 'daily', 'akismet_scheduled_delete' );
}
self::set_last_comment( $commentdata );
self::fix_scheduled_recheck();
return $commentdata;
}
public static function get_last_comment() {
return self::$last_comment;
}
public static function set_last_comment( $comment ) {
if ( is_null( $comment ) ) {
// This never happens in our code.
self::$last_comment = null;
} else {
// We filter it here so that it matches the filtered comment data that we'll have to compare against later.
// wp_filter_comment expects comment_author_IP
self::$last_comment = wp_filter_comment(
array_merge(
array( 'comment_author_IP' => self::get_ip_address() ),
$comment
)
);
}
}
// this fires on wp_insert_comment. we can't update comment_meta when auto_check_comment() runs
// because we don't know the comment ID at that point.
public static function auto_check_update_meta( $id, $comment ) {
// wp_insert_comment() might be called in other contexts, so make sure this is the same comment
// as was checked by auto_check_comment
if ( is_object( $comment ) && ! empty( self::$last_comment ) && is_array( self::$last_comment ) ) {
if ( self::matches_last_comment_by_id( $id ) ) {
load_plugin_textdomain( 'akismet' );
// normal result: true or false
if ( isset( self::$last_comment['akismet_result'] ) && self::$last_comment['akismet_result'] == 'true' ) {
self::update_comment_history( $comment->comment_ID, '', 'check-spam' );
if ( $comment->comment_approved != 'spam' ) {
self::update_comment_history(
$comment->comment_ID,
'',
'status-changed-' . $comment->comment_approved
);
}
} elseif ( isset( self::$last_comment['akismet_result'] ) && self::$last_comment['akismet_result'] == 'false' ) {
self::update_comment_history( $comment->comment_ID, '', 'check-ham' );
// Status could be spam or trash, depending on the WP version and whether this change applies:
// https://core.trac.wordpress.org/changeset/34726
if ( $comment->comment_approved == 'spam' || $comment->comment_approved == 'trash' ) {
if ( function_exists( 'wp_check_comment_disallowed_list' ) ) {
if ( wp_check_comment_disallowed_list( $comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent ) ) {
self::update_comment_history( $comment->comment_ID, '', 'wp-disallowed' );
} else {
self::update_comment_history( $comment->comment_ID, '', 'status-changed-' . $comment->comment_approved );
}
} else {
self::update_comment_history( $comment->comment_ID, '', 'status-changed-' . $comment->comment_approved );
}
}
} elseif ( isset( self::$last_comment['akismet_result'] ) && 'skipped' == self::$last_comment['akismet_result'] ) {
// The comment wasn't sent to Akismet because it matched the disallowed comment keys.
self::update_comment_history( $comment->comment_ID, '', 'wp-disallowed' );
self::update_comment_history( $comment->comment_ID, '', 'akismet-skipped-disallowed' );
} else if ( ! isset( self::$last_comment['akismet_result'] ) ) {
// Add a generic skipped history item.
self::update_comment_history( $comment->comment_ID, '', 'akismet-skipped' );
} else {
// abnormal result: error
self::update_comment_history(
$comment->comment_ID,
'',
'check-error',
array( 'response' => substr( self::$last_comment['akismet_result'], 0, 50 ) )
);
}
}
}
}
public static function delete_old_comments() {
global $wpdb;
/**
* Determines how many comments will be deleted in each batch.
*
* @param int The default, as defined by AKISMET_DELETE_LIMIT.
*/
$delete_limit = apply_filters( 'akismet_delete_comment_limit', defined( 'AKISMET_DELETE_LIMIT' ) ? AKISMET_DELETE_LIMIT : 10000 );
$delete_limit = max( 1, intval( $delete_limit ) );
/**
* Determines how many days a comment will be left in the Spam queue before being deleted.
*
* @param int The default number of days.
*/
$delete_interval = apply_filters( 'akismet_delete_comment_interval', 15 );
$delete_interval = max( 1, intval( $delete_interval ) );
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 ) ) ) {
if ( empty( $comment_ids ) ) {
return;
}
$wpdb->queries = array();
$comments = array();
foreach ( $comment_ids as $comment_id ) {
$comments[ $comment_id ] = get_comment( $comment_id );
do_action( 'delete_comment', $comment_id, $comments[ $comment_id ] );
do_action( 'akismet_batch_delete_count', __FUNCTION__ );
}
// Prepared as strings since comment_id is an unsigned BIGINT, and using %d will constrain the value to the maximum signed BIGINT.
$format_string = implode( ', ', array_fill( 0, is_countable( $comment_ids ) ? count( $comment_ids ) : 0, '%s' ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->comments} WHERE comment_id IN ( " . $format_string . ' )', $comment_ids ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN ( " . $format_string . ' )', $comment_ids ) );
foreach ( $comment_ids as $comment_id ) {
do_action( 'deleted_comment', $comment_id, $comments[ $comment_id ] );
unset( $comments[ $comment_id ] );
}
clean_comment_cache( $comment_ids );
do_action( 'akismet_delete_comment_batch', is_countable( $comment_ids ) ? count( $comment_ids ) : 0 );
}
if ( apply_filters( 'akismet_optimize_table', ( mt_rand( 1, 5000 ) == 11 ), $wpdb->comments ) ) { // lucky number
$wpdb->query( "OPTIMIZE TABLE {$wpdb->comments}" );
}
}
public static function delete_old_comments_meta() {
global $wpdb;
$interval = apply_filters( 'akismet_delete_commentmeta_interval', 15 );
// enforce a minimum of 1 day
$interval = absint( $interval );
if ( $interval < 1 ) {
$interval = 1;
}
// akismet_as_submitted meta values are large, so expire them
// after $interval days regardless of the comment status
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 ) ) ) {
if ( empty( $comment_ids ) ) {
return;
}
$wpdb->queries = array();
foreach ( $comment_ids as $comment_id ) {
delete_comment_meta( $comment_id, 'akismet_as_submitted' );
do_action( 'akismet_batch_delete_count', __FUNCTION__ );
}
do_action( 'akismet_delete_commentmeta_batch', is_countable( $comment_ids ) ? count( $comment_ids ) : 0 );
}
if ( apply_filters( 'akismet_optimize_table', ( mt_rand( 1, 5000 ) == 11 ), $wpdb->commentmeta ) ) { // lucky number
$wpdb->query( "OPTIMIZE TABLE {$wpdb->commentmeta}" );
}
}
// Clear out comments meta that no longer have corresponding comments in the database
public static function delete_orphaned_commentmeta() {
global $wpdb;
$last_meta_id = 0;
$start_time = isset( $_SERVER['REQUEST_TIME_FLOAT'] ) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true );
$max_exec_time = max( ini_get( 'max_execution_time' ) - 5, 3 );
while ( $commentmeta_results = $wpdb->get_results( $wpdb->prepare( "SELECT m.meta_id, m.comment_id, m.meta_key FROM {$wpdb->commentmeta} as m LEFT JOIN {$wpdb->comments} as c USING(comment_id) WHERE c.comment_id IS NULL AND m.meta_id > %d ORDER BY m.meta_id LIMIT 1000", $last_meta_id ) ) ) {
if ( empty( $commentmeta_results ) ) {
return;
}
$wpdb->queries = array();
$commentmeta_deleted = 0;
foreach ( $commentmeta_results as $commentmeta ) {
if ( 'akismet_' == substr( $commentmeta->meta_key, 0, 8 ) ) {
delete_comment_meta( $commentmeta->comment_id, $commentmeta->meta_key );
do_action( 'akismet_batch_delete_count', __FUNCTION__ );
++$commentmeta_deleted;
}
$last_meta_id = $commentmeta->meta_id;
}
do_action( 'akismet_delete_commentmeta_batch', $commentmeta_deleted );
// If we're getting close to max_execution_time, quit for this round.
if ( microtime( true ) - $start_time > $max_exec_time ) {
return;
}
}
if ( apply_filters( 'akismet_optimize_table', ( mt_rand( 1, 5000 ) == 11 ), $wpdb->commentmeta ) ) { // lucky number
$wpdb->query( "OPTIMIZE TABLE {$wpdb->commentmeta}" );
}
}
// how many approved comments does this author have?
public static function get_user_comments_approved( $user_id, $comment_author_email, $comment_author, $comment_author_url ) {
global $wpdb;
/**
* Which comment types should be ignored when counting a user's approved comments?
*
* Some plugins add entries to the comments table that are not actual
* comments that could have been checked by Akismet. Allow these comments
* to be excluded from the "approved comment count" query in order to
* avoid artificially inflating the approved comment count.
*
* @param array $comment_types An array of comment types that won't be considered
* when counting a user's approved comments.
*
* @since 4.2.2
*/
$excluded_comment_types = apply_filters( 'akismet_excluded_comment_types', array() );
$comment_type_where = '';
if ( is_array( $excluded_comment_types ) && ! empty( $excluded_comment_types ) ) {
$excluded_comment_types = array_unique( $excluded_comment_types );
foreach ( $excluded_comment_types as $excluded_comment_type ) {
$comment_type_where .= $wpdb->prepare( ' AND comment_type <> %s ', $excluded_comment_type );
}
}
if ( ! empty( $user_id ) ) {
return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE user_id = %d AND comment_approved = 1" . $comment_type_where, $user_id ) );
}
if ( ! empty( $comment_author_email ) ) {
return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_author_email = %s AND comment_author = %s AND comment_author_url = %s AND comment_approved = 1" . $comment_type_where, $comment_author_email, $comment_author, $comment_author_url ) );
}
return 0;
}
/**
* Get the full comment history for a given comment, as an array in reverse chronological order.
* Each entry will have an 'event', a 'time', and possible a 'message' member (if the entry is old enough).
* Some entries will also have a 'user' or 'meta' member.
*
* @param int $comment_id The relevant comment ID.
* @return array|bool An array of history events, or false if there is no history.
*/
public static function get_comment_history( $comment_id ) {
$history = get_comment_meta( $comment_id, 'akismet_history', false );
if ( empty( $history ) || empty( $history[0] ) ) {
return false;
}
/*
// To see all variants when testing.
$history[] = array( 'time' => 445856401, 'message' => 'Old versions of Akismet stored the message as a literal string in the commentmeta.', 'event' => null );
$history[] = array( 'time' => 445856402, 'event' => 'recheck-spam' );
$history[] = array( 'time' => 445856403, 'event' => 'check-spam' );
$history[] = array( 'time' => 445856404, 'event' => 'recheck-ham' );
$history[] = array( 'time' => 445856405, 'event' => 'check-ham' );
$history[] = array( 'time' => 445856406, 'event' => 'wp-blacklisted' );
$history[] = array( 'time' => 445856406, 'event' => 'wp-disallowed' );
$history[] = array( 'time' => 445856407, 'event' => 'report-spam' );
$history[] = array( 'time' => 445856408, 'event' => 'report-spam', 'user' => 'sam' );
$history[] = array( 'message' => 'sam reported this comment as spam (hardcoded message).', 'time' => 445856400, 'event' => 'report-spam', 'user' => 'sam' );
$history[] = array( 'time' => 445856409, 'event' => 'report-ham', 'user' => 'sam' );
$history[] = array( 'message' => 'sam reported this comment as ham (hardcoded message).', 'time' => 445856400, 'event' => 'report-ham', 'user' => 'sam' ); //
$history[] = array( 'time' => 445856410, 'event' => 'cron-retry-spam' );
$history[] = array( 'time' => 445856411, 'event' => 'cron-retry-ham' );
$history[] = array( 'time' => 445856412, 'event' => 'check-error' ); //
$history[] = array( 'time' => 445856413, 'event' => 'check-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) );
$history[] = array( 'time' => 445856414, 'event' => 'recheck-error' ); // Should not generate a message.
$history[] = array( 'time' => 445856415, 'event' => 'recheck-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) );
$history[] = array( 'time' => 445856416, 'event' => 'status-changedtrash' );
$history[] = array( 'time' => 445856417, 'event' => 'status-changedspam' );
$history[] = array( 'time' => 445856418, 'event' => 'status-changedhold' );
$history[] = array( 'time' => 445856419, 'event' => 'status-changedapprove' );
$history[] = array( 'time' => 445856420, 'event' => 'status-changed-trash' );
$history[] = array( 'time' => 445856421, 'event' => 'status-changed-spam' );
$history[] = array( 'time' => 445856422, 'event' => 'status-changed-hold' );
$history[] = array( 'time' => 445856423, 'event' => 'status-changed-approve' );
$history[] = array( 'time' => 445856424, 'event' => 'status-trash', 'user' => 'sam' );
$history[] = array( 'time' => 445856425, 'event' => 'status-spam', 'user' => 'sam' );
$history[] = array( 'time' => 445856426, 'event' => 'status-hold', 'user' => 'sam' );
$history[] = array( 'time' => 445856427, 'event' => 'status-approve', 'user' => 'sam' );
$history[] = array( 'time' => 445856427, 'event' => 'webhook-spam' );
$history[] = array( 'time' => 445856427, 'event' => 'webhook-ham' );
$history[] = array( 'time' => 445856427, 'event' => 'webhook-spam-noaction' );
$history[] = array( 'time' => 445856427, 'event' => 'webhook-ham-noaction' );
*/
usort( $history, array( 'Akismet', '_cmp_time' ) );
return $history;
}
/**
* Log an event for a given comment, storing it in comment_meta.
*
* @param int $comment_id The ID of the relevant comment.
* @param string $message The string description of the event. No longer used.
* @param string $event The event code.
* @param array $meta Metadata about the history entry. e.g., the user that reported or changed the status of a given comment.
*/
public static function update_comment_history( $comment_id, $message, $event = null, $meta = null ) {
global $current_user;
$user = '';
$event = array(
'time' => self::_get_microtime(),
'event' => $event,
);
if ( is_object( $current_user ) && isset( $current_user->user_login ) ) {
$event['user'] = $current_user->user_login;
}
if ( ! empty( $meta ) ) {
$event['meta'] = $meta;
}
// $unique = false so as to allow multiple values per comment
$r = add_comment_meta( $comment_id, 'akismet_history', $event, false );
}
public static function check_db_comment( $id, $recheck_reason = 'recheck_queue' ) {
global $wpdb;
if ( ! self::get_api_key() ) {
return new WP_Error( 'akismet-not-configured', __( 'Akismet is not configured. Please enter an API key.', 'akismet' ) );
}
$c = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $id ), ARRAY_A );
if ( ! $c ) {
return new WP_Error( 'invalid-comment-id', __( 'Comment not found.', 'akismet' ) );
}
$c['user_ip'] = $c['comment_author_IP'];
$c['user_agent'] = $c['comment_agent'];
$c['referrer'] = '';
$c['blog'] = get_option( 'home' );
$c['blog_lang'] = get_locale();
$c['blog_charset'] = get_option( 'blog_charset' );
$c['permalink'] = get_permalink( $c['comment_post_ID'] );
$c['recheck_reason'] = $recheck_reason;
$c['user_role'] = '';
if ( ! empty( $c['user_ID'] ) ) {
$c['user_role'] = self::get_user_roles( $c['user_ID'] );
}
if ( self::is_test_mode() ) {
$c['is_test'] = 'true';
}
$c = apply_filters( 'akismet_request_args', $c, 'comment-check' );
$response = self::http_post( self::build_query( $c ), 'comment-check' );
if ( ! empty( $response[1] ) ) {
return $response[1];
}
return false;
}
public static function recheck_comment( $id, $recheck_reason = 'recheck_queue' ) {
add_comment_meta( $id, 'akismet_rechecking', true );
$api_response = self::check_db_comment( $id, $recheck_reason );
if ( is_wp_error( $api_response ) ) {
// Invalid comment ID.
} elseif ( 'true' === $api_response ) {
wp_set_comment_status( $id, 'spam' );
update_comment_meta( $id, 'akismet_result', 'true' );
delete_comment_meta( $id, 'akismet_error' );
delete_comment_meta( $id, 'akismet_delayed_moderation_email' );
self::update_comment_history( $id, '', 'recheck-spam' );
} elseif ( 'false' === $api_response ) {
update_comment_meta( $id, 'akismet_result', 'false' );
delete_comment_meta( $id, 'akismet_error' );
delete_comment_meta( $id, 'akismet_delayed_moderation_email' );
self::update_comment_history( $id, '', 'recheck-ham' );
} else {
// abnormal result: error
update_comment_meta( $id, 'akismet_result', 'error' );
self::update_comment_history(
$id,
'',
'recheck-error',
array( 'response' => substr( $api_response, 0, 50 ) )
);
}
delete_comment_meta( $id, 'akismet_rechecking' );
return $api_response;
}
public static function transition_comment_status( $new_status, $old_status, $comment ) {
if ( $new_status == $old_status ) {
return;
}
if ( 'spam' === $new_status || 'spam' === $old_status ) {
// Clear the cache of the "X comments in your spam queue" count on the dashboard.
wp_cache_delete( 'akismet_spam_count', 'widget' );
}
// we don't need to record a history item for deleted comments
if ( $new_status == 'delete' ) {
return;
}
if ( ! current_user_can( 'edit_post', $comment->comment_post_ID ) && ! current_user_can( 'moderate_comments' ) ) {
return;
}
if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING == true ) {
return;
}
// if this is present, it means the status has been changed by a re-check, not an explicit user action
if ( get_comment_meta( $comment->comment_ID, 'akismet_rechecking' ) ) {
return;
}
if ( function_exists( 'getallheaders' ) ) {
$request_headers = getallheaders();
foreach ( $request_headers as $header => $value ) {
if ( strtolower( $header ) == 'x-akismet-webhook' ) {
// This change is due to a webhook request.
return;
}
}
}
// Assumption alert:
// We want to submit comments to Akismet only when a moderator explicitly spams or approves it - not if the status
// is changed automatically by another plugin. Unfortunately WordPress doesn't provide an unambiguous way to
// determine why the transition_comment_status action was triggered. And there are several different ways by which
// to spam and unspam comments: bulk actions, ajax, links in moderation emails, the dashboard, and perhaps others.
// We'll assume that this is an explicit user action if certain POST/GET variables exist.
if (
// status=spam: Marking as spam via the REST API or...
// 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?...
// status=approved: Unspamming via the REST API (Calypso) or...
( isset( $_POST['status'] ) && in_array( $_POST['status'], array( 'spam', 'unspam', 'approved' ) ) )
// spam=1: Clicking "Spam" underneath a comment in wp-admin and allowing the AJAX request to happen.
|| ( isset( $_POST['spam'] ) && (int) $_POST['spam'] == 1 )
// 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.
|| ( isset( $_POST['unspam'] ) && (int) $_POST['unspam'] == 1 )
// comment_status=spam/unspam: It's unclear where this is happening.
|| ( isset( $_POST['comment_status'] ) && in_array( $_POST['comment_status'], array( 'spam', 'unspam' ) ) )
// action=spam: Choosing "Mark as Spam" from the Bulk Actions dropdown in wp-admin (or the "Spam it" link in notification emails).
// action=unspam: Choosing "Not Spam" from the Bulk Actions dropdown in wp-admin.
// action=spamcomment: Following the "Spam" link below a comment in wp-admin (not allowing AJAX request to happen).
// action=unspamcomment: Following the "Not Spam" link below a comment in wp-admin (not allowing AJAX request to happen).
|| ( isset( $_GET['action'] ) && in_array( $_GET['action'], array( 'spam', 'unspam', 'spamcomment', 'unspamcomment' ) ) )
// action=editedcomment: Editing a comment via wp-admin (and possibly changing its status).
|| ( isset( $_POST['action'] ) && in_array( $_POST['action'], array( 'editedcomment' ) ) )
// for=jetpack: Moderation via the WordPress app, Calypso, anything powered by the Jetpack connection.
|| ( isset( $_GET['for'] ) && ( 'jetpack' == $_GET['for'] ) && ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) )
// Certain WordPress.com API requests
|| ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST )
// WordPress.org REST API requests
|| ( defined( 'REST_REQUEST' ) && REST_REQUEST )
) {
if ( $new_status == 'spam' && ( $old_status == 'approved' || $old_status == 'unapproved' || ! $old_status ) ) {
return self::submit_spam_comment( $comment->comment_ID );
} elseif ( $old_status == 'spam' && ( $new_status == 'approved' || $new_status == 'unapproved' ) ) {
return self::submit_nonspam_comment( $comment->comment_ID );
}
}
self::update_comment_history( $comment->comment_ID, '', 'status-' . $new_status );
}
public static function submit_spam_comment( $comment_id ) {
global $wpdb, $current_user, $current_site;
$comment_id = (int) $comment_id;
$comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ), ARRAY_A );
if ( ! $comment ) {
// it was deleted
return;
}
if ( 'spam' != $comment['comment_approved'] ) {
return;
}
self::update_comment_history( $comment_id, '', 'report-spam' );
// If the user hasn't configured Akismet, there's nothing else to do at this point.
if ( ! self::get_api_key() ) {
return;
}
// use the original version stored in comment_meta if available
$as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) );
if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) ) {
$comment = array_merge( $comment, $as_submitted );
}
$comment['blog'] = get_option( 'home' );
$comment['blog_lang'] = get_locale();
$comment['blog_charset'] = get_option( 'blog_charset' );
$comment['permalink'] = get_permalink( $comment['comment_post_ID'] );
if ( is_object( $current_user ) ) {
$comment['reporter'] = $current_user->user_login;
}
if ( is_object( $current_site ) ) {
$comment['site_domain'] = $current_site->domain;
}
$comment['user_role'] = '';
if ( ! empty( $comment['user_ID'] ) ) {
$comment['user_role'] = self::get_user_roles( $comment['user_ID'] );
}
if ( self::is_test_mode() ) {
$comment['is_test'] = 'true';
}
$post = get_post( $comment['comment_post_ID'] );
if ( ! is_null( $post ) ) {
$comment['comment_post_modified_gmt'] = $post->post_modified_gmt;
}
$comment['comment_check_response'] = self::last_comment_check_response( $comment_id );
$comment = apply_filters( 'akismet_request_args', $comment, 'submit-spam' );
$response = self::http_post( self::build_query( $comment ), 'submit-spam' );
update_comment_meta( $comment_id, 'akismet_user_result', 'true' );
if ( $comment['reporter'] ) {
update_comment_meta( $comment_id, 'akismet_user', $comment['reporter'] );
}
do_action( 'akismet_submit_spam_comment', $comment_id, $response[1] );
}
public static function submit_nonspam_comment( $comment_id ) {
global $wpdb, $current_user, $current_site;
$comment_id = (int) $comment_id;
$comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ), ARRAY_A );
if ( ! $comment ) {
// it was deleted
return;
}
self::update_comment_history( $comment_id, '', 'report-ham' );
// If the user hasn't configured Akismet, there's nothing else to do at this point.
if ( ! self::get_api_key() ) {
return;
}
// use the original version stored in comment_meta if available
$as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) );
if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) ) {
$comment = array_merge( $comment, $as_submitted );
}
$comment['blog'] = get_option( 'home' );
$comment['blog_lang'] = get_locale();
$comment['blog_charset'] = get_option( 'blog_charset' );
$comment['permalink'] = get_permalink( $comment['comment_post_ID'] );
$comment['user_role'] = '';
if ( is_object( $current_user ) ) {
$comment['reporter'] = $current_user->user_login;
}
if ( is_object( $current_site ) ) {
$comment['site_domain'] = $current_site->domain;
}
if ( ! empty( $comment['user_ID'] ) ) {
$comment['user_role'] = self::get_user_roles( $comment['user_ID'] );
}
if ( self::is_test_mode() ) {
$comment['is_test'] = 'true';
}
$post = get_post( $comment['comment_post_ID'] );
if ( ! is_null( $post ) ) {
$comment['comment_post_modified_gmt'] = $post->post_modified_gmt;
}
$comment['comment_check_response'] = self::last_comment_check_response( $comment_id );
$comment = apply_filters( 'akismet_request_args', $comment, 'submit-ham' );
$response = self::http_post( self::build_query( $comment ), 'submit-ham' );
update_comment_meta( $comment_id, 'akismet_user_result', 'false' );
if ( $comment['reporter'] ) {
update_comment_meta( $comment_id, 'akismet_user', $comment['reporter'] );
}
do_action( 'akismet_submit_nonspam_comment', $comment_id, $response[1] );
}
public static function cron_recheck() {
global $wpdb;
$api_key = self::get_api_key();
$status = self::verify_key( $api_key );
if ( get_option( 'akismet_alert_code' ) || $status == 'invalid' ) {
// since there is currently a problem with the key, reschedule a check for 6 hours hence
wp_schedule_single_event( time() + 21600, 'akismet_schedule_cron_recheck' );
do_action( 'akismet_scheduled_recheck', 'key-problem-' . get_option( 'akismet_alert_code' ) . '-' . $status );
return false;
}
delete_option( 'akismet_available_servers' );
$comment_errors = $wpdb->get_col( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error' LIMIT 100" );
load_plugin_textdomain( 'akismet' );
foreach ( (array) $comment_errors as $comment_id ) {
// if the comment no longer exists, or is too old, remove the meta entry from the queue to avoid getting stuck
$comment = get_comment( $comment_id );
if (
! $comment // Comment has been deleted
|| strtotime( $comment->comment_date_gmt ) < strtotime( '-15 days' ) // Comment is too old.
|| $comment->comment_approved !== '0' // Comment is no longer in the Pending queue
) {
delete_comment_meta( $comment_id, 'akismet_error' );
delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
continue;
}
add_comment_meta( $comment_id, 'akismet_rechecking', true );
$status = self::check_db_comment( $comment_id, 'retry' );
$event = '';
if ( $status == 'true' ) {
$event = 'cron-retry-spam';
} elseif ( $status == 'false' ) {
$event = 'cron-retry-ham';
}
// If we got back a legit response then update the comment history
// other wise just bail now and try again later. No point in
// re-trying all the comments once we hit one failure.
if ( ! empty( $event ) ) {
delete_comment_meta( $comment_id, 'akismet_error' );
self::update_comment_history( $comment_id, '', $event );
update_comment_meta( $comment_id, 'akismet_result', $status );
// make sure the comment status is still pending. if it isn't, that means the user has already moved it elsewhere.
$comment = get_comment( $comment_id );
if ( $comment && 'unapproved' == wp_get_comment_status( $comment_id ) ) {
if ( $status == 'true' ) {
wp_spam_comment( $comment_id );
} elseif ( $status == 'false' ) {
// comment is good, but it's still in the pending queue. depending on the moderation settings
// we may need to change it to approved.
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 ) ) {
wp_set_comment_status( $comment_id, 1 );
} elseif ( get_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ) ) {
wp_notify_moderator( $comment_id );
}
}
}
delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
} else {
// If this comment has been pending moderation for longer than MAX_DELAY_BEFORE_MODERATION_EMAIL,
// send a moderation email now.
if ( ( intval( gmdate( 'U' ) ) - strtotime( $comment->comment_date_gmt ) ) < self::MAX_DELAY_BEFORE_MODERATION_EMAIL ) {
delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
wp_notify_moderator( $comment_id );
}
delete_comment_meta( $comment_id, 'akismet_rechecking' );
wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
do_action( 'akismet_scheduled_recheck', 'check-db-comment-' . $status );
return;
}
delete_comment_meta( $comment_id, 'akismet_rechecking' );
}
$remaining = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error'" );
if ( $remaining && ! wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) {
wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
do_action( 'akismet_scheduled_recheck', 'remaining' );
}
}
public static function fix_scheduled_recheck() {
$future_check = wp_next_scheduled( 'akismet_schedule_cron_recheck' );
if ( ! $future_check ) {
return;
}
if ( get_option( 'akismet_alert_code' ) > 0 ) {
return;
}
$check_range = time() + 1200;
if ( $future_check > $check_range ) {
wp_clear_scheduled_hook( 'akismet_schedule_cron_recheck' );
wp_schedule_single_event( time() + 300, 'akismet_schedule_cron_recheck' );
do_action( 'akismet_scheduled_recheck', 'fix-scheduled-recheck' );
}
}
public static function add_comment_nonce( $post_id ) {
/**
* To disable the Akismet comment nonce, add a filter for the 'akismet_comment_nonce' tag
* and return any string value that is not 'true' or '' (empty string).
*
* Don't return boolean false, because that implies that the 'akismet_comment_nonce' option
* has not been set and that Akismet should just choose the default behavior for that
* situation.
*/
if ( ! self::get_api_key() ) {
return;
}
$akismet_comment_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) );
if ( $akismet_comment_nonce_option == 'true' || $akismet_comment_nonce_option == '' ) {
echo '<p style="display: none;">';
wp_nonce_field( 'akismet_comment_nonce_' . $post_id, 'akismet_comment_nonce', false );
echo '</p>';
}
}
public static function is_test_mode() {
return defined( 'AKISMET_TEST_MODE' ) && AKISMET_TEST_MODE;
}
public static function allow_discard() {
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
return false;
}
if ( is_user_logged_in() ) {
return false;
}
return ( get_option( 'akismet_strictness' ) === '1' );
}
public static function get_ip_address() {
return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null;
}
/**
* Using the unique values that we assign, do we consider these two comments
* to be the same instance of a comment?
*
* The only fields that matter in $comment1 and $comment2 are akismet_guid and akismet_skipped_microtime.
* We set both of these during the comment-check call, and if the comment has been saved to the DB,
* we save them as comment meta and add them back into the comment array before comparing the comments.
*
* @param mixed $comment1 A comment object or array.
* @param mixed $comment2 A comment object or array.
* @return bool Whether the two comments should be treated as the same comment.
*/
private static function comments_match( $comment1, $comment2 ) {
$comment1 = (array) $comment1;
$comment2 = (array) $comment2;
if ( ! empty( $comment1['akismet_guid'] ) && ! empty( $comment2['akismet_guid'] ) ) {
// If the comment got sent to the API and got a response, it will have a GUID.
return ( $comment1['akismet_guid'] == $comment2['akismet_guid'] );
} else if ( ! empty( $comment1['akismet_skipped_microtime'] ) && ! empty( $comment2['akismet_skipped_microtime'] ) ) {
// It won't have a GUID if it didn't get sent to the API because it matched the disallowed list,
// but it should have a microtimestamp to use here for matching against the comment DB entry it matches.
return ( strval( $comment1['akismet_skipped_microtime'] ) == strval( $comment2['akismet_skipped_microtime'] ) );
}
return false;
}
/**
* Does the supplied comment match the details of the one most recently stored in self::$last_comment?
*
* @param array $comment
* @return bool Whether the comment supplied as an argument is a match for the one we have stored in $last_comment.
*/
public static function matches_last_comment( $comment ) {
if ( ! self::$last_comment ) {
return false;
}
return self::comments_match( $comment, self::$last_comment );
}
/**
* Because of the order of operations, we don't always know the comment ID of the comment that we're checking,
* so we have to be able to match the comment we cached locally with the comment from the DB.
*
* @param int $comment_id
* @return bool Whether the comment represented by $comment_id is a match for the one we have stored in $last_comment.
*/
public static function matches_last_comment_by_id( $comment_id ) {
return self::matches_last_comment( self::get_fields_for_comment_matching( $comment_id ) );
}
/**
* Given a comment ID, retrieve the values that we use for matching comments together.
*
* @param int $comment_id
* @return array An array containing akismet_guid and akismet_skipped_microtime. Either or both may be falsy, but we hope that at least one is a string.
*/
public static function get_fields_for_comment_matching( $comment_id ) {
return array(
'akismet_guid' => get_comment_meta( $comment_id, 'akismet_guid', true ),
'akismet_skipped_microtime' => get_comment_meta( $comment_id, 'akismet_skipped_microtime', true ),
);
}
private static function get_user_agent() {
return isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null;
}
private static function get_referer() {
return isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : null;
}
// return a comma-separated list of role names for the given user
public static function get_user_roles( $user_id ) {
$comment_user = null;
$roles = false;
if ( ! class_exists( 'WP_User' ) ) {
return false;
}
if ( $user_id > 0 ) {
$comment_user = new WP_User( $user_id );
if ( isset( $comment_user->roles ) ) {
$roles = implode( ',', $comment_user->roles );
}
}
if ( is_multisite() && is_super_admin( $user_id ) ) {
if ( empty( $roles ) ) {
$roles = 'super_admin';
} else {
$comment_user->roles[] = 'super_admin';
$roles = implode( ',', $comment_user->roles );
}
}
return $roles;
}
// filter handler used to return a spam result to pre_comment_approved
public static function last_comment_status( $approved, $comment ) {
if ( is_null( self::$last_comment_result ) ) {
// We didn't have reason to store the result of the last check.
return $approved;
}
// Only do this if it's the correct comment.
if ( ! self::matches_last_comment( $comment ) ) {
self::log( "comment_is_spam mismatched comment, returning unaltered $approved" );
return $approved;
}
if ( 'trash' === $approved ) {
// If the last comment we checked has had its approval set to 'trash',
// then it failed the comment blacklist check. Let that blacklist override
// the spam check, since users have the (valid) expectation that when
// they fill out their blacklists, comments that match it will always
// end up in the trash.
return $approved;
}
// bump the counter here instead of when the filter is added to reduce the possibility of overcounting
if ( $incr = apply_filters( 'akismet_spam_count_incr', 1 ) ) {
update_option( 'akismet_spam_count', get_option( 'akismet_spam_count' ) + $incr );
}
return self::$last_comment_result;
}
/**
* If Akismet is temporarily unreachable, we don't want to "spam" the blogger with
* moderation emails for comments that will be automatically cleared or spammed on
* the next retry.
*
* For comments that will be rechecked later, empty the list of email addresses that
* the moderation email would be sent to.
*
* @param array $emails An array of email addresses that the moderation email will be sent to.
* @param int $comment_id The ID of the relevant comment.
* @return array An array of email addresses that the moderation email will be sent to.
*/
public static function disable_moderation_emails_if_unreachable( $emails, $comment_id ) {
if ( ! empty( self::$prevent_moderation_email_for_these_comments ) && ! empty( $emails ) ) {
$matching_fields = self::get_fields_for_comment_matching( $comment_id );
// self::$prevent_moderation_email_for_these_comments is an array of $commentdata objects
// saved immediately after the comment-check request completes.
foreach ( self::$prevent_moderation_email_for_these_comments as $possible_match ) {
if ( self::comments_match( $possible_match, $matching_fields ) ) {
update_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true );
return array();
}
}
}
return $emails;
}
public static function _cmp_time( $a, $b ) {
return $a['time'] > $b['time'] ? -1 : 1;
}
public static function _get_microtime() {
$mtime = explode( ' ', microtime() );
return $mtime[1] + $mtime[0];
}
/**
* Make a POST request to the Akismet API.
*
* @param string $request The body of the request.
* @param string $path The path for the request.
* @param string $ip The specific IP address to hit.
* @return array A two-member array consisting of the headers and the response body, both empty in the case of a failure.
*/
public static function http_post( $request, $path, $ip = null ) {
$akismet_ua = sprintf( 'WordPress/%s | Akismet/%s', $GLOBALS['wp_version'], constant( 'AKISMET_VERSION' ) );
$akismet_ua = apply_filters( 'akismet_ua', $akismet_ua );
$host = self::API_HOST;
$api_key = self::get_api_key();
if ( $api_key ) {
$request = add_query_arg( 'api_key', $api_key, $request );
}
$http_host = $host;
// use a specific IP if provided
// needed by Akismet_Admin::check_server_connectivity()
if ( $ip && long2ip( ip2long( $ip ) ) ) {
$http_host = $ip;
}
$http_args = array(
'body' => $request,
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' ),
'Host' => $host,
'User-Agent' => $akismet_ua,
),
'httpversion' => '1.0',
'timeout' => 15,
);
$akismet_url = $http_akismet_url = "http://{$http_host}/1.1/{$path}";
/**
* Try SSL first; if that fails, try without it and don't try it again for a while.
*/
$ssl = $ssl_failed = false;
// Check if SSL requests were disabled fewer than X hours ago.
$ssl_disabled = get_option( 'akismet_ssl_disabled' );
if ( $ssl_disabled && $ssl_disabled < ( time() - 60 * 60 * 24 ) ) { // 24 hours
$ssl_disabled = false;
delete_option( 'akismet_ssl_disabled' );
} elseif ( $ssl_disabled ) {
do_action( 'akismet_ssl_disabled' );
}
if ( ! $ssl_disabled && ( $ssl = wp_http_supports( array( 'ssl' ) ) ) ) {
$akismet_url = set_url_scheme( $akismet_url, 'https' );
do_action( 'akismet_https_request_pre' );
}
$response = wp_remote_post( $akismet_url, $http_args );
self::log( compact( 'akismet_url', 'http_args', 'response' ) );
if ( $ssl && is_wp_error( $response ) ) {
do_action( 'akismet_https_request_failure', $response );
// Intermittent connection problems may cause the first HTTPS
// request to fail and subsequent HTTP requests to succeed randomly.
// Retry the HTTPS request once before disabling SSL for a time.
$response = wp_remote_post( $akismet_url, $http_args );
self::log( compact( 'akismet_url', 'http_args', 'response' ) );
if ( is_wp_error( $response ) ) {
$ssl_failed = true;
do_action( 'akismet_https_request_failure', $response );
do_action( 'akismet_http_request_pre' );
// Try the request again without SSL.
$response = wp_remote_post( $http_akismet_url, $http_args );
self::log( compact( 'http_akismet_url', 'http_args', 'response' ) );
}
}
if ( is_wp_error( $response ) ) {
do_action( 'akismet_request_failure', $response );
return array( '', '' );
}
if ( $ssl_failed ) {
// The request failed when using SSL but succeeded without it. Disable SSL for future requests.
update_option( 'akismet_ssl_disabled', time() );
do_action( 'akismet_https_disabled' );
}
$simplified_response = array( $response['headers'], $response['body'] );
$alert_code_check_paths = array(
'verify-key',
'comment-check',
'get-stats',
);
if ( in_array( $path, $alert_code_check_paths ) ) {
self::update_alert( $simplified_response );
}
return $simplified_response;
}
// given a response from an API call like check_key_status(), update the alert code options if an alert is present.
public static function update_alert( $response ) {
$alert_option_prefix = 'akismet_alert_';
$alert_header_prefix = 'x-akismet-alert-';
$alert_header_names = array(
'code',
'msg',
'api-calls',
'usage-limit',
'upgrade-plan',
'upgrade-url',
'upgrade-type',
'upgrade-via-support',
);
foreach ( $alert_header_names as $alert_header_name ) {
$value = null;
if ( isset( $response[0][ $alert_header_prefix . $alert_header_name ] ) ) {
$value = $response[0][ $alert_header_prefix . $alert_header_name ];
}
$option_name = $alert_option_prefix . str_replace( '-', '_', $alert_header_name );
if ( $value != get_option( $option_name ) ) {
if ( ! $value ) {
delete_option( $option_name );
} else {
update_option( $option_name, $value );
}
}
}
}
/**
* Mark akismet-frontend.js as deferred. Because nothing depends on it, it can run at any time
* after it's loaded, and the browser won't have to wait for it to load to continue
* parsing the rest of the page.
*/
public static function set_form_js_async( $tag, $handle, $src ) {
if ( 'akismet-frontend' !== $handle ) {
return $tag;
}
return preg_replace( '/^<script /i', '<script defer ', $tag );
}
public static function get_akismet_form_fields() {
$fields = '';
$prefix = 'ak_';
// Contact Form 7 uses _wpcf7 as a prefix to know which fields to exclude from comment_content.
if ( 'wpcf7_form_elements' === current_filter() ) {
$prefix = '_wpcf7_ak_';
}
$fields .= '<p style="display: none !important;" class="akismet-fields-container" data-prefix="' . esc_attr( $prefix ) . '">';
$fields .= '<label>Δ<textarea name="' . $prefix . 'hp_textarea" cols="45" rows="8" maxlength="100"></textarea></label>';
if ( ! function_exists( 'amp_is_request' ) || ! amp_is_request() ) {
// Keep track of how many ak_js fields are in this page so that we don't re-use
// the same ID.
static $field_count = 0;
++$field_count;
$fields .= '<input type="hidden" id="ak_js_' . $field_count . '" name="' . $prefix . 'js" value="' . mt_rand( 0, 250 ) . '"/>';
$fields .= '<script>document.getElementById( "ak_js_' . $field_count . '" ).setAttribute( "value", ( new Date() ).getTime() );</script>';
}
$fields .= '</p>';
return $fields;
}
public static function output_custom_form_fields( $post_id ) {
if ( 'fluentform/form_element_start' === current_filter() && did_action( 'fluentform_form_element_start' ) ) {
// Already did this via the legacy filter.
return;
}
// phpcs:ignore WordPress.Security.EscapeOutput
echo self::get_akismet_form_fields();
}
public static function inject_custom_form_fields( $html ) {
$html = str_replace( '</form>', self::get_akismet_form_fields() . '</form>', $html );
return $html;
}
public static function append_custom_form_fields( $html ) {
$html .= self::get_akismet_form_fields();
return $html;
}
/**
* Ensure that any Akismet-added form fields are included in the comment-check call.
*
* @param array $form
* @param array $data Some plugins will supply the POST data via the filter, since they don't
* read it directly from $_POST.
* @return array $form
*/
public static function prepare_custom_form_values( $form, $data = null ) {
if ( 'fluentform/akismet_fields' === current_filter() && did_filter( 'fluentform_akismet_fields' ) ) {
// Already updated the form fields via the legacy filter.
return $form;
}
if ( is_null( $data ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$data = $_POST;
}
$prefix = 'ak_';
// Contact Form 7 uses _wpcf7 as a prefix to know which fields to exclude from comment_content.
if ( 'wpcf7_akismet_parameters' === current_filter() ) {
$prefix = '_wpcf7_ak_';
}
foreach ( $data as $key => $val ) {
if ( 0 === strpos( $key, $prefix ) ) {
$form[ 'POST_ak_' . substr( $key, strlen( $prefix ) ) ] = $val;
}
}
return $form;
}
private static function bail_on_activation( $message, $deactivate = true ) {
?>
<!doctype html>
<html>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
<style>
* {
text-align: center;
margin: 0;
padding: 0;
font-family: "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif;
}
p {
margin-top: 1em;
font-size: 18px;
}
</style>
</head>
<body>
<p><?php echo esc_html( $message ); ?></p>
</body>
</html>
<?php
if ( $deactivate ) {
$plugins = get_option( 'active_plugins' );
$akismet = plugin_basename( AKISMET__PLUGIN_DIR . 'akismet.php' );
$update = false;
foreach ( $plugins as $i => $plugin ) {
if ( $plugin === $akismet ) {
$plugins[ $i ] = false;
$update = true;
}
}
if ( $update ) {
update_option( 'active_plugins', array_filter( $plugins ) );
}
}
exit;
}
public static function view( $name, array $args = array() ) {
$args = apply_filters( 'akismet_view_arguments', $args, $name );
foreach ( $args as $key => $val ) {
$$key = $val;
}
load_plugin_textdomain( 'akismet' );
$file = AKISMET__PLUGIN_DIR . 'views/' . basename( $name ) . '.php';
if ( file_exists( $file ) ) {
include $file;
}
}
/**
* Attached to activate_{ plugin_basename( __FILES__ ) } by register_activation_hook()
*
* @static
*/
public static function plugin_activation() {
if ( version_compare( $GLOBALS['wp_version'], AKISMET__MINIMUM_WP_VERSION, '<' ) ) {
load_plugin_textdomain( 'akismet' );
$message = '<strong>' .
/* translators: 1: Current Akismet version number, 2: Minimum WordPress version number required. */
sprintf( esc_html__( 'Akismet %1$s requires WordPress %2$s or higher.', 'akismet' ), AKISMET_VERSION, AKISMET__MINIMUM_WP_VERSION ) . '</strong> ' .
/* translators: 1: WordPress documentation URL, 2: Akismet download URL. */
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/plugins/akismet' );
self::bail_on_activation( $message );
} elseif ( ! empty( $_SERVER['SCRIPT_NAME'] ) && false !== strpos( $_SERVER['SCRIPT_NAME'], '/wp-admin/plugins.php' ) ) {
add_option( 'Activated_Akismet', true );
}
}
/**
* Removes all connection options
*
* @static
*/
public static function plugin_deactivation() {
self::deactivate_key( self::get_api_key() );
// Remove any scheduled cron jobs.
$akismet_cron_events = array(
'akismet_schedule_cron_recheck',
'akismet_scheduled_delete',
);
foreach ( $akismet_cron_events as $akismet_cron_event ) {
$timestamp = wp_next_scheduled( $akismet_cron_event );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, $akismet_cron_event );
}
}
}
/**
* Essentially a copy of WP's build_query but one that doesn't expect pre-urlencoded values.
*
* @param array $args An array of key => value pairs
* @return string A string ready for use as a URL query string.
*/
public static function build_query( $args ) {
return _http_build_query( $args, '', '&' );
}
/**
* Log debugging info to the error log.
*
* Enabled when WP_DEBUG_LOG is enabled (and WP_DEBUG, since according to
* core, "WP_DEBUG_DISPLAY and WP_DEBUG_LOG perform no function unless
* WP_DEBUG is true), but can be disabled via the akismet_debug_log filter.
*
* @param mixed $akismet_debug The data to log.
*/
public static function log( $akismet_debug ) {
if ( apply_filters( 'akismet_debug_log', defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG && defined( 'AKISMET_DEBUG' ) && AKISMET_DEBUG ) ) {
error_log( print_r( compact( 'akismet_debug' ), true ) );
}
}
/**
* Check pingbacks for spam before they're saved to the DB.
*
* @param string $method The XML-RPC method that was called.
* @param array $args This and the $server arg are marked as optional since plugins might still be
* calling do_action( 'xmlrpc_action', [...] ) without the arguments that were added in WP 5.7.
* @param wp_xmlrpc_server $server
*/
public static function pre_check_pingback( $method, $args = array(), $server = null ) {
if ( $method !== 'pingback.ping' ) {
return;
}
/*
* $args looks like this:
*
* Array
* (
* [0] => http://www.example.net/?p=1 // Site that created the pingback.
* [1] => https://www.example.com/?p=2 // Post being pingback'd on this site.
* )
*/
if ( ! is_null( $server ) && ! empty( $args[1] ) ) {
$is_multicall = false;
$multicall_count = 0;
if ( 'system.multicall' === $server->message->methodName ) {
$is_multicall = true;
$multicall_count = is_countable( $server->message->params ) ? count( $server->message->params ) : 0;
}
$post_id = url_to_postid( $args[1] );
// If pingbacks aren't open on this post, we'll still check whether this request is part of a potential DDOS,
// but indicate to the server that pingbacks are indeed closed so we don't include this request in the user's stats,
// since the user has already done their part by disabling pingbacks.
$pingbacks_closed = false;
$post = get_post( $post_id );
if ( ! $post || ! pings_open( $post ) ) {
$pingbacks_closed = true;
}
$comment = array(
'comment_author_url' => $args[0],
'comment_post_ID' => $post_id,
'comment_author' => '',
'comment_author_email' => '',
'comment_content' => '',
'comment_type' => 'pingback',
'akismet_pre_check' => '1',
'comment_pingback_target' => $args[1],
'pingbacks_closed' => $pingbacks_closed ? '1' : '0',
'is_multicall' => $is_multicall,
'multicall_count' => $multicall_count,
);
$comment = self::auto_check_comment( $comment, 'xml-rpc' );
if ( isset( $comment['akismet_result'] ) && 'true' == $comment['akismet_result'] ) {
// Sad: tightly coupled with the IXR classes. Unfortunately the action provides no context and no way to return anything.
$server->error( new IXR_Error( 0, 'Invalid discovery target' ) );
// Also note that if this was part of a multicall, a spam result will prevent the subsequent calls from being executed.
// This is probably fine, but it raises the bar for what should be acceptable as a false positive.
}
}
}
/**
* Ensure that we are loading expected scalar values from akismet_as_submitted commentmeta.
*
* @param mixed $meta_value
* @return mixed
*/
private static function sanitize_comment_as_submitted( $meta_value ) {
if ( empty( $meta_value ) ) {
return $meta_value;
}
$meta_value = (array) $meta_value;
foreach ( $meta_value as $key => $value ) {
if ( ! is_scalar( $value ) ) {
unset( $meta_value[ $key ] );
} else {
// These can change, so they're not explicitly listed in comment_as_submitted_allowed_keys.
if ( strpos( $key, 'POST_ak_' ) === 0 ) {
continue;
}
if ( ! isset( self::$comment_as_submitted_allowed_keys[ $key ] ) ) {
unset( $meta_value[ $key ] );
}
}
}
return $meta_value;
}
public static function predefined_api_key() {
if ( defined( 'WPCOM_API_KEY' ) ) {
return true;
}
return apply_filters( 'akismet_predefined_api_key', false );
}
/**
* Controls the display of a privacy related notice underneath the comment
* form using the `akismet_comment_form_privacy_notice` option and filter
* respectively.
*
* Default is to not display the notice, leaving the choice to site admins,
* or integrators.
*/
public static function display_comment_form_privacy_notice() {
if ( 'display' !== apply_filters( 'akismet_comment_form_privacy_notice', get_option( 'akismet_comment_form_privacy_notice', 'hide' ) ) ) {
return;
}
echo apply_filters(
'akismet_comment_form_privacy_notice_markup',
'<p class="akismet_comment_form_privacy_notice">' .
wp_kses(
sprintf(
/* translators: %s: Akismet privacy URL */
__( 'This site uses Akismet to reduce spam. <a href="%s" target="_blank" rel="nofollow noopener">Learn how your comment data is processed.</a>', 'akismet' ),
'https://akismet.com/privacy/'
),
array(
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array(),
),
)
) .
'</p>'
);
}
public static function load_form_js() {
if (
! is_admin()
&& ( ! function_exists( 'amp_is_request' ) || ! amp_is_request() )
&& self::get_api_key()
) {
wp_register_script( 'akismet-frontend', plugin_dir_url( __FILE__ ) . '_inc/akismet-frontend.js', array(), filemtime( plugin_dir_path( __FILE__ ) . '_inc/akismet-frontend.js' ), true );
wp_enqueue_script( 'akismet-frontend' );
}
}
/**
* Add the form JavaScript when we detect that a supported form shortcode is being parsed.
*/
public static function load_form_js_via_filter( $return_value, $tag, $attr, $m ) {
if ( in_array( $tag, array( 'contact-form', 'gravityform', 'contact-form-7', 'formidable', 'fluentform' ) ) ) {
self::load_form_js();
}
return $return_value;
}
/**
* Was the last entry in the comment history created by Akismet?
*
* @param int $comment_id The ID of the comment.
* @return bool
*/
public static function last_comment_status_change_came_from_akismet( $comment_id ) {
$history = self::get_comment_history( $comment_id );
if ( empty( $history ) ) {
return false;
}
$most_recent_history_event = $history[0];
if ( ! isset( $most_recent_history_event['event'] ) ) {
return false;
}
$akismet_history_events = array(
'check-error',
'cron-retry-ham',
'cron-retry-spam',
'check-ham',
'check-spam',
'recheck-error',
'recheck-ham',
'recheck-spam',
'webhook-ham',
'webhook-spam',
);
if ( in_array( $most_recent_history_event['event'], $akismet_history_events ) ) {
return true;
}
return false;
}
/**
* Check the comment history to find out what the most recent comment-check
* response said about this comment.
*
* This value is then included in submit-ham and submit-spam requests to allow
* us to know whether the comment is actually a missed spam/ham or if it's
* just being reclassified after either never being checked or being mistakenly
* marked as ham/spam.
*
* @param int $comment_id The comment ID.
* @return string 'true', 'false', or an empty string if we don't have a record
* of comment-check being called.
*/
public static function last_comment_check_response( $comment_id ) {
$history = self::get_comment_history( $comment_id );
if ( $history ) {
$history = array_reverse( $history );
foreach ( $history as $akismet_history_entry ) {
// We've always been consistent in how history entries are formatted
// but comment_meta is writable by everyone, so don't assume that all
// entries contain the expected parts.
if ( ! is_array( $akismet_history_entry ) ) {
continue;
}
if ( ! isset( $akismet_history_entry['event'] ) ) {
continue;
}
if ( in_array(
$akismet_history_entry['event'],
array(
'recheck-spam',
'check-spam',
'cron-retry-spam',
'webhook-spam',
'webhook-spam-noaction',
),
true
) ) {
return 'true';
} elseif ( in_array(
$akismet_history_entry['event'],
array(
'recheck-ham',
'check-ham',
'cron-retry-ham',
'webhook-ham',
'webhook-ham-noaction',
),
true
) ) {
return 'false';
}
}
}
return '';
}
}
akismet/LICENSE.txt 0000644 00000043254 15043671570 0010044 0 ustar 00 GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
akismet/class.akismet-admin.php 0000644 00000165173 15043671570 0012566 0 ustar 00 <?php
// We plan to gradually remove all of the disabled lint rules below.
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// phpcs:disable Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
class Akismet_Admin {
const NONCE = 'akismet-update-key';
private static $initiated = false;
private static $notices = array();
private static $allowed = array(
'a' => array(
'href' => true,
'title' => true,
),
'b' => array(),
'code' => array(),
'del' => array(
'datetime' => true,
),
'em' => array(),
'i' => array(),
'q' => array(
'cite' => true,
),
'strike' => array(),
'strong' => array(),
);
/**
* List of pages where activation banner should be displayed.
*
* @var array
*/
private static $activation_banner_pages = array(
'edit-comments.php',
'options-discussion.php',
'plugins.php',
);
public static function init() {
if ( ! self::$initiated ) {
self::init_hooks();
}
if ( isset( $_POST['action'] ) && $_POST['action'] == 'enter-key' ) {
self::enter_api_key();
}
}
public static function init_hooks() {
// The standalone stats page was removed in 3.0 for an all-in-one config and stats page.
// Redirect any links that might have been bookmarked or in browser history.
if ( isset( $_GET['page'] ) && 'akismet-stats-display' == $_GET['page'] ) {
wp_safe_redirect( esc_url_raw( self::get_page_url( 'stats' ) ), 301 );
die;
}
self::$initiated = true;
add_action( 'admin_init', array( 'Akismet_Admin', 'admin_init' ) );
add_action( 'admin_menu', array( 'Akismet_Admin', 'admin_menu' ), 5 ); // Priority 5, so it's called before Jetpack's admin_menu.
add_action( 'admin_notices', array( 'Akismet_Admin', 'display_notice' ) );
add_action( 'admin_enqueue_scripts', array( 'Akismet_Admin', 'load_resources' ) );
add_action( 'activity_box_end', array( 'Akismet_Admin', 'dashboard_stats' ) );
add_action( 'rightnow_end', array( 'Akismet_Admin', 'rightnow_stats' ) );
add_action( 'manage_comments_nav', array( 'Akismet_Admin', 'check_for_spam_button' ) );
add_action( 'admin_action_akismet_recheck_queue', array( 'Akismet_Admin', 'recheck_queue' ) );
add_action( 'wp_ajax_akismet_recheck_queue', array( 'Akismet_Admin', 'recheck_queue' ) );
add_action( 'wp_ajax_comment_author_deurl', array( 'Akismet_Admin', 'remove_comment_author_url' ) );
add_action( 'wp_ajax_comment_author_reurl', array( 'Akismet_Admin', 'add_comment_author_url' ) );
add_action( 'jetpack_auto_activate_akismet', array( 'Akismet_Admin', 'connect_jetpack_user' ) );
add_filter( 'plugin_action_links', array( 'Akismet_Admin', 'plugin_action_links' ), 10, 2 );
add_filter( 'comment_row_actions', array( 'Akismet_Admin', 'comment_row_action' ), 10, 2 );
add_filter( 'plugin_action_links_' . plugin_basename( plugin_dir_path( __FILE__ ) . 'akismet.php' ), array( 'Akismet_Admin', 'admin_plugin_settings_link' ) );
add_filter( 'wxr_export_skip_commentmeta', array( 'Akismet_Admin', 'exclude_commentmeta_from_export' ), 10, 3 );
add_filter( 'all_plugins', array( 'Akismet_Admin', 'modify_plugin_description' ) );
// priority=1 because we need ours to run before core's comment anonymizer runs, and that's registered at priority=10
add_filter( 'wp_privacy_personal_data_erasers', array( 'Akismet_Admin', 'register_personal_data_eraser' ), 1 );
}
public static function admin_init() {
if ( get_option( 'Activated_Akismet' ) ) {
delete_option( 'Activated_Akismet' );
if ( ! headers_sent() ) {
$admin_url = self::get_page_url( 'init' );
wp_redirect( $admin_url );
}
}
load_plugin_textdomain( 'akismet' );
add_meta_box( 'akismet-status', __( 'Comment History', 'akismet' ), array( 'Akismet_Admin', 'comment_status_meta_box' ), 'comment', 'normal' );
if ( function_exists( 'wp_add_privacy_policy_content' ) ) {
wp_add_privacy_policy_content(
__( 'Akismet', 'akismet' ),
__( 'We collect information about visitors who comment on Sites that use our Akismet Anti-spam service. The information we collect depends on how the User sets up Akismet for the Site, but typically includes the commenter\'s IP address, user agent, referrer, and Site URL (along with other information directly provided by the commenter such as their name, username, email address, and the comment itself).', 'akismet' )
);
}
}
public static function admin_menu() {
if ( class_exists( 'Jetpack' ) ) {
add_action( 'jetpack_admin_menu', array( 'Akismet_Admin', 'load_menu' ) );
} else {
self::load_menu();
}
}
public static function admin_head() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
}
public static function admin_plugin_settings_link( $links ) {
$settings_link = '<a href="' . esc_url( self::get_page_url() ) . '">' . __( 'Settings', 'akismet' ) . '</a>';
array_unshift( $links, $settings_link );
return $links;
}
public static function load_menu() {
if ( class_exists( 'Jetpack' ) ) {
$hook = add_submenu_page( 'jetpack', __( 'Akismet Anti-spam', 'akismet' ), __( 'Akismet Anti-spam', 'akismet' ), 'manage_options', 'akismet-key-config', array( 'Akismet_Admin', 'display_page' ) );
} else {
$hook = add_options_page( __( 'Akismet Anti-spam', 'akismet' ), __( 'Akismet Anti-spam', 'akismet' ), 'manage_options', 'akismet-key-config', array( 'Akismet_Admin', 'display_page' ) );
}
if ( $hook ) {
add_action( "load-$hook", array( 'Akismet_Admin', 'admin_help' ) );
}
}
public static function load_resources() {
global $hook_suffix;
if ( in_array(
$hook_suffix,
apply_filters(
'akismet_admin_page_hook_suffixes',
array_merge(
array(
'index.php', // dashboard
'comment.php',
'post.php',
'settings_page_akismet-key-config',
'jetpack_page_akismet-key-config',
),
self::$activation_banner_pages
)
)
) ) {
$akismet_css_path = is_rtl() ? '_inc/rtl/akismet-rtl.css' : '_inc/akismet.css';
wp_register_style( 'akismet', plugin_dir_url( __FILE__ ) . $akismet_css_path, array(), self::get_asset_file_version( $akismet_css_path ) );
wp_enqueue_style( 'akismet' );
wp_register_style( 'akismet-font-inter', plugin_dir_url( __FILE__ ) . '_inc/fonts/inter.css', array(), self::get_asset_file_version( '_inc/fonts/inter.css' ) );
wp_enqueue_style( 'akismet-font-inter' );
$akismet_admin_css_path = is_rtl() ? '_inc/rtl/akismet-admin-rtl.css' : '_inc/akismet-admin.css';
wp_register_style( 'akismet-admin', plugin_dir_url( __FILE__ ) . $akismet_admin_css_path, array(), self::get_asset_file_version( $akismet_admin_css_path ) );
wp_enqueue_style( 'akismet-admin' );
// Enqueue the Akismet activation banner background separately so we can
// include the right path to the image. Shown on edit-comments.php and plugins.php.
if ( in_array( $hook_suffix, self::$activation_banner_pages, true ) ) {
$activation_banner_url = esc_url(
plugin_dir_url( __FILE__ ) . '_inc/img/akismet-activation-banner-elements.png'
);
$inline_css = '.akismet-activate {' . PHP_EOL .
'background-image: url(' . $activation_banner_url . ');' . PHP_EOL .
'}';
wp_add_inline_style( 'akismet-admin', $inline_css );
}
wp_register_script( 'akismet.js', plugin_dir_url( __FILE__ ) . '_inc/akismet.js', array( 'jquery' ), self::get_asset_file_version( '_inc/akismet.js' ) );
wp_enqueue_script( 'akismet.js' );
wp_register_script( 'akismet-admin.js', plugin_dir_url( __FILE__ ) . '_inc/akismet-admin.js', array(), self::get_asset_file_version( '/_inc/akismet-admin.js' ) );
wp_enqueue_script( 'akismet-admin.js' );
$inline_js = array(
'comment_author_url_nonce' => wp_create_nonce( 'comment_author_url_nonce' ),
'strings' => array(
'Remove this URL' => __( 'Remove this URL', 'akismet' ),
'Removing...' => __( 'Removing...', 'akismet' ),
'URL removed' => __( 'URL removed', 'akismet' ),
'(undo)' => __( '(undo)', 'akismet' ),
'Re-adding...' => __( 'Re-adding...', 'akismet' ),
),
);
if ( isset( $_GET['akismet_recheck'] ) && wp_verify_nonce( $_GET['akismet_recheck'], 'akismet_recheck' ) ) {
$inline_js['start_recheck'] = true;
}
if ( apply_filters( 'akismet_enable_mshots', true ) ) {
$inline_js['enable_mshots'] = true;
}
wp_localize_script( 'akismet.js', 'WPAkismet', $inline_js );
}
}
/**
* Add help to the Akismet page
*
* @return false if not the Akismet page
*/
public static function admin_help() {
$current_screen = get_current_screen();
// Screen Content
if ( current_user_can( 'manage_options' ) ) {
if ( ! Akismet::get_api_key() || ( isset( $_GET['view'] ) && $_GET['view'] == 'start' ) ) {
// setup page
$current_screen->add_help_tab(
array(
'id' => 'overview',
'title' => __( 'Overview', 'akismet' ),
'content' =>
'<p><strong>' . esc_html__( 'Akismet Setup', 'akismet' ) . '</strong></p>' .
'<p>' . esc_html__( 'Akismet filters out spam, so you can focus on more important things.', 'akismet' ) . '</p>' .
'<p>' . esc_html__( 'On this page, you are able to set up the Akismet plugin.', 'akismet' ) . '</p>',
)
);
$current_screen->add_help_tab(
array(
'id' => 'setup-signup',
'title' => __( 'New to Akismet', 'akismet' ),
'content' =>
'<p><strong>' . esc_html__( 'Akismet Setup', 'akismet' ) . '</strong></p>' .
'<p>' . esc_html__( 'You need to enter an API key to activate the Akismet service on your site.', 'akismet' ) . '</p>' .
/* translators: %s: a link to the signup page with the text 'Akismet.com'. */
'<p>' . sprintf( __( 'Sign up for an account on %s to get an API Key.', 'akismet' ), '<a href="https://akismet.com/plugin-signup/" target="_blank">Akismet.com</a>' ) . '</p>',
)
);
$current_screen->add_help_tab(
array(
'id' => 'setup-manual',
'title' => __( 'Enter an API Key', 'akismet' ),
'content' =>
'<p><strong>' . esc_html__( 'Akismet Setup', 'akismet' ) . '</strong></p>' .
'<p>' . esc_html__( 'If you already have an API key', 'akismet' ) . '</p>' .
'<ol>' .
'<li>' . esc_html__( 'Copy and paste the API key into the text field.', 'akismet' ) . '</li>' .
'<li>' . esc_html__( 'Click the Use this Key button.', 'akismet' ) . '</li>' .
'</ol>',
)
);
} elseif ( isset( $_GET['view'] ) && $_GET['view'] == 'stats' ) {
// stats page
$current_screen->add_help_tab(
array(
'id' => 'overview',
'title' => __( 'Overview', 'akismet' ),
'content' =>
'<p><strong>' . esc_html__( 'Akismet Stats', 'akismet' ) . '</strong></p>' .
'<p>' . esc_html__( 'Akismet filters out spam, so you can focus on more important things.', 'akismet' ) . '</p>' .
'<p>' . esc_html__( 'On this page, you are able to view stats on spam filtered on your site.', 'akismet' ) . '</p>',
)
);
} else {
// configuration page
$current_screen->add_help_tab(
array(
'id' => 'overview',
'title' => __( 'Overview', 'akismet' ),
'content' =>
'<p><strong>' . esc_html__( 'Akismet Configuration', 'akismet' ) . '</strong></p>' .
'<p>' . esc_html__( 'Akismet filters out spam, so you can focus on more important things.', 'akismet' ) . '</p>' .
'<p>' . esc_html__( 'On this page, you are able to update your Akismet settings and view spam stats.', 'akismet' ) . '</p>',
)
);
$current_screen->add_help_tab(
array(
'id' => 'settings',
'title' => __( 'Settings', 'akismet' ),
'content' =>
'<p><strong>' . esc_html__( 'Akismet Configuration', 'akismet' ) . '</strong></p>' .
( Akismet::predefined_api_key() ? '' : '<p><strong>' . esc_html__( 'API Key', 'akismet' ) . '</strong> - ' . esc_html__( 'Enter/remove an API key.', 'akismet' ) . '</p>' ) .
'<p><strong>' . esc_html__( 'Comments', 'akismet' ) . '</strong> - ' . esc_html__( 'Show the number of approved comments beside each comment author in the comments list page.', 'akismet' ) . '</p>' .
'<p><strong>' . esc_html__( 'Strictness', 'akismet' ) . '</strong> - ' . esc_html__( 'Choose to either discard the worst spam automatically or to always put all spam in spam folder.', 'akismet' ) . '</p>',
)
);
if ( ! Akismet::predefined_api_key() ) {
$current_screen->add_help_tab(
array(
'id' => 'account',
'title' => __( 'Account', 'akismet' ),
'content' =>
'<p><strong>' . esc_html__( 'Akismet Configuration', 'akismet' ) . '</strong></p>' .
'<p><strong>' . esc_html__( 'Subscription Type', 'akismet' ) . '</strong> - ' . esc_html__( 'The Akismet subscription plan', 'akismet' ) . '</p>' .
'<p><strong>' . esc_html__( 'Status', 'akismet' ) . '</strong> - ' . esc_html__( 'The subscription status - active, cancelled or suspended', 'akismet' ) . '</p>',
)
);
}
}
}
// Help Sidebar
$current_screen->set_help_sidebar(
'<p><strong>' . esc_html__( 'For more information:', 'akismet' ) . '</strong></p>' .
'<p><a href="https://akismet.com/faq/" target="_blank">' . esc_html__( 'Akismet FAQ', 'akismet' ) . '</a></p>' .
'<p><a href="https://akismet.com/support/" target="_blank">' . esc_html__( 'Akismet Support', 'akismet' ) . '</a></p>'
);
}
public static function enter_api_key() {
if ( ! current_user_can( 'manage_options' ) ) {
die( __( 'Cheatin’ uh?', 'akismet' ) );
}
if ( ! wp_verify_nonce( $_POST['_wpnonce'], self::NONCE ) ) {
return false;
}
foreach ( array( 'akismet_strictness', 'akismet_show_user_comments_approved' ) as $option ) {
update_option( $option, isset( $_POST[ $option ] ) && (int) $_POST[ $option ] == 1 ? '1' : '0' );
}
if ( ! empty( $_POST['akismet_comment_form_privacy_notice'] ) ) {
self::set_form_privacy_notice_option( $_POST['akismet_comment_form_privacy_notice'] );
} else {
self::set_form_privacy_notice_option( 'hide' );
}
if ( Akismet::predefined_api_key() ) {
return false; // shouldn't have option to save key if already defined
}
$new_key = preg_replace( '/[^a-f0-9]/i', '', $_POST['key'] );
$old_key = Akismet::get_api_key();
if ( empty( $new_key ) ) {
if ( ! empty( $old_key ) ) {
delete_option( 'wordpress_api_key' );
self::$notices[] = 'new-key-empty';
}
} elseif ( $new_key != $old_key ) {
self::save_key( $new_key );
}
return true;
}
public static function save_key( $api_key ) {
$key_status = Akismet::verify_key( $api_key );
if ( $key_status == 'valid' ) {
$akismet_user = self::get_akismet_user( $api_key );
if ( $akismet_user ) {
if ( in_array( $akismet_user->status, array( 'active', 'active-dunning', 'no-sub' ) ) ) {
update_option( 'wordpress_api_key', $api_key );
}
if ( $akismet_user->status == 'active' ) {
self::$notices['status'] = 'new-key-valid';
} elseif ( $akismet_user->status == 'notice' ) {
self::$notices['status'] = $akismet_user;
} else {
self::$notices['status'] = $akismet_user->status;
}
} else {
self::$notices['status'] = 'new-key-invalid';
}
} elseif ( in_array( $key_status, array( 'invalid', 'failed' ) ) ) {
self::$notices['status'] = 'new-key-' . $key_status;
}
}
public static function dashboard_stats() {
if ( did_action( 'rightnow_end' ) ) {
return; // We already displayed this info in the "Right Now" section
}
if ( ! $count = get_option( 'akismet_spam_count' ) ) {
return;
}
global $submenu;
echo '<h3>' . esc_html( _x( 'Spam', 'comments', 'akismet' ) ) . '</h3>';
echo '<p>' . sprintf(
/* translators: 1: Akismet website URL, 2: Comments page URL, 3: Number of spam comments. */
_n(
'<a href="%1$s">Akismet</a> has protected your site from <a href="%2$s">%3$s spam comment</a>.',
'<a href="%1$s">Akismet</a> has protected your site from <a href="%2$s">%3$s spam comments</a>.',
$count,
'akismet'
),
'https://akismet.com/wordpress/',
esc_url( add_query_arg( array( 'page' => 'akismet-admin' ), admin_url( isset( $submenu['edit-comments.php'] ) ? 'edit-comments.php' : 'edit.php' ) ) ),
number_format_i18n( $count )
) . '</p>';
}
// WP 2.5+
public static function rightnow_stats() {
if ( $count = get_option( 'akismet_spam_count' ) ) {
$intro = sprintf(
/* translators: 1: Akismet website URL, 2: Number of spam comments. */
_n(
'<a href="%1$s">Akismet</a> has protected your site from %2$s spam comment already. ',
'<a href="%1$s">Akismet</a> has protected your site from %2$s spam comments already. ',
$count,
'akismet'
),
'https://akismet.com/wordpress/',
number_format_i18n( $count )
);
} else {
/* translators: %s: Akismet website URL. */
$intro = sprintf( __( '<a href="%s">Akismet</a> blocks spam from getting to your blog. ', 'akismet' ), 'https://akismet.com/wordpress/' );
}
$link = add_query_arg( array( 'comment_status' => 'spam' ), admin_url( 'edit-comments.php' ) );
if ( $queue_count = self::get_spam_count() ) {
$queue_text = sprintf(
/* translators: 1: Number of comments, 2: Comments page URL. */
_n(
'There’s <a href="%2$s">%1$s comment</a> in your spam queue right now.',
'There are <a href="%2$s">%1$s comments</a> in your spam queue right now.',
$queue_count,
'akismet'
),
number_format_i18n( $queue_count ),
esc_url( $link )
);
} else {
/* translators: %s: Comments page URL. */
$queue_text = sprintf( __( "There’s nothing in your <a href='%s'>spam queue</a> at the moment.", 'akismet' ), esc_url( $link ) );
}
$text = $intro . '<br />' . $queue_text;
echo "<p class='akismet-right-now'>$text</p>\n";
}
public static function check_for_spam_button( $comment_status ) {
// The "Check for Spam" button should only appear when the page might be showing
// a comment with comment_approved=0, which means an un-trashed, un-spammed,
// not-yet-moderated comment.
if ( 'all' != $comment_status && 'moderated' != $comment_status ) {
return;
}
$link = '';
$comments_count = wp_count_comments();
echo '</div>';
echo '<div class="alignleft actions">';
$classes = array(
'button-secondary',
'checkforspam',
'button-disabled', // Disable button until the page is loaded
);
if ( $comments_count->moderated > 0 ) {
$classes[] = 'enable-on-load';
if ( ! Akismet::get_api_key() ) {
$link = self::get_page_url();
$classes[] = 'ajax-disabled';
}
}
echo '<a
class="' . esc_attr( implode( ' ', $classes ) ) . '"' .
( ! empty( $link ) ? ' href="' . esc_url( $link ) . '"' : '' ) .
/* translators: The placeholder is for showing how much of the process has completed, as a percent. e.g., "Checking for Spam (40%)" */
' data-progress-label="' . esc_attr( __( 'Checking for Spam (%1$s%)', 'akismet' ) ) . '"
data-success-url="' . esc_attr(
remove_query_arg(
array( 'akismet_recheck', 'akismet_recheck_error' ),
add_query_arg(
array(
'akismet_recheck_complete' => 1,
'recheck_count' => urlencode( '__recheck_count__' ),
'spam_count' => urlencode( '__spam_count__' ),
)
)
)
) . '"
data-failure-url="' . esc_attr( remove_query_arg( array( 'akismet_recheck', 'akismet_recheck_complete' ), add_query_arg( array( 'akismet_recheck_error' => 1 ) ) ) ) . '"
data-pending-comment-count="' . esc_attr( $comments_count->moderated ) . '"
data-nonce="' . esc_attr( wp_create_nonce( 'akismet_check_for_spam' ) ) . '"
' . ( ! in_array( 'ajax-disabled', $classes ) ? 'onclick="return false;"' : '' ) . '
>' . esc_html__( 'Check for Spam', 'akismet' ) . '</a>';
echo '<span class="checkforspam-spinner"></span>';
}
public static function recheck_queue() {
global $wpdb;
Akismet::fix_scheduled_recheck();
if ( ! ( isset( $_GET['recheckqueue'] ) || ( isset( $_REQUEST['action'] ) && 'akismet_recheck_queue' == $_REQUEST['action'] ) ) ) {
return;
}
if ( ! wp_verify_nonce( $_POST['nonce'], 'akismet_check_for_spam' ) ) {
wp_send_json(
array(
'error' => __( 'You don’t have permission to do that.', 'akismet' ),
)
);
return;
}
$result_counts = self::recheck_queue_portion( empty( $_POST['offset'] ) ? 0 : $_POST['offset'], empty( $_POST['limit'] ) ? 100 : $_POST['limit'] );
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
wp_send_json(
array(
'counts' => $result_counts,
)
);
} else {
$redirect_to = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : admin_url( 'edit-comments.php' );
wp_safe_redirect( $redirect_to );
exit;
}
}
public static function recheck_queue_portion( $start = 0, $limit = 100 ) {
global $wpdb;
$paginate = '';
if ( $limit <= 0 ) {
$limit = 100;
}
if ( $start < 0 ) {
$start = 0;
}
$moderation = $wpdb->get_col( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_approved = '0' LIMIT %d OFFSET %d", $limit, $start ) );
$result_counts = array(
'processed' => is_countable( $moderation ) ? count( $moderation ) : 0,
'spam' => 0,
'ham' => 0,
'error' => 0,
);
foreach ( $moderation as $comment_id ) {
$api_response = Akismet::recheck_comment( $comment_id, 'recheck_queue' );
if ( 'true' === $api_response ) {
++$result_counts['spam'];
} elseif ( 'false' === $api_response ) {
++$result_counts['ham'];
} else {
++$result_counts['error'];
}
}
return $result_counts;
}
// Adds an 'x' link next to author URLs, clicking will remove the author URL and show an undo link
public static function remove_comment_author_url() {
if ( ! empty( $_POST['id'] ) && check_admin_referer( 'comment_author_url_nonce' ) ) {
$comment_id = intval( $_POST['id'] );
$comment = get_comment( $comment_id, ARRAY_A );
if ( $comment && current_user_can( 'edit_comment', $comment['comment_ID'] ) ) {
$comment['comment_author_url'] = '';
do_action( 'comment_remove_author_url' );
print( wp_update_comment( $comment ) );
die();
}
}
}
public static function add_comment_author_url() {
if ( ! empty( $_POST['id'] ) && ! empty( $_POST['url'] ) && check_admin_referer( 'comment_author_url_nonce' ) ) {
$comment_id = intval( $_POST['id'] );
$comment = get_comment( $comment_id, ARRAY_A );
if ( $comment && current_user_can( 'edit_comment', $comment['comment_ID'] ) ) {
$comment['comment_author_url'] = esc_url( $_POST['url'] );
do_action( 'comment_add_author_url' );
print( wp_update_comment( $comment ) );
die();
}
}
}
public static function comment_row_action( $a, $comment ) {
$akismet_result = get_comment_meta( $comment->comment_ID, 'akismet_result', true );
if ( ! $akismet_result && get_comment_meta( $comment->comment_ID, 'akismet_skipped', true ) ) {
$akismet_result = 'skipped'; // Akismet chose to skip the comment-check request.
}
$akismet_error = get_comment_meta( $comment->comment_ID, 'akismet_error', true );
$user_result = get_comment_meta( $comment->comment_ID, 'akismet_user_result', true );
$comment_status = wp_get_comment_status( $comment->comment_ID );
$desc = null;
if ( $akismet_error ) {
$desc = __( 'Awaiting spam check', 'akismet' );
} elseif ( ! $user_result || $user_result == $akismet_result ) {
// Show the original Akismet result if the user hasn't overridden it, or if their decision was the same
if ( $akismet_result == 'true' && $comment_status != 'spam' && $comment_status != 'trash' ) {
$desc = __( 'Flagged as spam by Akismet', 'akismet' );
} elseif ( $akismet_result == 'false' && $comment_status == 'spam' ) {
$desc = __( 'Cleared by Akismet', 'akismet' );
}
} else {
$who = get_comment_meta( $comment->comment_ID, 'akismet_user', true );
if ( $user_result == 'true' ) {
/* translators: %s: Username. */
$desc = sprintf( __( 'Flagged as spam by %s', 'akismet' ), $who );
} else {
/* translators: %s: Username. */
$desc = sprintf( __( 'Un-spammed by %s', 'akismet' ), $who );
}
}
// add a History item to the hover links, just after Edit
if ( $akismet_result && is_array( $a ) ) {
$b = array();
foreach ( $a as $k => $item ) {
$b[ $k ] = $item;
if (
$k == 'edit'
|| $k == 'unspam'
) {
$b['history'] = '<a href="comment.php?action=editcomment&c=' . $comment->comment_ID . '#akismet-status" title="' . esc_attr__( 'View comment history', 'akismet' ) . '"> ' . esc_html__( 'History', 'akismet' ) . '</a>';
}
}
$a = $b;
}
if ( $desc ) {
echo '<span class="akismet-status" commentid="' . $comment->comment_ID . '"><a href="comment.php?action=editcomment&c=' . $comment->comment_ID . '#akismet-status" title="' . esc_attr__( 'View comment history', 'akismet' ) . '">' . esc_html( $desc ) . '</a></span>';
}
$show_user_comments_option = get_option( 'akismet_show_user_comments_approved' );
if ( $show_user_comments_option === false ) {
// Default to active if the user hasn't made a decision.
$show_user_comments_option = '1';
}
$show_user_comments = apply_filters( 'akismet_show_user_comments_approved', $show_user_comments_option );
$show_user_comments = $show_user_comments === 'false' ? false : $show_user_comments; // option used to be saved as 'false' / 'true'
if ( $show_user_comments ) {
$comment_count = Akismet::get_user_comments_approved( $comment->user_id, $comment->comment_author_email, $comment->comment_author, $comment->comment_author_url );
$comment_count = intval( $comment_count );
echo '<span class="akismet-user-comment-count" commentid="' . $comment->comment_ID . '" style="display:none;"><br><span class="akismet-user-comment-counts">';
/* translators: %s: Number of comments. */
echo sprintf( esc_html( _n( '%s approved', '%s approved', $comment_count, 'akismet' ) ), number_format_i18n( $comment_count ) ) . '</span></span>';
}
return $a;
}
public static function comment_status_meta_box( $comment ) {
$history = Akismet::get_comment_history( $comment->comment_ID );
if ( $history ) {
foreach ( $history as $row ) {
$message = '';
if ( ! empty( $row['message'] ) ) {
// Old versions of Akismet stored the message as a literal string in the commentmeta.
// New versions don't do that for two reasons:
// 1) Save space.
// 2) The message can be translated into the current language of the blog, not stuck
// in the language of the blog when the comment was made.
$message = esc_html( $row['message'] );
} elseif ( ! empty( $row['event'] ) ) {
// If possible, use a current translation.
switch ( $row['event'] ) {
case 'recheck-spam':
$message = esc_html( __( 'Akismet re-checked and caught this comment as spam.', 'akismet' ) );
break;
case 'check-spam':
$message = esc_html( __( 'Akismet caught this comment as spam.', 'akismet' ) );
break;
case 'recheck-ham':
$message = esc_html( __( 'Akismet re-checked and cleared this comment.', 'akismet' ) );
break;
case 'check-ham':
$message = esc_html( __( 'Akismet cleared this comment.', 'akismet' ) );
break;
case 'wp-blacklisted':
case 'wp-disallowed':
$message = sprintf(
/* translators: The placeholder is a WordPress PHP function name. */
esc_html( __( 'Comment was caught by %s.', 'akismet' ) ),
function_exists( 'wp_check_comment_disallowed_list' ) ? '<code>wp_check_comment_disallowed_list</code>' : '<code>wp_blacklist_check</code>'
);
break;
case 'report-spam':
if ( isset( $row['user'] ) ) {
/* translators: The placeholder is a username. */
$message = esc_html( sprintf( __( '%s reported this comment as spam.', 'akismet' ), $row['user'] ) );
} elseif ( ! $message ) {
$message = esc_html( __( 'This comment was reported as spam.', 'akismet' ) );
}
break;
case 'report-ham':
if ( isset( $row['user'] ) ) {
/* translators: The placeholder is a username. */
$message = esc_html( sprintf( __( '%s reported this comment as not spam.', 'akismet' ), $row['user'] ) );
} elseif ( ! $message ) {
$message = esc_html( __( 'This comment was reported as not spam.', 'akismet' ) );
}
break;
case 'cron-retry-spam':
$message = esc_html( __( 'Akismet caught this comment as spam during an automatic retry.', 'akismet' ) );
break;
case 'cron-retry-ham':
$message = esc_html( __( 'Akismet cleared this comment during an automatic retry.', 'akismet' ) );
break;
case 'check-error':
if ( isset( $row['meta'], $row['meta']['response'] ) ) {
/* translators: The placeholder is an error response returned by the API server. */
$message = sprintf( esc_html( __( 'Akismet was unable to check this comment (response: %s) but will automatically retry later.', 'akismet' ) ), '<code>' . esc_html( $row['meta']['response'] ) . '</code>' );
} else {
$message = esc_html( __( 'Akismet was unable to check this comment but will automatically retry later.', 'akismet' ) );
}
break;
case 'recheck-error':
if ( isset( $row['meta'], $row['meta']['response'] ) ) {
/* translators: The placeholder is an error response returned by the API server. */
$message = sprintf( esc_html( __( 'Akismet was unable to recheck this comment (response: %s).', 'akismet' ) ), '<code>' . esc_html( $row['meta']['response'] ) . '</code>' );
} else {
$message = esc_html( __( 'Akismet was unable to recheck this comment.', 'akismet' ) );
}
break;
case 'webhook-spam':
$message = esc_html( __( 'Akismet caught this comment as spam and updated its status via webhook.', 'akismet' ) );
break;
case 'webhook-ham':
$message = esc_html( __( 'Akismet cleared this comment and updated its status via webhook.', 'akismet' ) );
break;
case 'webhook-spam-noaction':
$message = esc_html( __( 'Akismet determined this comment was spam during a recheck. It did not update the comment status because it had already been modified by another user or plugin.', 'akismet' ) );
break;
case 'webhook-ham-noaction':
$message = esc_html( __( 'Akismet cleared this comment during a recheck. It did not update the comment status because it had already been modified by another user or plugin.', 'akismet' ) );
break;
case 'akismet-skipped':
$message = esc_html( __( 'This comment was not sent to Akismet when it was submitted because it was caught by something else.', 'akismet' ) );
break;
case 'akismet-skipped-disallowed':
$message = esc_html( __( 'This comment was not sent to Akismet when it was submitted because it was caught by the comment disallowed list.', 'akismet' ) );
break;
default:
if ( preg_match( '/^status-changed/', $row['event'] ) ) {
// Half of these used to be saved without the dash after 'status-changed'.
// See https://plugins.trac.wordpress.org/changeset/1150658/akismet/trunk
$new_status = preg_replace( '/^status-changed-?/', '', $row['event'] );
/* translators: The placeholder is a short string (like 'spam' or 'approved') denoting the new comment status. */
$message = sprintf( esc_html( __( 'Comment status was changed to %s', 'akismet' ) ), '<code>' . esc_html( $new_status ) . '</code>' );
} elseif ( preg_match( '/^status-/', $row['event'] ) ) {
$new_status = preg_replace( '/^status-/', '', $row['event'] );
if ( isset( $row['user'] ) ) {
/* translators: %1$s is a username; %2$s is a short string (like 'spam' or 'approved') denoting the new comment status. */
$message = sprintf( esc_html( __( '%1$s changed the comment status to %2$s.', 'akismet' ) ), $row['user'], '<code>' . esc_html( $new_status ) . '</code>' );
}
}
break;
}
}
if ( ! empty( $message ) ) {
echo '<p>';
if ( isset( $row['time'] ) ) {
$time = gmdate( 'D d M Y @ h:i:s a', (int) $row['time'] ) . ' GMT';
/* translators: The placeholder is an amount of time, like "7 seconds" or "3 days" returned by the function human_time_diff(). */
$time_html = '<span style="color: #999;" alt="' . esc_attr( $time ) . '" title="' . esc_attr( $time ) . '">' . sprintf( esc_html__( '%s ago', 'akismet' ), human_time_diff( $row['time'] ) ) . '</span>';
printf(
/* translators: %1$s is a human-readable time difference, like "3 hours ago", and %2$s is an already-translated phrase describing how a comment's status changed, like "This comment was reported as spam." */
esc_html( __( '%1$s - %2$s', 'akismet' ) ),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$time_html,
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$message
); // esc_html() is done above so that we can use HTML in $message.
} else {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $message; // esc_html() is done above so that we can use HTML in $message.
}
echo '</p>';
}
}
} else {
echo '<p>';
echo esc_html( __( 'No comment history.', 'akismet' ) );
echo '</p>';
}
}
public static function plugin_action_links( $links, $file ) {
if ( $file == plugin_basename( plugin_dir_url( __FILE__ ) . '/akismet.php' ) ) {
$links[] = '<a href="' . esc_url( self::get_page_url() ) . '">' . esc_html__( 'Settings', 'akismet' ) . '</a>';
}
return $links;
}
// Total spam in queue
// get_option( 'akismet_spam_count' ) is the total caught ever
public static function get_spam_count( $type = false ) {
global $wpdb;
if ( ! $type ) { // total
$count = wp_cache_get( 'akismet_spam_count', 'widget' );
if ( false === $count ) {
$count = wp_count_comments();
$count = $count->spam;
wp_cache_set( 'akismet_spam_count', $count, 'widget', 3600 );
}
return $count;
} elseif ( 'comments' == $type || 'comment' == $type ) { // comments
$type = '';
}
return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(comment_ID) FROM {$wpdb->comments} WHERE comment_approved = 'spam' AND comment_type = %s", $type ) );
}
// Check connectivity between the WordPress blog and Akismet's servers.
// Returns an associative array of server IP addresses, where the key is the IP address, and value is true (available) or false (unable to connect).
public static function check_server_ip_connectivity() {
$servers = $ips = array();
// Some web hosts may disable this function
if ( function_exists( 'gethostbynamel' ) ) {
$ips = gethostbynamel( 'rest.akismet.com' );
if ( $ips && is_array( $ips ) && count( $ips ) ) {
$api_key = Akismet::get_api_key();
foreach ( $ips as $ip ) {
$response = Akismet::verify_key( $api_key, $ip );
// even if the key is invalid, at least we know we have connectivity
if ( $response == 'valid' || $response == 'invalid' ) {
$servers[ $ip ] = 'connected';
} else {
$servers[ $ip ] = $response ? $response : 'unable to connect';
}
}
}
}
return $servers;
}
// Simpler connectivity check
public static function check_server_connectivity( $cache_timeout = 86400 ) {
$debug = array();
$debug['PHP_VERSION'] = PHP_VERSION;
$debug['WORDPRESS_VERSION'] = $GLOBALS['wp_version'];
$debug['AKISMET_VERSION'] = AKISMET_VERSION;
$debug['AKISMET__PLUGIN_DIR'] = AKISMET__PLUGIN_DIR;
$debug['SITE_URL'] = site_url();
$debug['HOME_URL'] = home_url();
$servers = get_option( 'akismet_available_servers' );
if ( ( time() - get_option( 'akismet_connectivity_time' ) < $cache_timeout ) && $servers !== false ) {
$servers = self::check_server_ip_connectivity();
update_option( 'akismet_available_servers', $servers );
update_option( 'akismet_connectivity_time', time() );
}
if ( wp_http_supports( array( 'ssl' ) ) ) {
$response = wp_remote_get( 'https://rest.akismet.com/1.1/test' );
} else {
$response = wp_remote_get( 'http://rest.akismet.com/1.1/test' );
}
$debug['gethostbynamel'] = function_exists( 'gethostbynamel' ) ? 'exists' : 'not here';
$debug['Servers'] = $servers;
$debug['Test Connection'] = $response;
Akismet::log( $debug );
if ( $response && 'connected' == wp_remote_retrieve_body( $response ) ) {
return true;
}
return false;
}
// Check the server connectivity and store the available servers in an option.
public static function get_server_connectivity( $cache_timeout = 86400 ) {
return self::check_server_connectivity( $cache_timeout );
}
/**
* Find out whether any comments in the Pending queue have not yet been checked by Akismet.
*
* @return bool
*/
public static function are_any_comments_waiting_to_be_checked() {
return ! ! get_comments(
array(
// Exclude comments that are not pending. This would happen if someone manually approved or spammed a comment
// that was waiting to be checked. The akismet_error meta entry will eventually be removed by the cron recheck job.
'status' => 'hold',
// This is the commentmeta that is saved when a comment couldn't be checked.
'meta_key' => 'akismet_error',
// We only need to know whether at least one comment is waiting for a check.
'number' => 1,
)
);
}
public static function get_page_url( $page = 'config' ) {
$args = array( 'page' => 'akismet-key-config' );
if ( $page == 'stats' ) {
$args = array(
'page' => 'akismet-key-config',
'view' => 'stats',
);
} elseif ( $page == 'delete_key' ) {
$args = array(
'page' => 'akismet-key-config',
'view' => 'start',
'action' => 'delete-key',
'_wpnonce' => wp_create_nonce( self::NONCE ),
);
} elseif ( $page === 'init' ) {
$args = array(
'page' => 'akismet-key-config',
'view' => 'start',
);
}
return add_query_arg( $args, menu_page_url( 'akismet-key-config', false ) );
}
public static function get_akismet_user( $api_key ) {
$akismet_user = false;
$request_args = array(
'key' => $api_key,
'blog' => get_option( 'home' ),
);
$request_args = apply_filters( 'akismet_request_args', $request_args, 'get-subscription' );
$subscription_verification = Akismet::http_post( Akismet::build_query( $request_args ), 'get-subscription' );
if ( ! empty( $subscription_verification[1] ) ) {
if ( 'invalid' !== $subscription_verification[1] ) {
$akismet_user = json_decode( $subscription_verification[1] );
}
}
return $akismet_user;
}
public static function get_stats( $api_key ) {
$stat_totals = array();
foreach ( array( '6-months', 'all' ) as $interval ) {
$request_args = array(
'blog' => get_option( 'home' ),
'key' => $api_key,
'from' => $interval,
);
$request_args = apply_filters( 'akismet_request_args', $request_args, 'get-stats' );
$response = Akismet::http_post( Akismet::build_query( $request_args ), 'get-stats' );
if ( ! empty( $response[1] ) ) {
$data = json_decode( $response[1] );
/*
* The json decoded response should be an object. If it's not an object, something's wrong, and the data
* shouldn't be added to the stats_totals array.
*/
if ( is_object( $data ) ) {
$stat_totals[ $interval ] = $data;
}
}
}
return $stat_totals;
}
public static function verify_wpcom_key( $api_key, $user_id, $extra = array() ) {
$request_args = array_merge(
array(
'user_id' => $user_id,
'api_key' => $api_key,
'get_account_type' => 'true',
),
$extra
);
$request_args = apply_filters( 'akismet_request_args', $request_args, 'verify-wpcom-key' );
$akismet_account = Akismet::http_post( Akismet::build_query( $request_args ), 'verify-wpcom-key' );
if ( ! empty( $akismet_account[1] ) ) {
$akismet_account = json_decode( $akismet_account[1] );
}
Akismet::log( compact( 'akismet_account' ) );
return $akismet_account;
}
public static function connect_jetpack_user() {
if ( $jetpack_user = self::get_jetpack_user() ) {
if ( isset( $jetpack_user['user_id'] ) && isset( $jetpack_user['api_key'] ) ) {
$akismet_user = self::verify_wpcom_key( $jetpack_user['api_key'], $jetpack_user['user_id'], array( 'action' => 'connect_jetpack_user' ) );
if ( is_object( $akismet_user ) ) {
self::save_key( $akismet_user->api_key );
return in_array( $akismet_user->status, array( 'active', 'active-dunning', 'no-sub' ) );
}
}
}
return false;
}
public static function display_alert() {
Akismet::view(
'notice',
array(
'type' => 'alert',
'code' => (int) get_option( 'akismet_alert_code' ),
'msg' => get_option( 'akismet_alert_msg' ),
)
);
}
public static function get_usage_limit_alert_data() {
return array(
'type' => 'usage-limit',
'code' => (int) get_option( 'akismet_alert_code' ),
'msg' => get_option( 'akismet_alert_msg' ),
'api_calls' => get_option( 'akismet_alert_api_calls' ),
'usage_limit' => get_option( 'akismet_alert_usage_limit' ),
'upgrade_plan' => get_option( 'akismet_alert_upgrade_plan' ),
'upgrade_url' => get_option( 'akismet_alert_upgrade_url' ),
'upgrade_type' => get_option( 'akismet_alert_upgrade_type' ),
'upgrade_via_support' => get_option( 'akismet_alert_upgrade_via_support' ) === 'true',
);
}
public static function display_usage_limit_alert() {
Akismet::view( 'notice', self::get_usage_limit_alert_data() );
}
public static function display_spam_check_warning() {
Akismet::fix_scheduled_recheck();
if ( wp_next_scheduled( 'akismet_schedule_cron_recheck' ) > time() && self::are_any_comments_waiting_to_be_checked() ) {
/*
* The 'akismet_display_cron_disabled_notice' filter can be used to control whether the WP-Cron disabled notice is displayed.
*/
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON && apply_filters( 'akismet_display_cron_disabled_notice', true ) ) {
Akismet::view( 'notice', array( 'type' => 'spam-check-cron-disabled' ) );
} else {
/* translators: The Akismet configuration page URL. */
$link_text = apply_filters( 'akismet_spam_check_warning_link_text', sprintf( __( 'Please check your <a href="%s">Akismet configuration</a> and contact your web host if problems persist.', 'akismet' ), esc_url( self::get_page_url() ) ) );
Akismet::view(
'notice',
array(
'type' => 'spam-check',
'link_text' => $link_text,
)
);
}
}
}
public static function display_api_key_warning() {
Akismet::view( 'notice', array( 'type' => 'plugin' ) );
}
public static function display_page() {
if ( ! Akismet::get_api_key() || ( isset( $_GET['view'] ) && $_GET['view'] == 'start' ) ) {
self::display_start_page();
} elseif ( isset( $_GET['view'] ) && $_GET['view'] == 'stats' ) {
self::display_stats_page();
} else {
self::display_configuration_page();
}
}
public static function display_start_page() {
if ( isset( $_GET['action'] ) ) {
if ( $_GET['action'] == 'delete-key' ) {
if ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], self::NONCE ) ) {
delete_option( 'wordpress_api_key' );
}
}
}
if ( $api_key = Akismet::get_api_key() && ( empty( self::$notices['status'] ) || 'existing-key-invalid' != self::$notices['status'] ) ) {
self::display_configuration_page();
return;
}
// the user can choose to auto connect their API key by clicking a button on the akismet done page
// if jetpack, get verified api key by using connected wpcom user id
// if no jetpack, get verified api key by using an akismet token
$akismet_user = false;
if ( isset( $_GET['token'] ) && preg_match( '/^(\d+)-[0-9a-f]{20}$/', $_GET['token'] ) ) {
$akismet_user = self::verify_wpcom_key( '', '', array( 'token' => $_GET['token'] ) );
} elseif ( $jetpack_user = self::get_jetpack_user() ) {
$akismet_user = self::verify_wpcom_key( $jetpack_user['api_key'], $jetpack_user['user_id'] );
}
if ( isset( $_GET['action'] ) ) {
if ( $_GET['action'] == 'save-key' ) {
if ( is_object( $akismet_user ) ) {
self::save_key( $akismet_user->api_key );
self::display_configuration_page();
return;
}
}
}
Akismet::view( 'start', compact( 'akismet_user' ) );
/*
// To see all variants when testing.
$akismet_user->status = 'no-sub';
Akismet::view( 'start', compact( 'akismet_user' ) );
$akismet_user->status = 'cancelled';
Akismet::view( 'start', compact( 'akismet_user' ) );
$akismet_user->status = 'suspended';
Akismet::view( 'start', compact( 'akismet_user' ) );
$akismet_user->status = 'other';
Akismet::view( 'start', compact( 'akismet_user' ) );
$akismet_user = false;
*/
}
public static function display_stats_page() {
Akismet::view( 'stats' );
}
public static function display_configuration_page() {
$api_key = Akismet::get_api_key();
$akismet_user = self::get_akismet_user( $api_key );
if ( ! $akismet_user ) {
// This could happen if the user's key became invalid after it was previously valid and successfully set up.
self::$notices['status'] = 'existing-key-invalid';
self::display_start_page();
return;
}
$stat_totals = self::get_stats( $api_key );
// If unset, create the new strictness option using the old discard option to determine its default.
// If the old option wasn't set, default to discarding the blatant spam.
if ( get_option( 'akismet_strictness' ) === false ) {
add_option( 'akismet_strictness', ( get_option( 'akismet_discard_month' ) === 'false' ? '0' : '1' ) );
}
// Sync the local "Total spam blocked" count with the authoritative count from the server.
if ( isset( $stat_totals['all'], $stat_totals['all']->spam ) ) {
update_option( 'akismet_spam_count', $stat_totals['all']->spam );
}
$notices = array();
if ( empty( self::$notices ) ) {
if ( ! empty( $stat_totals['all'] ) && isset( $stat_totals['all']->time_saved ) && $akismet_user->status == 'active' && $akismet_user->account_type == 'free-api-key' ) {
$time_saved = false;
if ( $stat_totals['all']->time_saved > 1800 ) {
$total_in_minutes = round( $stat_totals['all']->time_saved / 60 );
$total_in_hours = round( $total_in_minutes / 60 );
$total_in_days = round( $total_in_hours / 8 );
$cleaning_up = __( 'Cleaning up spam takes time.', 'akismet' );
if ( $total_in_days > 1 ) {
/* translators: %s: Number of days. */
$time_saved = $cleaning_up . ' ' . sprintf( _n( 'Akismet has saved you %s day!', 'Akismet has saved you %s days!', $total_in_days, 'akismet' ), number_format_i18n( $total_in_days ) );
} elseif ( $total_in_hours > 1 ) {
/* translators: %s: Number of hours. */
$time_saved = $cleaning_up . ' ' . sprintf( _n( 'Akismet has saved you %d hour!', 'Akismet has saved you %d hours!', $total_in_hours, 'akismet' ), $total_in_hours );
} elseif ( $total_in_minutes >= 30 ) {
/* translators: %s: Number of minutes. */
$time_saved = $cleaning_up . ' ' . sprintf( _n( 'Akismet has saved you %d minute!', 'Akismet has saved you %d minutes!', $total_in_minutes, 'akismet' ), $total_in_minutes );
}
}
$notices[] = array(
'type' => 'active-notice',
'time_saved' => $time_saved,
);
}
}
if ( ! Akismet::predefined_api_key() && ! isset( self::$notices['status'] ) && in_array( $akismet_user->status, array( 'cancelled', 'suspended', 'missing', 'no-sub' ) ) ) {
$notices[] = array( 'type' => $akismet_user->status );
}
$alert_code = get_option( 'akismet_alert_code' );
if ( isset( Akismet::$limit_notices[ $alert_code ] ) ) {
$notices[] = self::get_usage_limit_alert_data();
} elseif ( $alert_code > 0 ) {
$notices[] = array(
'type' => 'alert',
'code' => (int) get_option( 'akismet_alert_code' ),
'msg' => get_option( 'akismet_alert_msg' ),
);
}
/*
* To see all variants when testing.
*
* You may also want to comment out the akismet_view_arguments filter in Akismet::view()
* to ensure that you can see all of the notices (e.g. suspended, active-notice).
*/
// $notices[] = array( 'type' => 'active-notice', 'time_saved' => 'Cleaning up spam takes time. Akismet has saved you 1 minute!' );
// $notices[] = array( 'type' => 'plugin' );
// $notices[] = array( 'type' => 'notice', 'notice_header' => 'This is the notice header.', 'notice_text' => 'This is the notice text.' );
// $notices[] = array( 'type' => 'missing-functions' );
// $notices[] = array( 'type' => 'servers-be-down' );
// $notices[] = array( 'type' => 'active-dunning' );
// $notices[] = array( 'type' => 'cancelled' );
// $notices[] = array( 'type' => 'suspended' );
// $notices[] = array( 'type' => 'missing' );
// $notices[] = array( 'type' => 'no-sub' );
// $notices[] = array( 'type' => 'new-key-valid' );
// $notices[] = array( 'type' => 'new-key-invalid' );
// $notices[] = array( 'type' => 'existing-key-invalid' );
// $notices[] = array( 'type' => 'new-key-failed' );
// $notices[] = array( 'type' => 'usage-limit', 'api_calls' => '15000', 'usage_limit' => '10000', 'upgrade_plan' => 'Enterprise', 'upgrade_url' => 'https://akismet.com/account/', 'code' => 10502 );
// $notices[] = array( 'type' => 'spam-check', 'link_text' => 'Link text.' );
// $notices[] = array( 'type' => 'spam-check-cron-disabled' );
// $notices[] = array( 'type' => 'alert', 'code' => 123 );
// $notices[] = array( 'type' => 'alert', 'code' => Akismet::ALERT_CODE_COMMERCIAL );
Akismet::log( compact( 'stat_totals', 'akismet_user' ) );
Akismet::view( 'config', compact( 'api_key', 'akismet_user', 'stat_totals', 'notices' ) );
}
public static function display_notice() {
global $hook_suffix;
if ( in_array( $hook_suffix, array( 'jetpack_page_akismet-key-config', 'settings_page_akismet-key-config' ) ) ) {
// This page manages the notices and puts them inline where they make sense.
return;
}
// To see notice variants while testing.
// Akismet::view( 'notice', array( 'type' => 'spam-check-cron-disabled' ) );
// Akismet::view( 'notice', array( 'type' => 'spam-check' ) );
// Akismet::view( 'notice', array( 'type' => 'alert', 'code' => 123, 'msg' => 'Message' ) );
if ( in_array( $hook_suffix, array( 'edit-comments.php' ) ) && (int) get_option( 'akismet_alert_code' ) > 0 ) {
Akismet::verify_key( Akismet::get_api_key() ); // verify that the key is still in alert state
$alert_code = get_option( 'akismet_alert_code' );
if ( isset( Akismet::$limit_notices[ $alert_code ] ) ) {
self::display_usage_limit_alert();
} elseif ( $alert_code > 0 ) {
self::display_alert();
}
} elseif ( in_array( $hook_suffix, self::$activation_banner_pages, true ) && ! Akismet::get_api_key() ) {
// Show the "Set Up Akismet" banner on the comments and plugin pages if no API key has been set.
self::display_api_key_warning();
} elseif ( $hook_suffix == 'edit-comments.php' && wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) {
self::display_spam_check_warning();
}
if ( isset( $_GET['akismet_recheck_complete'] ) ) {
$recheck_count = (int) $_GET['recheck_count'];
$spam_count = (int) $_GET['spam_count'];
if ( $recheck_count === 0 ) {
$message = __( 'There were no comments to check. Akismet will only check comments awaiting moderation.', 'akismet' );
} else {
/* translators: %s: Number of comments. */
$message = sprintf( _n( 'Akismet checked %s comment.', 'Akismet checked %s comments.', $recheck_count, 'akismet' ), number_format( $recheck_count ) );
$message .= ' ';
if ( $spam_count === 0 ) {
$message .= __( 'No comments were caught as spam.', 'akismet' );
} else {
/* translators: %s: Number of comments. */
$message .= sprintf( _n( '%s comment was caught as spam.', '%s comments were caught as spam.', $spam_count, 'akismet' ), number_format( $spam_count ) );
}
}
echo '<div class="notice notice-success"><p>' . esc_html( $message ) . '</p></div>';
} elseif ( isset( $_GET['akismet_recheck_error'] ) ) {
echo '<div class="notice notice-error"><p>' . esc_html( __( 'Akismet could not recheck your comments for spam.', 'akismet' ) ) . '</p></div>';
}
}
public static function display_status() {
if ( ! self::get_server_connectivity() ) {
Akismet::view( 'notice', array( 'type' => 'servers-be-down' ) );
} elseif ( ! empty( self::$notices ) ) {
foreach ( self::$notices as $index => $type ) {
if ( is_object( $type ) ) {
$notice_header = $notice_text = '';
if ( property_exists( $type, 'notice_header' ) ) {
$notice_header = wp_kses( $type->notice_header, self::$allowed );
}
if ( property_exists( $type, 'notice_text' ) ) {
$notice_text = wp_kses( $type->notice_text, self::$allowed );
}
if ( property_exists( $type, 'status' ) ) {
$type = wp_kses( $type->status, self::$allowed );
Akismet::view( 'notice', compact( 'type', 'notice_header', 'notice_text' ) );
unset( self::$notices[ $index ] );
}
} else {
Akismet::view( 'notice', compact( 'type' ) );
unset( self::$notices[ $index ] );
}
}
}
}
private static function get_jetpack_user() {
if ( ! class_exists( 'Jetpack' ) ) {
return false;
}
if ( defined( 'JETPACK__VERSION' ) && version_compare( JETPACK__VERSION, '7.7', '<' ) ) {
// For version of Jetpack prior to 7.7.
Jetpack::load_xml_rpc_client();
}
$xml = new Jetpack_IXR_ClientMulticall( array( 'user_id' => get_current_user_id() ) );
$xml->addCall( 'wpcom.getUserID' );
$xml->addCall( 'akismet.getAPIKey' );
$xml->query();
Akismet::log( compact( 'xml' ) );
if ( ! $xml->isError() ) {
$responses = $xml->getResponse();
if ( ( is_countable( $responses ) ? count( $responses ) : 0 ) > 1 ) {
// Due to a quirk in how Jetpack does multi-calls, the response order
// can't be trusted to match the call order. It's a good thing our
// return values can be mostly differentiated from each other.
$first_response_value = array_shift( $responses[0] );
$second_response_value = array_shift( $responses[1] );
// If WPCOM ever reaches 100 billion users, this will fail. :-)
if ( preg_match( '/^[a-f0-9]{12}$/i', $first_response_value ) ) {
$api_key = $first_response_value;
$user_id = (int) $second_response_value;
} else {
$api_key = $second_response_value;
$user_id = (int) $first_response_value;
}
return compact( 'api_key', 'user_id' );
}
}
return false;
}
/**
* Some commentmeta isn't useful in an export file. Suppress it (when supported).
*
* @param bool $exclude
* @param string $key The meta key
* @param object $meta The meta object
* @return bool Whether to exclude this meta entry from the export.
*/
public static function exclude_commentmeta_from_export( $exclude, $key, $meta ) {
if ( in_array( $key, array( 'akismet_as_submitted', 'akismet_rechecking', 'akismet_delayed_moderation_email' ) ) ) {
return true;
}
return $exclude;
}
/**
* When Akismet is active, remove the "Activate Akismet" step from the plugin description.
*/
public static function modify_plugin_description( $all_plugins ) {
if ( isset( $all_plugins['akismet/akismet.php'] ) ) {
if ( Akismet::get_api_key() ) {
$all_plugins['akismet/akismet.php']['Description'] = __( 'Used by millions, Akismet is quite possibly the best way in the world to <strong>protect your blog from spam</strong>. Your site is fully configured and being protected, even while you sleep.', 'akismet' );
} else {
$all_plugins['akismet/akismet.php']['Description'] = __( 'Used by millions, Akismet is quite possibly the best way in the world to <strong>protect your blog from spam</strong>. It keeps your site protected even while you sleep. To get started, just go to <a href="admin.php?page=akismet-key-config">your Akismet Settings page</a> to set up your API key.', 'akismet' );
}
}
return $all_plugins;
}
private static function set_form_privacy_notice_option( $state ) {
if ( in_array( $state, array( 'display', 'hide' ) ) ) {
update_option( 'akismet_comment_form_privacy_notice', $state );
}
}
public static function register_personal_data_eraser( $erasers ) {
$erasers['akismet'] = array(
'eraser_friendly_name' => __( 'Akismet', 'akismet' ),
'callback' => array( 'Akismet_Admin', 'erase_personal_data' ),
);
return $erasers;
}
/**
* When a user requests that their personal data be removed, Akismet has a duty to discard
* any personal data we store outside of the comment itself. Right now, that is limited
* to the copy of the comment we store in the akismet_as_submitted commentmeta.
*
* FWIW, this information would be automatically deleted after 15 days.
*
* @param $email_address string The email address of the user who has requested erasure.
* @param $page int This function can (and will) be called multiple times to prevent timeouts,
* so this argument is used for pagination.
* @return array
* @see https://developer.wordpress.org/plugins/privacy/adding-the-personal-data-eraser-to-your-plugin/
*/
public static function erase_personal_data( $email_address, $page = 1 ) {
$items_removed = false;
$number = 50;
$page = (int) $page;
$comments = get_comments(
array(
'author_email' => $email_address,
'number' => $number,
'paged' => $page,
'order_by' => 'comment_ID',
'order' => 'ASC',
)
);
foreach ( (array) $comments as $comment ) {
$comment_as_submitted = get_comment_meta( $comment->comment_ID, 'akismet_as_submitted', true );
if ( $comment_as_submitted ) {
delete_comment_meta( $comment->comment_ID, 'akismet_as_submitted' );
$items_removed = true;
}
}
// Tell core if we have more comments to work on still
$done = ( is_countable( $comments ) ? count( $comments ) : 0 ) < $number;
return array(
'items_removed' => $items_removed,
'items_retained' => false, // always false in this example
'messages' => array(), // no messages in this example
'done' => $done,
);
}
/**
* Return an array of HTML elements that are allowed in a notice.
*
* @return array
*/
public static function get_notice_kses_allowed_elements() {
return self::$allowed;
}
/**
* Return a version to append to the URL of an asset file (e.g. CSS and images).
*
* @param string $relative_path Relative path to asset file
* @return string
*/
public static function get_asset_file_version( $relative_path ) {
$full_path = AKISMET__PLUGIN_DIR . $relative_path;
// If the AKISMET_VERSION contains a lower-case letter, it's a development version (e.g. 5.3.1a2).
// Use the file modified time in development.
if ( preg_match( '/[a-z]/', AKISMET_VERSION ) && file_exists( $full_path ) ) {
return filemtime( $full_path );
}
// Otherwise, use the AKISMET_VERSION.
return AKISMET_VERSION;
}
}
akismet/class.akismet-widget.php 0000644 00000010436 15043671570 0012750 0 ustar 00 <?php
/**
* @package Akismet
*/
// We plan to gradually remove all of the disabled lint rules below.
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
class Akismet_Widget extends WP_Widget {
function __construct() {
load_plugin_textdomain( 'akismet' );
add_action( 'wp_enqueue_scripts', array( $this, 'akismet_widget_enqueue_styles' ) );
parent::__construct(
'akismet_widget',
__( 'Akismet Widget', 'akismet' ),
array( 'description' => __( 'Display the number of spam comments Akismet has caught', 'akismet' ) )
);
}
public function akismet_widget_enqueue_styles() {
// Register the stylesheet handle
wp_register_style( 'akismet-widget-style', false ); // No external file, just a handle
// Enqueue the registered stylesheet
wp_enqueue_style( 'akismet-widget-style' );
// Add inline styles
$inline_css = "
.a-stats {
--akismet-color-mid-green: #357b49;
--akismet-color-white: #fff;
--akismet-color-light-grey: #f6f7f7;
max-width: 350px;
width: auto;
}
.a-stats * {
all: unset;
box-sizing: border-box;
}
.a-stats strong {
font-weight: 600;
}
.a-stats a.a-stats__link,
.a-stats a.a-stats__link:visited,
.a-stats a.a-stats__link:active {
background: var(--akismet-color-mid-green);
border: none;
box-shadow: none;
border-radius: 8px;
color: var(--akismet-color-white);
cursor: pointer;
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen-Sans', 'Ubuntu', 'Cantarell', 'Helvetica Neue', sans-serif;
font-weight: 500;
padding: 12px;
text-align: center;
text-decoration: none;
transition: all 0.2s ease;
}
/* Extra specificity to deal with TwentyTwentyOne focus style */
.widget .a-stats a.a-stats__link:focus {
background: var(--akismet-color-mid-green);
color: var(--akismet-color-white);
text-decoration: none;
}
.a-stats a.a-stats__link:hover {
filter: brightness(110%);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06), 0 0 2px rgba(0, 0, 0, 0.16);
}
.a-stats .count {
color: var(--akismet-color-white);
display: block;
font-size: 1.5em;
line-height: 1.4;
padding: 0 13px;
white-space: nowrap;
}
";
wp_add_inline_style( 'akismet-widget-style', $inline_css );
}
function form( $instance ) {
if ( $instance && isset( $instance['title'] ) ) {
$title = $instance['title'];
} else {
$title = __( 'Spam Blocked', 'akismet' );
}
?>
<p>
<label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php esc_html_e( 'Title:', 'akismet' ); ?></label>
<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
</p>
<?php
}
function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = strip_tags( $new_instance['title'] );
return $instance;
}
function widget( $args, $instance ) {
$count = get_option( 'akismet_spam_count' );
if ( ! isset( $instance['title'] ) ) {
$instance['title'] = __( 'Spam Blocked', 'akismet' );
}
echo $args['before_widget'];
if ( ! empty( $instance['title'] ) ) {
echo $args['before_title'];
echo esc_html( $instance['title'] );
echo $args['after_title'];
}
?>
<div class="a-stats">
<?php // Specifying colors inline for maximum specificity without using !important ?>
<a href="https://akismet.com" class="a-stats__link" target="_blank" rel="noopener" style="background-color: var(--akismet-color-mid-green); color: var(--akismet-color-white);">
<?php
echo wp_kses(
sprintf(
/* translators: The placeholder is the number of pieces of spam blocked by Akismet. */
_n(
'<strong class="count">%1$s spam</strong> blocked by <strong>Akismet</strong>',
'<strong class="count">%1$s spam</strong> blocked by <strong>Akismet</strong>',
$count,
'akismet'
),
number_format_i18n( $count )
),
array(
'strong' => array(
'class' => true,
),
)
);
?>
</a>
</div>
<?php
echo $args['after_widget'];
}
}
function akismet_register_widgets() {
register_widget( 'Akismet_Widget' );
}
add_action( 'widgets_init', 'akismet_register_widgets' );
akismet/index.php 0000644 00000000032 15043671570 0010024 0 ustar 00 <?php
# Silence is golden. akismet/views/activate.php 0000644 00000000263 15043671570 0011660 0 ustar 00 <div class="akismet-box">
<?php Akismet::view( 'title' ); ?>
<?php Akismet::view( 'setup' ); ?>
</div>
<br/>
<div class="akismet-box">
<?php Akismet::view( 'enter' ); ?>
</div> |