[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> comment.php (source)

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


Generated : Fri Oct 23 08:20:34 2026 Cross-referenced by PHPXref