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


Generated : Sun Aug 16 08:20:24 2026 Cross-referenced by PHPXref