Plugin Directory

Changeset 3323859


Ignore:
Timestamp:
07/07/2025 07:02:26 PM (9 months ago)
Author:
cfinke
Message:

Enable webhooks by default.

This change adds a request for a callback to each comment-check request. What does this mean? If the comment is found to not be spam right now but it meets certain conditions (which are ever-changing), it will be rechecked by the Akismet servers shortly. If after that recheck, the comment is found to be spam, the Akismet servers will ping the callback URL to let the site know that the comment's status has changed.

All of the functionality for this was already present in Akismet, but enabling it meant that we needed to improve how moderation emails are handled for comments that might change status shortly, and that's what this commit does.

Location:
akismet/trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • akismet/trunk/akismet.php

    r3309445 r3323859  
    77Plugin URI: https://akismet.com/
    88Description: Used by millions, Akismet is quite possibly the best way in the world to <strong>protect your blog from spam</strong>. Akismet Anti-spam keeps your site protected even while you sleep. To get started: activate the Akismet plugin and then go to your Akismet Settings page to set up your API key.
    9 Version: 5.5
     9Version: 5.5a1
    1010Requires at least: 5.8
    1111Requires PHP: 7.2
     
    4040}
    4141
    42 define( 'AKISMET_VERSION', '5.5' );
     42define( 'AKISMET_VERSION', '5.5a1' );
    4343define( 'AKISMET__MINIMUM_WP_VERSION', '5.8' );
    4444define( 'AKISMET__PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • akismet/trunk/class.akismet-admin.php

    r3277808 r3323859  
    697697                            $message = esc_html( __( 'Akismet cleared this comment.', 'akismet' ) );
    698698                            break;
     699                        case 'check-ham-pending':
     700                            $message = esc_html( __( 'Akismet provisionally cleared this comment.', 'akismet' ) );
     701                            break;
    699702                        case 'wp-blacklisted':
    700703                        case 'wp-disallowed':
     
    14311434     */
    14321435    public static function exclude_commentmeta_from_export( $exclude, $key, $meta ) {
    1433         if ( in_array( $key, array( 'akismet_as_submitted', 'akismet_rechecking', 'akismet_delayed_moderation_email' ) ) ) {
     1436        if (
     1437            in_array(
     1438                $key,
     1439                array(
     1440                    'akismet_as_submitted',
     1441                    'akismet_delay_moderation_email',
     1442                    'akismet_delayed_moderation_email',
     1443                    'akismet_rechecking',
     1444                    'akismet_schedule_approval_fallback',
     1445                    'akismet_schedule_email_fallback',
     1446                    'akismet_skipped_microtime',
     1447                )
     1448            )
     1449        ) {
    14341450            return true;
    14351451        }
  • akismet/trunk/class.akismet-rest-api.php

    r3182785 r3323859  
    515515                        Akismet::log( 'Found matching comment.', $comments );
    516516
    517                         $current_status = wp_get_comment_status( $comments[0] );
     517                        $comment = $comments[0];
     518
     519                        $current_status = wp_get_comment_status( $comment );
    518520
    519521                        $result = $webhook_comment['result'];
     
    525527                            if ( 'spam' != $current_status ) {
    526528                                // The comment is not classified as spam. If Akismet was the one to act on it, move it to spam.
    527                                 if ( Akismet::last_comment_status_change_came_from_akismet( $comments[0]->comment_ID ) ) {
     529                                if ( Akismet::last_comment_status_change_came_from_akismet( $comment->comment_ID ) ) {
    528530                                    Akismet::log( 'Comment is not spam; marking as spam.' );
    529531
    530                                     wp_spam_comment( $comments[0] );
    531                                     Akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-spam' );
     532                                    wp_spam_comment( $comment );
     533                                    Akismet::update_comment_history( $comment->comment_ID, '', 'webhook-spam' );
    532534                                } else {
    533535                                    Akismet::log( 'Comment is not spam, but it has already been manually handled by some other process.' );
    534                                     Akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-spam-noaction' );
     536                                    Akismet::update_comment_history( $comment->comment_ID, '', 'webhook-spam-noaction' );
    535537                                }
    536538                            }
     
    543545
    544546                                // The comment is classified as spam. If Akismet was the one to label it as spam, unspam it.
    545                                 if ( Akismet::last_comment_status_change_came_from_akismet( $comments[0]->comment_ID ) ) {
     547                                if ( Akismet::last_comment_status_change_came_from_akismet( $comment->comment_ID ) ) {
    546548                                    Akismet::log( 'Akismet marked it as spam; unspamming.' );
    547549
    548                                     wp_unspam_comment( $comments[0] );
    549                                     akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-ham' );
     550                                    wp_unspam_comment( $comment );
     551
     552                                    akismet::update_comment_history( $comment->comment_ID, '', 'webhook-ham' );
    550553                                } else {
    551554                                    Akismet::log( 'Comment is not spam, but it has already been manually handled by some other process.' );
    552                                     Akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-ham-noaction' );
     555                                    Akismet::update_comment_history( $comment->comment_ID, '', 'webhook-ham-noaction' );
     556                                }
     557                            } else if ( 'unapproved' == $current_status ) {
     558                                Akismet::log( 'Comment is pending.' );
     559
     560                                // The comment is in Pending. If Akismet was the one to put it there, approve it (but only if the site
     561                                // settings dictate that).
     562                                if ( Akismet::last_comment_status_change_came_from_akismet( $comment->comment_ID ) ) {
     563                                    Akismet::log( 'Akismet marked it as Pending; approving.' );
     564
     565                                    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 ) ) {
     566                                        wp_set_comment_status( $comment->comment_ID, 1 );
     567                                    }
     568
     569                                    akismet::update_comment_history( $comment->comment_ID, '', 'webhook-ham' );
     570                                } else {
     571                                    Akismet::log( 'Comment is not spam, but it has already been manually handled by some other process.' );
     572                                    Akismet::update_comment_history( $comment->comment_ID, '', 'webhook-ham-noaction' );
    553573                                }
    554574                            }
     575
     576                            $moderation_email_was_delayed = get_comment_meta( $comment->comment_ID, 'akismet_delayed_moderation_email', true );
     577
     578                            if ( $moderation_email_was_delayed ) {
     579                                Akismet::log( 'Moderation email was delayed for comment #' . $comment->comment_ID . '; sending now.' );
     580
     581                                delete_comment_meta( $comment->comment_ID, 'akismet_delayed_moderation_email' );
     582                                wp_new_comment_notify_moderator( $comment->comment_ID );
     583                                wp_new_comment_notify_postauthor( $comment->comment_ID );
     584                            }
     585
     586                            delete_comment_meta( $comment->comment_ID, 'akismet_delay_moderation_email' );
    555587                        }
    556588
  • akismet/trunk/class.akismet.php

    r3277808 r3323859  
    2323    private static $last_comment                                = '';
    2424    private static $initiated                                   = false;
    25     private static $prevent_moderation_email_for_these_comments = array();
    2625    private static $last_comment_result                         = null;
    2726    private static $comment_as_submitted_allowed_keys           = array(
     
    6564
    6665        add_action( 'wp_insert_comment', array( 'Akismet', 'auto_check_update_meta' ), 10, 2 );
     66        add_action( 'wp_insert_comment', array( 'Akismet', 'schedule_email_fallback' ), 10, 2 );
     67        add_action( 'wp_insert_comment', array( 'Akismet', 'schedule_approval_fallback' ), 10, 2 );
     68
    6769        add_filter( 'preprocess_comment', array( 'Akismet', 'auto_check_comment' ), 1 );
    6870        add_filter( 'rest_pre_insert_comment', array( 'Akismet', 'rest_auto_check_comment' ), 1 );
     
    7678        add_action( 'akismet_schedule_cron_recheck', array( 'Akismet', 'cron_recheck' ) );
    7779
     80        add_action( 'akismet_email_fallback', array( 'Akismet', 'email_fallback' ), 10, 3 );
     81        add_action( 'akismet_approval_fallback', array( 'Akismet', 'approval_fallback' ), 10, 3 );
     82
    7883        add_action( 'comment_form', array( 'Akismet', 'add_comment_nonce' ), 1 );
    7984        add_action( 'comment_form', array( 'Akismet', 'output_custom_form_fields' ) );
    8085        add_filter( 'script_loader_tag', array( 'Akismet', 'set_form_js_async' ), 10, 3 );
    8186
    82         add_filter( 'comment_moderation_recipients', array( 'Akismet', 'disable_moderation_emails_if_unreachable' ), 1000, 2 );
     87        add_filter( 'notify_moderator', array( 'Akismet', 'disable_emails_if_unreachable' ), 1000, 2 );
     88        add_filter( 'notify_post_author', array( 'Akismet', 'disable_emails_if_unreachable' ), 1000, 2 );
     89
    8390        add_filter( 'pre_comment_approved', array( 'Akismet', 'last_comment_status' ), 10, 2 );
    8491
     
    365372        }
    366373
     374        // Set the webhook callback URL. The Akismet servers may make a request to this URL
     375        // if a comment's spam status changes.
     376        $comment['callback'] = get_rest_url( null, 'akismet/v1/webhook' );
     377
    367378        /**
    368379         * Filter the data that is used to generate the request body for the API call.
     
    404415            $commentdata['akismet_guid'] = $response[0]['x-akismet-guid'];
    405416            $commentdata['comment_meta']['akismet_guid'] = $response[0]['x-akismet-guid'];
     417
     418            if ( 'false' === $response[1] ) {
     419                // If Akismet has indicated that there is more processing to be done before this comment
     420                // can be fully classified, delay moderation emails until that processing is complete.
     421                if ( isset( $response[0]['X-akismet-recheck-after'] ) ) {
     422                    // Prevent this comment from reaching Active status (keep in Pending) until
     423                    // it's finished being checked.
     424                    $commentdata['comment_approved'] = '0';
     425                    self::$last_comment_result = '0';
     426
     427                    // Indicate that we should schedule a fallback so that if the site never receives a
     428                    // followup from Akismet, the emails will still be sent. We don't schedule it here
     429                    // because we don't yet have the comment ID. Add an extra minute to ensure that the
     430                    // fallback email isn't sent while the recheck or webhook call is happening.
     431                    $delay = $response[0]['X-akismet-recheck-after'] * 2;
     432
     433                    $commentdata['comment_meta']['akismet_schedule_approval_fallback'] = $delay;
     434
     435                    // If this commentmeta is present, we'll prevent the moderation email from sending once.
     436                    $commentdata['comment_meta']['akismet_delay_moderation_email'] = true;
     437
     438                    self::log( 'Delaying moderation email for comment from ' . $commentdata['comment_author'] . ' for ' . $delay . ' seconds' );
     439
     440                    $commentdata['comment_meta']['akismet_schedule_email_fallback'] = $delay;
     441                }
     442            }
    406443        }
    407444
     
    451488            }
    452489
     490            $commentdata['comment_meta']['akismet_delay_moderation_email'] = true;
     491
    453492            if ( ! wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) {
    454493                wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
    455494                do_action( 'akismet_scheduled_recheck', 'invalid-response-' . $response[1] );
    456495            }
    457 
    458             self::$prevent_moderation_email_for_these_comments[] = $commentdata;
    459496        }
    460497
     
    508545                    }
    509546                } elseif ( isset( self::$last_comment['akismet_result'] ) && self::$last_comment['akismet_result'] == 'false' ) {
    510                     self::update_comment_history( $comment->comment_ID, '', 'check-ham' );
     547                    if ( get_comment_meta( $comment->comment_ID, 'akismet_schedule_approval_fallback', true ) ) {
     548                        self::update_comment_history( $comment->comment_ID, '', 'check-ham-pending' );
     549                    } else {
     550                        self::update_comment_history( $comment->comment_ID, '', 'check-ham' );
     551                    }
     552
    511553                    // Status could be spam or trash, depending on the WP version and whether this change applies:
    512554                    // https://core.trac.wordpress.org/changeset/34726
     
    542584    }
    543585
     586    /**
     587     * After the comment has been inserted, we have access to the comment ID. Now, we can
     588     * schedule the fallback moderation/notification emails using the comment ID instead
     589     * of relying on a lookup of the GUID in the commentmeta table.
     590     *
     591     * @param int $id The comment ID.
     592     * @param object $comment The comment object.
     593     */
     594    public static function schedule_email_fallback( $id, $comment ) {
     595        self::log( 'Checking whether to schedule_email_fallback for comment #' . $id );
     596
     597        // If the moderation/notification emails for this comment were delayed
     598        $email_delay = get_comment_meta( $id, 'akismet_schedule_email_fallback', true );
     599
     600        if ( $email_delay ) {
     601            delete_comment_meta( $id, 'akismet_schedule_email_fallback' );
     602
     603            wp_schedule_single_event( time() + $email_delay, 'akismet_email_fallback', array( $id ) );
     604
     605            self::log( 'Scheduled email fallback for ' . ( time() + $email_delay ) . ' for comment #' . $id );
     606        } else {
     607            self::log( 'No need to schedule_email_fallback for comment #' . $id );
     608        }
     609    }
     610
     611    /**
     612     * Send out the notification emails if they were previously delayed while waiting
     613     * for a recheck or webhook.
     614     *
     615     * @param int $comment_ID The comment ID.
     616     */
     617    public static function email_fallback( $comment_id ) {
     618        self::log( 'In email fallback for comment #' . $comment_id );
     619
     620        if ( get_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ) ) {
     621            self::log( 'Triggering notification emails for comment #' . $comment_id . '. They will be sent if comment is not spam.' );
     622
     623            delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
     624            wp_new_comment_notify_moderator( $comment_id );
     625            wp_new_comment_notify_postauthor( $comment_id );
     626        } else {
     627            self::log( 'No need to send fallback email for comment #' . $comment_id );
     628        }
     629
     630        delete_comment_meta( $comment_id, 'akismet_delay_moderation_email' );
     631    }
     632
     633    /**
     634     * After the comment has been inserted, we have access to the comment ID. Now, we can
     635     * schedule the fallback moderation/notification emails using the comment ID instead
     636     * of relying on a lookup of the GUID in the commentmeta table.
     637     *
     638     * @param int $id The comment ID.
     639     * @param object $comment The comment object.
     640     */
     641    public static function schedule_approval_fallback( $id, $comment ) {
     642        self::log( 'Checking whether to schedule_approval_fallback for comment #' . $id );
     643
     644        // If the moderation/notification emails for this comment were delayed
     645        $approval_delay = get_comment_meta( $id, 'akismet_schedule_approval_fallback', true );
     646
     647        if ( $approval_delay ) {
     648            delete_comment_meta( $id, 'akismet_schedule_approval_fallback' );
     649
     650            wp_schedule_single_event( time() + $approval_delay, 'akismet_approval_fallback', array( $id ) );
     651
     652            self::log( 'Scheduled approval fallback for ' . ( time() + $approval_delay ) . ' for comment #' . $id );
     653        } else {
     654            self::log( 'No need to schedule_approval_fallback for comment #' . $id );
     655        }
     656    }
     657
     658    /**
     659     * If no other process has approved or spammed this comment since it was put in pending, approve it.
     660     *
     661     * @param int $comment_ID The comment ID.
     662     */
     663    public static function approval_fallback( $comment_id ) {
     664        self::log( 'In approval fallback for comment #' . $comment_id );
     665
     666        if ( wp_get_comment_status( $comment_id ) == 'unapproved' ) {
     667            if ( self::last_comment_status_change_came_from_akismet( $comment_id ) ) {
     668                $comment = get_comment( $comment_id );
     669
     670                if ( ! $comment ) {
     671                    self::log( 'Comment #' . $comment_id . ' no longer exists.' );
     672                } else 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 ) ) {
     673                    self::log( 'Approving comment #' . $comment_id );
     674
     675                    wp_set_comment_status( $comment_id, 1 );
     676                } else {
     677                    self::log( 'Not approving comment #' . $comment_id . ' because it does not pass check_comment()' );
     678                }
     679
     680                self::update_comment_history( $comment->comment_ID, '', 'check-ham' );
     681            } else {
     682                self::log( 'No need to fallback approve comment #' . $comment_id . ' because it was not last modified by Akismet.' );
     683
     684                $history = self::get_comment_history( $comment_id );
     685
     686                if ( ! empty( $history ) ) {
     687                    $most_recent_history_event = $history[0];
     688
     689                    error_log( 'Comment history: ' . print_r( $history, true ) );
     690                }
     691            }
     692        } else {
     693            self::log( 'No need to fallback approve comment #' . $comment_id . ' because it is not pending.' );
     694        }
     695    }
     696
    544697    public static function delete_old_comments() {
    545698        global $wpdb;
     
    731884        $history[] = array( 'time' => 445856404, 'event' => 'recheck-ham' );
    732885        $history[] = array( 'time' => 445856405, 'event' => 'check-ham' );
     886        $history[] = array( 'time' => 445856405, 'event' => 'check-ham-pending' );
    733887        $history[] = array( 'time' => 445856406, 'event' => 'wp-blacklisted' );
    734888        $history[] = array( 'time' => 445856406, 'event' => 'wp-disallowed' );
     
    8491003            update_comment_meta( $id, 'akismet_result', 'true' );
    8501004            delete_comment_meta( $id, 'akismet_error' );
     1005            delete_comment_meta( $id, 'akismet_delay_moderation_email' );
    8511006            delete_comment_meta( $id, 'akismet_delayed_moderation_email' );
     1007            delete_comment_meta( $id, 'akismet_schedule_approval_fallback' );
     1008            delete_comment_meta( $id, 'akismet_schedule_email_fallback' );
    8521009            self::update_comment_history( $id, '', 'recheck-spam' );
    8531010        } elseif ( 'false' === $api_response ) {
    8541011            update_comment_meta( $id, 'akismet_result', 'false' );
    8551012            delete_comment_meta( $id, 'akismet_error' );
     1013            delete_comment_meta( $id, 'akismet_delay_moderation_email' );
    8561014            delete_comment_meta( $id, 'akismet_delayed_moderation_email' );
     1015            delete_comment_meta( $id, 'akismet_schedule_approval_fallback' );
     1016            delete_comment_meta( $id, 'akismet_schedule_email_fallback' );
    8571017            self::update_comment_history( $id, '', 'recheck-ham' );
    8581018        } else {
     
    11221282                ) {
    11231283                delete_comment_meta( $comment_id, 'akismet_error' );
     1284                delete_comment_meta( $comment_id, 'akismet_delay_moderation_email' );
    11241285                delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
     1286                delete_comment_meta( $comment_id, 'akismet_schedule_approval_fallback' );
     1287                delete_comment_meta( $comment_id, 'akismet_schedule_email_fallback' );
    11251288                continue;
    11261289            }
     
    11541317                            wp_set_comment_status( $comment_id, 1 );
    11551318                        } elseif ( get_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ) ) {
    1156                             wp_notify_moderator( $comment_id );
     1319                            wp_new_comment_notify_moderator( $comment_id );
     1320                            wp_new_comment_notify_postauthor( $comment_id );
    11571321                        }
    11581322                    }
    11591323                }
    11601324
     1325                delete_comment_meta( $comment_id, 'akismet_delay_moderation_email' );
    11611326                delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
    11621327            } else {
     
    11641329                // send a moderation email now.
    11651330                if ( ( intval( gmdate( 'U' ) ) - strtotime( $comment->comment_date_gmt ) ) < self::MAX_DELAY_BEFORE_MODERATION_EMAIL ) {
     1331                    delete_comment_meta( $comment_id, 'akismet_delay_moderation_email' );
    11661332                    delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' );
    1167                     wp_notify_moderator( $comment_id );
     1333
     1334                    wp_new_comment_notify_moderator( $comment_id );
     1335                    wp_new_comment_notify_postauthor( $comment_id );
    11681336                }
    11691337
     
    11711339                wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
    11721340                do_action( 'akismet_scheduled_recheck', 'check-db-comment-' . $status );
     1341
    11731342                return;
    11741343            }
     1344
    11751345            delete_comment_meta( $comment_id, 'akismet_rechecking' );
    11761346        }
    11771347
    11781348        $remaining = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error'" );
     1349
    11791350        if ( $remaining && ! wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) {
    11801351            wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' );
     
    13771548
    13781549    /**
    1379      * If Akismet is temporarily unreachable, we don't want to "spam" the blogger with
    1380      * moderation emails for comments that will be automatically cleared or spammed on
    1381      * the next retry.
    1382      *
    1383      * For comments that will be rechecked later, empty the list of email addresses that
    1384      * the moderation email would be sent to.
    1385      *
    1386      * @param array $emails An array of email addresses that the moderation email will be sent to.
     1550     * If Akismet is temporarily unreachable, we don't want to "spam" the blogger or post author
     1551     * with emails for comments that will be automatically cleared or spammed on the next retry.
     1552     *
     1553     * @param bool $maybe_notify Whether the notification email will be sent.
    13871554     * @param int   $comment_id The ID of the relevant comment.
    1388      * @return array An array of email addresses that the moderation email will be sent to.
    1389      */
    1390     public static function disable_moderation_emails_if_unreachable( $emails, $comment_id ) {
    1391         if ( ! empty( self::$prevent_moderation_email_for_these_comments ) && ! empty( $emails ) ) {
    1392             $matching_fields = self::get_fields_for_comment_matching( $comment_id );
    1393 
    1394             // self::$prevent_moderation_email_for_these_comments is an array of $commentdata objects
    1395             // saved immediately after the comment-check request completes.
    1396             foreach ( self::$prevent_moderation_email_for_these_comments as $possible_match ) {
    1397                 if ( self::comments_match( $possible_match, $matching_fields ) ) {
    1398                     update_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true );
    1399                     return array();
    1400                 }
    1401             }
    1402         }
    1403 
    1404         return $emails;
     1555     * @return bool Whether the notification email should still be sent.
     1556     */
     1557    public static function disable_emails_if_unreachable( $maybe_notify, $comment_id ) {
     1558        if ( $maybe_notify ) {
     1559            if ( get_comment_meta( $comment_id, 'akismet_delay_moderation_email', true ) ) {
     1560                self::log( 'Disabling notification email for comment #' . $comment_id );
     1561
     1562                update_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true );
     1563                delete_comment_meta( $comment_id, 'akismet_delay_moderation_email' );
     1564
     1565                // If we want to prevent the email from sending another time, we'll have to reset
     1566                // the akismet_delay_moderation_email commentmeta.
     1567
     1568                return false;
     1569            }
     1570        }
     1571
     1572        return $maybe_notify;
    14051573    }
    14061574
     
    19762144            'cron-retry-spam',
    19772145            'check-ham',
     2146            'check-ham-pending',
    19782147            'check-spam',
    19792148            'recheck-error',
  • akismet/trunk/readme.txt

    r3289308 r3323859  
    3232
    3333== Changelog ==
     34
     35= 5.5 =
     36*Release Date - TBD*
     37
     38* Enable webhooks so that Akismet can process comments asynchronously to detect more types of spam.
    3439
    3540= 5.4 =
Note: See TracChangeset for help on using the changeset viewer.