| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Core Comment API 4 * 5 * @package WordPress 6 * @subpackage Comment 7 */ 8 9 /** 10 * Checks whether a comment passes internal checks to be allowed to add. 11 * 12 * If manual comment moderation is set in the administration, then all checks, 13 * regardless of their type and substance, will fail and the function will 14 * return false. 15 * 16 * If the number of links exceeds the amount in the administration, then the 17 * check fails. If any of the parameter contents contain any disallowed words, 18 * then the check fails. 19 * 20 * If the comment author was approved before, then the comment is automatically 21 * approved. 22 * 23 * If all checks pass, the function will return true. 24 * 25 * @since 1.2.0 26 * 27 * @global wpdb $wpdb WordPress database abstraction object. 28 * 29 * @param string $author Comment author name. 30 * @param string $email Comment author email. 31 * @param string $url Comment author URL. 32 * @param string $comment Content of the comment. 33 * @param string $user_ip Comment author IP address. 34 * @param string $user_agent Comment author User-Agent. 35 * @param string $comment_type Comment type, either user-submitted comment, 36 * trackback, or pingback. 37 * @return bool If all checks pass, true, otherwise false. 38 */ 39 function check_comment( $author, $email, $url, $comment, $user_ip, $user_agent, $comment_type ) { 40 global $wpdb; 41 42 // If manual moderation is enabled, skip all checks and return false. 43 if ( '1' === get_option( 'comment_moderation' ) ) { 44 return false; 45 } 46 47 /** This filter is documented in wp-includes/comment-template.php */ 48 $comment = apply_filters( 'comment_text', $comment, null, array() ); 49 50 // Check for the number of external links if a max allowed number is set. 51 $max_links = get_option( 'comment_max_links' ); 52 if ( $max_links ) { 53 $num_links = preg_match_all( '/<a [^>]*href/i', $comment, $out ); 54 55 /** 56 * Filters the number of links found in a comment. 57 * 58 * @since 3.0.0 59 * @since 4.7.0 Added the `$comment` parameter. 60 * 61 * @param int $num_links The number of links found. 62 * @param string $url Comment author's URL. Included in allowed links total. 63 * @param string $comment Content of the comment. 64 */ 65 $num_links = apply_filters( 'comment_max_links_url', $num_links, $url, $comment ); 66 67 /* 68 * If the number of links in the comment exceeds the allowed amount, 69 * fail the check by returning false. 70 */ 71 if ( $num_links >= $max_links ) { 72 return false; 73 } 74 } 75 76 $mod_keys = trim( get_option( 'moderation_keys' ) ); 77 78 // If moderation 'keys' (keywords) are set, process them. 79 if ( ! empty( $mod_keys ) ) { 80 $words = explode( "\n", $mod_keys ); 81 82 foreach ( (array) $words as $word ) { 83 $word = trim( $word ); 84 85 // Skip empty lines. 86 if ( empty( $word ) ) { 87 continue; 88 } 89 90 /* 91 * Do some escaping magic so that '#' (number of) characters in the spam 92 * words don't break things: 93 */ 94 $word = preg_quote( $word, '#' ); 95 96 /* 97 * Check the comment fields for moderation keywords. If any are found, 98 * fail the check for the given field by returning false. 99 */ 100 $pattern = "#$word#iu"; 101 if ( preg_match( $pattern, $author ) ) { 102 return false; 103 } 104 if ( preg_match( $pattern, $email ) ) { 105 return false; 106 } 107 if ( preg_match( $pattern, $url ) ) { 108 return false; 109 } 110 if ( preg_match( $pattern, $comment ) ) { 111 return false; 112 } 113 if ( preg_match( $pattern, $user_ip ) ) { 114 return false; 115 } 116 if ( preg_match( $pattern, $user_agent ) ) { 117 return false; 118 } 119 } 120 } 121 122 /* 123 * Check if the option to approve comments by previously-approved authors is enabled. 124 * 125 * If it is enabled, check whether the comment author has a previously-approved comment, 126 * as well as whether there are any moderation keywords (if set) present in the author 127 * email address. If both checks pass, return true. Otherwise, return false. 128 */ 129 if ( '1' === get_option( 'comment_previously_approved' ) ) { 130 if ( 'trackback' !== $comment_type && 'pingback' !== $comment_type && '' !== $author && '' !== $email ) { 131 $comment_user = get_user_by( 'email', wp_unslash( $email ) ); 132 if ( ! empty( $comment_user->ID ) ) { 133 $ok_to_comment = $wpdb->get_var( 134 $wpdb->prepare( 135 "SELECT comment_approved 136 FROM $wpdb->comments 137 WHERE user_id = %d 138 AND comment_approved = '1' 139 LIMIT 1", 140 $comment_user->ID 141 ) 142 ); 143 } else { 144 // expected_slashed ($author, $email) 145 $ok_to_comment = $wpdb->get_var( 146 $wpdb->prepare( 147 "SELECT comment_approved 148 FROM $wpdb->comments 149 WHERE comment_author = %s 150 AND comment_author_email = %s 151 AND comment_approved = '1' 152 LIMIT 1", 153 $author, 154 $email 155 ) 156 ); 157 } 158 159 if ( '1' === $ok_to_comment && ( empty( $mod_keys ) || ! str_contains( $email, $mod_keys ) ) ) { 160 return true; 161 } else { 162 return false; 163 } 164 } else { 165 return false; 166 } 167 } 168 return true; 169 } 170 171 /** 172 * Retrieves the approved comments for a post. 173 * 174 * @since 2.0.0 175 * @since 4.1.0 Refactored to leverage WP_Comment_Query over a direct query. 176 * 177 * @param int $post_id The ID of the post. 178 * @param array $args { 179 * Optional. See WP_Comment_Query::__construct() for information on accepted arguments. 180 * 181 * @type int $status Comment status to limit results by. Defaults to approved comments. 182 * @type int $post_id Limit results to those affiliated with a given post ID. 183 * @type string $order How to order retrieved comments. Default 'ASC'. 184 * } 185 * @return WP_Comment[]|int[]|int The approved comments, or number of comments if `$count` 186 * argument is true. An empty array is returned when `$post_id` 187 * is falsey, even when `$count` is true. 188 * @phpstan-return ( 189 * $post_id is 0 ? array{} : ( 190 * $args is array{ count: true, ... } ? non-negative-int : ( 191 * $args is array{ fields: 'ids', ... } ? non-negative-int[] : array<int, WP_Comment> 192 * ) 193 * ) 194 * ) 195 */ 196 function get_approved_comments( $post_id, $args = array() ) { 197 if ( ! $post_id ) { 198 return array(); 199 } 200 201 $defaults = array( 202 'status' => 1, 203 'post_id' => $post_id, 204 'order' => 'ASC', 205 ); 206 $parsed_args = wp_parse_args( $args, $defaults ); 207 208 $query = new WP_Comment_Query(); 209 return $query->query( $parsed_args ); 210 } 211 212 /** 213 * Retrieves comment data given a comment ID or comment object. 214 * 215 * If an object is passed then the comment data will be cached and then returned 216 * after being passed through a filter. If the comment is empty, then the global 217 * comment variable will be used, if it is set. 218 * 219 * @since 2.0.0 220 * @since 7.1.0 Only numeric values are now treated as comment IDs; other unrecognized values 221 * return null instead of being cast to an integer ID. 222 * 223 * @global WP_Comment $comment Global comment object. 224 * 225 * @param WP_Comment|string|int $comment Comment to retrieve. 226 * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which 227 * correspond to a WP_Comment object, an associative array, or a numeric array, 228 * respectively. Default OBJECT. 229 * @return WP_Comment|array|null Depends on $output value. 230 * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output 231 * @phpstan-return ( 232 * $output is 'ARRAY_A' ? non-empty-array<string, mixed>|null : ( 233 * $output is 'ARRAY_N' ? non-empty-list<mixed>|null : WP_Comment|null 234 * ) 235 * ) 236 */ 237 function get_comment( $comment = null, $output = OBJECT ) { 238 if ( empty( $comment ) && isset( $GLOBALS['comment'] ) ) { 239 $comment = $GLOBALS['comment']; 240 } 241 242 if ( $comment instanceof WP_Comment ) { 243 $_comment = $comment; 244 } elseif ( is_object( $comment ) ) { 245 $_comment = new WP_Comment( $comment ); 246 } elseif ( is_numeric( $comment ) ) { 247 $_comment = WP_Comment::get_instance( (int) $comment ); 248 } else { 249 $_comment = null; 250 } 251 252 if ( ! $_comment ) { 253 return null; 254 } 255 256 /** 257 * Fires after a comment is retrieved. 258 * 259 * @since 2.3.0 260 * 261 * @param WP_Comment|null $_comment Comment data. 262 */ 263 $_comment = apply_filters( 'get_comment', $_comment ); 264 if ( ! ( $_comment instanceof WP_Comment ) ) { 265 return null; 266 } 267 268 if ( OBJECT === $output ) { 269 return $_comment; 270 } elseif ( ARRAY_A === $output ) { 271 return $_comment->to_array(); 272 } elseif ( ARRAY_N === $output ) { 273 return array_values( $_comment->to_array() ); 274 } 275 return $_comment; 276 } 277 278 /** 279 * Retrieves a list of comments. 280 * 281 * The comment list can be for the blog as a whole or for an individual post. 282 * 283 * @since 2.7.0 284 * 285 * @param string|array $args Optional. Array or string of arguments. See WP_Comment_Query::__construct() 286 * for information on accepted arguments. Default empty string. 287 * @return WP_Comment[]|int[]|int List of comments or number of found comments if `$count` argument is true. 288 * @phpstan-return ( 289 * $args is array{ count: true, ... } ? non-negative-int : ( 290 * $args is array{ fields: 'ids', ... } ? non-negative-int[] : array<int, WP_Comment> 291 * ) 292 * ) 293 */ 294 function get_comments( $args = '' ) { 295 $query = new WP_Comment_Query(); 296 return $query->query( $args ); 297 } 298 299 /** 300 * Retrieves all of the WordPress supported comment statuses. 301 * 302 * Comments have a limited set of valid status values, this provides the comment 303 * status values and descriptions. 304 * 305 * @since 2.7.0 306 * 307 * @return string[] List of comment status labels keyed by status. 308 */ 309 function get_comment_statuses() { 310 $status = array( 311 'hold' => __( 'Unapproved' ), 312 'approve' => _x( 'Approved', 'comment status' ), 313 'spam' => _x( 'Spam', 'comment status' ), 314 'trash' => _x( 'Trash', 'comment status' ), 315 ); 316 317 return $status; 318 } 319 320 /** 321 * Gets the default comment status for a post type. 322 * 323 * @since 4.3.0 324 * 325 * @param string $post_type Optional. Post type. Default 'post'. 326 * @param string $comment_type Optional. Comment type. Default 'comment'. 327 * @return string Either 'open' or 'closed'. 328 */ 329 function get_default_comment_status( $post_type = 'post', $comment_type = 'comment' ) { 330 switch ( $comment_type ) { 331 case 'pingback': 332 case 'trackback': 333 $supports = 'trackbacks'; 334 $option = 'ping'; 335 break; 336 default: 337 $supports = 'comments'; 338 $option = 'comment'; 339 break; 340 } 341 342 // Set the status. 343 if ( 'page' === $post_type ) { 344 $status = 'closed'; 345 } elseif ( post_type_supports( $post_type, $supports ) ) { 346 $status = get_option( "default_{$option}_status" ); 347 } else { 348 $status = 'closed'; 349 } 350 351 /** 352 * Filters the default comment status for the given post type. 353 * 354 * @since 4.3.0 355 * 356 * @param string $status Default status for the given post type, 357 * either 'open' or 'closed'. 358 * @param string $post_type Post type. Default is `post`. 359 * @param string $comment_type Type of comment. Default is `comment`. 360 */ 361 return apply_filters( 'get_default_comment_status', $status, $post_type, $comment_type ); 362 } 363 364 /** 365 * Retrieves the date the last comment was modified. 366 * 367 * @since 1.5.0 368 * @since 4.7.0 Replaced caching the modified date in a local static variable 369 * with the Object Cache API. 370 * 371 * @global wpdb $wpdb WordPress database abstraction object. 372 * 373 * @param string $timezone Which timezone to use in reference to 'gmt', 'blog', or 'server' locations. 374 * @return string|false Last comment modified date on success, false on failure. 375 */ 376 function get_lastcommentmodified( $timezone = 'server' ) { 377 global $wpdb; 378 379 $timezone = strtolower( $timezone ); 380 $key = "lastcommentmodified:$timezone"; 381 382 $comment_modified_date = wp_cache_get( $key, 'timeinfo' ); 383 if ( false !== $comment_modified_date ) { 384 return $comment_modified_date; 385 } 386 387 switch ( $timezone ) { 388 case 'gmt': 389 $comment_modified_date = $wpdb->get_var( "SELECT comment_date_gmt FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1" ); 390 break; 391 case 'blog': 392 $comment_modified_date = $wpdb->get_var( "SELECT comment_date FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1" ); 393 break; 394 case 'server': 395 $add_seconds_server = gmdate( 'Z' ); 396 397 $comment_modified_date = $wpdb->get_var( $wpdb->prepare( "SELECT DATE_ADD(comment_date_gmt, INTERVAL %s SECOND) FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1", $add_seconds_server ) ); 398 break; 399 } 400 401 if ( $comment_modified_date ) { 402 wp_cache_set( $key, $comment_modified_date, 'timeinfo' ); 403 404 return $comment_modified_date; 405 } 406 407 return false; 408 } 409 410 /** 411 * Retrieves the total comment counts for the whole site or a single post. 412 * 413 * @since 2.0.0 414 * 415 * @param int $post_id Optional. Restrict the comment counts to the given post. Default 0, which indicates that 416 * comment counts for the whole site will be retrieved. 417 * @return int[] { 418 * The number of comments keyed by their status. 419 * 420 * @type int $approved The number of approved comments. 421 * @type int $awaiting_moderation The number of comments awaiting moderation (a.k.a. pending). 422 * @type int $spam The number of spam comments. 423 * @type int $trash The number of trashed comments. 424 * @type int $post-trashed The number of comments for posts that are in the trash. 425 * @type int $total_comments The total number of non-trashed comments, including spam. 426 * @type int $all The total number of pending or approved comments. 427 * } 428 */ 429 function get_comment_count( $post_id = 0 ) { 430 $post_id = (int) $post_id; 431 432 $comment_count = array( 433 'approved' => 0, 434 'awaiting_moderation' => 0, 435 'spam' => 0, 436 'trash' => 0, 437 'post-trashed' => 0, 438 'total_comments' => 0, 439 'all' => 0, 440 ); 441 442 $args = array( 443 'count' => true, 444 'update_comment_meta_cache' => false, 445 'orderby' => 'none', 446 ); 447 if ( $post_id > 0 ) { 448 $args['post_id'] = $post_id; 449 } 450 $mapping = array( 451 'approved' => 'approve', 452 'awaiting_moderation' => 'hold', 453 'spam' => 'spam', 454 'trash' => 'trash', 455 'post-trashed' => 'post-trashed', 456 ); 457 $comment_count = array(); 458 foreach ( $mapping as $key => $value ) { 459 $comment_count[ $key ] = get_comments( array_merge( $args, array( 'status' => $value ) ) ); 460 } 461 462 $comment_count['all'] = $comment_count['approved'] + $comment_count['awaiting_moderation']; 463 $comment_count['total_comments'] = $comment_count['all'] + $comment_count['spam']; 464 465 return array_map( 'intval', $comment_count ); 466 } 467 468 // 469 // Comment meta functions. 470 // 471 472 /** 473 * Adds meta data field to a comment. 474 * 475 * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input. 476 * 477 * @since 2.9.0 478 * 479 * @link https://developer.wordpress.org/reference/functions/add_comment_meta/ 480 * 481 * @param int $comment_id Comment ID. 482 * @param string $meta_key Metadata name. 483 * @param mixed $meta_value Metadata value. Arrays and objects are stored as serialized data and 484 * will be returned as the same type when retrieved. Other data types will 485 * be stored as strings in the database: 486 * - false is stored and retrieved as an empty string ('') 487 * - true is stored and retrieved as '1' 488 * - numbers (both integer and float) are stored and retrieved as strings 489 * Must be serializable if non-scalar. 490 * @param bool $unique Optional. Whether the same key should not be added. 491 * Default false. 492 * @return int|false Meta ID on success, false on failure. 493 */ 494 function add_comment_meta( $comment_id, $meta_key, $meta_value, $unique = false ) { 495 return add_metadata( 'comment', $comment_id, $meta_key, $meta_value, $unique ); 496 } 497 498 /** 499 * Removes metadata matching criteria from a comment. 500 * 501 * You can match based on the key, or key and value. Removing based on key and 502 * value, will keep from removing duplicate metadata with the same key. It also 503 * allows removing all metadata matching key, if needed. 504 * 505 * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input. 506 * 507 * @since 2.9.0 508 * 509 * @link https://developer.wordpress.org/reference/functions/delete_comment_meta/ 510 * 511 * @param int $comment_id Comment ID. 512 * @param string $meta_key Metadata name. 513 * @param mixed $meta_value Optional. Metadata value. If provided, 514 * rows will only be removed that match the value. 515 * Must be serializable if non-scalar. Default empty string. 516 * @return bool True on success, false on failure. 517 */ 518 function delete_comment_meta( $comment_id, $meta_key, $meta_value = '' ) { 519 return delete_metadata( 'comment', $comment_id, $meta_key, $meta_value ); 520 } 521 522 /** 523 * Retrieves comment meta field for a comment. 524 * 525 * @since 2.9.0 526 * 527 * @link https://developer.wordpress.org/reference/functions/get_comment_meta/ 528 * 529 * @param int $comment_id Comment ID. 530 * @param string $key Optional. The meta key to retrieve. By default, 531 * returns data for all keys. Default empty string. 532 * @param bool $single Optional. Whether to return a single value. 533 * This parameter has no effect if `$key` is not specified. 534 * Default false. 535 * @return mixed An array of values if `$single` is false. 536 * The value of meta data field if `$single` is true. 537 * False for an invalid `$comment_id` (non-numeric, zero, or negative value). 538 * An empty array if a valid but non-existing comment ID is passed and `$single` is false. 539 * An empty string if a valid but non-existing comment ID is passed and `$single` is true. 540 * Note: Non-serialized values are returned as strings: 541 * - false values are returned as empty strings ('') 542 * - true values are returned as '1' 543 * - numbers are returned as strings 544 * Arrays and objects retain their original type. 545 * @phpstan-param int|numeric-string $comment_id 546 */ 547 function get_comment_meta( $comment_id, $key = '', $single = false ) { 548 return get_metadata( 'comment', $comment_id, $key, $single ); 549 } 550 551 /** 552 * Queue comment meta for lazy-loading. 553 * 554 * @since 6.3.0 555 * 556 * @param array $comment_ids List of comment IDs. 557 */ 558 function wp_lazyload_comment_meta( array $comment_ids ) { 559 if ( empty( $comment_ids ) ) { 560 return; 561 } 562 $lazyloader = wp_metadata_lazyloader(); 563 $lazyloader->queue_objects( 'comment', $comment_ids ); 564 } 565 566 /** 567 * Updates comment meta field based on comment ID. 568 * 569 * Use the $prev_value parameter to differentiate between meta fields with the 570 * same key and comment ID. 571 * 572 * If the meta field for the comment does not exist, it will be added. 573 * 574 * For historical reasons both the meta key and the meta value are expected to be "slashed" (slashes escaped) on input. 575 * 576 * @since 2.9.0 577 * 578 * @link https://developer.wordpress.org/reference/functions/update_comment_meta/ 579 * 580 * @param int $comment_id Comment ID. 581 * @param string $meta_key Metadata key. 582 * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. 583 * @param mixed $prev_value Optional. Previous value to check before updating. 584 * If specified, only update existing metadata entries with 585 * this value. Otherwise, update all entries. Default empty string. 586 * @return int|bool Meta ID if the key didn't exist, true on successful update, 587 * false on failure or if the value passed to the function 588 * is the same as the one that is already in the database. 589 */ 590 function update_comment_meta( $comment_id, $meta_key, $meta_value, $prev_value = '' ) { 591 return update_metadata( 'comment', $comment_id, $meta_key, $meta_value, $prev_value ); 592 } 593 594 /** 595 * Sets the cookies used to store an unauthenticated commentator's identity. Typically used 596 * to recall previous comments by this commentator that are still held in moderation. 597 * 598 * @since 3.4.0 599 * @since 4.9.6 The `$cookies_consent` parameter was added. 600 * 601 * @param WP_Comment $comment Comment object. 602 * @param WP_User $user Comment author's user object. The user may not exist. 603 * @param bool $cookies_consent Optional. Comment author's consent to store cookies. Default true. 604 */ 605 function wp_set_comment_cookies( $comment, $user, $cookies_consent = true ) { 606 // If the user already exists, or the user opted out of cookies, don't set cookies. 607 if ( $user->exists() ) { 608 return; 609 } 610 611 if ( false === $cookies_consent ) { 612 // Remove any existing cookies. 613 $past = time() - YEAR_IN_SECONDS; 614 setcookie( 'comment_author_' . COOKIEHASH, ' ', $past, COOKIEPATH, COOKIE_DOMAIN ); 615 setcookie( 'comment_author_email_' . COOKIEHASH, ' ', $past, COOKIEPATH, COOKIE_DOMAIN ); 616 setcookie( 'comment_author_url_' . COOKIEHASH, ' ', $past, COOKIEPATH, COOKIE_DOMAIN ); 617 618 return; 619 } 620 621 /** 622 * Filters the lifetime of the comment cookie in seconds. 623 * 624 * @since 2.8.0 625 * @since 6.6.0 The default `$seconds` value changed from 30000000 to YEAR_IN_SECONDS. 626 * 627 * @param int $seconds Comment cookie lifetime. Default YEAR_IN_SECONDS. 628 */ 629 $comment_cookie_lifetime = time() + apply_filters( 'comment_cookie_lifetime', YEAR_IN_SECONDS ); 630 631 $secure = ( 'https' === parse_url( home_url(), PHP_URL_SCHEME ) ); 632 633 setcookie( 'comment_author_' . COOKIEHASH, $comment->comment_author, $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN, $secure ); 634 setcookie( 'comment_author_email_' . COOKIEHASH, $comment->comment_author_email, $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN, $secure ); 635 setcookie( 'comment_author_url_' . COOKIEHASH, esc_url( $comment->comment_author_url ), $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN, $secure ); 636 } 637 638 /** 639 * Sanitizes the cookies sent to the user already. 640 * 641 * Will only do anything if the cookies have already been created for the user. 642 * Mostly used after cookies had been sent to use elsewhere. 643 * 644 * @since 2.0.4 645 */ 646 function sanitize_comment_cookies() { 647 if ( isset( $_COOKIE[ 'comment_author_' . COOKIEHASH ] ) ) { 648 /** 649 * Filters the comment author's name cookie before it is set. 650 * 651 * When this filter hook is evaluated in wp_filter_comment(), 652 * the comment author's name string is passed. 653 * 654 * @since 1.5.0 655 * 656 * @param string $author_cookie The comment author name cookie. 657 */ 658 $comment_author = apply_filters( 'pre_comment_author_name', $_COOKIE[ 'comment_author_' . COOKIEHASH ] ); 659 $comment_author = wp_unslash( $comment_author ); 660 $comment_author = esc_attr( $comment_author ); 661 662 $_COOKIE[ 'comment_author_' . COOKIEHASH ] = $comment_author; 663 } 664 665 if ( isset( $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] ) ) { 666 /** 667 * Filters the comment author's email cookie before it is set. 668 * 669 * When this filter hook is evaluated in wp_filter_comment(), 670 * the comment author's email string is passed. 671 * 672 * @since 1.5.0 673 * 674 * @param string $author_email_cookie The comment author email cookie. 675 */ 676 $comment_author_email = apply_filters( 'pre_comment_author_email', $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] ); 677 $comment_author_email = wp_unslash( $comment_author_email ); 678 $comment_author_email = esc_attr( $comment_author_email ); 679 680 $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] = $comment_author_email; 681 } 682 683 if ( isset( $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] ) ) { 684 /** 685 * Filters the comment author's URL cookie before it is set. 686 * 687 * When this filter hook is evaluated in wp_filter_comment(), 688 * the comment author's URL string is passed. 689 * 690 * @since 1.5.0 691 * 692 * @param string $author_url_cookie The comment author URL cookie. 693 */ 694 $comment_author_url = apply_filters( 'pre_comment_author_url', $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] ); 695 $comment_author_url = wp_unslash( $comment_author_url ); 696 697 $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] = $comment_author_url; 698 } 699 } 700 701 /** 702 * Validates whether this comment is allowed to be made. 703 * 704 * @since 2.0.0 705 * @since 4.7.0 The `$avoid_die` parameter was added, allowing the function 706 * to return a WP_Error object instead of dying. 707 * @since 5.5.0 The `$avoid_die` parameter was renamed to `$wp_error`. 708 * 709 * @global wpdb $wpdb WordPress database abstraction object. 710 * 711 * @param array $commentdata Contains information on the comment. 712 * @param bool $wp_error When true, a disallowed comment will result in the function 713 * returning a WP_Error object, rather than executing wp_die(). 714 * Default false. 715 * @return int|string|WP_Error Allowed comments return the approval status (0|1|'spam'|'trash'). 716 * If `$wp_error` is true, disallowed comments return a WP_Error. 717 */ 718 function wp_allow_comment( $commentdata, $wp_error = false ) { 719 global $wpdb; 720 721 /* 722 * Simple duplicate check. 723 * expected_slashed ($comment_post_ID, $comment_author, $comment_author_email, $comment_content) 724 */ 725 $dupe = $wpdb->prepare( 726 "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_parent = %s AND comment_approved != 'trash' AND ( comment_author = %s ", 727 wp_unslash( $commentdata['comment_post_ID'] ), 728 wp_unslash( $commentdata['comment_parent'] ), 729 wp_unslash( $commentdata['comment_author'] ) 730 ); 731 if ( $commentdata['comment_author_email'] ) { 732 $dupe .= $wpdb->prepare( 733 'AND comment_author_email = %s ', 734 wp_unslash( $commentdata['comment_author_email'] ) 735 ); 736 } 737 $dupe .= $wpdb->prepare( 738 ') AND comment_content = %s LIMIT 1', 739 wp_unslash( $commentdata['comment_content'] ) 740 ); 741 742 $dupe_id = $wpdb->get_var( $dupe ); 743 744 /** 745 * Filters the ID, if any, of the duplicate comment found when creating a new comment. 746 * 747 * Return an empty value from this filter to allow what WP considers a duplicate comment. 748 * 749 * @since 4.4.0 750 * 751 * @param int $dupe_id ID of the comment identified as a duplicate. 752 * @param array $commentdata Data for the comment being created. 753 */ 754 $dupe_id = apply_filters( 'duplicate_comment_id', $dupe_id, $commentdata ); 755 756 if ( $dupe_id ) { 757 /** 758 * Fires immediately after a duplicate comment is detected. 759 * 760 * @since 3.0.0 761 * 762 * @param array $commentdata Comment data. 763 */ 764 do_action( 'comment_duplicate_trigger', $commentdata ); 765 766 /** 767 * Filters duplicate comment error message. 768 * 769 * @since 5.2.0 770 * 771 * @param string $comment_duplicate_message Duplicate comment error message. 772 */ 773 $comment_duplicate_message = apply_filters( 'comment_duplicate_message', __( 'Duplicate comment detected; it looks as though you’ve already said that!' ) ); 774 775 if ( $wp_error ) { 776 return new WP_Error( 'comment_duplicate', $comment_duplicate_message, 409 ); 777 } else { 778 if ( wp_doing_ajax() ) { 779 die( $comment_duplicate_message ); 780 } 781 782 wp_die( $comment_duplicate_message, 409 ); 783 } 784 } 785 786 /** 787 * Fires immediately before a comment is marked approved. 788 * 789 * Allows checking for comment flooding. 790 * 791 * @since 2.3.0 792 * @since 4.7.0 The `$avoid_die` parameter was added. 793 * @since 5.5.0 The `$avoid_die` parameter was renamed to `$wp_error`. 794 * 795 * @param string $comment_author_ip Comment author's IP address. 796 * @param string $comment_author_email Comment author's email. 797 * @param string $comment_date_gmt GMT date the comment was posted. 798 * @param bool $wp_error Whether to return a WP_Error object instead of executing 799 * wp_die() or die() if a comment flood is occurring. 800 */ 801 do_action( 802 'check_comment_flood', 803 $commentdata['comment_author_IP'], 804 $commentdata['comment_author_email'], 805 $commentdata['comment_date_gmt'], 806 $wp_error 807 ); 808 809 /** 810 * Filters whether a comment is part of a comment flood. 811 * 812 * The default check is wp_check_comment_flood(). See check_comment_flood_db(). 813 * 814 * @since 4.7.0 815 * @since 5.5.0 The `$avoid_die` parameter was renamed to `$wp_error`. 816 * 817 * @param bool $is_flood Is a comment flooding occurring? Default false. 818 * @param string $comment_author_ip Comment author's IP address. 819 * @param string $comment_author_email Comment author's email. 820 * @param string $comment_date_gmt GMT date the comment was posted. 821 * @param bool $wp_error Whether to return a WP_Error object instead of executing 822 * wp_die() or die() if a comment flood is occurring. 823 */ 824 $is_flood = apply_filters( 825 'wp_is_comment_flood', 826 false, 827 $commentdata['comment_author_IP'], 828 $commentdata['comment_author_email'], 829 $commentdata['comment_date_gmt'], 830 $wp_error 831 ); 832 833 if ( $is_flood ) { 834 /** This filter is documented in wp-includes/comment.php */ 835 $comment_flood_message = apply_filters( 'comment_flood_message', __( 'You are posting comments too quickly. Slow down.' ) ); 836 837 return new WP_Error( 'comment_flood', $comment_flood_message, 429 ); 838 } 839 840 return wp_check_comment_data( $commentdata ); 841 } 842 843 /** 844 * Hooks WP's native database-based comment-flood check. 845 * 846 * This wrapper maintains backward compatibility with plugins that expect to 847 * be able to unhook the legacy check_comment_flood_db() function from 848 * 'check_comment_flood' using remove_action(). 849 * 850 * @since 2.3.0 851 * @since 4.7.0 Converted to be an add_filter() wrapper. 852 */ 853 function check_comment_flood_db() { 854 add_filter( 'wp_is_comment_flood', 'wp_check_comment_flood', 10, 5 ); 855 } 856 857 /** 858 * Checks whether comment flooding is occurring. 859 * 860 * Won't run, if current user can manage options, so to not block 861 * administrators. 862 * 863 * @since 4.7.0 864 * 865 * @global wpdb $wpdb WordPress database abstraction object. 866 * 867 * @param bool $is_flood Is a comment flooding occurring? 868 * @param string $ip Comment author's IP address. 869 * @param string $email Comment author's email address. 870 * @param string $date MySQL time string. 871 * @param bool $avoid_die When true, a disallowed comment will result in the function 872 * returning without executing wp_die() or die(). Default false. 873 * @return bool Whether comment flooding is occurring. 874 */ 875 function wp_check_comment_flood( $is_flood, $ip, $email, $date, $avoid_die = false ) { 876 global $wpdb; 877 878 // Another callback has declared a flood. Trust it. 879 if ( true === $is_flood ) { 880 return $is_flood; 881 } 882 883 // Don't throttle admins or moderators. 884 if ( current_user_can( 'manage_options' ) || current_user_can( 'moderate_comments' ) ) { 885 return false; 886 } 887 888 $hour_ago = gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS ); 889 890 if ( is_user_logged_in() ) { 891 $user = get_current_user_id(); 892 $check_column = '`user_id`'; 893 } else { 894 $user = $ip; 895 $check_column = '`comment_author_IP`'; 896 } 897 898 $sql = $wpdb->prepare( 899 "SELECT `comment_date_gmt` FROM `$wpdb->comments` WHERE `comment_date_gmt` >= %s AND ( $check_column = %s OR `comment_author_email` = %s ) ORDER BY `comment_date_gmt` DESC LIMIT 1", 900 $hour_ago, 901 $user, 902 $email 903 ); 904 905 $lasttime = $wpdb->get_var( $sql ); 906 907 if ( $lasttime ) { 908 $time_lastcomment = mysql2date( 'U', $lasttime, false ); 909 $time_newcomment = mysql2date( 'U', $date, false ); 910 911 /** 912 * Filters the comment flood status. 913 * 914 * @since 2.1.0 915 * 916 * @param bool $bool Whether a comment flood is occurring. Default false. 917 * @param int $time_lastcomment Timestamp of when the last comment was posted. 918 * @param int $time_newcomment Timestamp of when the new comment was posted. 919 */ 920 $flood_die = apply_filters( 'comment_flood_filter', false, $time_lastcomment, $time_newcomment ); 921 922 if ( $flood_die ) { 923 /** 924 * Fires before the comment flood message is triggered. 925 * 926 * @since 1.5.0 927 * 928 * @param int $time_lastcomment Timestamp of when the last comment was posted. 929 * @param int $time_newcomment Timestamp of when the new comment was posted. 930 */ 931 do_action( 'comment_flood_trigger', $time_lastcomment, $time_newcomment ); 932 933 if ( $avoid_die ) { 934 return true; 935 } else { 936 /** 937 * Filters the comment flood error message. 938 * 939 * @since 5.2.0 940 * 941 * @param string $comment_flood_message Comment flood error message. 942 */ 943 $comment_flood_message = apply_filters( 'comment_flood_message', __( 'You are posting comments too quickly. Slow down.' ) ); 944 945 if ( wp_doing_ajax() ) { 946 die( $comment_flood_message ); 947 } 948 949 wp_die( $comment_flood_message, 429 ); 950 } 951 } 952 } 953 954 return false; 955 } 956 957 /** 958 * Separates an array of comments into an array keyed by comment_type. 959 * 960 * @since 2.7.0 961 * 962 * @param WP_Comment[] $comments Array of comments. 963 * @return array<string, WP_Comment[]> Array of comments keyed by comment type. 964 */ 965 function separate_comments( &$comments ) { 966 $comments_by_type = array( 967 'comment' => array(), 968 'trackback' => array(), 969 'pingback' => array(), 970 'pings' => array(), 971 ); 972 973 $count = count( $comments ); 974 975 for ( $i = 0; $i < $count; $i++ ) { 976 $type = $comments[ $i ]->comment_type; 977 978 if ( empty( $type ) ) { 979 $type = 'comment'; 980 } 981 982 $comments_by_type[ $type ][] = &$comments[ $i ]; 983 984 if ( 'trackback' === $type || 'pingback' === $type ) { 985 $comments_by_type['pings'][] = &$comments[ $i ]; 986 } 987 } 988 989 return $comments_by_type; 990 } 991 992 /** 993 * Calculates the total number of comment pages. 994 * 995 * @since 2.7.0 996 * 997 * @uses Walker_Comment 998 * 999 * @global WP_Query $wp_query WordPress Query object. 1000 * 1001 * @param WP_Comment[] $comments Optional. Array of WP_Comment objects. Defaults to `$wp_query->comments`. 1002 * @param int $per_page Optional. Comments per page. Defaults to the value of `comments_per_page` 1003 * query var, option of the same name, or 1 (in that order). 1004 * @param bool $threaded Optional. Control over flat or threaded comments. Defaults to the value 1005 * of `thread_comments` option. 1006 * @return int Number of comment pages. 1007 */ 1008 function get_comment_pages_count( $comments = null, $per_page = null, $threaded = null ) { 1009 global $wp_query; 1010 1011 if ( null === $comments && null === $per_page && null === $threaded && ! empty( $wp_query->max_num_comment_pages ) ) { 1012 return $wp_query->max_num_comment_pages; 1013 } 1014 1015 if ( ( ! $comments || ! is_array( $comments ) ) && ! empty( $wp_query->comments ) ) { 1016 $comments = $wp_query->comments; 1017 } 1018 1019 if ( empty( $comments ) ) { 1020 return 0; 1021 } 1022 1023 if ( ! get_option( 'page_comments' ) ) { 1024 return 1; 1025 } 1026 1027 if ( ! isset( $per_page ) ) { 1028 $per_page = (int) get_query_var( 'comments_per_page' ); 1029 } 1030 if ( 0 === $per_page ) { 1031 $per_page = (int) get_option( 'comments_per_page' ); 1032 } 1033 if ( 0 === $per_page ) { 1034 return 1; 1035 } 1036 1037 if ( ! isset( $threaded ) ) { 1038 $threaded = get_option( 'thread_comments' ); 1039 } 1040 1041 if ( $threaded ) { 1042 $walker = new Walker_Comment(); 1043 $count = ceil( $walker->get_number_of_root_elements( $comments ) / $per_page ); 1044 } else { 1045 $count = ceil( count( $comments ) / $per_page ); 1046 } 1047 1048 return (int) $count; 1049 } 1050 1051 /** 1052 * Calculates what page number a comment will appear on for comment paging. 1053 * 1054 * @since 2.7.0 1055 * 1056 * @global wpdb $wpdb WordPress database abstraction object. 1057 * 1058 * @param int $comment_id Comment ID. 1059 * @param array $args { 1060 * Array of optional arguments. 1061 * 1062 * @type string $type Limit paginated comments to those matching a given type. 1063 * Accepts 'comment', 'trackback', 'pingback', 'pings' 1064 * (trackbacks and pingbacks), or 'all'. Default 'all'. 1065 * @type int $per_page Per-page count to use when calculating pagination. 1066 * Defaults to the value of the 'comments_per_page' option. 1067 * @type int|string $max_depth If greater than 1, comment page will be determined 1068 * for the top-level parent `$comment_id`. 1069 * Defaults to the value of the 'thread_comments_depth' option. 1070 * } 1071 * @return int|null Comment page number or null on error. 1072 */ 1073 function get_page_of_comment( $comment_id, $args = array() ) { 1074 global $wpdb; 1075 1076 $page = null; 1077 1078 $comment = get_comment( $comment_id ); 1079 if ( ! $comment ) { 1080 return null; 1081 } 1082 1083 $defaults = array( 1084 'type' => 'all', 1085 'page' => '', 1086 'per_page' => '', 1087 'max_depth' => '', 1088 ); 1089 $args = wp_parse_args( $args, $defaults ); 1090 $original_args = $args; 1091 1092 // Order of precedence: 1. `$args['per_page']`, 2. 'comments_per_page' query_var, 3. 'comments_per_page' option. 1093 if ( get_option( 'page_comments' ) ) { 1094 if ( '' === $args['per_page'] ) { 1095 $args['per_page'] = get_query_var( 'comments_per_page' ); 1096 } 1097 1098 if ( '' === $args['per_page'] ) { 1099 $args['per_page'] = get_option( 'comments_per_page' ); 1100 } 1101 } 1102 1103 if ( empty( $args['per_page'] ) ) { 1104 $args['per_page'] = 0; 1105 $args['page'] = 0; 1106 } 1107 1108 if ( $args['per_page'] < 1 ) { 1109 $page = 1; 1110 } 1111 1112 if ( null === $page ) { 1113 if ( '' === $args['max_depth'] ) { 1114 if ( get_option( 'thread_comments' ) ) { 1115 $args['max_depth'] = get_option( 'thread_comments_depth' ); 1116 } else { 1117 $args['max_depth'] = -1; 1118 } 1119 } 1120 1121 // Find this comment's top-level parent if threading is enabled. 1122 if ( $args['max_depth'] > 1 && '0' !== $comment->comment_parent ) { 1123 return get_page_of_comment( $comment->comment_parent, $args ); 1124 } 1125 1126 $comment_args = array( 1127 'type' => $args['type'], 1128 'post_id' => $comment->comment_post_ID, 1129 'fields' => 'ids', 1130 'count' => true, 1131 'status' => 'approve', 1132 'orderby' => 'none', 1133 'parent' => 0, 1134 'date_query' => array( 1135 array( 1136 'column' => "$wpdb->comments.comment_date_gmt", 1137 'before' => $comment->comment_date_gmt, 1138 ), 1139 ), 1140 ); 1141 1142 if ( is_user_logged_in() ) { 1143 $comment_args['include_unapproved'] = array( get_current_user_id() ); 1144 } else { 1145 $unapproved_email = wp_get_unapproved_comment_author_email(); 1146 1147 if ( $unapproved_email ) { 1148 $comment_args['include_unapproved'] = array( $unapproved_email ); 1149 } 1150 } 1151 1152 /** 1153 * Filters the arguments used to query comments in get_page_of_comment(). 1154 * 1155 * @since 5.5.0 1156 * 1157 * @see WP_Comment_Query::__construct() 1158 * 1159 * @param array $comment_args { 1160 * Array of WP_Comment_Query arguments. 1161 * 1162 * @type string $type Limit paginated comments to those matching a given type. 1163 * Accepts 'comment', 'trackback', 'pingback', 'pings' 1164 * (trackbacks and pingbacks), or 'all'. Default 'all'. 1165 * @type int $post_id ID of the post. 1166 * @type string $fields Comment fields to return. 1167 * @type bool $count Whether to return a comment count (true) or array 1168 * of comment objects (false). 1169 * @type string $status Comment status. 1170 * @type int $parent Parent ID of comment to retrieve children of. 1171 * @type array $date_query Date query clauses to limit comments by. See WP_Date_Query. 1172 * @type array $include_unapproved Array of IDs or email addresses whose unapproved comments 1173 * will be included in paginated comments. 1174 * } 1175 */ 1176 $comment_args = apply_filters( 'get_page_of_comment_query_args', $comment_args ); 1177 1178 $comment_query = new WP_Comment_Query(); 1179 $older_comment_count = $comment_query->query( $comment_args ); 1180 1181 // No older comments? Then it's page #1. 1182 if ( 0 === $older_comment_count ) { 1183 $page = 1; 1184 1185 // Divide comments older than this one by comments per page to get this comment's page number. 1186 } else { 1187 $page = (int) ceil( ( $older_comment_count + 1 ) / $args['per_page'] ); 1188 } 1189 } 1190 1191 /** 1192 * Filters the calculated page on which a comment appears. 1193 * 1194 * @since 4.4.0 1195 * @since 4.7.0 Introduced the `$comment_id` parameter. 1196 * 1197 * @param int $page Comment page. 1198 * @param array $args { 1199 * Arguments used to calculate pagination. These include arguments auto-detected by the function, 1200 * based on query vars, system settings, etc. For pristine arguments passed to the function, 1201 * see `$original_args`. 1202 * 1203 * @type string $type Type of comments to count. 1204 * @type int $page Calculated current page. 1205 * @type int $per_page Calculated number of comments per page. 1206 * @type int $max_depth Maximum comment threading depth allowed. 1207 * } 1208 * @param array $original_args { 1209 * Array of arguments passed to the function. Some or all of these may not be set. 1210 * 1211 * @type string $type Type of comments to count. 1212 * @type int $page Current comment page. 1213 * @type int $per_page Number of comments per page. 1214 * @type int $max_depth Maximum comment threading depth allowed. 1215 * } 1216 * @param int $comment_id ID of the comment. 1217 */ 1218 return apply_filters( 'get_page_of_comment', (int) $page, $args, $original_args, $comment_id ); 1219 } 1220 1221 /** 1222 * Retrieves the maximum character lengths for the comment form fields. 1223 * 1224 * @since 4.5.0 1225 * 1226 * @global wpdb $wpdb WordPress database abstraction object. 1227 * 1228 * @return int[] Array of maximum lengths keyed by field name. 1229 */ 1230 function wp_get_comment_fields_max_lengths() { 1231 global $wpdb; 1232 1233 $lengths = array( 1234 'comment_author' => 245, 1235 'comment_author_email' => 100, 1236 'comment_author_url' => 200, 1237 'comment_content' => 65525, 1238 ); 1239 1240 if ( $wpdb->is_mysql ) { 1241 foreach ( $lengths as $column => $length ) { 1242 $col_length = $wpdb->get_col_length( $wpdb->comments, $column ); 1243 $max_length = 0; 1244 1245 // No point if we can't get the DB column lengths. 1246 if ( is_wp_error( $col_length ) ) { 1247 break; 1248 } 1249 1250 if ( ! is_array( $col_length ) && (int) $col_length > 0 ) { 1251 $max_length = (int) $col_length; 1252 } elseif ( is_array( $col_length ) && isset( $col_length['length'] ) && (int) $col_length['length'] > 0 ) { 1253 $max_length = (int) $col_length['length']; 1254 1255 if ( ! empty( $col_length['type'] ) && 'byte' === $col_length['type'] ) { 1256 $max_length = $max_length - 10; 1257 } 1258 } 1259 1260 if ( $max_length > 0 ) { 1261 $lengths[ $column ] = $max_length; 1262 } 1263 } 1264 } 1265 1266 /** 1267 * Filters the lengths for the comment form fields. 1268 * 1269 * @since 4.5.0 1270 * 1271 * @param int[] $lengths Array of maximum lengths keyed by field name. 1272 */ 1273 return apply_filters( 'wp_get_comment_fields_max_lengths', $lengths ); 1274 } 1275 1276 /** 1277 * Compares the lengths of comment data against the maximum character limits. 1278 * 1279 * @since 4.7.0 1280 * 1281 * @param array $comment_data Array of arguments for inserting a comment. 1282 * @return WP_Error|true WP_Error when a comment field exceeds the limit, 1283 * otherwise true. 1284 */ 1285 function wp_check_comment_data_max_lengths( $comment_data ) { 1286 $max_lengths = wp_get_comment_fields_max_lengths(); 1287 1288 if ( isset( $comment_data['comment_author'] ) && mb_strlen( $comment_data['comment_author'], '8bit' ) > $max_lengths['comment_author'] ) { 1289 return new WP_Error( 'comment_author_column_length', __( '<strong>Error:</strong> Your name is too long.' ), 200 ); 1290 } 1291 1292 if ( isset( $comment_data['comment_author_email'] ) && strlen( $comment_data['comment_author_email'] ) > $max_lengths['comment_author_email'] ) { 1293 return new WP_Error( 'comment_author_email_column_length', __( '<strong>Error:</strong> Your email address is too long.' ), 200 ); 1294 } 1295 1296 if ( isset( $comment_data['comment_author_url'] ) && strlen( $comment_data['comment_author_url'] ) > $max_lengths['comment_author_url'] ) { 1297 return new WP_Error( 'comment_author_url_column_length', __( '<strong>Error:</strong> Your URL is too long.' ), 200 ); 1298 } 1299 1300 if ( isset( $comment_data['comment_content'] ) && mb_strlen( $comment_data['comment_content'], '8bit' ) > $max_lengths['comment_content'] ) { 1301 return new WP_Error( 'comment_content_column_length', __( '<strong>Error:</strong> Your comment is too long.' ), 200 ); 1302 } 1303 1304 return true; 1305 } 1306 1307 /** 1308 * Checks whether comment data passes internal checks or has disallowed content. 1309 * 1310 * @since 6.7.0 1311 * 1312 * @global wpdb $wpdb WordPress database abstraction object. 1313 * 1314 * @param array $comment_data Array of arguments for inserting a comment. 1315 * @return int|string|WP_Error The approval status on success (0|1|'spam'|'trash'), 1316 * WP_Error otherwise. 1317 */ 1318 function wp_check_comment_data( $comment_data ) { 1319 global $wpdb; 1320 1321 if ( ! empty( $comment_data['user_id'] ) ) { 1322 $user = get_userdata( $comment_data['user_id'] ); 1323 $post_author = (int) $wpdb->get_var( 1324 $wpdb->prepare( 1325 "SELECT post_author FROM $wpdb->posts WHERE ID = %d LIMIT 1", 1326 $comment_data['comment_post_ID'] 1327 ) 1328 ); 1329 } 1330 1331 if ( isset( $user ) && ( $comment_data['user_id'] === $post_author || $user->has_cap( 'moderate_comments' ) ) ) { 1332 // The author and the admins get respect. 1333 $approved = 1; 1334 } else { 1335 // Everyone else's comments will be checked. 1336 if ( check_comment( 1337 $comment_data['comment_author'], 1338 $comment_data['comment_author_email'], 1339 $comment_data['comment_author_url'], 1340 $comment_data['comment_content'], 1341 $comment_data['comment_author_IP'], 1342 $comment_data['comment_agent'], 1343 $comment_data['comment_type'] 1344 ) ) { 1345 $approved = 1; 1346 } else { 1347 $approved = 0; 1348 } 1349 1350 if ( wp_check_comment_disallowed_list( 1351 $comment_data['comment_author'], 1352 $comment_data['comment_author_email'], 1353 $comment_data['comment_author_url'], 1354 $comment_data['comment_content'], 1355 $comment_data['comment_author_IP'], 1356 $comment_data['comment_agent'] 1357 ) ) { 1358 $approved = EMPTY_TRASH_DAYS ? 'trash' : 'spam'; 1359 } 1360 } 1361 1362 /** 1363 * Filters a comment's approval status before it is set. 1364 * 1365 * @since 2.1.0 1366 * @since 4.9.0 Returning a WP_Error value from the filter will short-circuit comment insertion 1367 * and allow skipping further processing. 1368 * 1369 * @param int|string|WP_Error $approved The approval status. Accepts 1, 0, 'spam', 'trash', 1370 * or WP_Error. 1371 * @param array $commentdata Comment data. 1372 */ 1373 return apply_filters( 'pre_comment_approved', $approved, $comment_data ); 1374 } 1375 1376 /** 1377 * Checks if a comment contains disallowed characters or words. 1378 * 1379 * @since 5.5.0 1380 * 1381 * @param string $author The author of the comment. 1382 * @param string $email The email of the comment. 1383 * @param string $url The url used in the comment. 1384 * @param string $comment The comment content. 1385 * @param string $user_ip The comment author's IP address. 1386 * @param string $user_agent The author's browser user agent. 1387 * @return bool True if the comment contains disallowed content, false otherwise. 1388 */ 1389 function wp_check_comment_disallowed_list( $author, $email, $url, $comment, $user_ip, $user_agent ) { 1390 /** 1391 * Fires before the comment is tested for disallowed characters or words. 1392 * 1393 * @since 1.5.0 1394 * @deprecated 5.5.0 Use {@see 'wp_check_comment_disallowed_list'} instead. 1395 * 1396 * @param string $author Comment author. 1397 * @param string $email Comment author's email. 1398 * @param string $url Comment author's URL. 1399 * @param string $comment Comment content. 1400 * @param string $user_ip Comment author's IP address. 1401 * @param string $user_agent Comment author's browser user agent. 1402 */ 1403 do_action_deprecated( 1404 'wp_blacklist_check', 1405 array( $author, $email, $url, $comment, $user_ip, $user_agent ), 1406 '5.5.0', 1407 'wp_check_comment_disallowed_list', 1408 __( 'Please consider writing more inclusive code.' ) 1409 ); 1410 1411 /** 1412 * Fires before the comment is tested for disallowed characters or words. 1413 * 1414 * @since 5.5.0 1415 * 1416 * @param string $author Comment author. 1417 * @param string $email Comment author's email. 1418 * @param string $url Comment author's URL. 1419 * @param string $comment Comment content. 1420 * @param string $user_ip Comment author's IP address. 1421 * @param string $user_agent Comment author's browser user agent. 1422 */ 1423 do_action( 'wp_check_comment_disallowed_list', $author, $email, $url, $comment, $user_ip, $user_agent ); 1424 1425 $mod_keys = trim( get_option( 'disallowed_keys' ) ); 1426 if ( '' === $mod_keys ) { 1427 return false; // If moderation keys are empty. 1428 } 1429 1430 // Ensure HTML tags are not being used to bypass the list of disallowed characters and words. 1431 $comment_without_html = wp_strip_all_tags( $comment ); 1432 1433 $words = explode( "\n", $mod_keys ); 1434 1435 foreach ( (array) $words as $word ) { 1436 $word = trim( $word ); 1437 1438 // Skip empty lines. 1439 if ( empty( $word ) ) { 1440 continue; } 1441 1442 // Do some escaping magic so that '#' chars in the spam words don't break things: 1443 $word = preg_quote( $word, '#' ); 1444 1445 $pattern = "#$word#iu"; 1446 if ( preg_match( $pattern, $author ) 1447 || preg_match( $pattern, $email ) 1448 || preg_match( $pattern, $url ) 1449 || preg_match( $pattern, $comment ) 1450 || preg_match( $pattern, $comment_without_html ) 1451 || preg_match( $pattern, $user_ip ) 1452 || preg_match( $pattern, $user_agent ) 1453 ) { 1454 return true; 1455 } 1456 } 1457 return false; 1458 } 1459 1460 /** 1461 * Retrieves the total comment counts for the whole site or a single post. 1462 * 1463 * The comment stats are cached and then retrieved, if they already exist in the 1464 * cache. 1465 * 1466 * @see get_comment_count() Which handles fetching the live comment counts. 1467 * 1468 * @since 2.5.0 1469 * 1470 * @param int $post_id Optional. Restrict the comment counts to the given post. Default 0, which indicates that 1471 * comment counts for the whole site will be retrieved. 1472 * @return stdClass { 1473 * The number of comments keyed by their status. 1474 * 1475 * @type int $approved The number of approved comments. 1476 * @type int $moderated The number of comments awaiting moderation (a.k.a. pending). 1477 * @type int $spam The number of spam comments. 1478 * @type int $trash The number of trashed comments. 1479 * @type int $post-trashed The number of comments for posts that are in the trash. 1480 * @type int $total_comments The total number of non-trashed comments, including spam. 1481 * @type int $all The total number of pending or approved comments. 1482 * } 1483 */ 1484 function wp_count_comments( $post_id = 0 ) { 1485 $post_id = (int) $post_id; 1486 1487 /** 1488 * Filters the comments count for a given post or the whole site. 1489 * 1490 * @since 2.7.0 1491 * 1492 * @param array|stdClass $count An empty array or an object containing comment counts. 1493 * @param int $post_id The post ID. Can be 0 to represent the whole site. 1494 */ 1495 $filtered = apply_filters( 'wp_count_comments', array(), $post_id ); 1496 if ( ! empty( $filtered ) ) { 1497 return $filtered; 1498 } 1499 1500 $count = wp_cache_get( "comments-{$post_id}", 'counts' ); 1501 if ( false !== $count ) { 1502 return $count; 1503 } 1504 1505 $stats = get_comment_count( $post_id ); 1506 $stats['moderated'] = $stats['awaiting_moderation']; 1507 unset( $stats['awaiting_moderation'] ); 1508 1509 $stats_object = (object) $stats; 1510 wp_cache_set( "comments-{$post_id}", $stats_object, 'counts' ); 1511 1512 return $stats_object; 1513 } 1514 1515 /** 1516 * Trashes or deletes a comment. 1517 * 1518 * The comment is moved to Trash instead of permanently deleted unless Trash is 1519 * disabled, item is already in the Trash, or $force_delete is true. 1520 * 1521 * The post comment count will be updated if the comment was approved and has a 1522 * post ID available. 1523 * 1524 * @since 2.0.0 1525 * 1526 * @global wpdb $wpdb WordPress database abstraction object. 1527 * 1528 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object. 1529 * @param bool $force_delete Whether to bypass Trash and force deletion. Default false. 1530 * @return bool True on success, false on failure. 1531 */ 1532 function wp_delete_comment( $comment_id, $force_delete = false ) { 1533 global $wpdb; 1534 1535 $comment = get_comment( $comment_id ); 1536 if ( ! $comment ) { 1537 return false; 1538 } 1539 1540 if ( ! $force_delete && EMPTY_TRASH_DAYS && ! in_array( wp_get_comment_status( $comment ), array( 'trash', 'spam' ), true ) ) { 1541 return wp_trash_comment( $comment_id ); 1542 } 1543 1544 /** 1545 * Fires immediately before a comment is deleted from the database. 1546 * 1547 * @since 1.2.0 1548 * @since 4.9.0 Added the `$comment` parameter. 1549 * 1550 * @param string $comment_id The comment ID as a numeric string. 1551 * @param WP_Comment $comment The comment to be deleted. 1552 */ 1553 do_action( 'delete_comment', $comment->comment_ID, $comment ); 1554 1555 // Move children up a level. 1556 $children = $wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_parent = %d", $comment->comment_ID ) ); 1557 if ( ! empty( $children ) ) { 1558 $wpdb->update( $wpdb->comments, array( 'comment_parent' => $comment->comment_parent ), array( 'comment_parent' => $comment->comment_ID ) ); 1559 clean_comment_cache( $children ); 1560 } 1561 1562 // Delete metadata. 1563 $meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->commentmeta WHERE comment_id = %d", $comment->comment_ID ) ); 1564 foreach ( $meta_ids as $mid ) { 1565 delete_metadata_by_mid( 'comment', $mid ); 1566 } 1567 1568 if ( ! $wpdb->delete( $wpdb->comments, array( 'comment_ID' => $comment->comment_ID ) ) ) { 1569 return false; 1570 } 1571 1572 /** 1573 * Fires immediately after a comment is deleted from the database. 1574 * 1575 * @since 2.9.0 1576 * @since 4.9.0 Added the `$comment` parameter. 1577 * 1578 * @param string $comment_id The comment ID as a numeric string. 1579 * @param WP_Comment $comment The deleted comment. 1580 */ 1581 do_action( 'deleted_comment', $comment->comment_ID, $comment ); 1582 1583 $post_id = $comment->comment_post_ID; 1584 if ( $post_id && '1' === $comment->comment_approved ) { 1585 wp_update_comment_count( $post_id ); 1586 } 1587 1588 clean_comment_cache( $comment->comment_ID ); 1589 1590 /** This action is documented in wp-includes/comment.php */ 1591 do_action( 'wp_set_comment_status', $comment->comment_ID, 'delete' ); 1592 1593 wp_transition_comment_status( 'delete', $comment->comment_approved, $comment ); 1594 1595 return true; 1596 } 1597 1598 /** 1599 * Moves a comment to the Trash 1600 * 1601 * If Trash is disabled, comment is permanently deleted. 1602 * 1603 * @since 2.9.0 1604 * @since 6.9.0 Any child notes are deleted when deleting a note. 1605 * 1606 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object. 1607 * @return bool True on success, false on failure. 1608 */ 1609 function wp_trash_comment( $comment_id ) { 1610 if ( ! EMPTY_TRASH_DAYS ) { 1611 $comment = get_comment( $comment_id ); 1612 $success = wp_delete_comment( $comment_id, true ); 1613 1614 if ( ! $success ) { 1615 return false; 1616 } 1617 1618 // Also delete children of top level 'note' type comments. 1619 if ( $comment && 'note' === $comment->comment_type && 0 === (int) $comment->comment_parent ) { 1620 $children = $comment->get_children( 1621 array( 1622 'fields' => 'ids', 1623 'status' => 'all', 1624 'type' => 'note', 1625 ) 1626 ); 1627 1628 foreach ( $children as $child_id ) { 1629 if ( ! wp_delete_comment( $child_id, true ) ) { 1630 $success = false; 1631 } 1632 } 1633 } 1634 1635 return $success; 1636 } 1637 1638 $comment = get_comment( $comment_id ); 1639 if ( ! $comment ) { 1640 return false; 1641 } 1642 1643 /** 1644 * Fires immediately before a comment is sent to the Trash. 1645 * 1646 * @since 2.9.0 1647 * @since 4.9.0 Added the `$comment` parameter. 1648 * 1649 * @param string $comment_id The comment ID as a numeric string. 1650 * @param WP_Comment $comment The comment to be trashed. 1651 */ 1652 do_action( 'trash_comment', $comment->comment_ID, $comment ); 1653 1654 if ( wp_set_comment_status( $comment, 'trash' ) ) { 1655 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_status' ); 1656 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_time' ); 1657 add_comment_meta( $comment->comment_ID, '_wp_trash_meta_status', $comment->comment_approved ); 1658 add_comment_meta( $comment->comment_ID, '_wp_trash_meta_time', time() ); 1659 1660 /** 1661 * Fires immediately after a comment is sent to Trash. 1662 * 1663 * @since 2.9.0 1664 * @since 4.9.0 Added the `$comment` parameter. 1665 * 1666 * @param string $comment_id The comment ID as a numeric string. 1667 * @param WP_Comment $comment The trashed comment. 1668 */ 1669 do_action( 'trashed_comment', $comment->comment_ID, $comment ); 1670 1671 // For top level 'note' type comments, also trash children. 1672 if ( 'note' === $comment->comment_type && 0 === (int) $comment->comment_parent ) { 1673 $children = $comment->get_children( 1674 array( 1675 'fields' => 'ids', 1676 'status' => 'all', 1677 'type' => 'note', 1678 ) 1679 ); 1680 1681 $success = true; 1682 foreach ( $children as $child_id ) { 1683 if ( ! wp_trash_comment( $child_id ) ) { 1684 $success = false; 1685 } 1686 } 1687 return $success; 1688 } 1689 1690 return true; 1691 } 1692 1693 return false; 1694 } 1695 1696 /** 1697 * Removes a comment from the Trash 1698 * 1699 * @since 2.9.0 1700 * 1701 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object. 1702 * @return bool True on success, false on failure. 1703 */ 1704 function wp_untrash_comment( $comment_id ) { 1705 $comment = get_comment( $comment_id ); 1706 if ( ! $comment ) { 1707 return false; 1708 } 1709 1710 /** 1711 * Fires immediately before a comment is restored from the Trash. 1712 * 1713 * @since 2.9.0 1714 * @since 4.9.0 Added the `$comment` parameter. 1715 * 1716 * @param string $comment_id The comment ID as a numeric string. 1717 * @param WP_Comment $comment The comment to be untrashed. 1718 */ 1719 do_action( 'untrash_comment', $comment->comment_ID, $comment ); 1720 1721 $status = (string) get_comment_meta( $comment->comment_ID, '_wp_trash_meta_status', true ); 1722 if ( empty( $status ) ) { 1723 $status = '0'; 1724 } 1725 1726 if ( wp_set_comment_status( $comment, $status ) ) { 1727 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_time' ); 1728 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_status' ); 1729 1730 /** 1731 * Fires immediately after a comment is restored from the Trash. 1732 * 1733 * @since 2.9.0 1734 * @since 4.9.0 Added the `$comment` parameter. 1735 * 1736 * @param string $comment_id The comment ID as a numeric string. 1737 * @param WP_Comment $comment The untrashed comment. 1738 */ 1739 do_action( 'untrashed_comment', $comment->comment_ID, $comment ); 1740 1741 return true; 1742 } 1743 1744 return false; 1745 } 1746 1747 /** 1748 * Marks a comment as Spam. 1749 * 1750 * @since 2.9.0 1751 * 1752 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object. 1753 * @return bool True on success, false on failure. 1754 */ 1755 function wp_spam_comment( $comment_id ) { 1756 $comment = get_comment( $comment_id ); 1757 if ( ! $comment ) { 1758 return false; 1759 } 1760 1761 /** 1762 * Fires immediately before a comment is marked as Spam. 1763 * 1764 * @since 2.9.0 1765 * @since 4.9.0 Added the `$comment` parameter. 1766 * 1767 * @param int $comment_id The comment ID. 1768 * @param WP_Comment $comment The comment to be marked as spam. 1769 */ 1770 do_action( 'spam_comment', $comment->comment_ID, $comment ); 1771 1772 if ( wp_set_comment_status( $comment, 'spam' ) ) { 1773 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_status' ); 1774 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_time' ); 1775 add_comment_meta( $comment->comment_ID, '_wp_trash_meta_status', $comment->comment_approved ); 1776 add_comment_meta( $comment->comment_ID, '_wp_trash_meta_time', time() ); 1777 1778 /** 1779 * Fires immediately after a comment is marked as Spam. 1780 * 1781 * @since 2.9.0 1782 * @since 4.9.0 Added the `$comment` parameter. 1783 * 1784 * @param int $comment_id The comment ID. 1785 * @param WP_Comment $comment The comment marked as spam. 1786 */ 1787 do_action( 'spammed_comment', $comment->comment_ID, $comment ); 1788 1789 return true; 1790 } 1791 1792 return false; 1793 } 1794 1795 /** 1796 * Removes a comment from the Spam. 1797 * 1798 * @since 2.9.0 1799 * 1800 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object. 1801 * @return bool True on success, false on failure. 1802 */ 1803 function wp_unspam_comment( $comment_id ) { 1804 $comment = get_comment( $comment_id ); 1805 if ( ! $comment ) { 1806 return false; 1807 } 1808 1809 /** 1810 * Fires immediately before a comment is unmarked as Spam. 1811 * 1812 * @since 2.9.0 1813 * @since 4.9.0 Added the `$comment` parameter. 1814 * 1815 * @param string $comment_id The comment ID as a numeric string. 1816 * @param WP_Comment $comment The comment to be unmarked as spam. 1817 */ 1818 do_action( 'unspam_comment', $comment->comment_ID, $comment ); 1819 1820 $status = (string) get_comment_meta( $comment->comment_ID, '_wp_trash_meta_status', true ); 1821 if ( empty( $status ) ) { 1822 $status = '0'; 1823 } 1824 1825 if ( wp_set_comment_status( $comment, $status ) ) { 1826 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_status' ); 1827 delete_comment_meta( $comment->comment_ID, '_wp_trash_meta_time' ); 1828 1829 /** 1830 * Fires immediately after a comment is unmarked as Spam. 1831 * 1832 * @since 2.9.0 1833 * @since 4.9.0 Added the `$comment` parameter. 1834 * 1835 * @param string $comment_id The comment ID as a numeric string. 1836 * @param WP_Comment $comment The comment unmarked as spam. 1837 */ 1838 do_action( 'unspammed_comment', $comment->comment_ID, $comment ); 1839 1840 return true; 1841 } 1842 1843 return false; 1844 } 1845 1846 /** 1847 * Retrieves the status of a comment by comment ID. 1848 * 1849 * @since 1.0.0 1850 * 1851 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object 1852 * @return string|false Status might be 'trash', 'approved', 'unapproved', 'spam'. False on failure. 1853 */ 1854 function wp_get_comment_status( $comment_id ) { 1855 $comment = get_comment( $comment_id ); 1856 if ( ! $comment ) { 1857 return false; 1858 } 1859 1860 $approved = $comment->comment_approved; 1861 1862 if ( null === $approved ) { 1863 return false; 1864 } elseif ( '1' === $approved ) { 1865 return 'approved'; 1866 } elseif ( '0' === $approved ) { 1867 return 'unapproved'; 1868 } elseif ( 'spam' === $approved ) { 1869 return 'spam'; 1870 } elseif ( 'trash' === $approved ) { 1871 return 'trash'; 1872 } else { 1873 return false; 1874 } 1875 } 1876 1877 /** 1878 * Calls hooks for when a comment status transition occurs. 1879 * 1880 * Calls hooks for comment status transitions. If the new comment status is not the same 1881 * as the previous comment status, then two hooks will be ran, the first is 1882 * {@see 'transition_comment_status'} with new status, old status, and comment data. 1883 * The next action called is {@see 'comment_$old_status_to_$new_status'}. It has 1884 * the comment data. 1885 * 1886 * The final action will run whether or not the comment statuses are the same. 1887 * The action is named {@see 'comment_$new_status_$comment->comment_type'}. 1888 * 1889 * @since 2.7.0 1890 * 1891 * @param string $new_status New comment status. 1892 * @param string $old_status Previous comment status. 1893 * @param WP_Comment $comment Comment object. 1894 */ 1895 function wp_transition_comment_status( $new_status, $old_status, $comment ) { 1896 /* 1897 * Translate raw statuses to human-readable formats for the hooks. 1898 * This is not a complete list of comment status, it's only the ones 1899 * that need to be renamed. 1900 */ 1901 $comment_statuses = array( 1902 0 => 'unapproved', 1903 'hold' => 'unapproved', // wp_set_comment_status() uses "hold". 1904 1 => 'approved', 1905 'approve' => 'approved', // wp_set_comment_status() uses "approve". 1906 ); 1907 if ( isset( $comment_statuses[ $new_status ] ) ) { 1908 $new_status = $comment_statuses[ $new_status ]; 1909 } 1910 if ( isset( $comment_statuses[ $old_status ] ) ) { 1911 $old_status = $comment_statuses[ $old_status ]; 1912 } 1913 1914 // Call the hooks. 1915 if ( $new_status !== $old_status ) { 1916 /** 1917 * Fires when the comment status is in transition. 1918 * 1919 * @since 2.7.0 1920 * 1921 * @param string $new_status The new comment status. 1922 * @param string $old_status The old comment status. 1923 * @param WP_Comment $comment Comment object. 1924 */ 1925 do_action( 'transition_comment_status', $new_status, $old_status, $comment ); 1926 1927 /** 1928 * Fires when the comment status is in transition from one specific status to another. 1929 * 1930 * The dynamic portions of the hook name, `$old_status`, and `$new_status`, 1931 * refer to the old and new comment statuses, respectively. 1932 * 1933 * Possible hook names include: 1934 * 1935 * - `comment_unapproved_to_approved` 1936 * - `comment_spam_to_approved` 1937 * - `comment_approved_to_unapproved` 1938 * - `comment_spam_to_unapproved` 1939 * - `comment_unapproved_to_spam` 1940 * - `comment_approved_to_spam` 1941 * 1942 * @since 2.7.0 1943 * 1944 * @param WP_Comment $comment Comment object. 1945 */ 1946 do_action( "comment_{$old_status}_to_{$new_status}", $comment ); 1947 } 1948 /** 1949 * Fires when the status of a specific comment type is in transition. 1950 * 1951 * The dynamic portions of the hook name, `$new_status`, and `$comment->comment_type`, 1952 * refer to the new comment status, and the type of comment, respectively. 1953 * 1954 * Typical comment types include 'comment', 'pingback', or 'trackback'. 1955 * 1956 * Possible hook names include: 1957 * 1958 * - `comment_approved_comment` 1959 * - `comment_approved_pingback` 1960 * - `comment_approved_trackback` 1961 * - `comment_unapproved_comment` 1962 * - `comment_unapproved_pingback` 1963 * - `comment_unapproved_trackback` 1964 * - `comment_spam_comment` 1965 * - `comment_spam_pingback` 1966 * - `comment_spam_trackback` 1967 * 1968 * @since 2.7.0 1969 * 1970 * @param string $comment_id The comment ID as a numeric string. 1971 * @param WP_Comment $comment Comment object. 1972 */ 1973 do_action( "comment_{$new_status}_{$comment->comment_type}", $comment->comment_ID, $comment ); 1974 } 1975 1976 /** 1977 * Clears the lastcommentmodified cached value when a comment status is changed. 1978 * 1979 * Deletes the lastcommentmodified cache key when a comment enters or leaves 1980 * 'approved' status. 1981 * 1982 * @since 4.7.0 1983 * @access private 1984 * 1985 * @param string $new_status The new comment status. 1986 * @param string $old_status The old comment status. 1987 */ 1988 function _clear_modified_cache_on_transition_comment_status( $new_status, $old_status ) { 1989 if ( 'approved' === $new_status || 'approved' === $old_status ) { 1990 $data = array(); 1991 foreach ( array( 'server', 'gmt', 'blog' ) as $timezone ) { 1992 $data[] = "lastcommentmodified:$timezone"; 1993 } 1994 wp_cache_delete_multiple( $data, 'timeinfo' ); 1995 } 1996 } 1997 1998 /** 1999 * Gets current commenter's name, email, and URL. 2000 * 2001 * Expects cookies content to already be sanitized. User of this function might 2002 * wish to recheck the returned array for validity. 2003 * 2004 * @see sanitize_comment_cookies() Use to sanitize cookies 2005 * 2006 * @since 2.0.4 2007 * 2008 * @return array { 2009 * An array of current commenter variables. 2010 * 2011 * @type string $comment_author The name of the current commenter, or an empty string. 2012 * @type string $comment_author_email The email address of the current commenter, or an empty string. 2013 * @type string $comment_author_url The URL address of the current commenter, or an empty string. 2014 * } 2015 */ 2016 function wp_get_current_commenter() { 2017 // Cookies should already be sanitized. 2018 2019 $comment_author = ''; 2020 if ( isset( $_COOKIE[ 'comment_author_' . COOKIEHASH ] ) ) { 2021 $comment_author = $_COOKIE[ 'comment_author_' . COOKIEHASH ]; 2022 } 2023 2024 $comment_author_email = ''; 2025 if ( isset( $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] ) ) { 2026 $comment_author_email = $_COOKIE[ 'comment_author_email_' . COOKIEHASH ]; 2027 } 2028 2029 $comment_author_url = ''; 2030 if ( isset( $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] ) ) { 2031 $comment_author_url = $_COOKIE[ 'comment_author_url_' . COOKIEHASH ]; 2032 } 2033 2034 /** 2035 * Filters the current commenter's name, email, and URL. 2036 * 2037 * @since 3.1.0 2038 * 2039 * @param array $comment_author_data { 2040 * An array of current commenter variables. 2041 * 2042 * @type string $comment_author The name of the current commenter, or an empty string. 2043 * @type string $comment_author_email The email address of the current commenter, or an empty string. 2044 * @type string $comment_author_url The URL address of the current commenter, or an empty string. 2045 * } 2046 */ 2047 return apply_filters( 'wp_get_current_commenter', compact( 'comment_author', 'comment_author_email', 'comment_author_url' ) ); 2048 } 2049 2050 /** 2051 * Gets unapproved comment author's email. 2052 * 2053 * Used to allow the commenter to see their pending comment. 2054 * 2055 * @since 5.1.0 2056 * @since 5.7.0 The window within which the author email for an unapproved comment 2057 * can be retrieved was extended to 10 minutes. 2058 * 2059 * @return string The unapproved comment author's email (when supplied). 2060 */ 2061 function wp_get_unapproved_comment_author_email() { 2062 $commenter_email = ''; 2063 2064 if ( ! empty( $_GET['unapproved'] ) && ! empty( $_GET['moderation-hash'] ) ) { 2065 $comment_id = (int) $_GET['unapproved']; 2066 $comment = get_comment( $comment_id ); 2067 2068 if ( $comment && hash_equals( $_GET['moderation-hash'], wp_hash( $comment->comment_date_gmt ) ) ) { 2069 // The comment will only be viewable by the comment author for 10 minutes. 2070 $comment_preview_expires = strtotime( $comment->comment_date_gmt . '+10 minutes' ); 2071 2072 if ( time() < $comment_preview_expires ) { 2073 $commenter_email = $comment->comment_author_email; 2074 } 2075 } 2076 } 2077 2078 if ( ! $commenter_email ) { 2079 $commenter = wp_get_current_commenter(); 2080 $commenter_email = $commenter['comment_author_email']; 2081 } 2082 2083 return $commenter_email; 2084 } 2085 2086 /** 2087 * Inserts a comment into the database. 2088 * 2089 * @since 2.0.0 2090 * @since 4.4.0 Introduced the `$comment_meta` argument. 2091 * @since 5.5.0 Default value for `$comment_type` argument changed to `comment`. 2092 * 2093 * @global wpdb $wpdb WordPress database abstraction object. 2094 * 2095 * @param array $commentdata { 2096 * Array of arguments for inserting a new comment. 2097 * 2098 * @type string $comment_agent The HTTP user agent of the `$comment_author` when 2099 * the comment was submitted. Default empty. 2100 * @type int|string $comment_approved Whether the comment has been approved. Default 1. 2101 * @type string $comment_author The name of the author of the comment. Default empty. 2102 * @type string $comment_author_email The email address of the `$comment_author`. Default empty. 2103 * @type string $comment_author_IP The IP address of the `$comment_author`. Default empty. 2104 * @type string $comment_author_url The URL address of the `$comment_author`. Default empty. 2105 * @type string $comment_content The content of the comment. Default empty. 2106 * @type string $comment_date The date the comment was submitted. To set the date 2107 * manually, `$comment_date_gmt` must also be specified. 2108 * Default is the current time. 2109 * @type string $comment_date_gmt The date the comment was submitted in the GMT timezone. 2110 * Default is `$comment_date` in the site's GMT timezone. 2111 * @type int $comment_karma The karma of the comment. Default 0. 2112 * @type int $comment_parent ID of this comment's parent, if any. Default 0. 2113 * @type int $comment_post_ID ID of the post that relates to the comment, if any. 2114 * Default 0. 2115 * @type string $comment_type Comment type. Default 'comment'. 2116 * @type array $comment_meta Optional. Array of key/value pairs to be stored in commentmeta for the 2117 * new comment. 2118 * @type int $user_id ID of the user who submitted the comment. Default 0. 2119 * } 2120 * @return int|false The new comment's ID on success, false on failure. 2121 */ 2122 function wp_insert_comment( $commentdata ) { 2123 global $wpdb; 2124 2125 $data = wp_unslash( $commentdata ); 2126 2127 $comment_author = ! isset( $data['comment_author'] ) ? '' : $data['comment_author']; 2128 $comment_author_email = ! isset( $data['comment_author_email'] ) ? '' : $data['comment_author_email']; 2129 $comment_author_url = ! isset( $data['comment_author_url'] ) ? '' : $data['comment_author_url']; 2130 $comment_author_ip = ! isset( $data['comment_author_IP'] ) ? '' : $data['comment_author_IP']; 2131 2132 $comment_date = ! isset( $data['comment_date'] ) ? current_time( 'mysql' ) : $data['comment_date']; 2133 $comment_date_gmt = ! isset( $data['comment_date_gmt'] ) ? get_gmt_from_date( $comment_date ) : $data['comment_date_gmt']; 2134 2135 $comment_post_id = ! isset( $data['comment_post_ID'] ) ? 0 : $data['comment_post_ID']; 2136 $comment_content = ! isset( $data['comment_content'] ) ? '' : $data['comment_content']; 2137 $comment_karma = ! isset( $data['comment_karma'] ) ? 0 : $data['comment_karma']; 2138 $comment_approved = ! isset( $data['comment_approved'] ) ? 1 : $data['comment_approved']; 2139 $comment_agent = ! isset( $data['comment_agent'] ) ? '' : $data['comment_agent']; 2140 $comment_type = empty( $data['comment_type'] ) ? 'comment' : $data['comment_type']; 2141 $comment_parent = ! isset( $data['comment_parent'] ) ? 0 : $data['comment_parent']; 2142 2143 $user_id = ! isset( $data['user_id'] ) ? 0 : $data['user_id']; 2144 2145 $compacted = array( 2146 'comment_post_ID' => $comment_post_id, 2147 'comment_author_IP' => $comment_author_ip, 2148 ); 2149 2150 $compacted += compact( 2151 'comment_author', 2152 'comment_author_email', 2153 'comment_author_url', 2154 'comment_date', 2155 'comment_date_gmt', 2156 'comment_content', 2157 'comment_karma', 2158 'comment_approved', 2159 'comment_agent', 2160 'comment_type', 2161 'comment_parent', 2162 'user_id' 2163 ); 2164 2165 if ( ! $wpdb->insert( $wpdb->comments, $compacted ) ) { 2166 return false; 2167 } 2168 2169 $id = (int) $wpdb->insert_id; 2170 2171 if ( 1 === (int) $comment_approved ) { 2172 wp_update_comment_count( $comment_post_id ); 2173 2174 $data = array(); 2175 foreach ( array( 'server', 'gmt', 'blog' ) as $timezone ) { 2176 $data[] = "lastcommentmodified:$timezone"; 2177 } 2178 wp_cache_delete_multiple( $data, 'timeinfo' ); 2179 } 2180 2181 clean_comment_cache( $id ); 2182 2183 $comment = get_comment( $id ); 2184 2185 // If metadata is provided, store it. 2186 if ( isset( $commentdata['comment_meta'] ) && is_array( $commentdata['comment_meta'] ) ) { 2187 foreach ( $commentdata['comment_meta'] as $meta_key => $meta_value ) { 2188 add_comment_meta( $comment->comment_ID, $meta_key, $meta_value, true ); 2189 } 2190 } 2191 2192 /** 2193 * Fires immediately after a comment is inserted into the database. 2194 * 2195 * @since 2.8.0 2196 * 2197 * @param int $id The comment ID. 2198 * @param WP_Comment $comment Comment object. 2199 */ 2200 do_action( 'wp_insert_comment', $id, $comment ); 2201 2202 return $id; 2203 } 2204 2205 /** 2206 * Filters and sanitizes comment data. 2207 * 2208 * Sets the comment data 'filtered' field to true when finished. This can be 2209 * checked as to whether the comment should be filtered and to keep from 2210 * filtering the same comment more than once. 2211 * 2212 * @since 2.0.0 2213 * 2214 * @param array $commentdata Contains information on the comment. 2215 * @return array Parsed comment information. 2216 */ 2217 function wp_filter_comment( $commentdata ) { 2218 if ( isset( $commentdata['user_ID'] ) ) { 2219 /** 2220 * Filters the comment author's user ID before it is set. 2221 * 2222 * The first time this filter is evaluated, `user_ID` is checked 2223 * (for back-compat), followed by the standard `user_id` value. 2224 * 2225 * @since 1.5.0 2226 * 2227 * @param int $user_id The comment author's user ID. 2228 */ 2229 $commentdata['user_id'] = apply_filters( 'pre_user_id', $commentdata['user_ID'] ); 2230 } elseif ( isset( $commentdata['user_id'] ) ) { 2231 /** This filter is documented in wp-includes/comment.php */ 2232 $commentdata['user_id'] = apply_filters( 'pre_user_id', $commentdata['user_id'] ); 2233 } 2234 2235 /** 2236 * Filters the comment author's browser user agent before it is set. 2237 * 2238 * @since 1.5.0 2239 * 2240 * @param string $comment_agent The comment author's browser user agent. 2241 */ 2242 $commentdata['comment_agent'] = apply_filters( 'pre_comment_user_agent', ( $commentdata['comment_agent'] ?? '' ) ); 2243 /** This filter is documented in wp-includes/comment.php */ 2244 $commentdata['comment_author'] = apply_filters( 'pre_comment_author_name', $commentdata['comment_author'] ); 2245 /** 2246 * Filters the comment content before it is set. 2247 * 2248 * @since 1.5.0 2249 * 2250 * @param string $comment_content The comment content. 2251 */ 2252 $commentdata['comment_content'] = apply_filters( 'pre_comment_content', $commentdata['comment_content'] ); 2253 /** 2254 * Filters the comment author's IP address before it is set. 2255 * 2256 * @since 1.5.0 2257 * 2258 * @param string $comment_author_ip The comment author's IP address. 2259 */ 2260 $commentdata['comment_author_IP'] = apply_filters( 'pre_comment_user_ip', $commentdata['comment_author_IP'] ); 2261 /** This filter is documented in wp-includes/comment.php */ 2262 $commentdata['comment_author_url'] = apply_filters( 'pre_comment_author_url', $commentdata['comment_author_url'] ); 2263 /** This filter is documented in wp-includes/comment.php */ 2264 $commentdata['comment_author_email'] = apply_filters( 'pre_comment_author_email', $commentdata['comment_author_email'] ); 2265 2266 $commentdata['filtered'] = true; 2267 2268 return $commentdata; 2269 } 2270 2271 /** 2272 * Determines whether a comment should be blocked because of comment flood. 2273 * 2274 * @since 2.1.0 2275 * 2276 * @param bool $block Whether plugin has already blocked comment. 2277 * @param int $time_lastcomment Timestamp for last comment. 2278 * @param int $time_newcomment Timestamp for new comment. 2279 * @return bool Whether comment should be blocked. 2280 */ 2281 function wp_throttle_comment_flood( $block, $time_lastcomment, $time_newcomment ) { 2282 if ( $block ) { // A plugin has already blocked... we'll let that decision stand. 2283 return $block; 2284 } 2285 if ( ( $time_newcomment - $time_lastcomment ) < 15 ) { 2286 return true; 2287 } 2288 return false; 2289 } 2290 2291 /** 2292 * Adds a new comment to the database. 2293 * 2294 * Filters new comment to ensure that the fields are sanitized and valid before 2295 * inserting comment into database. Calls {@see 'comment_post'} action with comment ID 2296 * and whether comment is approved by WordPress. Also has {@see 'preprocess_comment'} 2297 * filter for processing the comment data before the function handles it. 2298 * 2299 * We use `REMOTE_ADDR` here directly. If you are behind a proxy, you should ensure 2300 * that it is properly set, such as in wp-config.php, for your environment. 2301 * 2302 * See {@link https://core.trac.wordpress.org/ticket/9235} 2303 * 2304 * @since 1.5.0 2305 * @since 4.3.0 Introduced the `comment_agent` and `comment_author_IP` arguments. 2306 * @since 4.7.0 The `$avoid_die` parameter was added, allowing the function 2307 * to return a WP_Error object instead of dying. 2308 * @since 5.5.0 The `$avoid_die` parameter was renamed to `$wp_error`. 2309 * @since 5.5.0 Introduced the `comment_type` argument. 2310 * 2311 * @see wp_insert_comment() 2312 * @global wpdb $wpdb WordPress database abstraction object. 2313 * 2314 * @param array $commentdata { 2315 * Comment data. 2316 * 2317 * @type string $comment_author The name of the comment author. 2318 * @type string $comment_author_email The comment author email address. 2319 * @type string $comment_author_url The comment author URL. 2320 * @type string $comment_content The content of the comment. 2321 * @type string $comment_date The date the comment was submitted. Default is the current time. 2322 * @type string $comment_date_gmt The date the comment was submitted in the GMT timezone. 2323 * Default is `$comment_date` in the GMT timezone. 2324 * @type string $comment_type Comment type. Default 'comment'. 2325 * @type int $comment_parent The ID of this comment's parent, if any. Default 0. 2326 * @type int $comment_post_ID The ID of the post that relates to the comment. 2327 * @type int $user_id The ID of the user who submitted the comment. Default 0. 2328 * @type int $user_ID Kept for backward-compatibility. Use `$user_id` instead. 2329 * @type string $comment_agent Comment author user agent. Default is the value of 'HTTP_USER_AGENT' 2330 * in the `$_SERVER` superglobal sent in the original request. 2331 * @type string $comment_author_IP Comment author IP address in IPv4 format. Default is the value of 2332 * 'REMOTE_ADDR' in the `$_SERVER` superglobal sent in the original request. 2333 * } 2334 * @param bool $wp_error Should errors be returned as WP_Error objects instead of 2335 * executing wp_die()? Default false. 2336 * @return int|false|WP_Error The ID of the comment on success, false or WP_Error on failure. 2337 */ 2338 function wp_new_comment( $commentdata, $wp_error = false ) { 2339 global $wpdb; 2340 2341 /* 2342 * Normalize `user_ID` to `user_id`, but pass the old key 2343 * to the `preprocess_comment` filter for backward compatibility. 2344 */ 2345 if ( isset( $commentdata['user_ID'] ) ) { 2346 $commentdata['user_ID'] = (int) $commentdata['user_ID']; 2347 $commentdata['user_id'] = $commentdata['user_ID']; 2348 } elseif ( isset( $commentdata['user_id'] ) ) { 2349 $commentdata['user_id'] = (int) $commentdata['user_id']; 2350 $commentdata['user_ID'] = $commentdata['user_id']; 2351 } 2352 2353 $prefiltered_user_id = ( isset( $commentdata['user_id'] ) ) ? (int) $commentdata['user_id'] : 0; 2354 2355 if ( ! isset( $commentdata['comment_author_IP'] ) ) { 2356 $commentdata['comment_author_IP'] = $_SERVER['REMOTE_ADDR']; 2357 } 2358 2359 if ( ! isset( $commentdata['comment_agent'] ) ) { 2360 $commentdata['comment_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? ''; 2361 } 2362 2363 /** 2364 * Filters a comment's data before it is sanitized and inserted into the database. 2365 * 2366 * @since 1.5.0 2367 * @since 5.6.0 Comment data includes the `comment_agent` and `comment_author_IP` values. 2368 * 2369 * @param array $commentdata Comment data. 2370 */ 2371 $commentdata = apply_filters( 'preprocess_comment', $commentdata ); 2372 2373 $commentdata['comment_post_ID'] = (int) $commentdata['comment_post_ID']; 2374 2375 // Normalize `user_ID` to `user_id` again, after the filter. 2376 if ( isset( $commentdata['user_ID'] ) && $prefiltered_user_id !== (int) $commentdata['user_ID'] ) { 2377 $commentdata['user_ID'] = (int) $commentdata['user_ID']; 2378 $commentdata['user_id'] = $commentdata['user_ID']; 2379 } elseif ( isset( $commentdata['user_id'] ) ) { 2380 $commentdata['user_id'] = (int) $commentdata['user_id']; 2381 $commentdata['user_ID'] = $commentdata['user_id']; 2382 } 2383 2384 $commentdata['comment_parent'] = isset( $commentdata['comment_parent'] ) ? absint( $commentdata['comment_parent'] ) : 0; 2385 2386 $parent_status = ( $commentdata['comment_parent'] > 0 ) ? wp_get_comment_status( $commentdata['comment_parent'] ) : ''; 2387 2388 $commentdata['comment_parent'] = ( 'approved' === $parent_status || 'unapproved' === $parent_status ) ? $commentdata['comment_parent'] : 0; 2389 2390 $commentdata['comment_author_IP'] = preg_replace( '/[^0-9a-fA-F:., ]/', '', $commentdata['comment_author_IP'] ); 2391 2392 $commentdata['comment_agent'] = substr( $commentdata['comment_agent'], 0, 254 ); 2393 2394 if ( empty( $commentdata['comment_date'] ) ) { 2395 $commentdata['comment_date'] = current_time( 'mysql' ); 2396 } 2397 2398 if ( empty( $commentdata['comment_date_gmt'] ) ) { 2399 $commentdata['comment_date_gmt'] = current_time( 'mysql', true ); 2400 } 2401 2402 if ( empty( $commentdata['comment_type'] ) ) { 2403 $commentdata['comment_type'] = 'comment'; 2404 } 2405 2406 $commentdata['comment_approved'] = wp_allow_comment( $commentdata, $wp_error ); 2407 2408 if ( is_wp_error( $commentdata['comment_approved'] ) ) { 2409 return $commentdata['comment_approved']; 2410 } 2411 2412 $commentdata = wp_filter_comment( $commentdata ); 2413 2414 if ( ! in_array( $commentdata['comment_approved'], array( 'trash', 'spam' ), true ) ) { 2415 // Validate the comment again after filters are applied to comment data. 2416 $commentdata['comment_approved'] = wp_check_comment_data( $commentdata ); 2417 } 2418 2419 if ( is_wp_error( $commentdata['comment_approved'] ) ) { 2420 return $commentdata['comment_approved']; 2421 } 2422 2423 $comment_id = wp_insert_comment( $commentdata ); 2424 2425 if ( ! $comment_id ) { 2426 $fields = array( 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_content' ); 2427 2428 foreach ( $fields as $field ) { 2429 if ( isset( $commentdata[ $field ] ) ) { 2430 $commentdata[ $field ] = $wpdb->strip_invalid_text_for_column( $wpdb->comments, $field, $commentdata[ $field ] ); 2431 } 2432 } 2433 2434 $commentdata = wp_filter_comment( $commentdata ); 2435 2436 $commentdata['comment_approved'] = wp_allow_comment( $commentdata, $wp_error ); 2437 if ( is_wp_error( $commentdata['comment_approved'] ) ) { 2438 return $commentdata['comment_approved']; 2439 } 2440 2441 $comment_id = wp_insert_comment( $commentdata ); 2442 if ( ! $comment_id ) { 2443 return false; 2444 } 2445 } 2446 2447 /** 2448 * Fires immediately after a comment is inserted into the database. 2449 * 2450 * @since 1.2.0 2451 * @since 4.5.0 The `$commentdata` parameter was added. 2452 * 2453 * @param int $comment_id The comment ID. 2454 * @param int|string $comment_approved 1 if the comment is approved, 0 if not, 'spam' if spam. 2455 * @param array $commentdata Comment data. 2456 */ 2457 do_action( 'comment_post', $comment_id, $commentdata['comment_approved'], $commentdata ); 2458 2459 return $comment_id; 2460 } 2461 2462 /** 2463 * Sends a comment moderation notification to the comment moderator. 2464 * 2465 * @since 4.4.0 2466 * 2467 * @param int $comment_id ID of the comment. 2468 * @return bool True on success, false on failure. 2469 */ 2470 function wp_new_comment_notify_moderator( $comment_id ) { 2471 $comment = get_comment( $comment_id ); 2472 2473 // Only send notifications for pending comments. 2474 $maybe_notify = ( '0' === $comment->comment_approved ); 2475 2476 /** This filter is documented in wp-includes/pluggable.php */ 2477 $maybe_notify = apply_filters( 'notify_moderator', $maybe_notify, $comment_id ); 2478 2479 if ( ! $maybe_notify ) { 2480 return false; 2481 } 2482 2483 return wp_notify_moderator( $comment_id ); 2484 } 2485 2486 /** 2487 * Sends a notification of a new comment to the post author. 2488 * 2489 * Uses the {@see 'notify_post_author'} filter to determine whether the post author 2490 * should be notified when a new comment is added, overriding site setting. 2491 * 2492 * @since 4.4.0 2493 * @since 7.1.0 The comment approval status is now checked before the 2494 * {@see 'notify_post_author'} filter, and invalid comment IDs 2495 * return false without firing the filter. 2496 * 2497 * @param int $comment_id Comment ID. 2498 * @return bool True on success, false on failure. 2499 */ 2500 function wp_new_comment_notify_postauthor( $comment_id ) { 2501 $comment = get_comment( $comment_id ); 2502 if ( ! ( $comment instanceof WP_Comment ) ) { 2503 return false; 2504 } 2505 $comment_id = (int) $comment->comment_ID; 2506 $is_note = ( 'note' === $comment->comment_type ); 2507 2508 /* 2509 * Determine the default notification behavior. Notes are eligible regardless 2510 * of approval status, based on the 'wp_notes_notify' option. Other comments 2511 * are only eligible once approved, based on the 'comments_notify' option. 2512 */ 2513 if ( $is_note ) { 2514 $maybe_notify = (bool) get_option( 'wp_notes_notify', 1 ); 2515 } elseif ( '1' !== $comment->comment_approved ) { 2516 $maybe_notify = false; 2517 } else { 2518 $maybe_notify = (bool) get_option( 'comments_notify' ); 2519 } 2520 2521 /** 2522 * Filters whether to send the post author new comment and note notification emails, 2523 * overriding the site settings and defaults. By default, notifications are sent for 2524 * all notes and for approved comments. 2525 * 2526 * @since 4.4.0 2527 * @since 7.1.0 Comment approval status is checked before this filter, 2528 * and the filter no longer fires for invalid comment IDs. 2529 * 2530 * @param bool $maybe_notify Whether to notify the post author about the new comment. 2531 * @param int $comment_id The ID of the comment for the notification. 2532 */ 2533 $maybe_notify = apply_filters( 'notify_post_author', $maybe_notify, $comment_id ); 2534 2535 /* 2536 * wp_notify_postauthor() checks if notifying the author of their own comment. 2537 * By default, it won't, but filters can override this. 2538 */ 2539 if ( ! $maybe_notify ) { 2540 return false; 2541 } 2542 2543 return wp_notify_postauthor( $comment_id ); 2544 } 2545 2546 /** 2547 * Send a notification to the post author when a new note is added via the REST API. 2548 * 2549 * @since 6.9.0 2550 * 2551 * @param WP_Comment $comment The comment object. 2552 */ 2553 function wp_new_comment_via_rest_notify_postauthor( $comment ) { 2554 if ( $comment instanceof WP_Comment && 'note' === $comment->comment_type ) { 2555 wp_new_comment_notify_postauthor( (int) $comment->comment_ID ); 2556 } 2557 } 2558 2559 /** 2560 * Sets the status of a comment. 2561 * 2562 * The {@see 'wp_set_comment_status'} action is called after the comment is handled. 2563 * If the comment status is not in the list, then false is returned. 2564 * 2565 * @since 1.0.0 2566 * 2567 * @global wpdb $wpdb WordPress database abstraction object. 2568 * 2569 * @param int|WP_Comment $comment_id Comment ID or WP_Comment object. 2570 * @param string $comment_status New comment status, either 'hold', 'approve', 'spam', or 'trash'. 2571 * @param bool $wp_error Whether to return a WP_Error object if there is a failure. Default false. 2572 * @return bool|WP_Error True on success, false or WP_Error on failure. 2573 */ 2574 function wp_set_comment_status( $comment_id, $comment_status, $wp_error = false ) { 2575 global $wpdb; 2576 2577 switch ( $comment_status ) { 2578 case 'hold': 2579 case '0': 2580 $status = '0'; 2581 break; 2582 case 'approve': 2583 case '1': 2584 $status = '1'; 2585 add_action( 'wp_set_comment_status', 'wp_new_comment_notify_postauthor' ); 2586 break; 2587 case 'spam': 2588 $status = 'spam'; 2589 break; 2590 case 'trash': 2591 $status = 'trash'; 2592 break; 2593 default: 2594 return false; 2595 } 2596 2597 $comment_old = clone get_comment( $comment_id ); 2598 2599 if ( ! $wpdb->update( $wpdb->comments, array( 'comment_approved' => $status ), array( 'comment_ID' => $comment_old->comment_ID ) ) ) { 2600 if ( $wp_error ) { 2601 return new WP_Error( 'db_update_error', __( 'Could not update comment status.' ), $wpdb->last_error ); 2602 } else { 2603 return false; 2604 } 2605 } 2606 2607 clean_comment_cache( $comment_old->comment_ID ); 2608 2609 $comment = get_comment( $comment_old->comment_ID ); 2610 2611 /** 2612 * Fires immediately after transitioning a comment's status from one to another in the database 2613 * and removing the comment from the object cache, but prior to all status transition hooks. 2614 * 2615 * @since 1.5.0 2616 * 2617 * @param string $comment_id Comment ID as a numeric string. 2618 * @param string $comment_status Current comment status. Possible values include 2619 * 'hold', '0', 'approve', '1', 'spam', and 'trash'. 2620 */ 2621 do_action( 'wp_set_comment_status', $comment->comment_ID, $comment_status ); 2622 2623 wp_transition_comment_status( $comment_status, $comment_old->comment_approved, $comment ); 2624 2625 wp_update_comment_count( $comment->comment_post_ID ); 2626 2627 return true; 2628 } 2629 2630 /** 2631 * Updates an existing comment in the database. 2632 * 2633 * Filters the comment and makes sure certain fields are valid before updating. 2634 * 2635 * @since 2.0.0 2636 * @since 4.9.0 Add updating comment meta during comment update. 2637 * @since 5.5.0 The `$wp_error` parameter was added. 2638 * @since 5.5.0 The return values for an invalid comment or post ID 2639 * were changed to false instead of 0. 2640 * 2641 * @global wpdb $wpdb WordPress database abstraction object. 2642 * 2643 * @param array $commentarr Contains information on the comment. 2644 * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. 2645 * @return int|false|WP_Error The value 1 if the comment was updated, 0 if not updated. 2646 * False or a WP_Error object on failure. 2647 */ 2648 function wp_update_comment( $commentarr, $wp_error = false ) { 2649 global $wpdb; 2650 2651 // First, get all of the original fields. 2652 $comment = get_comment( $commentarr['comment_ID'], ARRAY_A ); 2653 2654 if ( empty( $comment ) ) { 2655 if ( $wp_error ) { 2656 return new WP_Error( 'invalid_comment_id', __( 'Invalid comment ID.' ) ); 2657 } else { 2658 return false; 2659 } 2660 } 2661 2662 // Make sure that the comment post ID is valid (if specified). 2663 if ( ! empty( $commentarr['comment_post_ID'] ) && ! get_post( $commentarr['comment_post_ID'] ) ) { 2664 if ( $wp_error ) { 2665 return new WP_Error( 'invalid_post_id', __( 'Invalid post ID.' ) ); 2666 } else { 2667 return false; 2668 } 2669 } 2670 2671 $filter_comment = false; 2672 if ( ! has_filter( 'pre_comment_content', 'wp_filter_kses' ) ) { 2673 $filter_comment = ! user_can( $comment['user_id'] ?? 0, 'unfiltered_html' ); 2674 } 2675 2676 if ( $filter_comment ) { 2677 add_filter( 'pre_comment_content', 'wp_filter_kses' ); 2678 } 2679 2680 // Escape data pulled from DB. 2681 $comment = wp_slash( $comment ); 2682 2683 $old_status = $comment['comment_approved']; 2684 2685 // Merge old and new fields with new fields overwriting old ones. 2686 $commentarr = array_merge( $comment, $commentarr ); 2687 2688 $commentarr = wp_filter_comment( $commentarr ); 2689 2690 if ( $filter_comment ) { 2691 remove_filter( 'pre_comment_content', 'wp_filter_kses' ); 2692 } 2693 2694 // Now extract the merged array. 2695 $data = wp_unslash( $commentarr ); 2696 2697 /** 2698 * Filters the comment content before it is updated in the database. 2699 * 2700 * @since 1.5.0 2701 * 2702 * @param string $comment_content The comment data. 2703 */ 2704 $data['comment_content'] = apply_filters( 'comment_save_pre', $data['comment_content'] ); 2705 2706 $data['comment_date_gmt'] = get_gmt_from_date( $data['comment_date'] ); 2707 2708 if ( ! isset( $data['comment_approved'] ) ) { 2709 $data['comment_approved'] = 1; 2710 } elseif ( 'hold' === $data['comment_approved'] ) { 2711 $data['comment_approved'] = 0; 2712 } elseif ( 'approve' === $data['comment_approved'] ) { 2713 $data['comment_approved'] = 1; 2714 } 2715 2716 $comment_id = $data['comment_ID']; 2717 $comment_post_id = $data['comment_post_ID']; 2718 2719 /** 2720 * Filters the comment data immediately before it is updated in the database. 2721 * 2722 * Note: data being passed to the filter is already unslashed. 2723 * 2724 * @since 4.7.0 2725 * @since 5.5.0 Returning a WP_Error value from the filter will short-circuit comment update 2726 * and allow skipping further processing. 2727 * 2728 * @param array|WP_Error $data The new, processed comment data, or WP_Error. 2729 * @param array $comment The old, unslashed comment data. 2730 * @param array $commentarr The new, raw comment data. 2731 */ 2732 $data = apply_filters( 'wp_update_comment_data', $data, $comment, $commentarr ); 2733 2734 // Do not continue on failure. 2735 if ( is_wp_error( $data ) ) { 2736 if ( $wp_error ) { 2737 return $data; 2738 } else { 2739 return false; 2740 } 2741 } 2742 2743 $keys = array( 2744 'comment_post_ID', 2745 'comment_author', 2746 'comment_author_email', 2747 'comment_author_url', 2748 'comment_author_IP', 2749 'comment_date', 2750 'comment_date_gmt', 2751 'comment_content', 2752 'comment_karma', 2753 'comment_approved', 2754 'comment_agent', 2755 'comment_type', 2756 'comment_parent', 2757 'user_id', 2758 ); 2759 2760 $data = wp_array_slice_assoc( $data, $keys ); 2761 2762 $result = $wpdb->update( $wpdb->comments, $data, array( 'comment_ID' => $comment_id ) ); 2763 2764 if ( false === $result ) { 2765 if ( $wp_error ) { 2766 return new WP_Error( 'db_update_error', __( 'Could not update comment in the database.' ), $wpdb->last_error ); 2767 } else { 2768 return false; 2769 } 2770 } 2771 2772 // If metadata is provided, store it. 2773 if ( isset( $commentarr['comment_meta'] ) && is_array( $commentarr['comment_meta'] ) ) { 2774 foreach ( $commentarr['comment_meta'] as $meta_key => $meta_value ) { 2775 update_comment_meta( $comment_id, $meta_key, $meta_value ); 2776 } 2777 } 2778 2779 clean_comment_cache( $comment_id ); 2780 wp_update_comment_count( $comment_post_id ); 2781 2782 /** 2783 * Fires immediately after a comment is updated in the database. 2784 * 2785 * The hook also fires immediately before comment status transition hooks are fired. 2786 * 2787 * @since 1.2.0 2788 * @since 4.6.0 Added the `$data` parameter. 2789 * 2790 * @param int $comment_id The comment ID. 2791 * @param array $data Comment data. 2792 */ 2793 do_action( 'edit_comment', $comment_id, $data ); 2794 2795 $comment = get_comment( $comment_id ); 2796 2797 wp_transition_comment_status( $comment->comment_approved, $old_status, $comment ); 2798 2799 return $result; 2800 } 2801 2802 /** 2803 * Determines whether to defer comment counting. 2804 * 2805 * When setting $defer to true, all post comment counts will not be updated 2806 * until $defer is set to false. When $defer is set to false, then all 2807 * previously deferred updated post comment counts will then be automatically 2808 * updated without having to call wp_update_comment_count() after. 2809 * 2810 * @since 2.5.0 2811 * 2812 * @param bool $defer 2813 * @return bool Whether comment counting is deferred. 2814 */ 2815 function wp_defer_comment_counting( $defer = null ) { 2816 static $_defer = false; 2817 2818 if ( is_bool( $defer ) ) { 2819 $_defer = $defer; 2820 // Flush any deferred counts. 2821 if ( ! $defer ) { 2822 wp_update_comment_count( null, true ); 2823 } 2824 } 2825 2826 return $_defer; 2827 } 2828 2829 /** 2830 * Updates the comment count for post(s). 2831 * 2832 * When $do_deferred is false (is by default) and the comments have been set to 2833 * be deferred, the post_id will be added to a queue, which will be updated at a 2834 * later date and only updated once per post ID. 2835 * 2836 * If the comments have not be set up to be deferred, then the post will be 2837 * updated. When $do_deferred is set to true, then all previous deferred post 2838 * IDs will be updated along with the current $post_id. 2839 * 2840 * @since 2.1.0 2841 * 2842 * @see wp_update_comment_count_now() For what could cause a false return value 2843 * 2844 * @param int|null $post_id Post ID. 2845 * @param bool $do_deferred Optional. Whether to process previously deferred 2846 * post comment counts. Default false. 2847 * @return bool|null True on success, false on failure or if post with ID does 2848 * not exist. 2849 */ 2850 function wp_update_comment_count( $post_id, $do_deferred = false ) { 2851 static $_deferred = array(); 2852 2853 if ( empty( $post_id ) && ! $do_deferred ) { 2854 return false; 2855 } 2856 2857 if ( $do_deferred ) { 2858 $_deferred = array_unique( $_deferred ); 2859 foreach ( $_deferred as $i => $_post_id ) { 2860 wp_update_comment_count_now( $_post_id ); 2861 unset( $_deferred[ $i ] ); 2862 /** @todo Move this outside of the foreach and reset $_deferred to an array instead */ 2863 } 2864 } 2865 2866 if ( wp_defer_comment_counting() ) { 2867 $_deferred[] = $post_id; 2868 return true; 2869 } elseif ( $post_id ) { 2870 return wp_update_comment_count_now( $post_id ); 2871 } 2872 return null; 2873 } 2874 2875 /** 2876 * Updates the comment count for the post. 2877 * 2878 * @since 2.5.0 2879 * 2880 * @global wpdb $wpdb WordPress database abstraction object. 2881 * 2882 * @param int $post_id Post ID 2883 * @return bool True on success, false if the post does not exist. 2884 */ 2885 function wp_update_comment_count_now( $post_id ) { 2886 global $wpdb; 2887 2888 $post_id = (int) $post_id; 2889 2890 if ( ! $post_id ) { 2891 return false; 2892 } 2893 2894 wp_cache_delete( 'comments-0', 'counts' ); 2895 wp_cache_delete( "comments-{$post_id}", 'counts' ); 2896 2897 $post = get_post( $post_id ); 2898 2899 if ( ! $post ) { 2900 return false; 2901 } 2902 2903 $old = (int) $post->comment_count; 2904 2905 /** 2906 * Filters a post's comment count before it is updated in the database. 2907 * 2908 * @since 4.5.0 2909 * 2910 * @param int|null $new The new comment count. Default null. 2911 * @param int $old The old comment count. 2912 * @param int $post_id Post ID. 2913 */ 2914 $new = apply_filters( 'pre_wp_update_comment_count_now', null, $old, $post_id ); 2915 2916 if ( is_null( $new ) ) { 2917 $new = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type != 'note'", $post_id ) ); 2918 } else { 2919 $new = (int) $new; 2920 } 2921 2922 $wpdb->update( $wpdb->posts, array( 'comment_count' => $new ), array( 'ID' => $post_id ) ); 2923 2924 clean_post_cache( $post ); 2925 2926 /** 2927 * Fires immediately after a post's comment count is updated in the database. 2928 * 2929 * @since 2.3.0 2930 * 2931 * @param int $post_id Post ID. 2932 * @param int $new The new comment count. 2933 * @param int $old The old comment count. 2934 */ 2935 do_action( 'wp_update_comment_count', $post_id, $new, $old ); 2936 2937 /** This action is documented in wp-includes/post.php */ 2938 do_action( "edit_post_{$post->post_type}", $post_id, $post ); 2939 2940 /** This action is documented in wp-includes/post.php */ 2941 do_action( 'edit_post', $post_id, $post ); 2942 2943 return true; 2944 } 2945 2946 // 2947 // Ping and trackback functions. 2948 // 2949 2950 /** 2951 * Finds a pingback server URI based on the given URL. 2952 * 2953 * Checks the HTML for the rel="pingback" link and X-Pingback headers. It does 2954 * a check for the X-Pingback headers first and returns that, if available. 2955 * The check for the rel="pingback" has more overhead than just the header. 2956 * 2957 * @since 1.5.0 2958 * 2959 * @param string $url URL to ping. 2960 * @param string $deprecated Not Used. 2961 * @return string|false String containing URI on success, false on failure. 2962 */ 2963 function discover_pingback_server_uri( $url, $deprecated = '' ) { 2964 if ( ! empty( $deprecated ) ) { 2965 _deprecated_argument( __FUNCTION__, '2.7.0' ); 2966 } 2967 2968 $pingback_str_dquote = 'rel="pingback"'; 2969 $pingback_str_squote = 'rel=\'pingback\''; 2970 2971 /** @todo Should use Filter Extension or custom preg_match instead. */ 2972 $parsed_url = parse_url( $url ); 2973 2974 if ( ! isset( $parsed_url['host'] ) ) { // Not a URL. This should never happen. 2975 return false; 2976 } 2977 2978 // Do not search for a pingback server on our own uploads. 2979 $uploads_dir = wp_get_upload_dir(); 2980 if ( str_starts_with( $url, $uploads_dir['baseurl'] ) ) { 2981 return false; 2982 } 2983 2984 $response = wp_safe_remote_head( 2985 $url, 2986 array( 2987 'timeout' => 2, 2988 'httpversion' => '1.0', 2989 ) 2990 ); 2991 2992 if ( is_wp_error( $response ) ) { 2993 return false; 2994 } 2995 2996 if ( wp_remote_retrieve_header( $response, 'X-Pingback' ) ) { 2997 return wp_remote_retrieve_header( $response, 'X-Pingback' ); 2998 } 2999 3000 // Not an (x)html, sgml, or xml page, no use going further. 3001 if ( preg_match( '#(image|audio|video|model)/#is', wp_remote_retrieve_header( $response, 'Content-Type' ) ) ) { 3002 return false; 3003 } 3004 3005 // Now do a GET since we're going to look in the HTML headers (and we're sure it's not a binary file). 3006 $response = wp_safe_remote_get( 3007 $url, 3008 array( 3009 'timeout' => 2, 3010 'httpversion' => '1.0', 3011 ) 3012 ); 3013 3014 if ( is_wp_error( $response ) ) { 3015 return false; 3016 } 3017 3018 $contents = wp_remote_retrieve_body( $response ); 3019 3020 $pingback_link_offset_dquote = strpos( $contents, $pingback_str_dquote ); 3021 $pingback_link_offset_squote = strpos( $contents, $pingback_str_squote ); 3022 3023 if ( $pingback_link_offset_dquote || $pingback_link_offset_squote ) { 3024 $quote = ( $pingback_link_offset_dquote ) ? '"' : '\''; 3025 $pingback_link_offset = ( '"' === $quote ) ? $pingback_link_offset_dquote : $pingback_link_offset_squote; 3026 $pingback_href_pos = strpos( $contents, 'href=', $pingback_link_offset ); 3027 $pingback_href_start = $pingback_href_pos + 6; 3028 $pingback_href_end = strpos( $contents, $quote, $pingback_href_start ); 3029 $pingback_server_url_len = $pingback_href_end - $pingback_href_start; 3030 $pingback_server_url = substr( $contents, $pingback_href_start, $pingback_server_url_len ); 3031 3032 // We may find rel="pingback" but an incomplete pingback URL. 3033 if ( $pingback_server_url_len > 0 ) { // We got it! 3034 return $pingback_server_url; 3035 } 3036 } 3037 3038 return false; 3039 } 3040 3041 /** 3042 * Performs all pingbacks, enclosures, trackbacks, and sends to pingback services. 3043 * 3044 * @since 2.1.0 3045 * @since 5.6.0 Introduced `do_all_pings` action hook for individual services. 3046 */ 3047 function do_all_pings() { 3048 /** 3049 * Fires immediately after the `do_pings` event to hook services individually. 3050 * 3051 * @since 5.6.0 3052 */ 3053 do_action( 'do_all_pings' ); 3054 } 3055 3056 /** 3057 * Performs all pingbacks. 3058 * 3059 * @since 5.6.0 3060 */ 3061 function do_all_pingbacks() { 3062 $pings = get_posts( 3063 array( 3064 'post_type' => get_post_types(), 3065 'suppress_filters' => false, 3066 'nopaging' => true, 3067 'meta_key' => '_pingme', 3068 'fields' => 'ids', 3069 ) 3070 ); 3071 3072 foreach ( $pings as $ping ) { 3073 delete_post_meta( $ping, '_pingme' ); 3074 pingback( null, $ping ); 3075 } 3076 } 3077 3078 /** 3079 * Performs all enclosures. 3080 * 3081 * @since 5.6.0 3082 */ 3083 function do_all_enclosures() { 3084 $enclosures = get_posts( 3085 array( 3086 'post_type' => get_post_types(), 3087 'suppress_filters' => false, 3088 'nopaging' => true, 3089 'meta_key' => '_encloseme', 3090 'fields' => 'ids', 3091 ) 3092 ); 3093 3094 foreach ( $enclosures as $enclosure ) { 3095 delete_post_meta( $enclosure, '_encloseme' ); 3096 do_enclose( null, $enclosure ); 3097 } 3098 } 3099 3100 /** 3101 * Performs all trackbacks. 3102 * 3103 * @since 5.6.0 3104 */ 3105 function do_all_trackbacks() { 3106 $trackbacks = get_posts( 3107 array( 3108 'post_type' => get_post_types(), 3109 'suppress_filters' => false, 3110 'nopaging' => true, 3111 'meta_key' => '_trackbackme', 3112 'fields' => 'ids', 3113 ) 3114 ); 3115 3116 foreach ( $trackbacks as $trackback ) { 3117 delete_post_meta( $trackback, '_trackbackme' ); 3118 do_trackbacks( $trackback ); 3119 } 3120 } 3121 3122 /** 3123 * Performs trackbacks. 3124 * 3125 * @since 1.5.0 3126 * @since 4.7.0 `$post` can be a WP_Post object. 3127 * 3128 * @global wpdb $wpdb WordPress database abstraction object. 3129 * 3130 * @param int|WP_Post $post Post ID or object to do trackbacks on. 3131 * @return void|false Returns false on failure. 3132 */ 3133 function do_trackbacks( $post ) { 3134 global $wpdb; 3135 3136 $post = get_post( $post ); 3137 3138 if ( ! $post ) { 3139 return false; 3140 } 3141 3142 $to_ping = get_to_ping( $post ); 3143 $pinged = get_pung( $post ); 3144 3145 if ( empty( $to_ping ) ) { 3146 $wpdb->update( $wpdb->posts, array( 'to_ping' => '' ), array( 'ID' => $post->ID ) ); 3147 return; 3148 } 3149 3150 if ( empty( $post->post_excerpt ) ) { 3151 /** This filter is documented in wp-includes/post-template.php */ 3152 $excerpt = apply_filters( 'the_content', $post->post_content, $post->ID ); 3153 } else { 3154 /** This filter is documented in wp-includes/post-template.php */ 3155 $excerpt = apply_filters( 'the_excerpt', $post->post_excerpt ); 3156 } 3157 3158 $excerpt = str_replace( ']]>', ']]>', $excerpt ); 3159 $excerpt = wp_html_excerpt( $excerpt, 252, '…' ); 3160 3161 /** This filter is documented in wp-includes/post-template.php */ 3162 $post_title = apply_filters( 'the_title', $post->post_title, $post->ID ); 3163 $post_title = strip_tags( $post_title ); 3164 3165 foreach ( (array) $to_ping as $tb_ping ) { 3166 $tb_ping = trim( $tb_ping ); 3167 if ( ! in_array( $tb_ping, $pinged, true ) ) { 3168 trackback( $tb_ping, $post_title, $excerpt, $post->ID ); 3169 $pinged[] = $tb_ping; 3170 } else { 3171 $wpdb->query( 3172 $wpdb->prepare( 3173 "UPDATE $wpdb->posts SET to_ping = TRIM(REPLACE(to_ping, %s, '')) WHERE ID = %d", 3174 $tb_ping, 3175 $post->ID 3176 ) 3177 ); 3178 } 3179 } 3180 } 3181 3182 /** 3183 * Sends pings to all of the ping site services. 3184 * 3185 * @since 1.2.0 3186 * 3187 * @param int $post_id Post ID. 3188 * @return int Same post ID as provided. 3189 */ 3190 function generic_ping( $post_id = 0 ) { 3191 $services = get_option( 'ping_sites' ); 3192 3193 $services = explode( "\n", $services ); 3194 foreach ( (array) $services as $service ) { 3195 $service = trim( $service ); 3196 if ( '' !== $service ) { 3197 weblog_ping( $service ); 3198 } 3199 } 3200 3201 return $post_id; 3202 } 3203 3204 /** 3205 * Determines whether pings should be disabled for the current environment. 3206 * 3207 * By default, all pings (outgoing pingbacks, trackbacks, and ping service 3208 * notifications, as well as incoming pingbacks and trackbacks) are disabled 3209 * for non-production environments ('local', 'development', 'staging'). 3210 * 3211 * @since 7.1.0 3212 * 3213 * @return bool True if pings should be disabled, false otherwise. 3214 */ 3215 function wp_should_disable_pings_for_environment() { 3216 $environment_type = wp_get_environment_type(); 3217 $should_disable = 'production' !== $environment_type; 3218 3219 /** 3220 * Filters whether pings should be disabled for the current environment. 3221 * 3222 * Returning false re-enables pings in non-production environments. 3223 * Returning true disables pings even in production. 3224 * 3225 * @since 7.1.0 3226 * 3227 * @param bool $should_disable Whether pings should be disabled. Default true 3228 * for non-production environments, false for production. 3229 * @param string $environment_type The current environment type as returned by 3230 * wp_get_environment_type(). 3231 */ 3232 return apply_filters( 'wp_should_disable_pings_for_environment', $should_disable, $environment_type ); 3233 } 3234 3235 /** 3236 * Removes outgoing ping callbacks in non-production environments. 3237 * 3238 * Hooked to `do_all_pings` at priority 1 so it runs before the default 3239 * priority 10 callbacks. Does not remove `do_all_enclosures`. 3240 * 3241 * @since 7.1.0 3242 */ 3243 function wp_maybe_disable_outgoing_pings_for_environment() { 3244 if ( wp_should_disable_pings_for_environment() ) { 3245 remove_action( 'do_all_pings', 'do_all_pingbacks' ); 3246 remove_action( 'do_all_pings', 'do_all_trackbacks' ); 3247 remove_action( 'do_all_pings', 'generic_ping' ); 3248 } 3249 } 3250 3251 /** 3252 * Rejects incoming trackbacks in non-production environments. 3253 * 3254 * Hooked to `pre_trackback_post` which fires in `wp-trackback.php` before the 3255 * trackback is processed. Calls `trackback_response()` which sends an XML error 3256 * response and terminates the request. 3257 * 3258 * @since 7.1.0 3259 */ 3260 function wp_maybe_disable_trackback_for_environment() { 3261 if ( wp_should_disable_pings_for_environment() ) { 3262 trackback_response( 1, __( 'Trackbacks are disabled in non-production environments.' ) ); 3263 } 3264 } 3265 3266 /** 3267 * Removes the pingback XML-RPC method in non-production environments. 3268 * 3269 * @since 7.1.0 3270 * 3271 * @param string[] $methods An array of XML-RPC methods, keyed by their methodName. 3272 * @return string[] Modified array of XML-RPC methods. 3273 */ 3274 function wp_maybe_disable_xmlrpc_pingback_for_environment( $methods ) { 3275 if ( wp_should_disable_pings_for_environment() ) { 3276 unset( $methods['pingback.ping'] ); 3277 } 3278 3279 return $methods; 3280 } 3281 3282 /** 3283 * Pings back the links found in a post. 3284 * 3285 * @since 0.71 3286 * @since 4.7.0 `$post` can be a WP_Post object. 3287 * @since 6.8.0 Returns an array of pingback statuses indexed by link. 3288 * 3289 * @param string $content Post content to check for links. If empty will retrieve from post. 3290 * @param int|WP_Post $post Post ID or object. 3291 * @return array<string, bool> An array of pingback statuses indexed by link. 3292 */ 3293 function pingback( $content, $post ) { 3294 require_once ABSPATH . WPINC . '/class-IXR.php'; 3295 require_once ABSPATH . WPINC . '/class-wp-http-ixr-client.php'; 3296 3297 // Original code by Mort (http://mort.mine.nu:8080). 3298 $post_links = array(); 3299 3300 $post = get_post( $post ); 3301 3302 if ( ! $post ) { 3303 return array(); 3304 } 3305 3306 $pung = get_pung( $post ); 3307 3308 if ( empty( $content ) ) { 3309 $content = $post->post_content; 3310 } 3311 3312 /* 3313 * Step 1. 3314 * Parsing the post, external links (if any) are stored in the $post_links array. 3315 */ 3316 $post_links_temp = wp_extract_urls( $content ); 3317 3318 $ping_status = array(); 3319 /* 3320 * Step 2. 3321 * Walking through the links array. 3322 * First we get rid of links pointing to sites, not to specific files. 3323 * Example: 3324 * http://dummy-weblog.org 3325 * http://dummy-weblog.org/ 3326 * http://dummy-weblog.org/post.php 3327 * We don't wanna ping first and second types, even if they have a valid <link/>. 3328 */ 3329 foreach ( (array) $post_links_temp as $link_test ) { 3330 // If we haven't pung it already and it isn't a link to itself. 3331 if ( ! in_array( $link_test, $pung, true ) && ( url_to_postid( $link_test ) !== $post->ID ) 3332 // Also, let's never ping local attachments. 3333 && ! is_local_attachment( $link_test ) 3334 ) { 3335 $test = parse_url( $link_test ); 3336 if ( $test ) { 3337 if ( isset( $test['query'] ) ) { 3338 $post_links[] = $link_test; 3339 } elseif ( isset( $test['path'] ) && ( '/' !== $test['path'] ) && ( '' !== $test['path'] ) ) { 3340 $post_links[] = $link_test; 3341 } 3342 } 3343 } 3344 } 3345 3346 $post_links = array_unique( $post_links ); 3347 3348 /** 3349 * Fires just before pinging back links found in a post. 3350 * 3351 * @since 2.0.0 3352 * 3353 * @param string[] $post_links Array of link URLs to be checked (passed by reference). 3354 * @param string[] $pung Array of link URLs already pinged (passed by reference). 3355 * @param int $post_id The post ID. 3356 */ 3357 do_action_ref_array( 'pre_ping', array( &$post_links, &$pung, $post->ID ) ); 3358 3359 foreach ( (array) $post_links as $pagelinkedto ) { 3360 $pingback_server_url = discover_pingback_server_uri( $pagelinkedto ); 3361 3362 if ( $pingback_server_url ) { 3363 // Allow an additional 60 seconds for each pingback to complete. 3364 if ( function_exists( 'set_time_limit' ) ) { 3365 set_time_limit( 60 ); 3366 } 3367 3368 // Now, the RPC call. 3369 $pagelinkedfrom = get_permalink( $post ); 3370 3371 // Using a timeout of 3 seconds should be enough to cover slow servers. 3372 $client = new WP_HTTP_IXR_Client( $pingback_server_url ); 3373 $client->timeout = 3; 3374 /** 3375 * Filters the user agent sent when pinging-back a URL. 3376 * 3377 * @since 2.9.0 3378 * 3379 * @param string $concat_useragent The user agent concatenated with ' -- WordPress/' 3380 * and the WordPress version. 3381 * @param string $useragent The useragent. 3382 * @param string $pingback_server_url The server URL being linked to. 3383 * @param string $pagelinkedto URL of page linked to. 3384 * @param string $pagelinkedfrom URL of page linked from. 3385 */ 3386 $client->useragent = apply_filters( 'pingback_useragent', $client->useragent . ' -- WordPress/' . get_bloginfo( 'version' ), $client->useragent, $pingback_server_url, $pagelinkedto, $pagelinkedfrom ); 3387 // When set to true, this outputs debug messages by itself. 3388 $client->debug = false; 3389 3390 $status = $client->query( 'pingback.ping', $pagelinkedfrom, $pagelinkedto ); 3391 3392 if ( $status // Ping registered. 3393 || ( isset( $client->error->code ) && 48 === $client->error->code ) // Already registered. 3394 ) { 3395 add_ping( $post, $pagelinkedto ); 3396 } 3397 $ping_status[ $pagelinkedto ] = $status; 3398 } 3399 } 3400 3401 return $ping_status; 3402 } 3403 3404 /** 3405 * Checks whether blog is public before returning sites. 3406 * 3407 * @since 2.1.0 3408 * 3409 * @param mixed $sites Will return if blog is public, will not return if not public. 3410 * @return mixed Empty string if blog is not public, returns $sites, if site is public. 3411 */ 3412 function privacy_ping_filter( $sites ) { 3413 if ( '0' !== get_option( 'blog_public' ) ) { 3414 return $sites; 3415 } else { 3416 return ''; 3417 } 3418 } 3419 3420 /** 3421 * Sends a Trackback. 3422 * 3423 * Updates database when sending trackback to prevent duplicates. 3424 * 3425 * @since 0.71 3426 * 3427 * @global wpdb $wpdb WordPress database abstraction object. 3428 * 3429 * @param string $trackback_url URL to send trackbacks. 3430 * @param string $title Title of post. 3431 * @param string $excerpt Excerpt of post. 3432 * @param int $post_id Post ID. 3433 * @return int|false|null Database query from update. 3434 */ 3435 function trackback( $trackback_url, $title, $excerpt, $post_id ) { 3436 global $wpdb; 3437 3438 if ( empty( $trackback_url ) ) { 3439 return null; 3440 } 3441 3442 $options = array(); 3443 $options['timeout'] = 10; 3444 $options['body'] = array( 3445 'title' => $title, 3446 'url' => get_permalink( $post_id ), 3447 'blog_name' => get_option( 'blogname' ), 3448 'excerpt' => $excerpt, 3449 ); 3450 3451 $response = wp_safe_remote_post( $trackback_url, $options ); 3452 3453 if ( is_wp_error( $response ) ) { 3454 return null; 3455 } 3456 3457 $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET pinged = CONCAT(pinged, '\n', %s) WHERE ID = %d", $trackback_url, $post_id ) ); 3458 return $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET to_ping = TRIM(REPLACE(to_ping, %s, '')) WHERE ID = %d", $trackback_url, $post_id ) ); 3459 } 3460 3461 /** 3462 * Sends a pingback. 3463 * 3464 * @since 1.2.0 3465 * 3466 * @param string $server Host of blog to connect to. 3467 * @param string $path Path to send the ping. 3468 */ 3469 function weblog_ping( $server = '', $path = '' ) { 3470 require_once ABSPATH . WPINC . '/class-IXR.php'; 3471 require_once ABSPATH . WPINC . '/class-wp-http-ixr-client.php'; 3472 3473 // Using a timeout of 3 seconds should be enough to cover slow servers. 3474 $client = new WP_HTTP_IXR_Client( $server, ( ( ! strlen( trim( $path ) ) || ( '/' === $path ) ) ? false : $path ) ); 3475 $client->timeout = 3; 3476 $client->useragent .= ' -- WordPress/' . get_bloginfo( 'version' ); 3477 3478 // When set to true, this outputs debug messages by itself. 3479 $client->debug = false; 3480 $home = trailingslashit( home_url() ); 3481 if ( ! $client->query( 'weblogUpdates.extendedPing', get_option( 'blogname' ), $home, get_bloginfo( 'rss2_url' ) ) ) { // Then try a normal ping. 3482 $client->query( 'weblogUpdates.ping', get_option( 'blogname' ), $home ); 3483 } 3484 } 3485 3486 /** 3487 * Default filter attached to pingback_ping_source_uri to validate the pingback's Source URI. 3488 * 3489 * @since 3.5.1 3490 * 3491 * @see wp_http_validate_url() 3492 * 3493 * @param string $source_uri 3494 * @return string Validated source URI. 3495 */ 3496 function pingback_ping_source_uri( $source_uri ) { 3497 return (string) wp_http_validate_url( $source_uri ); 3498 } 3499 3500 /** 3501 * Default filter attached to xmlrpc_pingback_error. 3502 * 3503 * Returns a generic pingback error code unless the error code is 48, 3504 * which reports that the pingback is already registered. 3505 * 3506 * @since 3.5.1 3507 * 3508 * @link https://www.hixie.ch/specs/pingback/pingback#TOC3 3509 * 3510 * @param IXR_Error $ixr_error 3511 * @return IXR_Error 3512 */ 3513 function xmlrpc_pingback_error( $ixr_error ) { 3514 if ( 48 === $ixr_error->code ) { 3515 return $ixr_error; 3516 } 3517 return new IXR_Error( 0, '' ); 3518 } 3519 3520 // 3521 // Cache. 3522 // 3523 3524 /** 3525 * Removes a comment from the object cache. 3526 * 3527 * @since 2.3.0 3528 * 3529 * @param int|array $ids Comment ID or an array of comment IDs to remove from cache. 3530 */ 3531 function clean_comment_cache( $ids ) { 3532 $comment_ids = (array) $ids; 3533 wp_cache_delete_multiple( $comment_ids, 'comment' ); 3534 foreach ( $comment_ids as $id ) { 3535 /** 3536 * Fires immediately after a comment has been removed from the object cache. 3537 * 3538 * @since 4.5.0 3539 * 3540 * @param int $id Comment ID. 3541 */ 3542 do_action( 'clean_comment_cache', $id ); 3543 } 3544 3545 wp_cache_set_comments_last_changed(); 3546 } 3547 3548 /** 3549 * Updates the comment cache of given comments. 3550 * 3551 * Will add the comments in $comments to the cache. If comment ID already exists 3552 * in the comment cache then it will not be updated. The comment is added to the 3553 * cache using the comment group with the key using the ID of the comments. 3554 * 3555 * @since 2.3.0 3556 * @since 4.4.0 Introduced the `$update_meta_cache` parameter. 3557 * 3558 * @param WP_Comment[] $comments Array of comment objects 3559 * @param bool $update_meta_cache Whether to update commentmeta cache. Default true. 3560 */ 3561 function update_comment_cache( $comments, $update_meta_cache = true ) { 3562 $data = array(); 3563 foreach ( (array) $comments as $comment ) { 3564 $data[ $comment->comment_ID ] = $comment; 3565 } 3566 wp_cache_add_multiple( $data, 'comment' ); 3567 3568 if ( $update_meta_cache ) { 3569 // Avoid `wp_list_pluck()` in case `$comments` is passed by reference. 3570 $comment_ids = array(); 3571 foreach ( $comments as $comment ) { 3572 $comment_ids[] = $comment->comment_ID; 3573 } 3574 update_meta_cache( 'comment', $comment_ids ); 3575 } 3576 } 3577 3578 /** 3579 * Adds any comments from the given IDs to the cache that do not already exist in cache. 3580 * 3581 * @since 4.4.0 3582 * @since 6.1.0 This function is no longer marked as "private". 3583 * @since 6.3.0 Use wp_lazyload_comment_meta() for lazy-loading of comment meta. 3584 * 3585 * @see update_comment_cache() 3586 * @global wpdb $wpdb WordPress database abstraction object. 3587 * 3588 * @param int[] $comment_ids Array of comment IDs. 3589 * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. 3590 */ 3591 function _prime_comment_caches( $comment_ids, $update_meta_cache = true ) { 3592 global $wpdb; 3593 3594 $non_cached_ids = _get_non_cached_ids( $comment_ids, 'comment' ); 3595 if ( ! empty( $non_cached_ids ) ) { 3596 $fresh_comments = $wpdb->get_results( sprintf( "SELECT $wpdb->comments.* FROM $wpdb->comments WHERE comment_ID IN (%s)", implode( ',', array_map( 'intval', $non_cached_ids ) ) ) ); 3597 3598 update_comment_cache( $fresh_comments, false ); 3599 } 3600 3601 if ( $update_meta_cache ) { 3602 wp_lazyload_comment_meta( $comment_ids ); 3603 } 3604 } 3605 3606 // 3607 // Internal. 3608 // 3609 3610 /** 3611 * Closes comments on old posts on the fly, without any extra DB queries. Hooked to the_posts. 3612 * 3613 * @since 2.7.0 3614 * @access private 3615 * 3616 * @param WP_Post[] $posts Array of post objects. 3617 * @param WP_Query $query Query object. 3618 * @return WP_Post[] 3619 */ 3620 function _close_comments_for_old_posts( $posts, $query ) { 3621 if ( empty( $posts ) || ! $query->is_singular() || ! get_option( 'close_comments_for_old_posts' ) ) { 3622 return $posts; 3623 } 3624 3625 /** 3626 * Filters the list of post types to automatically close comments for. 3627 * 3628 * @since 3.2.0 3629 * 3630 * @param string[] $post_types An array of post type names. 3631 */ 3632 $post_types = apply_filters( 'close_comments_for_post_types', array( 'post' ) ); 3633 if ( ! in_array( $posts[0]->post_type, $post_types, true ) ) { 3634 return $posts; 3635 } 3636 3637 $days_old = (int) get_option( 'close_comments_days_old' ); 3638 if ( ! $days_old ) { 3639 return $posts; 3640 } 3641 3642 if ( time() - strtotime( $posts[0]->post_date_gmt ) > ( $days_old * DAY_IN_SECONDS ) ) { 3643 $posts[0]->comment_status = 'closed'; 3644 $posts[0]->ping_status = 'closed'; 3645 } 3646 3647 return $posts; 3648 } 3649 3650 /** 3651 * Closes comments on an old post. Hooked to comments_open and pings_open. 3652 * 3653 * @since 2.7.0 3654 * @access private 3655 * 3656 * @param bool $open Comments open or closed. 3657 * @param int $post_id Post ID. 3658 * @return bool $open 3659 */ 3660 function _close_comments_for_old_post( $open, $post_id ) { 3661 if ( ! $open ) { 3662 return $open; 3663 } 3664 3665 if ( ! get_option( 'close_comments_for_old_posts' ) ) { 3666 return $open; 3667 } 3668 3669 $days_old = (int) get_option( 'close_comments_days_old' ); 3670 if ( ! $days_old ) { 3671 return $open; 3672 } 3673 3674 $post = get_post( $post_id ); 3675 3676 /** This filter is documented in wp-includes/comment.php */ 3677 $post_types = apply_filters( 'close_comments_for_post_types', array( 'post' ) ); 3678 if ( ! in_array( $post->post_type, $post_types, true ) ) { 3679 return $open; 3680 } 3681 3682 // Undated drafts should not show up as comments closed. 3683 if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) { 3684 return $open; 3685 } 3686 3687 if ( time() - strtotime( $post->post_date_gmt ) > ( $days_old * DAY_IN_SECONDS ) ) { 3688 return false; 3689 } 3690 3691 return $open; 3692 } 3693 3694 /** 3695 * Handles the submission of a comment, usually posted to wp-comments-post.php via a comment form. 3696 * 3697 * This function expects unslashed data, as opposed to functions such as `wp_new_comment()` which 3698 * expect slashed data. 3699 * 3700 * @since 4.4.0 3701 * 3702 * @param array $comment_data { 3703 * Comment data. 3704 * 3705 * @type string|int $comment_post_ID The ID of the post that relates to the comment. 3706 * @type string $author The name of the comment author. 3707 * @type string $email The comment author email address. 3708 * @type string $url The comment author URL. 3709 * @type string $comment The content of the comment. 3710 * @type string|int $comment_parent The ID of this comment's parent, if any. Default 0. 3711 * @type string $_wp_unfiltered_html_comment The nonce value for allowing unfiltered HTML. 3712 * } 3713 * @return WP_Comment|WP_Error A WP_Comment object on success, a WP_Error object on failure. 3714 */ 3715 function wp_handle_comment_submission( $comment_data ) { 3716 $comment_post_id = 0; 3717 $comment_author = ''; 3718 $comment_author_email = ''; 3719 $comment_author_url = ''; 3720 $comment_content = ''; 3721 $comment_parent = 0; 3722 $user_id = 0; 3723 3724 if ( isset( $comment_data['comment_post_ID'] ) ) { 3725 $comment_post_id = (int) $comment_data['comment_post_ID']; 3726 } 3727 if ( isset( $comment_data['author'] ) && is_string( $comment_data['author'] ) ) { 3728 $comment_author = trim( strip_tags( $comment_data['author'] ) ); 3729 } 3730 if ( isset( $comment_data['email'] ) && is_string( $comment_data['email'] ) ) { 3731 $comment_author_email = trim( $comment_data['email'] ); 3732 } 3733 if ( isset( $comment_data['url'] ) && is_string( $comment_data['url'] ) ) { 3734 $comment_author_url = trim( $comment_data['url'] ); 3735 } 3736 if ( isset( $comment_data['comment'] ) && is_string( $comment_data['comment'] ) ) { 3737 $comment_content = trim( $comment_data['comment'] ); 3738 } 3739 if ( isset( $comment_data['comment_parent'] ) ) { 3740 $comment_parent = absint( $comment_data['comment_parent'] ); 3741 $comment_parent_object = get_comment( $comment_parent ); 3742 3743 if ( 3744 0 !== $comment_parent && 3745 ( 3746 ! $comment_parent_object instanceof WP_Comment || 3747 0 === (int) $comment_parent_object->comment_approved 3748 ) 3749 ) { 3750 /** 3751 * Fires when a comment reply is attempted to an unapproved comment. 3752 * 3753 * @since 6.2.0 3754 * 3755 * @param int $comment_post_id Post ID. 3756 * @param int $comment_parent Parent comment ID. 3757 */ 3758 do_action( 'comment_reply_to_unapproved_comment', $comment_post_id, $comment_parent ); 3759 3760 return new WP_Error( 'comment_reply_to_unapproved_comment', __( 'Sorry, replies to unapproved comments are not allowed.' ), 403 ); 3761 } 3762 } 3763 3764 $post = get_post( $comment_post_id ); 3765 3766 if ( empty( $post->comment_status ) ) { 3767 3768 /** 3769 * Fires when a comment is attempted on a post that does not exist. 3770 * 3771 * @since 1.5.0 3772 * 3773 * @param int $comment_post_id Post ID. 3774 */ 3775 do_action( 'comment_id_not_found', $comment_post_id ); 3776 3777 return new WP_Error( 'comment_id_not_found' ); 3778 3779 } 3780 3781 // get_post_status() will get the parent status for attachments. 3782 $status = get_post_status( $post ); 3783 3784 if ( ( 'private' === $status ) && ! current_user_can( 'read_post', $comment_post_id ) ) { 3785 return new WP_Error( 'comment_id_not_found' ); 3786 } 3787 3788 $status_obj = get_post_status_object( $status ); 3789 3790 if ( ! comments_open( $comment_post_id ) ) { 3791 3792 /** 3793 * Fires when a comment is attempted on a post that has comments closed. 3794 * 3795 * @since 1.5.0 3796 * 3797 * @param int $comment_post_id Post ID. 3798 */ 3799 do_action( 'comment_closed', $comment_post_id ); 3800 3801 return new WP_Error( 'comment_closed', __( 'Sorry, comments are closed for this item.' ), 403 ); 3802 3803 } elseif ( 'trash' === $status ) { 3804 3805 /** 3806 * Fires when a comment is attempted on a trashed post. 3807 * 3808 * @since 2.9.0 3809 * 3810 * @param int $comment_post_id Post ID. 3811 */ 3812 do_action( 'comment_on_trash', $comment_post_id ); 3813 3814 return new WP_Error( 'comment_on_trash' ); 3815 3816 } elseif ( ! $status_obj->public && ! $status_obj->private ) { 3817 3818 /** 3819 * Fires when a comment is attempted on a post in draft mode. 3820 * 3821 * @since 1.5.1 3822 * 3823 * @param int $comment_post_id Post ID. 3824 */ 3825 do_action( 'comment_on_draft', $comment_post_id ); 3826 3827 if ( current_user_can( 'read_post', $comment_post_id ) ) { 3828 return new WP_Error( 'comment_on_draft', __( 'Sorry, comments are not allowed for this item.' ), 403 ); 3829 } else { 3830 return new WP_Error( 'comment_on_draft' ); 3831 } 3832 } elseif ( post_password_required( $comment_post_id ) ) { 3833 3834 /** 3835 * Fires when a comment is attempted on a password-protected post. 3836 * 3837 * @since 2.9.0 3838 * 3839 * @param int $comment_post_id Post ID. 3840 */ 3841 do_action( 'comment_on_password_protected', $comment_post_id ); 3842 3843 return new WP_Error( 'comment_on_password_protected' ); 3844 3845 } else { 3846 /** 3847 * Fires before a comment is posted. 3848 * 3849 * @since 2.8.0 3850 * 3851 * @param int $comment_post_id Post ID. 3852 */ 3853 do_action( 'pre_comment_on_post', $comment_post_id ); 3854 } 3855 3856 // If the user is logged in. 3857 $user = wp_get_current_user(); 3858 if ( $user->exists() ) { 3859 if ( empty( $user->display_name ) ) { 3860 $user->display_name = $user->user_login; 3861 } 3862 3863 $comment_author = $user->display_name; 3864 $comment_author_email = $user->user_email; 3865 $comment_author_url = $user->user_url; 3866 $user_id = $user->ID; 3867 3868 if ( current_user_can( 'unfiltered_html' ) ) { 3869 if ( ! isset( $comment_data['_wp_unfiltered_html_comment'] ) 3870 || ! wp_verify_nonce( $comment_data['_wp_unfiltered_html_comment'], 'unfiltered-html-comment_' . $comment_post_id ) 3871 ) { 3872 kses_remove_filters(); // Start with a clean slate. 3873 kses_init_filters(); // Set up the filters. 3874 remove_filter( 'pre_comment_content', 'wp_filter_post_kses' ); 3875 add_filter( 'pre_comment_content', 'wp_filter_kses' ); 3876 } 3877 } 3878 } else { 3879 if ( get_option( 'comment_registration' ) ) { 3880 return new WP_Error( 'not_logged_in', __( 'Sorry, you must be logged in to comment.' ), 403 ); 3881 } 3882 } 3883 3884 $comment_type = 'comment'; 3885 3886 if ( get_option( 'require_name_email' ) && ! $user->exists() ) { 3887 if ( '' === $comment_author_email || '' === $comment_author ) { 3888 return new WP_Error( 'require_name_email', __( '<strong>Error:</strong> Please fill the required fields.' ), 200 ); 3889 } elseif ( ! is_email( $comment_author_email ) ) { 3890 return new WP_Error( 'require_valid_email', __( '<strong>Error:</strong> Please enter a valid email address.' ), 200 ); 3891 } 3892 } 3893 3894 $commentdata = array( 3895 'comment_post_ID' => $comment_post_id, 3896 ); 3897 3898 $commentdata += compact( 3899 'comment_author', 3900 'comment_author_email', 3901 'comment_author_url', 3902 'comment_content', 3903 'comment_type', 3904 'comment_parent', 3905 'user_id' 3906 ); 3907 3908 /** 3909 * Filters whether an empty comment should be allowed. 3910 * 3911 * @since 5.1.0 3912 * 3913 * @param bool $allow_empty_comment Whether to allow empty comments. Default false. 3914 * @param array $commentdata Array of comment data to be sent to wp_insert_comment(). 3915 */ 3916 $allow_empty_comment = apply_filters( 'allow_empty_comment', false, $commentdata ); 3917 if ( '' === $comment_content && ! $allow_empty_comment ) { 3918 return new WP_Error( 'require_valid_comment', __( '<strong>Error:</strong> Please type your comment text.' ), 200 ); 3919 } 3920 3921 $check_max_lengths = wp_check_comment_data_max_lengths( $commentdata ); 3922 if ( is_wp_error( $check_max_lengths ) ) { 3923 return $check_max_lengths; 3924 } 3925 3926 $comment_id = wp_new_comment( wp_slash( $commentdata ), true ); 3927 if ( is_wp_error( $comment_id ) ) { 3928 return $comment_id; 3929 } 3930 3931 if ( ! $comment_id ) { 3932 return new WP_Error( 'comment_save_error', __( '<strong>Error:</strong> The comment could not be saved. Please try again later.' ), 500 ); 3933 } 3934 3935 return get_comment( $comment_id ); 3936 } 3937 3938 /** 3939 * Registers the personal data exporter for comments. 3940 * 3941 * @since 4.9.6 3942 * 3943 * @param array[] $exporters An array of personal data exporters. 3944 * @return array[] An array of personal data exporters. 3945 */ 3946 function wp_register_comment_personal_data_exporter( $exporters ) { 3947 $exporters['wordpress-comments'] = array( 3948 'exporter_friendly_name' => __( 'WordPress Comments' ), 3949 'callback' => 'wp_comments_personal_data_exporter', 3950 ); 3951 3952 return $exporters; 3953 } 3954 3955 /** 3956 * Finds and exports personal data associated with an email address from the comments table. 3957 * 3958 * @since 4.9.6 3959 * 3960 * @param string $email_address The comment author email address. 3961 * @param int $page Comment page number. 3962 * @return array { 3963 * An array of personal data. 3964 * 3965 * @type array[] $data An array of personal data arrays. 3966 * @type bool $done Whether the exporter is finished. 3967 * } 3968 */ 3969 function wp_comments_personal_data_exporter( $email_address, $page = 1 ) { 3970 // Limit us to 500 comments at a time to avoid timing out. 3971 $number = 500; 3972 $page = (int) $page; 3973 3974 $data_to_export = array(); 3975 3976 $comments = get_comments( 3977 array( 3978 'author_email' => $email_address, 3979 'number' => $number, 3980 'paged' => $page, 3981 'orderby' => 'comment_ID', 3982 'order' => 'ASC', 3983 'update_comment_meta_cache' => false, 3984 ) 3985 ); 3986 3987 $comment_prop_to_export = array( 3988 'comment_author' => __( 'Comment Author' ), 3989 'comment_author_email' => __( 'Comment Author Email' ), 3990 'comment_author_url' => __( 'Comment Author URL' ), 3991 'comment_author_IP' => __( 'Comment Author IP' ), 3992 'comment_agent' => __( 'Comment Author User Agent' ), 3993 'comment_date' => __( 'Comment Date' ), 3994 'comment_content' => __( 'Comment Content' ), 3995 'comment_link' => __( 'Comment URL' ), 3996 ); 3997 3998 foreach ( (array) $comments as $comment ) { 3999 $comment_data_to_export = array(); 4000 4001 foreach ( $comment_prop_to_export as $key => $name ) { 4002 $value = ''; 4003 4004 switch ( $key ) { 4005 case 'comment_author': 4006 case 'comment_author_email': 4007 case 'comment_author_url': 4008 case 'comment_author_IP': 4009 case 'comment_agent': 4010 case 'comment_date': 4011 $value = $comment->{$key}; 4012 break; 4013 4014 case 'comment_content': 4015 $value = get_comment_text( $comment->comment_ID ); 4016 break; 4017 4018 case 'comment_link': 4019 $value = get_comment_link( $comment->comment_ID ); 4020 $value = sprintf( 4021 '<a href="%s" target="_blank">%s</a>', 4022 esc_url( $value ), 4023 esc_html( $value ) 4024 ); 4025 break; 4026 } 4027 4028 if ( ! empty( $value ) ) { 4029 $comment_data_to_export[] = array( 4030 'name' => $name, 4031 'value' => $value, 4032 ); 4033 } 4034 } 4035 4036 $data_to_export[] = array( 4037 'group_id' => 'comments', 4038 'group_label' => __( 'Comments' ), 4039 'group_description' => __( 'User’s comment data.' ), 4040 'item_id' => "comment-{$comment->comment_ID}", 4041 'data' => $comment_data_to_export, 4042 ); 4043 } 4044 4045 $done = count( $comments ) < $number; 4046 4047 return array( 4048 'data' => $data_to_export, 4049 'done' => $done, 4050 ); 4051 } 4052 4053 /** 4054 * Registers the personal data eraser for comments. 4055 * 4056 * @since 4.9.6 4057 * 4058 * @param array $erasers An array of personal data erasers. 4059 * @return array An array of personal data erasers. 4060 */ 4061 function wp_register_comment_personal_data_eraser( $erasers ) { 4062 $erasers['wordpress-comments'] = array( 4063 'eraser_friendly_name' => __( 'WordPress Comments' ), 4064 'callback' => 'wp_comments_personal_data_eraser', 4065 ); 4066 4067 return $erasers; 4068 } 4069 4070 /** 4071 * Erases personal data associated with an email address from the comments table. 4072 * 4073 * @since 4.9.6 4074 * 4075 * @global wpdb $wpdb WordPress database abstraction object. 4076 * 4077 * @param string $email_address The comment author email address. 4078 * @param int $page Comment page number. 4079 * @return array { 4080 * Data removal results. 4081 * 4082 * @type bool $items_removed Whether items were actually removed. 4083 * @type bool $items_retained Whether items were retained. 4084 * @type string[] $messages An array of messages to add to the personal data export file. 4085 * @type bool $done Whether the eraser is finished. 4086 * } 4087 */ 4088 function wp_comments_personal_data_eraser( $email_address, $page = 1 ) { 4089 global $wpdb; 4090 4091 if ( empty( $email_address ) ) { 4092 return array( 4093 'items_removed' => false, 4094 'items_retained' => false, 4095 'messages' => array(), 4096 'done' => true, 4097 ); 4098 } 4099 4100 // Limit us to 500 comments at a time to avoid timing out. 4101 $number = 500; 4102 $page = (int) $page; 4103 $items_removed = false; 4104 $items_retained = false; 4105 4106 $comments = get_comments( 4107 array( 4108 'author_email' => $email_address, 4109 'number' => $number, 4110 'paged' => $page, 4111 'orderby' => 'comment_ID', 4112 'order' => 'ASC', 4113 'include_unapproved' => true, 4114 ) 4115 ); 4116 4117 /* translators: Name of a comment's author after being anonymized. */ 4118 $anon_author = __( 'Anonymous' ); 4119 $messages = array(); 4120 4121 foreach ( (array) $comments as $comment ) { 4122 $anonymized_comment = array(); 4123 $anonymized_comment['comment_agent'] = ''; 4124 $anonymized_comment['comment_author'] = $anon_author; 4125 $anonymized_comment['comment_author_email'] = ''; 4126 $anonymized_comment['comment_author_IP'] = wp_privacy_anonymize_data( 'ip', $comment->comment_author_IP ); 4127 $anonymized_comment['comment_author_url'] = ''; 4128 $anonymized_comment['user_id'] = 0; 4129 4130 $comment_id = (int) $comment->comment_ID; 4131 4132 /** 4133 * Filters whether to anonymize the comment. 4134 * 4135 * @since 4.9.6 4136 * 4137 * @param bool|string $anon_message Whether to apply the comment anonymization (bool) or a custom 4138 * message (string). Default true. 4139 * @param WP_Comment $comment WP_Comment object. 4140 * @param array $anonymized_comment Anonymized comment data. 4141 */ 4142 $anon_message = apply_filters( 'wp_anonymize_comment', true, $comment, $anonymized_comment ); 4143 4144 if ( true !== $anon_message ) { 4145 if ( $anon_message && is_string( $anon_message ) ) { 4146 $messages[] = esc_html( $anon_message ); 4147 } else { 4148 /* translators: %d: Comment ID. */ 4149 $messages[] = sprintf( __( 'Comment %d contains personal data but could not be anonymized.' ), $comment_id ); 4150 } 4151 4152 $items_retained = true; 4153 4154 continue; 4155 } 4156 4157 $args = array( 4158 'comment_ID' => $comment_id, 4159 ); 4160 4161 $updated = $wpdb->update( $wpdb->comments, $anonymized_comment, $args ); 4162 4163 if ( $updated ) { 4164 $items_removed = true; 4165 clean_comment_cache( $comment_id ); 4166 } else { 4167 $items_retained = true; 4168 } 4169 } 4170 4171 $done = count( $comments ) < $number; 4172 4173 return array( 4174 'items_removed' => $items_removed, 4175 'items_retained' => $items_retained, 4176 'messages' => $messages, 4177 'done' => $done, 4178 ); 4179 } 4180 4181 /** 4182 * Sets the last changed time for the 'comment' cache group. 4183 * 4184 * @since 5.0.0 4185 */ 4186 function wp_cache_set_comments_last_changed() { 4187 wp_cache_set_last_changed( 'comment' ); 4188 } 4189 4190 /** 4191 * Updates the comment type for a batch of comments. 4192 * 4193 * @since 5.5.0 4194 * 4195 * @global wpdb $wpdb WordPress database abstraction object. 4196 */ 4197 function _wp_batch_update_comment_type() { 4198 global $wpdb; 4199 4200 $lock_name = 'update_comment_type.lock'; 4201 4202 // Try to lock. 4203 $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_name, time() ) ); 4204 4205 if ( ! $lock_result ) { 4206 $lock_result = get_option( $lock_name ); 4207 4208 // Bail if we were unable to create a lock, or if the existing lock is still valid. 4209 if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) { 4210 wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_update_comment_type_batch' ); 4211 return; 4212 } 4213 } 4214 4215 // Update the lock, as by this point we've definitely got a lock, just need to fire the actions. 4216 update_option( $lock_name, time() ); 4217 4218 // Check if there's still an empty comment type. 4219 $empty_comment_type = $wpdb->get_var( 4220 "SELECT comment_ID FROM $wpdb->comments 4221 WHERE comment_type = '' 4222 LIMIT 1" 4223 ); 4224 4225 // No empty comment type, we're done here. 4226 if ( ! $empty_comment_type ) { 4227 update_option( 'finished_updating_comment_type', true ); 4228 delete_option( $lock_name ); 4229 return; 4230 } 4231 4232 // Empty comment type found? We'll need to run this script again. 4233 wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_update_comment_type_batch' ); 4234 4235 /** 4236 * Filters the comment batch size for updating the comment type. 4237 * 4238 * @since 5.5.0 4239 * 4240 * @param int $comment_batch_size The comment batch size. Default 100. 4241 */ 4242 $comment_batch_size = (int) apply_filters( 'wp_update_comment_type_batch_size', 100 ); 4243 4244 // Get the IDs of the comments to update. 4245 $comment_ids = $wpdb->get_col( 4246 $wpdb->prepare( 4247 "SELECT comment_ID 4248 FROM {$wpdb->comments} 4249 WHERE comment_type = '' 4250 ORDER BY comment_ID DESC 4251 LIMIT %d", 4252 $comment_batch_size 4253 ) 4254 ); 4255 4256 if ( $comment_ids ) { 4257 $comment_id_list = implode( ',', $comment_ids ); 4258 4259 // Update the `comment_type` field value to be `comment` for the next batch of comments. 4260 $wpdb->query( 4261 "UPDATE {$wpdb->comments} 4262 SET comment_type = 'comment' 4263 WHERE comment_type = '' 4264 AND comment_ID IN ({$comment_id_list})" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 4265 ); 4266 4267 // Make sure to clean the comment cache. 4268 clean_comment_cache( $comment_ids ); 4269 } 4270 4271 delete_option( $lock_name ); 4272 } 4273 4274 /** 4275 * In order to avoid the _wp_batch_update_comment_type() job being accidentally removed, 4276 * check that it's still scheduled while we haven't finished updating comment types. 4277 * 4278 * @ignore 4279 * @since 5.5.0 4280 */ 4281 function _wp_check_for_scheduled_update_comment_type() { 4282 if ( ! get_option( 'finished_updating_comment_type' ) && ! wp_next_scheduled( 'wp_update_comment_type_batch' ) ) { 4283 wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_update_comment_type_batch' ); 4284 } 4285 } 4286 4287 /** 4288 * Register initial note status meta. 4289 * 4290 * @since 6.9.0 4291 */ 4292 function wp_create_initial_comment_meta() { 4293 register_meta( 4294 'comment', 4295 '_wp_note_status', 4296 array( 4297 'type' => 'string', 4298 'description' => __( 'Note resolution status' ), 4299 'single' => true, 4300 'show_in_rest' => array( 4301 'schema' => array( 4302 'type' => 'string', 4303 'enum' => array( 'resolved', 'reopen' ), 4304 ), 4305 ), 4306 'auth_callback' => function ( $allowed, $meta_key, $object_id ) { 4307 return current_user_can( 'edit_comment', $object_id ); 4308 }, 4309 ) 4310 ); 4311 } 4312 4313 /** 4314 * Strips inline note markers from rendered block output. 4315 * 4316 * Inline notes - notes anchored to a text selection within a block rather than 4317 * the whole block - are anchored in raw block content with 4318 * `<mark class="wp-note" data-id="N">...</mark>` so the marker survives edits, 4319 * but the public HTML should not expose note metadata. This filter unwraps the 4320 * marker entirely - dropping the `<mark>` open tag and its matching closer while 4321 * keeping the marked text - so nothing leaks to the front end. The raw 4322 * `post_content` (and the REST `raw` view, revisions, exports) keeps the marker 4323 * so the editor can re-attach it on reload. 4324 * 4325 * Only note markers are unwrapped: {@see WP_HTML_Tag_Processor::has_class()} 4326 * matches the `wp-note` class by exact token, so a `<mark>` a user or plugin 4327 * added (e.g. a `core/text-color` highlight, or an unrelated `wp-note-foo` 4328 * class) is never flagged and survives byte-for-byte with all of its attributes 4329 * intact. A naive regex would be wrong here: a `\bwp-note\b` word boundary also 4330 * matches `wp-note-foo`, which is why the class check goes through the HTML API 4331 * instead. 4332 * 4333 * The HTML API has no public token-removal method yet, so an anonymous 4334 * {@see WP_HTML_Tag_Processor} subclass unwraps each note `<mark>` and its 4335 * matching closer directly on the parsed token stream. Walking tokens - rather 4336 * than matching `<mark>` with a regex - means a `</mark>`-looking sequence inside 4337 * a comment or attribute value can never be mistaken for a real tag, and a 4338 * nesting stack keeps each note opener paired with its own closer so overlapping 4339 * notes and any user highlight `<mark>` left intact still resolve correctly. 4340 * 4341 * The low-level {@see WP_HTML_Tag_Processor} is used deliberately, rather than 4342 * the tree-building {@see WP_HTML_Processor}. Note markers live in user-editable 4343 * content, so the markup is not guaranteed to be well formed. On certain 4344 * ill-formed nesting the tree builder aborts, which would leave note markers - 4345 * and their metadata - in the rendered output. Scanning tokens instead removes 4346 * every `wp-note` marker it encounters and degrades gracefully: an unbalanced or 4347 * stray tag is left exactly as it was rather than corrupting surrounding markup. 4348 * 4349 * @since 7.1.0 4350 * 4351 * @param string $block_content Rendered block HTML. 4352 * @return string Block HTML with `wp-note` markers unwrapped. 4353 */ 4354 function wp_strip_inline_note_markers( $block_content ) { 4355 if ( ! str_contains( $block_content, 'wp-note' ) ) { 4356 return $block_content; 4357 } 4358 4359 /* 4360 * Anonymous subclass exposing token removal, which WP_HTML_Tag_Processor 4361 * does not provide publicly yet. Removing the current token via its bookmark 4362 * span unwraps the `<mark>` (opener or closer) while keeping the text it 4363 * wraps. 4364 */ 4365 $processor = new class( $block_content ) extends WP_HTML_Tag_Processor { 4366 /** 4367 * Removes the current token, keeping any text it wraps. 4368 */ 4369 public function remove_token(): void { 4370 // Always called after next_tag() returned true, so the bookmark is set. 4371 $this->set_bookmark( 'here' ); 4372 $span = $this->bookmarks['here']; 4373 4374 $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, '' ); 4375 } 4376 }; 4377 4378 /* 4379 * Walk every `<mark>`, tracking note nesting on a stack so each note opener 4380 * pairs with its own closer, and unwrap only the note markers. 4381 */ 4382 $mark_stack = array(); 4383 $query = array( 4384 'tag_name' => 'MARK', 4385 'tag_closers' => 'visit', 4386 ); 4387 while ( $processor->next_tag( $query ) ) { 4388 if ( $processor->is_tag_closer() ) { 4389 $is_note = array_pop( $mark_stack ); 4390 } else { 4391 $is_note = $processor->has_class( 'wp-note' ); 4392 $mark_stack[] = $is_note; 4393 } 4394 4395 if ( true === $is_note ) { 4396 $processor->remove_token(); 4397 } 4398 } 4399 4400 return $processor->get_updated_html(); 4401 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Jul 25 08:20:20 2026 | Cross-referenced by PHPXref |