[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-content/plugins/akismet/ -> class.akismet-rest-api.php (source)

   1  <?php
   2  
   3  class Akismet_REST_API {
   4      /**
   5       * Register the REST API routes.
   6       */
   7  	public static function init() {
   8          if ( ! function_exists( 'register_rest_route' ) ) {
   9              // The REST API wasn't integrated into core until 4.4, and we support 4.0+ (for now).
  10              return false;
  11          }
  12  
  13          register_rest_route( 'akismet/v1', '/key', array(
  14              array(
  15                  'methods' => WP_REST_Server::READABLE,
  16                  'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ),
  17                  'callback' => array( 'Akismet_REST_API', 'get_key' ),
  18              ), array(
  19                  'methods' => WP_REST_Server::EDITABLE,
  20                  'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ),
  21                  'callback' => array( 'Akismet_REST_API', 'set_key' ),
  22                  'args' => array(
  23                      'key' => array(
  24                          'required' => true,
  25                          'type' => 'string',
  26                          'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ),
  27                          'description' => __( 'A 12-character Akismet API key. Available at akismet.com/get/', 'akismet' ),
  28                      ),
  29                  ),
  30              ), array(
  31                  'methods' => WP_REST_Server::DELETABLE,
  32                  'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ),
  33                  'callback' => array( 'Akismet_REST_API', 'delete_key' ),
  34              )
  35          ) );
  36  
  37          register_rest_route( 'akismet/v1', '/settings/', array(
  38              array(
  39                  'methods' => WP_REST_Server::READABLE,
  40                  'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ),
  41                  'callback' => array( 'Akismet_REST_API', 'get_settings' ),
  42              ),
  43              array(
  44                  'methods' => WP_REST_Server::EDITABLE,
  45                  'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ),
  46                  'callback' => array( 'Akismet_REST_API', 'set_boolean_settings' ),
  47                  'args' => array(
  48                      'akismet_strictness' => array(
  49                          'required' => false,
  50                          'type' => 'boolean',
  51                          'description' => __( 'If true, Akismet will automatically discard the worst spam automatically rather than putting it in the spam folder.', 'akismet' ),
  52                      ),
  53                      'akismet_show_user_comments_approved' => array(
  54                          'required' => false,
  55                          'type' => 'boolean',
  56                          'description' => __( 'If true, show the number of approved comments beside each comment author in the comments list page.', 'akismet' ),
  57                      ),
  58                  ),
  59              )
  60          ) );
  61  
  62          register_rest_route( 'akismet/v1', '/stats', array(
  63              'methods' => WP_REST_Server::READABLE,
  64              'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ),
  65              'callback' => array( 'Akismet_REST_API', 'get_stats' ),
  66              'args' => array(
  67                  'interval' => array(
  68                      'required' => false,
  69                      'type' => 'string',
  70                      'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_interval' ),
  71                      'description' => __( 'The time period for which to retrieve stats. Options: 60-days, 6-months, all', 'akismet' ),
  72                      'default' => 'all',
  73                  ),
  74              ),
  75          ) );
  76  
  77          register_rest_route( 'akismet/v1', '/stats/(?P<interval>[\w+])', array(
  78              'args' => array(
  79                  'interval' => array(
  80                      'description' => __( 'The time period for which to retrieve stats. Options: 60-days, 6-months, all', 'akismet' ),
  81                      'type' => 'string',
  82                  ),
  83              ),
  84              array(
  85                  'methods' => WP_REST_Server::READABLE,
  86                  'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ),
  87                  'callback' => array( 'Akismet_REST_API', 'get_stats' ),
  88              )
  89          ) );
  90  
  91          register_rest_route( 'akismet/v1', '/alert', array(
  92              array(
  93                  'methods' => WP_REST_Server::READABLE,
  94                  'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ),
  95                  'callback' => array( 'Akismet_REST_API', 'get_alert' ),
  96                  'args' => array(
  97                      'key' => array(
  98                          'required' => false,
  99                          'type' => 'string',
 100                          'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ),
 101                          'description' => __( 'A 12-character Akismet API key. Available at akismet.com/get/', 'akismet' ),
 102                      ),
 103                  ),
 104              ),
 105              array(
 106                  'methods' => WP_REST_Server::EDITABLE,
 107                  'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ),
 108                  'callback' => array( 'Akismet_REST_API', 'set_alert' ),
 109                  'args' => array(
 110                      'key' => array(
 111                          'required' => false,
 112                          'type' => 'string',
 113                          'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ),
 114                          'description' => __( 'A 12-character Akismet API key. Available at akismet.com/get/', 'akismet' ),
 115                      ),
 116                  ),
 117              ),
 118              array(
 119                  'methods' => WP_REST_Server::DELETABLE,
 120                  'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ),
 121                  'callback' => array( 'Akismet_REST_API', 'delete_alert' ),
 122                  'args' => array(
 123                      'key' => array(
 124                          'required' => false,
 125                          'type' => 'string',
 126                          'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ),
 127                          'description' => __( 'A 12-character Akismet API key. Available at akismet.com/get/', 'akismet' ),
 128                      ),
 129                  ),
 130              )
 131          ) );
 132  
 133          register_rest_route(
 134              'akismet/v1',
 135              '/webhook',
 136              array(
 137                  'methods' => WP_REST_Server::CREATABLE,
 138                  'callback' => array( 'Akismet_REST_API', 'receive_webhook' ),
 139                  'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ),
 140              )
 141          );
 142      }
 143  
 144      /**
 145       * Get the current Akismet API key.
 146       *
 147       * @param WP_REST_Request $request
 148       * @return WP_Error|WP_REST_Response
 149       */
 150  	public static function get_key( $request = null ) {
 151          return rest_ensure_response( Akismet::get_api_key() );
 152      }
 153  
 154      /**
 155       * Set the API key, if possible.
 156       *
 157       * @param WP_REST_Request $request
 158       * @return WP_Error|WP_REST_Response
 159       */
 160  	public static function set_key( $request ) {
 161          if ( defined( 'WPCOM_API_KEY' ) ) {
 162              return rest_ensure_response( new WP_Error( 'hardcoded_key', __( 'This site\'s API key is hardcoded and cannot be changed via the API.', 'akismet' ), array( 'status'=> 409 ) ) );
 163          }
 164  
 165          $new_api_key = $request->get_param( 'key' );
 166  
 167          if ( ! self::key_is_valid( $new_api_key ) ) {
 168              return rest_ensure_response( new WP_Error( 'invalid_key', __( 'The value provided is not a valid and registered API key.', 'akismet' ), array( 'status' => 400 ) ) );
 169          }
 170  
 171          update_option( 'wordpress_api_key', $new_api_key );
 172  
 173          return self::get_key();
 174      }
 175  
 176      /**
 177       * Unset the API key, if possible.
 178       *
 179       * @param WP_REST_Request $request
 180       * @return WP_Error|WP_REST_Response
 181       */
 182  	public static function delete_key( $request ) {
 183          if ( defined( 'WPCOM_API_KEY' ) ) {
 184              return rest_ensure_response( new WP_Error( 'hardcoded_key', __( 'This site\'s API key is hardcoded and cannot be deleted.', 'akismet' ), array( 'status'=> 409 ) ) );
 185          }
 186  
 187          delete_option( 'wordpress_api_key' );
 188  
 189          return rest_ensure_response( true );
 190      }
 191  
 192      /**
 193       * Get the Akismet settings.
 194       *
 195       * @param WP_REST_Request $request
 196       * @return WP_Error|WP_REST_Response
 197       */
 198  	public static function get_settings( $request = null ) {
 199          return rest_ensure_response( array(
 200              'akismet_strictness' => ( get_option( 'akismet_strictness', '1' ) === '1' ),
 201              'akismet_show_user_comments_approved' => ( get_option( 'akismet_show_user_comments_approved', '1' ) === '1' ),
 202          ) );
 203      }
 204  
 205      /**
 206       * Update the Akismet settings.
 207       *
 208       * @param WP_REST_Request $request
 209       * @return WP_Error|WP_REST_Response
 210       */
 211  	public static function set_boolean_settings( $request ) {
 212          foreach ( array(
 213              'akismet_strictness',
 214              'akismet_show_user_comments_approved',
 215          ) as $setting_key ) {
 216  
 217              $setting_value = $request->get_param( $setting_key );
 218              if ( is_null( $setting_value ) ) {
 219                  // This setting was not specified.
 220                  continue;
 221              }
 222  
 223              // From 4.7+, WP core will ensure that these are always boolean
 224              // values because they are registered with 'type' => 'boolean',
 225              // but we need to do this ourselves for prior versions.
 226              $setting_value = Akismet_REST_API::parse_boolean( $setting_value );
 227  
 228              update_option( $setting_key, $setting_value ? '1' : '0' );
 229          }
 230  
 231          return self::get_settings();
 232      }
 233  
 234      /**
 235       * Parse a numeric or string boolean value into a boolean.
 236       *
 237       * @param mixed $value The value to convert into a boolean.
 238       * @return bool The converted value.
 239       */
 240  	public static function parse_boolean( $value ) {
 241          switch ( $value ) {
 242              case true:
 243              case 'true':
 244              case '1':
 245              case 1:
 246                  return true;
 247  
 248              case false:
 249              case 'false':
 250              case '0':
 251              case 0:
 252                  return false;
 253  
 254              default:
 255                  return (bool) $value;
 256          }
 257      }
 258  
 259      /**
 260       * Get the Akismet stats for a given time period.
 261       *
 262       * Possible `interval` values:
 263       * - all
 264       * - 60-days
 265       * - 6-months
 266       *
 267       * @param WP_REST_Request $request
 268       * @return WP_Error|WP_REST_Response
 269       */
 270  	public static function get_stats( $request ) {
 271          $api_key = Akismet::get_api_key();
 272  
 273          $interval = $request->get_param( 'interval' );
 274  
 275          $stat_totals = array();
 276  
 277          $request_args = array(
 278              'blog' => get_option( 'home' ),
 279              'key' => $api_key,
 280              'from' => $interval,
 281          );
 282  
 283          $request_args = apply_filters( 'akismet_request_args', $request_args, 'get-stats' );
 284  
 285          $response = Akismet::http_post( Akismet::build_query( $request_args ), 'get-stats' );
 286  
 287          if ( ! empty( $response[1] ) ) {
 288              $stat_totals[$interval] = json_decode( $response[1] );
 289          }
 290  
 291          return rest_ensure_response( $stat_totals );
 292      }
 293  
 294      /**
 295       * Get the current alert code and message. Alert codes are used to notify the site owner
 296       * if there's a problem, like a connection issue between their site and the Akismet API,
 297       * invalid requests being sent, etc.
 298       *
 299       * @param WP_REST_Request $request
 300       * @return WP_Error|WP_REST_Response
 301       */
 302  	public static function get_alert( $request ) {
 303          return rest_ensure_response( array(
 304              'code' => get_option( 'akismet_alert_code' ),
 305              'message' => get_option( 'akismet_alert_msg' ),
 306          ) );
 307      }
 308  
 309      /**
 310       * Update the current alert code and message by triggering a call to the Akismet server.
 311       *
 312       * @param WP_REST_Request $request
 313       * @return WP_Error|WP_REST_Response
 314       */
 315  	public static function set_alert( $request ) {
 316          delete_option( 'akismet_alert_code' );
 317          delete_option( 'akismet_alert_msg' );
 318  
 319          // Make a request so the most recent alert code and message are retrieved.
 320          Akismet::verify_key( Akismet::get_api_key() );
 321  
 322          return self::get_alert( $request );
 323      }
 324  
 325      /**
 326       * Clear the current alert code and message.
 327       *
 328       * @param WP_REST_Request $request
 329       * @return WP_Error|WP_REST_Response
 330       */
 331  	public static function delete_alert( $request ) {
 332          delete_option( 'akismet_alert_code' );
 333          delete_option( 'akismet_alert_msg' );
 334  
 335          return self::get_alert( $request );
 336      }
 337  
 338  	private static function key_is_valid( $key ) {
 339          $request_args = array(
 340              'key' => $key,
 341              'blog' => get_option( 'home' ),
 342          );
 343  
 344          $request_args = apply_filters( 'akismet_request_args', $request_args, 'verify-key' );
 345  
 346          $response = Akismet::http_post( Akismet::build_query( $request_args ), 'verify-key' );
 347  
 348          if ( $response[1] == 'valid' ) {
 349              return true;
 350          }
 351  
 352          return false;
 353      }
 354  
 355  	public static function privileged_permission_callback() {
 356          return current_user_can( 'manage_options' );
 357      }
 358  
 359      /**
 360       * For calls that Akismet.com makes to the site to clear outdated alert codes, use the API key for authorization.
 361       */
 362  	public static function remote_call_permission_callback( $request ) {
 363          $local_key = Akismet::get_api_key();
 364  
 365          return $local_key && ( strtolower( $request->get_param( 'key' ) ) === strtolower( $local_key ) );
 366      }
 367  
 368  	public static function sanitize_interval( $interval, $request, $param ) {
 369          $interval = trim( $interval );
 370  
 371          $valid_intervals = array( '60-days', '6-months', 'all', );
 372  
 373          if ( ! in_array( $interval, $valid_intervals ) ) {
 374              $interval = 'all';
 375          }
 376  
 377          return $interval;
 378      }
 379  
 380  	public static function sanitize_key( $key, $request, $param ) {
 381          return trim( $key );
 382      }
 383  
 384      /**
 385       * Process a webhook request from the Akismet servers.
 386       *
 387       * @param WP_REST_Request $request
 388       * @return WP_Error|WP_REST_Response
 389       */
 390  	public static function receive_webhook( $request ) {
 391          Akismet::log( array( 'Webhook request received', $request->get_body() ) );
 392  
 393          /**
 394           * The request body should look like this:
 395           * array(
 396           *     'key' => '1234567890abcd',
 397           *     'endpoint' => '[comment-check|submit-ham|submit-spam]',
 398           *     'comments' => array(
 399           *         array(
 400           *             'guid' => '[...]',
 401           *             'result' => '[true|false]',
 402           *             'comment_author' => '[...]',
 403           *             [...]
 404           *         ),
 405           *         array(
 406           *             'guid' => '[...]',
 407           *             [...],
 408           *         ),
 409           *         [...]
 410           *     )
 411           * )
 412           *
 413           * Multiple comments can be included in each request, and the only truly required
 414           * field for each is the guid, although it would be friendly to include also
 415           * comment_post_ID, comment_parent, and comment_author_email, if possible to make
 416           * searching easier.
 417           */
 418  
 419          // The response will include statuses for the result of each comment that was supplied.
 420          $response = array(
 421              'comments' => array(),
 422          );
 423  
 424          $endpoint = $request->get_param( 'endpoint' );
 425  
 426          switch ( $endpoint ) {
 427              case 'comment-check':
 428                  $webhook_comments = $request->get_param( 'comments' );
 429  
 430                  if ( ! is_array( $webhook_comments ) ) {
 431                      return rest_ensure_response( new WP_Error( 'malformed_request', __( 'The \'comments\' parameter must be an array.', 'akismet' ), array( 'status' => 400 ) ) );
 432                  }
 433  
 434                  foreach ( $webhook_comments as $webhook_comment ) {
 435                      $guid = $webhook_comment['guid'];
 436  
 437                      if ( ! $guid ) {
 438                          // Without the GUID, we can't be sure that we're matching the right comment.
 439                          // We'll make it a rule that any comment without a GUID is ignored intentionally.
 440                          continue;
 441                      }
 442  
 443                      // Search on the fields that are indexed in the comments table, plus the GUID.
 444                      // The GUID is the only thing we really need to search on, but comment_meta
 445                      // is not indexed in a useful way if there are many many comments. This
 446                      // should help narrow it down first.
 447                      $queryable_fields = array(
 448                          'comment_post_ID' => 'post_id',
 449                          'comment_parent' => 'parent',
 450                          'comment_author_email' => 'author_email',
 451                      );
 452  
 453                      $query_args = array();
 454                      $query_args['status'] = 'any';
 455                      $query_args['meta_key'] = 'akismet_guid';
 456                      $query_args['meta_value'] = $guid;
 457  
 458                      foreach ( $queryable_fields as $queryable_field => $wp_comment_query_field ) {
 459                          if ( isset( $webhook_comment[ $queryable_field ] ) ) {
 460                              $query_args[ $wp_comment_query_field ] = $webhook_comment[ $queryable_field ];
 461                          }
 462                      }
 463  
 464                      $comments_query = new WP_Comment_Query( $query_args );
 465                      $comments = $comments_query->comments;
 466  
 467                      if ( ! $comments ) {
 468                          // Unexpected, although the comment could have been deleted since being submitted.
 469                          Akismet::log( 'Webhook failed: no matching comment found.' );
 470  
 471                          $response['comments'][ $guid ] = array( 'status' => 'error', 'message' => __( 'Could not find matching comment.', 'akismet' ) );
 472  
 473                          continue;
 474                      } if ( count( $comments ) > 1 ) {
 475                          // Two comments shouldn't be able to match the same GUID.
 476                          Akismet::log( 'Webhook failed: multiple matching comments found.', $comments );
 477  
 478                          $response['comments'][ $guid ] = array( 'status' => 'error', 'message' => __( 'Multiple comments matched request.', 'akismet' ) );
 479  
 480                          continue;
 481                      } else {
 482                          // We have one single match, as hoped for.
 483                          Akismet::log( 'Found matching comment.', $comments );
 484  
 485                          $current_status = wp_get_comment_status( $comments[0] );
 486  
 487                          $result = $webhook_comment['result'];
 488  
 489                          if ( 'true' == $result ) {
 490                              Akismet::log( 'Comment should be spam' );
 491  
 492                              // The comment should be classified as spam.
 493                              if ( 'spam' != $current_status ) {
 494                                  // The comment is not classified as spam. If Akismet was the one to act on it, move it to spam.
 495                                  if ( Akismet::last_comment_status_change_came_from_akismet( $comments[0]->comment_ID ) ) {
 496                                      Akismet::log( 'Comment is not spam; marking as spam.' );
 497  
 498                                      wp_spam_comment( $comments[0] );
 499                                      Akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-spam' );
 500                                  } else {
 501                                      Akismet::log( 'Comment is not spam, but it has already been manually handled by some other process.' );
 502                                      Akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-spam-noaction' );
 503                                  }
 504                              }
 505                          } else if ( 'false' == $result ) {
 506                              Akismet::log( 'Comment should be ham' );
 507  
 508                              // The comment should be classified as ham.
 509                              if ( 'spam' == $current_status ) {
 510                                  Akismet::log( 'Comment is spam.' );
 511  
 512                                  // The comment is classified as spam. If Akismet was the one to label it as spam, unspam it.
 513                                  if ( Akismet::last_comment_status_change_came_from_akismet( $comments[0]->comment_ID ) ) {
 514                                      Akismet::log( 'Akismet marked it as spam; unspamming.' );
 515  
 516                                      wp_unspam_comment( $comments[0] );
 517                                      akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-ham' );
 518                                  } else {
 519                                      Akismet::log( 'Comment is not spam, but it has already been manually handled by some other process.' );
 520                                      Akismet::update_comment_history( $comments[0]->comment_ID, '', 'webhook-ham-noaction' );
 521                                  }
 522                              }
 523                          }
 524  
 525                          $response['comments'][ $guid ] = array( 'status' => 'success' );
 526                      }
 527                  }
 528  
 529                  break;
 530              case 'submit-ham':
 531              case 'submit-spam':
 532                  // Nothing to do for submit-ham or submit-spam.
 533                  break;
 534              default:
 535                  // Unsupported endpoint.
 536                  break;
 537          }
 538  
 539          /**
 540           * Allow plugins to do things with a successfully processed webhook request, like logging.
 541           *
 542           * @since 5.3.2
 543           *
 544           * @param WP_REST_Request $request The REST request object.
 545           */
 546          do_action( 'akismet_webhook_received', $request );
 547  
 548          Akismet::log( 'Done processing webhook.' );
 549  
 550          return rest_ensure_response( $response );
 551      }
 552  }


Generated : Fri Mar 29 08:20:02 2024 Cross-referenced by PHPXref