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