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


Generated : Sat Oct 3 08:20:34 2026 Cross-referenced by PHPXref