WordPress 3.8
[autoinstalls/wordpress.git] / wp-includes / comment.php
1 <?php
2 /**
3  * Manages WordPress comments
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 comment moderation is set in the administration, then all comments,
13  * regardless of their type and whitelist will be set to false. If the number of
14  * links exceeds the amount in the administration, then the check fails. If any
15  * of the parameter contents match the blacklist of words, then the check fails.
16  *
17  * If the number of links exceeds the amount in the administration, then the
18  * check fails. If any of the parameter contents match the blacklist of words,
19  * then the check fails.
20  *
21  * If the comment author was approved before, then the comment is
22  * automatically whitelisted.
23  *
24  * If none of the checks fail, then the failback is to set the check to pass
25  * (return true).
26  *
27  * @since 1.2.0
28  * @uses $wpdb
29  *
30  * @param string $author Comment Author's name
31  * @param string $email Comment Author's email
32  * @param string $url Comment Author's URL
33  * @param string $comment Comment contents
34  * @param string $user_ip Comment Author's IP address
35  * @param string $user_agent Comment Author's User Agent
36  * @param string $comment_type Comment type, either user submitted comment,
37  *              trackback, or pingback
38  * @return bool Whether the checks passed (true) and the comments should be
39  *              displayed or set to moderated
40  */
41 function check_comment($author, $email, $url, $comment, $user_ip, $user_agent, $comment_type) {
42         global $wpdb;
43
44         if ( 1 == get_option('comment_moderation') )
45                 return false; // If moderation is set to manual
46
47         /** This filter is documented in wp-includes/comment-template.php */
48         $comment = apply_filters( 'comment_text', $comment );
49
50         // Check # of external links
51         if ( $max_links = get_option( 'comment_max_links' ) ) {
52                 $num_links = preg_match_all( '/<a [^>]*href/i', $comment, $out );
53                 /**
54                  * Filter the maximum number of links allowed in a comment.
55                  *
56                  * @since 3.0.0
57                  *
58                  * @param int    $num_links The number of links allowed.
59                  * @param string $url       Comment author's URL. Included in allowed links total.
60                  */
61                 $num_links = apply_filters( 'comment_max_links_url', $num_links, $url );
62                 if ( $num_links >= $max_links )
63                         return false;
64         }
65
66         $mod_keys = trim(get_option('moderation_keys'));
67         if ( !empty($mod_keys) ) {
68                 $words = explode("\n", $mod_keys );
69
70                 foreach ( (array) $words as $word) {
71                         $word = trim($word);
72
73                         // Skip empty lines
74                         if ( empty($word) )
75                                 continue;
76
77                         // Do some escaping magic so that '#' chars in the
78                         // spam words don't break things:
79                         $word = preg_quote($word, '#');
80
81                         $pattern = "#$word#i";
82                         if ( preg_match($pattern, $author) ) return false;
83                         if ( preg_match($pattern, $email) ) return false;
84                         if ( preg_match($pattern, $url) ) return false;
85                         if ( preg_match($pattern, $comment) ) return false;
86                         if ( preg_match($pattern, $user_ip) ) return false;
87                         if ( preg_match($pattern, $user_agent) ) return false;
88                 }
89         }
90
91         // Comment whitelisting:
92         if ( 1 == get_option('comment_whitelist')) {
93                 if ( 'trackback' != $comment_type && 'pingback' != $comment_type && $author != '' && $email != '' ) {
94                         // expected_slashed ($author, $email)
95                         $ok_to_comment = $wpdb->get_var("SELECT comment_approved FROM $wpdb->comments WHERE comment_author = '$author' AND comment_author_email = '$email' and comment_approved = '1' LIMIT 1");
96                         if ( ( 1 == $ok_to_comment ) &&
97                                 ( empty($mod_keys) || false === strpos( $email, $mod_keys) ) )
98                                         return true;
99                         else
100                                 return false;
101                 } else {
102                         return false;
103                 }
104         }
105         return true;
106 }
107
108 /**
109  * Retrieve the approved comments for post $post_id.
110  *
111  * @since 2.0.0
112  * @uses $wpdb
113  *
114  * @param int $post_id The ID of the post
115  * @return array $comments The approved comments
116  */
117 function get_approved_comments($post_id) {
118         global $wpdb;
119         return $wpdb->get_results($wpdb->prepare("SELECT * FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' ORDER BY comment_date", $post_id));
120 }
121
122 /**
123  * Retrieves comment data given a comment ID or comment object.
124  *
125  * If an object is passed then the comment data will be cached and then returned
126  * after being passed through a filter. If the comment is empty, then the global
127  * comment variable will be used, if it is set.
128  *
129  * @since 2.0.0
130  * @uses $wpdb
131  *
132  * @param object|string|int $comment Comment to retrieve.
133  * @param string $output Optional. OBJECT or ARRAY_A or ARRAY_N constants.
134  * @return object|array|null Depends on $output value.
135  */
136 function get_comment(&$comment, $output = OBJECT) {
137         global $wpdb;
138         $null = null;
139
140         if ( empty($comment) ) {
141                 if ( isset($GLOBALS['comment']) )
142                         $_comment = & $GLOBALS['comment'];
143                 else
144                         $_comment = null;
145         } elseif ( is_object($comment) ) {
146                 wp_cache_add($comment->comment_ID, $comment, 'comment');
147                 $_comment = $comment;
148         } else {
149                 if ( isset($GLOBALS['comment']) && ($GLOBALS['comment']->comment_ID == $comment) ) {
150                         $_comment = & $GLOBALS['comment'];
151                 } elseif ( ! $_comment = wp_cache_get($comment, 'comment') ) {
152                         $_comment = $wpdb->get_row($wpdb->prepare("SELECT * FROM $wpdb->comments WHERE comment_ID = %d LIMIT 1", $comment));
153                         if ( ! $_comment )
154                                 return $null;
155                         wp_cache_add($_comment->comment_ID, $_comment, 'comment');
156                 }
157         }
158
159         /**
160          * Fires after a comment is retrieved.
161          *
162          * @since 2.3.0
163          *
164          * @param mixed $_comment Comment data.
165          */
166         $_comment = apply_filters( 'get_comment', $_comment );
167
168         if ( $output == OBJECT ) {
169                 return $_comment;
170         } elseif ( $output == ARRAY_A ) {
171                 $__comment = get_object_vars($_comment);
172                 return $__comment;
173         } elseif ( $output == ARRAY_N ) {
174                 $__comment = array_values(get_object_vars($_comment));
175                 return $__comment;
176         } else {
177                 return $_comment;
178         }
179 }
180
181 /**
182  * Retrieve a list of comments.
183  *
184  * The comment list can be for the blog as a whole or for an individual post.
185  *
186  * The list of comment arguments are 'status', 'orderby', 'comment_date_gmt',
187  * 'order', 'number', 'offset', and 'post_id'.
188  *
189  * @since 2.7.0
190  * @uses $wpdb
191  *
192  * @param mixed $args Optional. Array or string of options to override defaults.
193  * @return array List of comments.
194  */
195 function get_comments( $args = '' ) {
196         $query = new WP_Comment_Query;
197         return $query->query( $args );
198 }
199
200 /**
201  * WordPress Comment Query class.
202  *
203  * @since 3.1.0
204  */
205 class WP_Comment_Query {
206         /**
207          * Metadata query container
208          *
209          * @since 3.5.0
210          * @access public
211          * @var object WP_Meta_Query
212          */
213         var $meta_query = false;
214
215         /**
216          * Date query container
217          *
218          * @since 3.7.0
219          * @access public
220          * @var object WP_Date_Query
221          */
222         var $date_query = false;
223
224         /**
225          * Execute the query
226          *
227          * @since 3.1.0
228          *
229          * @param string|array $query_vars
230          * @return int|array
231          */
232         function query( $query_vars ) {
233                 global $wpdb;
234
235                 $defaults = array(
236                         'author_email' => '',
237                         'ID' => '',
238                         'karma' => '',
239                         'number' => '',
240                         'offset' => '',
241                         'orderby' => '',
242                         'order' => 'DESC',
243                         'parent' => '',
244                         'post_ID' => '',
245                         'post_id' => 0,
246                         'post_author' => '',
247                         'post_name' => '',
248                         'post_parent' => '',
249                         'post_status' => '',
250                         'post_type' => '',
251                         'status' => '',
252                         'type' => '',
253                         'user_id' => '',
254                         'search' => '',
255                         'count' => false,
256                         'meta_key' => '',
257                         'meta_value' => '',
258                         'meta_query' => '',
259                         'date_query' => null, // See WP_Date_Query
260                 );
261
262                 $groupby = '';
263
264                 $this->query_vars = wp_parse_args( $query_vars, $defaults );
265
266                 // Parse meta query
267                 $this->meta_query = new WP_Meta_Query();
268                 $this->meta_query->parse_query_vars( $this->query_vars );
269
270                 /**
271                  * Fires before comments are retrieved.
272                  *
273                  * @since 3.1.0
274                  *
275                  * @param WP_Comment_Query &$this Current instance of WP_Comment_Query, passed by reference.
276                  */
277                 do_action_ref_array( 'pre_get_comments', array( &$this ) );
278                 extract( $this->query_vars, EXTR_SKIP );
279
280                 // $args can be whatever, only use the args defined in defaults to compute the key
281                 $key = md5( serialize( compact(array_keys($defaults)) )  );
282                 $last_changed = wp_cache_get( 'last_changed', 'comment' );
283                 if ( ! $last_changed ) {
284                         $last_changed = microtime();
285                         wp_cache_set( 'last_changed', $last_changed, 'comment' );
286                 }
287                 $cache_key = "get_comments:$key:$last_changed";
288
289                 if ( $cache = wp_cache_get( $cache_key, 'comment' ) )
290                         return $cache;
291
292                 $post_id = absint($post_id);
293
294                 if ( 'hold' == $status )
295                         $approved = "comment_approved = '0'";
296                 elseif ( 'approve' == $status )
297                         $approved = "comment_approved = '1'";
298                 elseif ( ! empty( $status ) && 'all' != $status )
299                         $approved = $wpdb->prepare( "comment_approved = %s", $status );
300                 else
301                         $approved = "( comment_approved = '0' OR comment_approved = '1' )";
302
303                 $order = ( 'ASC' == strtoupper($order) ) ? 'ASC' : 'DESC';
304
305                 if ( ! empty( $orderby ) ) {
306                         $ordersby = is_array($orderby) ? $orderby : preg_split('/[,\s]/', $orderby);
307                         $allowed_keys = array(
308                                 'comment_agent',
309                                 'comment_approved',
310                                 'comment_author',
311                                 'comment_author_email',
312                                 'comment_author_IP',
313                                 'comment_author_url',
314                                 'comment_content',
315                                 'comment_date',
316                                 'comment_date_gmt',
317                                 'comment_ID',
318                                 'comment_karma',
319                                 'comment_parent',
320                                 'comment_post_ID',
321                                 'comment_type',
322                                 'user_id',
323                         );
324                         if ( ! empty( $this->query_vars['meta_key'] ) ) {
325                                 $allowed_keys[] = $this->query_vars['meta_key'];
326                                 $allowed_keys[] = 'meta_value';
327                                 $allowed_keys[] = 'meta_value_num';
328                         }
329                         $ordersby = array_intersect( $ordersby, $allowed_keys );
330                         foreach ( $ordersby as $key => $value ) {
331                                 if ( $value == $this->query_vars['meta_key'] || $value == 'meta_value' ) {
332                                         $ordersby[ $key ] = "$wpdb->commentmeta.meta_value";
333                                 } elseif ( $value == 'meta_value_num' ) {
334                                         $ordersby[ $key ] = "$wpdb->commentmeta.meta_value+0";
335                                 }
336                         }
337                         $orderby = empty( $ordersby ) ? 'comment_date_gmt' : implode(', ', $ordersby);
338                 } else {
339                         $orderby = 'comment_date_gmt';
340                 }
341
342                 $number = absint($number);
343                 $offset = absint($offset);
344
345                 if ( !empty($number) ) {
346                         if ( $offset )
347                                 $limits = 'LIMIT ' . $offset . ',' . $number;
348                         else
349                                 $limits = 'LIMIT ' . $number;
350                 } else {
351                         $limits = '';
352                 }
353
354                 if ( $count )
355                         $fields = 'COUNT(*)';
356                 else
357                         $fields = '*';
358
359                 $join = '';
360                 $where = $approved;
361
362                 if ( ! empty($post_id) )
363                         $where .= $wpdb->prepare( ' AND comment_post_ID = %d', $post_id );
364                 if ( '' !== $author_email )
365                         $where .= $wpdb->prepare( ' AND comment_author_email = %s', $author_email );
366                 if ( '' !== $karma )
367                         $where .= $wpdb->prepare( ' AND comment_karma = %d', $karma );
368                 if ( 'comment' == $type ) {
369                         $where .= " AND comment_type = ''";
370                 } elseif( 'pings' == $type ) {
371                         $where .= ' AND comment_type IN ("pingback", "trackback")';
372                 } elseif ( ! empty( $type ) ) {
373                         $where .= $wpdb->prepare( ' AND comment_type = %s', $type );
374                 }
375                 if ( '' !== $parent )
376                         $where .= $wpdb->prepare( ' AND comment_parent = %d', $parent );
377                 if ( '' !== $user_id )
378                         $where .= $wpdb->prepare( ' AND user_id = %d', $user_id );
379                 if ( '' !== $search )
380                         $where .= $this->get_search_sql( $search, array( 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_author_IP', 'comment_content' ) );
381
382                 $post_fields = array_filter( compact( array( 'post_author', 'post_name', 'post_parent', 'post_status', 'post_type', ) ) );
383                 if ( ! empty( $post_fields ) ) {
384                         $join = "JOIN $wpdb->posts ON $wpdb->posts.ID = $wpdb->comments.comment_post_ID";
385                         foreach( $post_fields as $field_name => $field_value )
386                                 $where .= $wpdb->prepare( " AND {$wpdb->posts}.{$field_name} = %s", $field_value );
387                 }
388
389                 if ( ! empty( $this->meta_query->queries ) ) {
390                         $clauses = $this->meta_query->get_sql( 'comment', $wpdb->comments, 'comment_ID', $this );
391                         $join .= $clauses['join'];
392                         $where .= $clauses['where'];
393                         $groupby = "{$wpdb->comments}.comment_ID";
394                 }
395
396                 if ( ! empty( $date_query ) && is_array( $date_query ) ) {
397                         $date_query_object = new WP_Date_Query( $date_query, 'comment_date' );
398                         $where .= $date_query_object->get_sql();
399                 }
400
401                 $pieces = array( 'fields', 'join', 'where', 'orderby', 'order', 'limits', 'groupby' );
402                 /**
403                  * Filter the comment query clauses.
404                  *
405                  * @since 3.1.0
406                  *
407                  * @param array            $pieces A compacted array of comment query clauses.
408                  * @param WP_Comment_Query &$this  Current instance of WP_Comment_Query, passed by reference.
409                  */
410                 $clauses = apply_filters_ref_array( 'comments_clauses', array( compact( $pieces ), &$this ) );
411                 foreach ( $pieces as $piece )
412                         $$piece = isset( $clauses[ $piece ] ) ? $clauses[ $piece ] : '';
413
414                 if ( $groupby )
415                         $groupby = 'GROUP BY ' . $groupby;
416
417                 $query = "SELECT $fields FROM $wpdb->comments $join WHERE $where $groupby ORDER BY $orderby $order $limits";
418
419                 if ( $count )
420                         return $wpdb->get_var( $query );
421
422                 $comments = $wpdb->get_results( $query );
423                 /**
424                  * Filter the comment query results.
425                  *
426                  * @since 3.1.0
427                  *
428                  * @param array            $comments An array of comments.
429                  * @param WP_Comment_Query &$this    Current instance of WP_Comment_Query, passed by reference.
430                  */
431                 $comments = apply_filters_ref_array( 'the_comments', array( $comments, &$this ) );
432
433                 wp_cache_add( $cache_key, $comments, 'comment' );
434
435                 return $comments;
436         }
437
438         /**
439          * Used internally to generate an SQL string for searching across multiple columns
440          *
441          * @access protected
442          * @since 3.1.0
443          *
444          * @param string $string
445          * @param array $cols
446          * @return string
447          */
448         function get_search_sql( $string, $cols ) {
449                 $string = esc_sql( like_escape( $string ) );
450
451                 $searches = array();
452                 foreach ( $cols as $col )
453                         $searches[] = "$col LIKE '%$string%'";
454
455                 return ' AND (' . implode(' OR ', $searches) . ')';
456         }
457 }
458
459 /**
460  * Retrieve all of the WordPress supported comment statuses.
461  *
462  * Comments have a limited set of valid status values, this provides the comment
463  * status values and descriptions.
464  *
465  * @package WordPress
466  * @subpackage Post
467  * @since 2.7.0
468  *
469  * @return array List of comment statuses.
470  */
471 function get_comment_statuses() {
472         $status = array(
473                 'hold'          => __('Unapproved'),
474                 /* translators: comment status  */
475                 'approve'       => _x('Approved', 'adjective'),
476                 /* translators: comment status */
477                 'spam'          => _x('Spam', 'adjective'),
478         );
479
480         return $status;
481 }
482
483 /**
484  * The date the last comment was modified.
485  *
486  * @since 1.5.0
487  * @uses $wpdb
488  *
489  * @param string $timezone Which timezone to use in reference to 'gmt', 'blog',
490  *              or 'server' locations.
491  * @return string Last comment modified date.
492  */
493 function get_lastcommentmodified($timezone = 'server') {
494         global $wpdb;
495         static $cache_lastcommentmodified = array();
496
497         if ( isset($cache_lastcommentmodified[$timezone]) )
498                 return $cache_lastcommentmodified[$timezone];
499
500         $add_seconds_server = date('Z');
501
502         switch ( strtolower($timezone)) {
503                 case 'gmt':
504                         $lastcommentmodified = $wpdb->get_var("SELECT comment_date_gmt FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1");
505                         break;
506                 case 'blog':
507                         $lastcommentmodified = $wpdb->get_var("SELECT comment_date FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1");
508                         break;
509                 case 'server':
510                         $lastcommentmodified = $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));
511                         break;
512         }
513
514         $cache_lastcommentmodified[$timezone] = $lastcommentmodified;
515
516         return $lastcommentmodified;
517 }
518
519 /**
520  * The amount of comments in a post or total comments.
521  *
522  * A lot like {@link wp_count_comments()}, in that they both return comment
523  * stats (albeit with different types). The {@link wp_count_comments()} actual
524  * caches, but this function does not.
525  *
526  * @since 2.0.0
527  * @uses $wpdb
528  *
529  * @param int $post_id Optional. Comment amount in post if > 0, else total comments blog wide.
530  * @return array The amount of spam, approved, awaiting moderation, and total comments.
531  */
532 function get_comment_count( $post_id = 0 ) {
533         global $wpdb;
534
535         $post_id = (int) $post_id;
536
537         $where = '';
538         if ( $post_id > 0 ) {
539                 $where = $wpdb->prepare("WHERE comment_post_ID = %d", $post_id);
540         }
541
542         $totals = (array) $wpdb->get_results("
543                 SELECT comment_approved, COUNT( * ) AS total
544                 FROM {$wpdb->comments}
545                 {$where}
546                 GROUP BY comment_approved
547         ", ARRAY_A);
548
549         $comment_count = array(
550                 "approved"              => 0,
551                 "awaiting_moderation"   => 0,
552                 "spam"                  => 0,
553                 "total_comments"        => 0
554         );
555
556         foreach ( $totals as $row ) {
557                 switch ( $row['comment_approved'] ) {
558                         case 'spam':
559                                 $comment_count['spam'] = $row['total'];
560                                 $comment_count["total_comments"] += $row['total'];
561                                 break;
562                         case 1:
563                                 $comment_count['approved'] = $row['total'];
564                                 $comment_count['total_comments'] += $row['total'];
565                                 break;
566                         case 0:
567                                 $comment_count['awaiting_moderation'] = $row['total'];
568                                 $comment_count['total_comments'] += $row['total'];
569                                 break;
570                         default:
571                                 break;
572                 }
573         }
574
575         return $comment_count;
576 }
577
578 //
579 // Comment meta functions
580 //
581
582 /**
583  * Add meta data field to a comment.
584  *
585  * @since 2.9.0
586  * @uses add_metadata
587  * @link http://codex.wordpress.org/Function_Reference/add_comment_meta
588  *
589  * @param int $comment_id Comment ID.
590  * @param string $meta_key Metadata name.
591  * @param mixed $meta_value Metadata value.
592  * @param bool $unique Optional, default is false. Whether the same key should not be added.
593  * @return int|bool Meta ID on success, false on failure.
594  */
595 function add_comment_meta($comment_id, $meta_key, $meta_value, $unique = false) {
596         return add_metadata('comment', $comment_id, $meta_key, $meta_value, $unique);
597 }
598
599 /**
600  * Remove metadata matching criteria from a comment.
601  *
602  * You can match based on the key, or key and value. Removing based on key and
603  * value, will keep from removing duplicate metadata with the same key. It also
604  * allows removing all metadata matching key, if needed.
605  *
606  * @since 2.9.0
607  * @uses delete_metadata
608  * @link http://codex.wordpress.org/Function_Reference/delete_comment_meta
609  *
610  * @param int $comment_id comment ID
611  * @param string $meta_key Metadata name.
612  * @param mixed $meta_value Optional. Metadata value.
613  * @return bool True on success, false on failure.
614  */
615 function delete_comment_meta($comment_id, $meta_key, $meta_value = '') {
616         return delete_metadata('comment', $comment_id, $meta_key, $meta_value);
617 }
618
619 /**
620  * Retrieve comment meta field for a comment.
621  *
622  * @since 2.9.0
623  * @uses get_metadata
624  * @link http://codex.wordpress.org/Function_Reference/get_comment_meta
625  *
626  * @param int $comment_id Comment ID.
627  * @param string $key Optional. The meta key to retrieve. By default, returns data for all keys.
628  * @param bool $single Whether to return a single value.
629  * @return mixed Will be an array if $single is false. Will be value of meta data field if $single
630  *  is true.
631  */
632 function get_comment_meta($comment_id, $key = '', $single = false) {
633         return get_metadata('comment', $comment_id, $key, $single);
634 }
635
636 /**
637  * Update comment meta field based on comment ID.
638  *
639  * Use the $prev_value parameter to differentiate between meta fields with the
640  * same key and comment ID.
641  *
642  * If the meta field for the comment does not exist, it will be added.
643  *
644  * @since 2.9.0
645  * @uses update_metadata
646  * @link http://codex.wordpress.org/Function_Reference/update_comment_meta
647  *
648  * @param int $comment_id Comment ID.
649  * @param string $meta_key Metadata key.
650  * @param mixed $meta_value Metadata value.
651  * @param mixed $prev_value Optional. Previous value to check before removing.
652  * @return bool True on success, false on failure.
653  */
654 function update_comment_meta($comment_id, $meta_key, $meta_value, $prev_value = '') {
655         return update_metadata('comment', $comment_id, $meta_key, $meta_value, $prev_value);
656 }
657
658 /**
659  * Sets the cookies used to store an unauthenticated commentator's identity. Typically used
660  * to recall previous comments by this commentator that are still held in moderation.
661  *
662  * @param object $comment Comment object.
663  * @param object $user Comment author's object.
664  *
665  * @since 3.4.0
666  */
667 function wp_set_comment_cookies($comment, $user) {
668         if ( $user->exists() )
669                 return;
670
671         /**
672          * Filter the lifetime of the comment cookie in seconds.
673          *
674          * @since 2.8.0
675          *
676          * @param int $seconds Comment cookie lifetime. Default 30000000.
677          */
678         $comment_cookie_lifetime = apply_filters( 'comment_cookie_lifetime', 30000000 );
679         setcookie('comment_author_' . COOKIEHASH, $comment->comment_author, time() + $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN);
680         setcookie('comment_author_email_' . COOKIEHASH, $comment->comment_author_email, time() + $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN);
681         setcookie('comment_author_url_' . COOKIEHASH, esc_url($comment->comment_author_url), time() + $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN);
682 }
683
684 /**
685  * Sanitizes the cookies sent to the user already.
686  *
687  * Will only do anything if the cookies have already been created for the user.
688  * Mostly used after cookies had been sent to use elsewhere.
689  *
690  * @since 2.0.4
691  */
692 function sanitize_comment_cookies() {
693         if ( isset( $_COOKIE['comment_author_' . COOKIEHASH] ) ) {
694                 /**
695                  * Filter the comment author's name cookie before it is set.
696                  *
697                  * When this filter hook is evaluated in wp_filter_comment(),
698                  * the comment author's name string is passed.
699                  *
700                  * @since 1.5.0
701                  *
702                  * @param string $author_cookie The comment author name cookie.
703                  */
704                 $comment_author = apply_filters( 'pre_comment_author_name', $_COOKIE['comment_author_' . COOKIEHASH] );
705                 $comment_author = wp_unslash($comment_author);
706                 $comment_author = esc_attr($comment_author);
707                 $_COOKIE['comment_author_' . COOKIEHASH] = $comment_author;
708         }
709
710         if ( isset( $_COOKIE['comment_author_email_' . COOKIEHASH] ) ) {
711                 /**
712                  * Filter the comment author's email cookie before it is set.
713                  *
714                  * When this filter hook is evaluated in wp_filter_comment(),
715                  * the comment author's email string is passed.
716                  *
717                  * @since 1.5.0
718                  *
719                  * @param string $author_email_cookie The comment author email cookie.
720                  */
721                 $comment_author_email = apply_filters( 'pre_comment_author_email', $_COOKIE['comment_author_email_' . COOKIEHASH] );
722                 $comment_author_email = wp_unslash($comment_author_email);
723                 $comment_author_email = esc_attr($comment_author_email);
724                 $_COOKIE['comment_author_email_'.COOKIEHASH] = $comment_author_email;
725         }
726
727         if ( isset( $_COOKIE['comment_author_url_' . COOKIEHASH] ) ) {
728                 /**
729                  * Filter the comment author's URL cookie before it is set.
730                  *
731                  * When this filter hook is evaluated in wp_filter_comment(),
732                  * the comment author's URL string is passed.
733                  *
734                  * @since 1.5.0
735                  *
736                  * @param string $author_url_cookie The comment author URL cookie.
737                  */
738                 $comment_author_url = apply_filters( 'pre_comment_author_url', $_COOKIE['comment_author_url_' . COOKIEHASH] );
739                 $comment_author_url = wp_unslash($comment_author_url);
740                 $_COOKIE['comment_author_url_'.COOKIEHASH] = $comment_author_url;
741         }
742 }
743
744 /**
745  * Validates whether this comment is allowed to be made.
746  *
747  * @since 2.0.0
748  * @uses $wpdb
749  * @uses apply_filters() Calls 'pre_comment_approved' hook on the type of comment
750  * @uses apply_filters() Calls 'comment_duplicate_trigger' hook on commentdata.
751  * @uses do_action() Calls 'check_comment_flood' hook on $comment_author_IP, $comment_author_email, and $comment_date_gmt
752  *
753  * @param array $commentdata Contains information on the comment
754  * @return mixed Signifies the approval status (0|1|'spam')
755  */
756 function wp_allow_comment($commentdata) {
757         global $wpdb;
758         extract($commentdata, EXTR_SKIP);
759
760         // Simple duplicate check
761         // expected_slashed ($comment_post_ID, $comment_author, $comment_author_email, $comment_content)
762         $dupe = $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_parent = %s AND comment_approved != 'trash' AND ( comment_author = %s ", wp_unslash( $comment_post_ID ), wp_unslash( $comment_parent ), wp_unslash( $comment_author ) );
763         if ( $comment_author_email )
764                 $dupe .= $wpdb->prepare( "OR comment_author_email = %s ", wp_unslash( $comment_author_email ) );
765         $dupe .= $wpdb->prepare( ") AND comment_content = %s LIMIT 1", wp_unslash( $comment_content ) );
766         if ( $wpdb->get_var($dupe) ) {
767                 /**
768                  * Fires immediately after a duplicate comment is detected.
769                  *
770                  * @since 3.0.0
771                  *
772                  * @param array $commentdata Comment data.
773                  */
774                 do_action( 'comment_duplicate_trigger', $commentdata );
775                 if ( defined('DOING_AJAX') )
776                         die( __('Duplicate comment detected; it looks as though you&#8217;ve already said that!') );
777
778                 wp_die( __('Duplicate comment detected; it looks as though you&#8217;ve already said that!') );
779         }
780
781         /**
782          * Fires immediately before a comment is marked approved.
783          *
784          * Allows checking for comment flooding.
785          *
786          * @since 2.3.0
787          *
788          * @param string $comment_author_IP    Comment author's IP address.
789          * @param string $comment_author_email Comment author's email.
790          * @param string $comment_date_gmt     GMT date the comment was posted.
791          */
792         do_action( 'check_comment_flood', $comment_author_IP, $comment_author_email, $comment_date_gmt );
793
794         if ( ! empty( $user_id ) ) {
795                 $user = get_userdata( $user_id );
796                 $post_author = $wpdb->get_var($wpdb->prepare("SELECT post_author FROM $wpdb->posts WHERE ID = %d LIMIT 1", $comment_post_ID));
797         }
798
799         if ( isset( $user ) && ( $user_id == $post_author || $user->has_cap( 'moderate_comments' ) ) ) {
800                 // The author and the admins get respect.
801                 $approved = 1;
802         } else {
803                 // Everyone else's comments will be checked.
804                 if ( check_comment($comment_author, $comment_author_email, $comment_author_url, $comment_content, $comment_author_IP, $comment_agent, $comment_type) )
805                         $approved = 1;
806                 else
807                         $approved = 0;
808                 if ( wp_blacklist_check($comment_author, $comment_author_email, $comment_author_url, $comment_content, $comment_author_IP, $comment_agent) )
809                         $approved = 'spam';
810         }
811
812         /**
813          * Filter a comment's approval status before it is set.
814          *
815          * @since 2.1.0
816          *
817          * @param bool|string $approved    The approval status. Accepts 1, 0, or 'spam'.
818          * @param array       $commentdata Comment data.
819          */
820         $approved = apply_filters( 'pre_comment_approved', $approved, $commentdata );
821         return $approved;
822 }
823
824 /**
825  * Check whether comment flooding is occurring.
826  *
827  * Won't run, if current user can manage options, so to not block
828  * administrators.
829  *
830  * @since 2.3.0
831  * @uses $wpdb
832  * @uses apply_filters() Calls 'comment_flood_filter' filter with first
833  *              parameter false, last comment timestamp, new comment timestamp.
834  * @uses do_action() Calls 'comment_flood_trigger' action with parameters with
835  *              last comment timestamp and new comment timestamp.
836  *
837  * @param string $ip Comment IP.
838  * @param string $email Comment author email address.
839  * @param string $date MySQL time string.
840  */
841 function check_comment_flood_db( $ip, $email, $date ) {
842         global $wpdb;
843         if ( current_user_can( 'manage_options' ) )
844                 return; // don't throttle admins
845         $hour_ago = gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS );
846         if ( $lasttime = $wpdb->get_var( $wpdb->prepare( "SELECT `comment_date_gmt` FROM `$wpdb->comments` WHERE `comment_date_gmt` >= %s AND ( `comment_author_IP` = %s OR `comment_author_email` = %s ) ORDER BY `comment_date_gmt` DESC LIMIT 1", $hour_ago, $ip, $email ) ) ) {
847                 $time_lastcomment = mysql2date('U', $lasttime, false);
848                 $time_newcomment  = mysql2date('U', $date, false);
849                 /**
850                  * Filter the comment flood status.
851                  *
852                  * @since 2.1.0
853                  *
854                  * @param bool $bool             Whether a comment flood is occurring. Default false.
855                  * @param int  $time_lastcomment Timestamp of when the last comment was posted.
856                  * @param int  $time_newcomment  Timestamp of when the new comment was posted.
857                  */
858                 $flood_die = apply_filters( 'comment_flood_filter', false, $time_lastcomment, $time_newcomment );
859                 if ( $flood_die ) {
860                         /**
861                          * Fires before the comment flood message is triggered.
862                          *
863                          * @since 1.5.0
864                          *
865                          * @param int $time_lastcomment Timestamp of when the last comment was posted.
866                          * @param int $time_newcomment  Timestamp of when the new comment was posted.
867                          */
868                         do_action( 'comment_flood_trigger', $time_lastcomment, $time_newcomment );
869
870                         if ( defined('DOING_AJAX') )
871                                 die( __('You are posting comments too quickly. Slow down.') );
872
873                         wp_die( __('You are posting comments too quickly. Slow down.'), '', array('response' => 403) );
874                 }
875         }
876 }
877
878 /**
879  * Separates an array of comments into an array keyed by comment_type.
880  *
881  * @since 2.7.0
882  *
883  * @param array $comments Array of comments
884  * @return array Array of comments keyed by comment_type.
885  */
886 function separate_comments(&$comments) {
887         $comments_by_type = array('comment' => array(), 'trackback' => array(), 'pingback' => array(), 'pings' => array());
888         $count = count($comments);
889         for ( $i = 0; $i < $count; $i++ ) {
890                 $type = $comments[$i]->comment_type;
891                 if ( empty($type) )
892                         $type = 'comment';
893                 $comments_by_type[$type][] = &$comments[$i];
894                 if ( 'trackback' == $type || 'pingback' == $type )
895                         $comments_by_type['pings'][] = &$comments[$i];
896         }
897
898         return $comments_by_type;
899 }
900
901 /**
902  * Calculate the total number of comment pages.
903  *
904  * @since 2.7.0
905  * @uses get_query_var() Used to fill in the default for $per_page parameter.
906  * @uses get_option() Used to fill in defaults for parameters.
907  * @uses Walker_Comment
908  *
909  * @param array $comments Optional array of comment objects. Defaults to $wp_query->comments
910  * @param int $per_page Optional comments per page.
911  * @param boolean $threaded Optional control over flat or threaded comments.
912  * @return int Number of comment pages.
913  */
914 function get_comment_pages_count( $comments = null, $per_page = null, $threaded = null ) {
915         global $wp_query;
916
917         if ( null === $comments && null === $per_page && null === $threaded && !empty($wp_query->max_num_comment_pages) )
918                 return $wp_query->max_num_comment_pages;
919
920         if ( !$comments || !is_array($comments) )
921                 $comments = $wp_query->comments;
922
923         if ( empty($comments) )
924                 return 0;
925
926         if ( ! get_option( 'page_comments' ) )
927                 return 1;
928
929         if ( !isset($per_page) )
930                 $per_page = (int) get_query_var('comments_per_page');
931         if ( 0 === $per_page )
932                 $per_page = (int) get_option('comments_per_page');
933         if ( 0 === $per_page )
934                 return 1;
935
936         if ( !isset($threaded) )
937                 $threaded = get_option('thread_comments');
938
939         if ( $threaded ) {
940                 $walker = new Walker_Comment;
941                 $count = ceil( $walker->get_number_of_root_elements( $comments ) / $per_page );
942         } else {
943                 $count = ceil( count( $comments ) / $per_page );
944         }
945
946         return $count;
947 }
948
949 /**
950  * Calculate what page number a comment will appear on for comment paging.
951  *
952  * @since 2.7.0
953  * @uses get_comment() Gets the full comment of the $comment_ID parameter.
954  * @uses get_option() Get various settings to control function and defaults.
955  * @uses get_page_of_comment() Used to loop up to top level comment.
956  *
957  * @param int $comment_ID Comment ID.
958  * @param array $args Optional args.
959  * @return int|null Comment page number or null on error.
960  */
961 function get_page_of_comment( $comment_ID, $args = array() ) {
962         global $wpdb;
963
964         if ( !$comment = get_comment( $comment_ID ) )
965                 return;
966
967         $defaults = array( 'type' => 'all', 'page' => '', 'per_page' => '', 'max_depth' => '' );
968         $args = wp_parse_args( $args, $defaults );
969
970         if ( '' === $args['per_page'] && get_option('page_comments') )
971                 $args['per_page'] = get_query_var('comments_per_page');
972         if ( empty($args['per_page']) ) {
973                 $args['per_page'] = 0;
974                 $args['page'] = 0;
975         }
976         if ( $args['per_page'] < 1 )
977                 return 1;
978
979         if ( '' === $args['max_depth'] ) {
980                 if ( get_option('thread_comments') )
981                         $args['max_depth'] = get_option('thread_comments_depth');
982                 else
983                         $args['max_depth'] = -1;
984         }
985
986         // Find this comment's top level parent if threading is enabled
987         if ( $args['max_depth'] > 1 && 0 != $comment->comment_parent )
988                 return get_page_of_comment( $comment->comment_parent, $args );
989
990         $allowedtypes = array(
991                 'comment' => '',
992                 'pingback' => 'pingback',
993                 'trackback' => 'trackback',
994         );
995
996         $comtypewhere = ( 'all' != $args['type'] && isset($allowedtypes[$args['type']]) ) ? " AND comment_type = '" . $allowedtypes[$args['type']] . "'" : '';
997
998         // Count comments older than this one
999         $oldercoms = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(comment_ID) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_parent = 0 AND comment_approved = '1' AND comment_date_gmt < '%s'" . $comtypewhere, $comment->comment_post_ID, $comment->comment_date_gmt ) );
1000
1001         // No older comments? Then it's page #1.
1002         if ( 0 == $oldercoms )
1003                 return 1;
1004
1005         // Divide comments older than this one by comments per page to get this comment's page number
1006         return ceil( ( $oldercoms + 1 ) / $args['per_page'] );
1007 }
1008
1009 /**
1010  * Does comment contain blacklisted characters or words.
1011  *
1012  * @since 1.5.0
1013  * @uses do_action() Calls 'wp_blacklist_check' hook for all parameters.
1014  *
1015  * @param string $author The author of the comment
1016  * @param string $email The email of the comment
1017  * @param string $url The url used in the comment
1018  * @param string $comment The comment content
1019  * @param string $user_ip The comment author IP address
1020  * @param string $user_agent The author's browser user agent
1021  * @return bool True if comment contains blacklisted content, false if comment does not
1022  */
1023 function wp_blacklist_check($author, $email, $url, $comment, $user_ip, $user_agent) {
1024         /**
1025          * Fires before the comment is tested for blacklisted characters or words.
1026          *
1027          * @since 1.5.0
1028          *
1029          * @param string $author     Comment author.
1030          * @param string $email      Comment author's email.
1031          * @param string $url        Comment author's URL.
1032          * @param string $comment    Comment content.
1033          * @param string $user_ip    Comment author's IP address.
1034          * @param string $user_agent Comment author's browser user agent.
1035          */
1036         do_action( 'wp_blacklist_check', $author, $email, $url, $comment, $user_ip, $user_agent );
1037
1038         $mod_keys = trim( get_option('blacklist_keys') );
1039         if ( '' == $mod_keys )
1040                 return false; // If moderation keys are empty
1041         $words = explode("\n", $mod_keys );
1042
1043         foreach ( (array) $words as $word ) {
1044                 $word = trim($word);
1045
1046                 // Skip empty lines
1047                 if ( empty($word) ) { continue; }
1048
1049                 // Do some escaping magic so that '#' chars in the
1050                 // spam words don't break things:
1051                 $word = preg_quote($word, '#');
1052
1053                 $pattern = "#$word#i";
1054                 if (
1055                            preg_match($pattern, $author)
1056                         || preg_match($pattern, $email)
1057                         || preg_match($pattern, $url)
1058                         || preg_match($pattern, $comment)
1059                         || preg_match($pattern, $user_ip)
1060                         || preg_match($pattern, $user_agent)
1061                  )
1062                         return true;
1063         }
1064         return false;
1065 }
1066
1067 /**
1068  * Retrieve total comments for blog or single post.
1069  *
1070  * The properties of the returned object contain the 'moderated', 'approved',
1071  * and spam comments for either the entire blog or single post. Those properties
1072  * contain the amount of comments that match the status. The 'total_comments'
1073  * property contains the integer of total comments.
1074  *
1075  * The comment stats are cached and then retrieved, if they already exist in the
1076  * cache.
1077  *
1078  * @since 2.5.0
1079  *
1080  * @param int $post_id Optional. Post ID.
1081  * @return object Comment stats.
1082  */
1083 function wp_count_comments( $post_id = 0 ) {
1084         global $wpdb;
1085
1086         $post_id = (int) $post_id;
1087
1088         /**
1089          * Filter the comments count for a given post.
1090          *
1091          * @since 2.7.0
1092          *
1093          * @param array $count   An empty array.
1094          * @param int   $post_id The post ID.
1095          */
1096         $stats = apply_filters( 'wp_count_comments', array(), $post_id );
1097         if ( !empty($stats) )
1098                 return $stats;
1099
1100         $count = wp_cache_get("comments-{$post_id}", 'counts');
1101
1102         if ( false !== $count )
1103                 return $count;
1104
1105         $where = '';
1106         if ( $post_id > 0 )
1107                 $where = $wpdb->prepare( "WHERE comment_post_ID = %d", $post_id );
1108
1109         $count = $wpdb->get_results( "SELECT comment_approved, COUNT( * ) AS num_comments FROM {$wpdb->comments} {$where} GROUP BY comment_approved", ARRAY_A );
1110
1111         $total = 0;
1112         $approved = array('0' => 'moderated', '1' => 'approved', 'spam' => 'spam', 'trash' => 'trash', 'post-trashed' => 'post-trashed');
1113         foreach ( (array) $count as $row ) {
1114                 // Don't count post-trashed toward totals
1115                 if ( 'post-trashed' != $row['comment_approved'] && 'trash' != $row['comment_approved'] )
1116                         $total += $row['num_comments'];
1117                 if ( isset( $approved[$row['comment_approved']] ) )
1118                         $stats[$approved[$row['comment_approved']]] = $row['num_comments'];
1119         }
1120
1121         $stats['total_comments'] = $total;
1122         foreach ( $approved as $key ) {
1123                 if ( empty($stats[$key]) )
1124                         $stats[$key] = 0;
1125         }
1126
1127         $stats = (object) $stats;
1128         wp_cache_set("comments-{$post_id}", $stats, 'counts');
1129
1130         return $stats;
1131 }
1132
1133 /**
1134  * Trashes or deletes a comment.
1135  *
1136  * The comment is moved to trash instead of permanently deleted unless trash is
1137  * disabled, item is already in the trash, or $force_delete is true.
1138  *
1139  * The post comment count will be updated if the comment was approved and has a
1140  * post ID available.
1141  *
1142  * @since 2.0.0
1143  * @uses $wpdb
1144  * @uses do_action() Calls 'delete_comment' hook on comment ID
1145  * @uses do_action() Calls 'deleted_comment' hook on comment ID after deletion, on success
1146  * @uses do_action() Calls 'wp_set_comment_status' hook on comment ID with 'delete' set for the second parameter
1147  * @uses wp_transition_comment_status() Passes new and old comment status along with $comment object
1148  *
1149  * @param int $comment_id Comment ID
1150  * @param bool $force_delete Whether to bypass trash and force deletion. Default is false.
1151  * @return bool True on success, false on failure.
1152  */
1153 function wp_delete_comment($comment_id, $force_delete = false) {
1154         global $wpdb;
1155         if (!$comment = get_comment($comment_id))
1156                 return false;
1157
1158         if ( !$force_delete && EMPTY_TRASH_DAYS && !in_array( wp_get_comment_status($comment_id), array( 'trash', 'spam' ) ) )
1159                 return wp_trash_comment($comment_id);
1160
1161         /**
1162          * Fires immediately before a comment is deleted from the database.
1163          *
1164          * @since 1.2.0
1165          *
1166          * @param int $comment_id The comment ID.
1167          */
1168         do_action( 'delete_comment', $comment_id );
1169
1170         // Move children up a level.
1171         $children = $wpdb->get_col( $wpdb->prepare("SELECT comment_ID FROM $wpdb->comments WHERE comment_parent = %d", $comment_id) );
1172         if ( !empty($children) ) {
1173                 $wpdb->update($wpdb->comments, array('comment_parent' => $comment->comment_parent), array('comment_parent' => $comment_id));
1174                 clean_comment_cache($children);
1175         }
1176
1177         // Delete metadata
1178         $meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->commentmeta WHERE comment_id = %d", $comment_id ) );
1179         foreach ( $meta_ids as $mid )
1180                 delete_metadata_by_mid( 'comment', $mid );
1181
1182         if ( ! $wpdb->delete( $wpdb->comments, array( 'comment_ID' => $comment_id ) ) )
1183                 return false;
1184
1185         /**
1186          * Fires immediately after a comment is deleted from the database.
1187          *
1188          * @since 2.9.0
1189          *
1190          * @param int $comment_id The comment ID.
1191          */
1192         do_action( 'deleted_comment', $comment_id );
1193
1194         $post_id = $comment->comment_post_ID;
1195         if ( $post_id && $comment->comment_approved == 1 )
1196                 wp_update_comment_count($post_id);
1197
1198         clean_comment_cache($comment_id);
1199
1200         /**
1201          * Fires immediately before changing the comment's status to 'delete'.
1202          *
1203          * @since 1.5.0
1204          *
1205          * @param int    $comment_id The comment ID.
1206          * @param string $status     The new 'delete' comment status.
1207          */
1208         do_action( 'wp_set_comment_status', $comment_id, 'delete' );
1209         wp_transition_comment_status('delete', $comment->comment_approved, $comment);
1210         return true;
1211 }
1212
1213 /**
1214  * Moves a comment to the Trash
1215  *
1216  * If trash is disabled, comment is permanently deleted.
1217  *
1218  * @since 2.9.0
1219  * @uses do_action() on 'trash_comment' before trashing
1220  * @uses do_action() on 'trashed_comment' after trashing
1221  * @uses wp_delete_comment() if trash is disabled
1222  *
1223  * @param int $comment_id Comment ID.
1224  * @return bool True on success, false on failure.
1225  */
1226 function wp_trash_comment($comment_id) {
1227         if ( !EMPTY_TRASH_DAYS )
1228                 return wp_delete_comment($comment_id, true);
1229
1230         if ( !$comment = get_comment($comment_id) )
1231                 return false;
1232
1233         /**
1234          * Fires immediately before a comment is sent to the Trash.
1235          *
1236          * @since 2.9.0
1237          *
1238          * @param int $comment_id The comment ID.
1239          */
1240         do_action( 'trash_comment', $comment_id );
1241
1242         if ( wp_set_comment_status($comment_id, 'trash') ) {
1243                 add_comment_meta($comment_id, '_wp_trash_meta_status', $comment->comment_approved);
1244                 add_comment_meta($comment_id, '_wp_trash_meta_time', time() );
1245
1246                 /**
1247                  * Fires immediately after a comment is sent to Trash.
1248                  *
1249                  * @since 2.9.0
1250                  *
1251                  * @param int $comment_id The comment ID.
1252                  */
1253                 do_action( 'trashed_comment', $comment_id );
1254                 return true;
1255         }
1256
1257         return false;
1258 }
1259
1260 /**
1261  * Removes a comment from the Trash
1262  *
1263  * @since 2.9.0
1264  * @uses do_action() on 'untrash_comment' before untrashing
1265  * @uses do_action() on 'untrashed_comment' after untrashing
1266  *
1267  * @param int $comment_id Comment ID.
1268  * @return bool True on success, false on failure.
1269  */
1270 function wp_untrash_comment($comment_id) {
1271         if ( ! (int)$comment_id )
1272                 return false;
1273
1274         /**
1275          * Fires immediately before a comment is restored from the Trash.
1276          *
1277          * @since 2.9.0
1278          *
1279          * @param int $comment_id The comment ID.
1280          */
1281         do_action( 'untrash_comment', $comment_id );
1282
1283         $status = (string) get_comment_meta($comment_id, '_wp_trash_meta_status', true);
1284         if ( empty($status) )
1285                 $status = '0';
1286
1287         if ( wp_set_comment_status($comment_id, $status) ) {
1288                 delete_comment_meta($comment_id, '_wp_trash_meta_time');
1289                 delete_comment_meta($comment_id, '_wp_trash_meta_status');
1290                 /**
1291                  * Fires immediately after a comment is restored from the Trash.
1292                  *
1293                  * @since 2.9.0
1294                  *
1295                  * @param int $comment_id The comment ID.
1296                  */
1297                 do_action( 'untrashed_comment', $comment_id );
1298                 return true;
1299         }
1300
1301         return false;
1302 }
1303
1304 /**
1305  * Marks a comment as Spam
1306  *
1307  * @since 2.9.0
1308  * @uses do_action() on 'spam_comment' before spamming
1309  * @uses do_action() on 'spammed_comment' after spamming
1310  *
1311  * @param int $comment_id Comment ID.
1312  * @return bool True on success, false on failure.
1313  */
1314 function wp_spam_comment($comment_id) {
1315         if ( !$comment = get_comment($comment_id) )
1316                 return false;
1317
1318         /**
1319          * Fires immediately before a comment is marked as Spam.
1320          *
1321          * @since 2.9.0
1322          *
1323          * @param int $comment_id The comment ID.
1324          */
1325         do_action( 'spam_comment', $comment_id );
1326
1327         if ( wp_set_comment_status($comment_id, 'spam') ) {
1328                 add_comment_meta($comment_id, '_wp_trash_meta_status', $comment->comment_approved);
1329                 /**
1330                  * Fires immediately after a comment is marked as Spam.
1331                  *
1332                  * @since 2.9.0
1333                  *
1334                  * @param int $comment_id The comment ID.
1335                  */
1336                 do_action( 'spammed_comment', $comment_id );
1337                 return true;
1338         }
1339
1340         return false;
1341 }
1342
1343 /**
1344  * Removes a comment from the Spam
1345  *
1346  * @since 2.9.0
1347  * @uses do_action() on 'unspam_comment' before unspamming
1348  * @uses do_action() on 'unspammed_comment' after unspamming
1349  *
1350  * @param int $comment_id Comment ID.
1351  * @return bool True on success, false on failure.
1352  */
1353 function wp_unspam_comment($comment_id) {
1354         if ( ! (int)$comment_id )
1355                 return false;
1356
1357         /**
1358          * Fires immediately before a comment is unmarked as Spam.
1359          *
1360          * @since 2.9.0
1361          *
1362          * @param int $comment_id The comment ID.
1363          */
1364         do_action( 'unspam_comment', $comment_id );
1365
1366         $status = (string) get_comment_meta($comment_id, '_wp_trash_meta_status', true);
1367         if ( empty($status) )
1368                 $status = '0';
1369
1370         if ( wp_set_comment_status($comment_id, $status) ) {
1371                 delete_comment_meta($comment_id, '_wp_trash_meta_status');
1372                 /**
1373                  * Fires immediately after a comment is unmarked as Spam.
1374                  *
1375                  * @since 2.9.0
1376                  *
1377                  * @param int $comment_id The comment ID.
1378                  */
1379                 do_action( 'unspammed_comment', $comment_id );
1380                 return true;
1381         }
1382
1383         return false;
1384 }
1385
1386 /**
1387  * The status of a comment by ID.
1388  *
1389  * @since 1.0.0
1390  *
1391  * @param int $comment_id Comment ID
1392  * @return string|bool Status might be 'trash', 'approved', 'unapproved', 'spam'. False on failure.
1393  */
1394 function wp_get_comment_status($comment_id) {
1395         $comment = get_comment($comment_id);
1396         if ( !$comment )
1397                 return false;
1398
1399         $approved = $comment->comment_approved;
1400
1401         if ( $approved == null )
1402                 return false;
1403         elseif ( $approved == '1' )
1404                 return 'approved';
1405         elseif ( $approved == '0' )
1406                 return 'unapproved';
1407         elseif ( $approved == 'spam' )
1408                 return 'spam';
1409         elseif ( $approved == 'trash' )
1410                 return 'trash';
1411         else
1412                 return false;
1413 }
1414
1415 /**
1416  * Call hooks for when a comment status transition occurs.
1417  *
1418  * Calls hooks for comment status transitions. If the new comment status is not the same
1419  * as the previous comment status, then two hooks will be ran, the first is
1420  * 'transition_comment_status' with new status, old status, and comment data. The
1421  * next action called is 'comment_OLDSTATUS_to_NEWSTATUS' the NEWSTATUS is the
1422  * $new_status parameter and the OLDSTATUS is $old_status parameter; it has the
1423  * comment data.
1424  *
1425  * The final action will run whether or not the comment statuses are the same. The
1426  * action is named 'comment_NEWSTATUS_COMMENTTYPE', NEWSTATUS is from the $new_status
1427  * parameter and COMMENTTYPE is comment_type comment data.
1428  *
1429  * @since 2.7.0
1430  *
1431  * @param string $new_status New comment status.
1432  * @param string $old_status Previous comment status.
1433  * @param object $comment Comment data.
1434  */
1435 function wp_transition_comment_status($new_status, $old_status, $comment) {
1436         /*
1437          * Translate raw statuses to human readable formats for the hooks.
1438          * This is not a complete list of comment status, it's only the ones
1439          * that need to be renamed
1440          */
1441         $comment_statuses = array(
1442                 0         => 'unapproved',
1443                 'hold'    => 'unapproved', // wp_set_comment_status() uses "hold"
1444                 1         => 'approved',
1445                 'approve' => 'approved', // wp_set_comment_status() uses "approve"
1446         );
1447         if ( isset($comment_statuses[$new_status]) ) $new_status = $comment_statuses[$new_status];
1448         if ( isset($comment_statuses[$old_status]) ) $old_status = $comment_statuses[$old_status];
1449
1450         // Call the hooks
1451         if ( $new_status != $old_status ) {
1452                 /**
1453                  * Fires when the comment status is in transition.
1454                  *
1455                  * @since 2.7.0
1456                  *
1457                  * @param int|string $new_status The new comment status.
1458                  * @param int|string $old_status The old comment status.
1459                  * @param object     $comment    The comment data.
1460                  */
1461                 do_action( 'transition_comment_status', $new_status, $old_status, $comment );
1462                 /**
1463                  * Fires when the comment status is in transition from one specific status to another.
1464                  *
1465                  * The dynamic portions of the hook name, $old_status, and $new_status,
1466                  * refer to the old and new comment statuses, respectively.
1467                  *
1468                  * @since 2.7.0
1469                  *
1470                  * @param object $comment Comment object.
1471                  */
1472                 do_action( "comment_{$old_status}_to_{$new_status}", $comment );
1473         }
1474         /**
1475          * Fires when the status of a specific comment type is in transition.
1476          *
1477          * The dynamic portions of the hook name, $new_status, and $comment->comment_type,
1478          * refer to the new comment status, and the type of comment, respectively.
1479          *
1480          * Typical comment types include an empty string (standard comment), 'pingback',
1481          * or 'trackback'.
1482          *
1483          * @since 2.7.0
1484          *
1485          * @param int $comment_ID The comment ID.
1486          * @param obj $comment    Comment object.
1487          */
1488         do_action( "comment_{$new_status}_{$comment->comment_type}", $comment->comment_ID, $comment );
1489 }
1490
1491 /**
1492  * Get current commenter's name, email, and URL.
1493  *
1494  * Expects cookies content to already be sanitized. User of this function might
1495  * wish to recheck the returned array for validity.
1496  *
1497  * @see sanitize_comment_cookies() Use to sanitize cookies
1498  *
1499  * @since 2.0.4
1500  *
1501  * @return array Comment author, email, url respectively.
1502  */
1503 function wp_get_current_commenter() {
1504         // Cookies should already be sanitized.
1505
1506         $comment_author = '';
1507         if ( isset($_COOKIE['comment_author_'.COOKIEHASH]) )
1508                 $comment_author = $_COOKIE['comment_author_'.COOKIEHASH];
1509
1510         $comment_author_email = '';
1511         if ( isset($_COOKIE['comment_author_email_'.COOKIEHASH]) )
1512                 $comment_author_email = $_COOKIE['comment_author_email_'.COOKIEHASH];
1513
1514         $comment_author_url = '';
1515         if ( isset($_COOKIE['comment_author_url_'.COOKIEHASH]) )
1516                 $comment_author_url = $_COOKIE['comment_author_url_'.COOKIEHASH];
1517
1518         /**
1519          * Filter the current commenter's name, email, and URL.
1520          *
1521          * @since 3.1.0
1522          *
1523          * @param string $comment_author       Comment author's name.
1524          * @param string $comment_author_email Comment author's email.
1525          * @param string $comment_author_url   Comment author's URL.
1526          */
1527         return apply_filters( 'wp_get_current_commenter', compact('comment_author', 'comment_author_email', 'comment_author_url') );
1528 }
1529
1530 /**
1531  * Inserts a comment to the database.
1532  *
1533  * The available comment data key names are 'comment_author_IP', 'comment_date',
1534  * 'comment_date_gmt', 'comment_parent', 'comment_approved', and 'user_id'.
1535  *
1536  * @since 2.0.0
1537  * @uses $wpdb
1538  *
1539  * @param array $commentdata Contains information on the comment.
1540  * @return int The new comment's ID.
1541  */
1542 function wp_insert_comment($commentdata) {
1543         global $wpdb;
1544         extract(wp_unslash($commentdata), EXTR_SKIP);
1545
1546         if ( ! isset($comment_author_IP) )
1547                 $comment_author_IP = '';
1548         if ( ! isset($comment_date) )
1549                 $comment_date = current_time('mysql');
1550         if ( ! isset($comment_date_gmt) )
1551                 $comment_date_gmt = get_gmt_from_date($comment_date);
1552         if ( ! isset($comment_parent) )
1553                 $comment_parent = 0;
1554         if ( ! isset($comment_approved) )
1555                 $comment_approved = 1;
1556         if ( ! isset($comment_karma) )
1557                 $comment_karma = 0;
1558         if ( ! isset($user_id) )
1559                 $user_id = 0;
1560         if ( ! isset($comment_type) )
1561                 $comment_type = '';
1562
1563         $data = compact('comment_post_ID', 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_author_IP', 'comment_date', 'comment_date_gmt', 'comment_content', 'comment_karma', 'comment_approved', 'comment_agent', 'comment_type', 'comment_parent', 'user_id');
1564         $wpdb->insert($wpdb->comments, $data);
1565
1566         $id = (int) $wpdb->insert_id;
1567
1568         if ( $comment_approved == 1 )
1569                 wp_update_comment_count($comment_post_ID);
1570
1571         $comment = get_comment($id);
1572
1573         /**
1574          * Fires immediately after a comment is inserted into the database.
1575          *
1576          * @since 2.8.0
1577          *
1578          * @param int $id      The comment ID.
1579          * @param obj $comment Comment object.
1580          */
1581         do_action( 'wp_insert_comment', $id, $comment );
1582
1583         wp_cache_set( 'last_changed', microtime(), 'comment' );
1584
1585         return $id;
1586 }
1587
1588 /**
1589  * Filters and sanitizes comment data.
1590  *
1591  * Sets the comment data 'filtered' field to true when finished. This can be
1592  * checked as to whether the comment should be filtered and to keep from
1593  * filtering the same comment more than once.
1594  *
1595  * @since 2.0.0
1596  *
1597  * @param array $commentdata Contains information on the comment.
1598  * @return array Parsed comment information.
1599  */
1600 function wp_filter_comment($commentdata) {
1601         if ( isset( $commentdata['user_ID'] ) ) {
1602                 /**
1603                  * Filter the comment author's user id before it is set.
1604                  *
1605                  * The first time this filter is evaluated, 'user_ID' is checked
1606                  * (for back-compat), followed by the standard 'user_id' value.
1607                  *
1608                  * @since 1.5.0
1609                  *
1610                  * @param int $user_ID The comment author's user ID.
1611                  */
1612                 $commentdata['user_id'] = apply_filters( 'pre_user_id', $commentdata['user_ID'] );
1613         } elseif ( isset( $commentdata['user_id'] ) ) {
1614                 /** This filter is documented in wp-includes/comment.php */
1615                 $commentdata['user_id'] = apply_filters( 'pre_user_id', $commentdata['user_id'] );
1616         }
1617
1618         /**
1619          * Filter the comment author's browser user agent before it is set.
1620          *
1621          * @since 1.5.0
1622          *
1623          * @param int $comment_agent The comment author's browser user agent.
1624          */
1625         $commentdata['comment_agent'] = apply_filters( 'pre_comment_user_agent', ( isset( $commentdata['comment_agent'] ) ? $commentdata['comment_agent'] : '' ) );
1626         /** This filter is documented in wp-includes/comment.php */
1627         $commentdata['comment_author'] = apply_filters( 'pre_comment_author_name', $commentdata['comment_author'] );
1628         /**
1629          * Filter the comment content before it is set.
1630          *
1631          * @since 1.5.0
1632          *
1633          * @param int $comment_content The comment content.
1634          */
1635         $commentdata['comment_content'] = apply_filters( 'pre_comment_content', $commentdata['comment_content'] );
1636         /**
1637          * Filter the comment author's IP before it is set.
1638          *
1639          * @since 1.5.0
1640          *
1641          * @param int $comment_author_ip The comment author's IP.
1642          */
1643         $commentdata['comment_author_IP'] = apply_filters( 'pre_comment_user_ip', $commentdata['comment_author_IP'] );
1644         /** This filter is documented in wp-includes/comment.php */
1645         $commentdata['comment_author_url'] = apply_filters( 'pre_comment_author_url', $commentdata['comment_author_url'] );
1646         /** This filter is documented in wp-includes/comment.php */
1647         $commentdata['comment_author_email'] = apply_filters( 'pre_comment_author_email', $commentdata['comment_author_email'] );
1648         $commentdata['filtered'] = true;
1649         return $commentdata;
1650 }
1651
1652 /**
1653  * Whether a comment should be blocked because of comment flood.
1654  *
1655  * @since 2.1.0
1656  *
1657  * @param bool $block Whether plugin has already blocked comment.
1658  * @param int $time_lastcomment Timestamp for last comment.
1659  * @param int $time_newcomment Timestamp for new comment.
1660  * @return bool Whether comment should be blocked.
1661  */
1662 function wp_throttle_comment_flood($block, $time_lastcomment, $time_newcomment) {
1663         if ( $block ) // a plugin has already blocked... we'll let that decision stand
1664                 return $block;
1665         if ( ($time_newcomment - $time_lastcomment) < 15 )
1666                 return true;
1667         return false;
1668 }
1669
1670 /**
1671  * Adds a new comment to the database.
1672  *
1673  * Filters new comment to ensure that the fields are sanitized and valid before
1674  * inserting comment into database. Calls 'comment_post' action with comment ID
1675  * and whether comment is approved by WordPress. Also has 'preprocess_comment'
1676  * filter for processing the comment data before the function handles it.
1677  *
1678  * We use REMOTE_ADDR here directly. If you are behind a proxy, you should ensure
1679  * that it is properly set, such as in wp-config.php, for your environment.
1680  * See {@link http://core.trac.wordpress.org/ticket/9235}
1681  *
1682  * @since 1.5.0
1683  * @param array $commentdata Contains information on the comment.
1684  * @return int The ID of the comment after adding.
1685  */
1686 function wp_new_comment( $commentdata ) {
1687         /**
1688          * Filter a comment's data before it is sanitized and inserted into the database.
1689          *
1690          * @since 1.5.0
1691          *
1692          * @param array $commentdata Comment data.
1693          */
1694         $commentdata = apply_filters( 'preprocess_comment', $commentdata );
1695
1696         $commentdata['comment_post_ID'] = (int) $commentdata['comment_post_ID'];
1697         if ( isset($commentdata['user_ID']) )
1698                 $commentdata['user_id'] = $commentdata['user_ID'] = (int) $commentdata['user_ID'];
1699         elseif ( isset($commentdata['user_id']) )
1700                 $commentdata['user_id'] = (int) $commentdata['user_id'];
1701
1702         $commentdata['comment_parent'] = isset($commentdata['comment_parent']) ? absint($commentdata['comment_parent']) : 0;
1703         $parent_status = ( 0 < $commentdata['comment_parent'] ) ? wp_get_comment_status($commentdata['comment_parent']) : '';
1704         $commentdata['comment_parent'] = ( 'approved' == $parent_status || 'unapproved' == $parent_status ) ? $commentdata['comment_parent'] : 0;
1705
1706         $commentdata['comment_author_IP'] = preg_replace( '/[^0-9a-fA-F:., ]/', '',$_SERVER['REMOTE_ADDR'] );
1707         $commentdata['comment_agent']     = isset( $_SERVER['HTTP_USER_AGENT'] ) ? substr( $_SERVER['HTTP_USER_AGENT'], 0, 254 ) : '';
1708
1709         $commentdata['comment_date']     = current_time('mysql');
1710         $commentdata['comment_date_gmt'] = current_time('mysql', 1);
1711
1712         $commentdata = wp_filter_comment($commentdata);
1713
1714         $commentdata['comment_approved'] = wp_allow_comment($commentdata);
1715
1716         $comment_ID = wp_insert_comment($commentdata);
1717
1718         /**
1719          * Fires immediately after a comment is inserted into the database.
1720          *
1721          * @since 1.2.0
1722          *
1723          * @param int $comment_ID       The comment ID.
1724          * @param int $comment_approved 1 (true) if the comment is approved, 0 (false) if not.
1725          */
1726         do_action( 'comment_post', $comment_ID, $commentdata['comment_approved'] );
1727
1728         if ( 'spam' !== $commentdata['comment_approved'] ) { // If it's spam save it silently for later crunching
1729                 if ( '0' == $commentdata['comment_approved'] ) {
1730                         wp_notify_moderator( $comment_ID );
1731                 }
1732
1733                 // wp_notify_postauthor() checks if notifying the author of their own comment.
1734                 // By default, it won't, but filters can override this.
1735                 if ( get_option( 'comments_notify' ) && $commentdata['comment_approved'] ) {
1736                         wp_notify_postauthor( $comment_ID );
1737                 }
1738         }
1739
1740         return $comment_ID;
1741 }
1742
1743 /**
1744  * Sets the status of a comment.
1745  *
1746  * The 'wp_set_comment_status' action is called after the comment is handled.
1747  * If the comment status is not in the list, then false is returned.
1748  *
1749  * @since 1.0.0
1750  * @uses wp_transition_comment_status() Passes new and old comment status along with $comment object
1751  *
1752  * @param int $comment_id Comment ID.
1753  * @param string $comment_status New comment status, either 'hold', 'approve', 'spam', or 'trash'.
1754  * @param bool $wp_error Whether to return a WP_Error object if there is a failure. Default is false.
1755  * @return bool|WP_Error True on success, false or WP_Error on failure.
1756  */
1757 function wp_set_comment_status($comment_id, $comment_status, $wp_error = false) {
1758         global $wpdb;
1759
1760         $status = '0';
1761         switch ( $comment_status ) {
1762                 case 'hold':
1763                 case '0':
1764                         $status = '0';
1765                         break;
1766                 case 'approve':
1767                 case '1':
1768                         $status = '1';
1769                         if ( get_option('comments_notify') ) {
1770                                 wp_notify_postauthor( $comment_id );
1771                         }
1772                         break;
1773                 case 'spam':
1774                         $status = 'spam';
1775                         break;
1776                 case 'trash':
1777                         $status = 'trash';
1778                         break;
1779                 default:
1780                         return false;
1781         }
1782
1783         $comment_old = clone get_comment($comment_id);
1784
1785         if ( !$wpdb->update( $wpdb->comments, array('comment_approved' => $status), array('comment_ID' => $comment_id) ) ) {
1786                 if ( $wp_error )
1787                         return new WP_Error('db_update_error', __('Could not update comment status'), $wpdb->last_error);
1788                 else
1789                         return false;
1790         }
1791
1792         clean_comment_cache($comment_id);
1793
1794         $comment = get_comment($comment_id);
1795
1796         /**
1797          * Fires after a comment status has been updated in the database.
1798          *
1799          * The hook also fires immediately before comment status transition hooks are fired.
1800          *
1801          * @since 1.5.0
1802          *
1803          * @param int         $comment_id     The comment ID.
1804          * @param string|bool $comment_status The comment status. Possible values include 'hold',
1805          *                                    'approve', 'spam', 'trash', or false.
1806          */
1807         do_action( 'wp_set_comment_status', $comment_id, $comment_status );
1808         wp_transition_comment_status($comment_status, $comment_old->comment_approved, $comment);
1809
1810         wp_update_comment_count($comment->comment_post_ID);
1811
1812         return true;
1813 }
1814
1815 /**
1816  * Updates an existing comment in the database.
1817  *
1818  * Filters the comment and makes sure certain fields are valid before updating.
1819  *
1820  * @since 2.0.0
1821  * @uses $wpdb
1822  * @uses wp_transition_comment_status() Passes new and old comment status along with $comment object
1823  *
1824  * @param array $commentarr Contains information on the comment.
1825  * @return int Comment was updated if value is 1, or was not updated if value is 0.
1826  */
1827 function wp_update_comment($commentarr) {
1828         global $wpdb;
1829
1830         // First, get all of the original fields
1831         $comment = get_comment($commentarr['comment_ID'], ARRAY_A);
1832         if ( empty( $comment ) )
1833                 return 0;
1834
1835         // Escape data pulled from DB.
1836         $comment = wp_slash($comment);
1837
1838         $old_status = $comment['comment_approved'];
1839
1840         // Merge old and new fields with new fields overwriting old ones.
1841         $commentarr = array_merge($comment, $commentarr);
1842
1843         $commentarr = wp_filter_comment( $commentarr );
1844
1845         // Now extract the merged array.
1846         extract(wp_unslash($commentarr), EXTR_SKIP);
1847
1848         /**
1849          * Filter the comment content before it is updated in the database.
1850          *
1851          * @since 1.5.0
1852          *
1853          * @param string $comment_content The comment data.
1854          */
1855         $comment_content = apply_filters( 'comment_save_pre', $comment_content );
1856
1857         $comment_date_gmt = get_gmt_from_date($comment_date);
1858
1859         if ( !isset($comment_approved) )
1860                 $comment_approved = 1;
1861         else if ( 'hold' == $comment_approved )
1862                 $comment_approved = 0;
1863         else if ( 'approve' == $comment_approved )
1864                 $comment_approved = 1;
1865
1866         $data = compact( 'comment_content', 'comment_author', 'comment_author_email', 'comment_approved', 'comment_karma', 'comment_author_url', 'comment_date', 'comment_date_gmt', 'comment_parent' );
1867         $rval = $wpdb->update( $wpdb->comments, $data, compact( 'comment_ID' ) );
1868
1869         clean_comment_cache($comment_ID);
1870         wp_update_comment_count($comment_post_ID);
1871         /**
1872          * Fires immediately after a comment is updated in the database.
1873          *
1874          * The hook also fires immediately before comment status transition hooks are fired.
1875          *
1876          * @since 1.2.0
1877          *
1878          * @param int $comment_ID The comment ID.
1879          */
1880         do_action( 'edit_comment', $comment_ID );
1881         $comment = get_comment($comment_ID);
1882         wp_transition_comment_status($comment->comment_approved, $old_status, $comment);
1883         return $rval;
1884 }
1885
1886 /**
1887  * Whether to defer comment counting.
1888  *
1889  * When setting $defer to true, all post comment counts will not be updated
1890  * until $defer is set to false. When $defer is set to false, then all
1891  * previously deferred updated post comment counts will then be automatically
1892  * updated without having to call wp_update_comment_count() after.
1893  *
1894  * @since 2.5.0
1895  * @staticvar bool $_defer
1896  *
1897  * @param bool $defer
1898  * @return unknown
1899  */
1900 function wp_defer_comment_counting($defer=null) {
1901         static $_defer = false;
1902
1903         if ( is_bool($defer) ) {
1904                 $_defer = $defer;
1905                 // flush any deferred counts
1906                 if ( !$defer )
1907                         wp_update_comment_count( null, true );
1908         }
1909
1910         return $_defer;
1911 }
1912
1913 /**
1914  * Updates the comment count for post(s).
1915  *
1916  * When $do_deferred is false (is by default) and the comments have been set to
1917  * be deferred, the post_id will be added to a queue, which will be updated at a
1918  * later date and only updated once per post ID.
1919  *
1920  * If the comments have not be set up to be deferred, then the post will be
1921  * updated. When $do_deferred is set to true, then all previous deferred post
1922  * IDs will be updated along with the current $post_id.
1923  *
1924  * @since 2.1.0
1925  * @see wp_update_comment_count_now() For what could cause a false return value
1926  *
1927  * @param int $post_id Post ID
1928  * @param bool $do_deferred Whether to process previously deferred post comment counts
1929  * @return bool True on success, false on failure
1930  */
1931 function wp_update_comment_count($post_id, $do_deferred=false) {
1932         static $_deferred = array();
1933
1934         if ( $do_deferred ) {
1935                 $_deferred = array_unique($_deferred);
1936                 foreach ( $_deferred as $i => $_post_id ) {
1937                         wp_update_comment_count_now($_post_id);
1938                         unset( $_deferred[$i] ); /** @todo Move this outside of the foreach and reset $_deferred to an array instead */
1939                 }
1940         }
1941
1942         if ( wp_defer_comment_counting() ) {
1943                 $_deferred[] = $post_id;
1944                 return true;
1945         }
1946         elseif ( $post_id ) {
1947                 return wp_update_comment_count_now($post_id);
1948         }
1949
1950 }
1951
1952 /**
1953  * Updates the comment count for the post.
1954  *
1955  * @since 2.5.0
1956  * @uses $wpdb
1957  * @uses do_action() Calls 'wp_update_comment_count' hook on $post_id, $new, and $old
1958  * @uses do_action() Calls 'edit_posts' hook on $post_id and $post
1959  *
1960  * @param int $post_id Post ID
1961  * @return bool True on success, false on '0' $post_id or if post with ID does not exist.
1962  */
1963 function wp_update_comment_count_now($post_id) {
1964         global $wpdb;
1965         $post_id = (int) $post_id;
1966         if ( !$post_id )
1967                 return false;
1968         if ( !$post = get_post($post_id) )
1969                 return false;
1970
1971         $old = (int) $post->comment_count;
1972         $new = (int) $wpdb->get_var( $wpdb->prepare("SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1'", $post_id) );
1973         $wpdb->update( $wpdb->posts, array('comment_count' => $new), array('ID' => $post_id) );
1974
1975         clean_post_cache( $post );
1976
1977         /**
1978          * Fires immediately after a post's comment count is updated in the database.
1979          *
1980          * @since 2.3.0
1981          *
1982          * @param int $post_id Post ID.
1983          * @param int $new     The new comment count.
1984          * @param int $old     The old comment count.
1985          */
1986         do_action( 'wp_update_comment_count', $post_id, $new, $old );
1987         /** This action is documented in wp-includes/post.php */
1988         do_action( 'edit_post', $post_id, $post );
1989
1990         return true;
1991 }
1992
1993 //
1994 // Ping and trackback functions.
1995 //
1996
1997 /**
1998  * Finds a pingback server URI based on the given URL.
1999  *
2000  * Checks the HTML for the rel="pingback" link and x-pingback headers. It does
2001  * a check for the x-pingback headers first and returns that, if available. The
2002  * check for the rel="pingback" has more overhead than just the header.
2003  *
2004  * @since 1.5.0
2005  *
2006  * @param string $url URL to ping.
2007  * @param int $deprecated Not Used.
2008  * @return bool|string False on failure, string containing URI on success.
2009  */
2010 function discover_pingback_server_uri( $url, $deprecated = '' ) {
2011         if ( !empty( $deprecated ) )
2012                 _deprecated_argument( __FUNCTION__, '2.7' );
2013
2014         $pingback_str_dquote = 'rel="pingback"';
2015         $pingback_str_squote = 'rel=\'pingback\'';
2016
2017         /** @todo Should use Filter Extension or custom preg_match instead. */
2018         $parsed_url = parse_url($url);
2019
2020         if ( ! isset( $parsed_url['host'] ) ) // Not an URL. This should never happen.
2021                 return false;
2022
2023         //Do not search for a pingback server on our own uploads
2024         $uploads_dir = wp_upload_dir();
2025         if ( 0 === strpos($url, $uploads_dir['baseurl']) )
2026                 return false;
2027
2028         $response = wp_safe_remote_head( $url, array( 'timeout' => 2, 'httpversion' => '1.0' ) );
2029
2030         if ( is_wp_error( $response ) )
2031                 return false;
2032
2033         if ( wp_remote_retrieve_header( $response, 'x-pingback' ) )
2034                 return wp_remote_retrieve_header( $response, 'x-pingback' );
2035
2036         // Not an (x)html, sgml, or xml page, no use going further.
2037         if ( preg_match('#(image|audio|video|model)/#is', wp_remote_retrieve_header( $response, 'content-type' )) )
2038                 return false;
2039
2040         // Now do a GET since we're going to look in the html headers (and we're sure it's not a binary file)
2041         $response = wp_safe_remote_get( $url, array( 'timeout' => 2, 'httpversion' => '1.0' ) );
2042
2043         if ( is_wp_error( $response ) )
2044                 return false;
2045
2046         $contents = wp_remote_retrieve_body( $response );
2047
2048         $pingback_link_offset_dquote = strpos($contents, $pingback_str_dquote);
2049         $pingback_link_offset_squote = strpos($contents, $pingback_str_squote);
2050         if ( $pingback_link_offset_dquote || $pingback_link_offset_squote ) {
2051                 $quote = ($pingback_link_offset_dquote) ? '"' : '\'';
2052                 $pingback_link_offset = ($quote=='"') ? $pingback_link_offset_dquote : $pingback_link_offset_squote;
2053                 $pingback_href_pos = @strpos($contents, 'href=', $pingback_link_offset);
2054                 $pingback_href_start = $pingback_href_pos+6;
2055                 $pingback_href_end = @strpos($contents, $quote, $pingback_href_start);
2056                 $pingback_server_url_len = $pingback_href_end - $pingback_href_start;
2057                 $pingback_server_url = substr($contents, $pingback_href_start, $pingback_server_url_len);
2058
2059                 // We may find rel="pingback" but an incomplete pingback URL
2060                 if ( $pingback_server_url_len > 0 ) { // We got it!
2061                         return $pingback_server_url;
2062                 }
2063         }
2064
2065         return false;
2066 }
2067
2068 /**
2069  * Perform all pingbacks, enclosures, trackbacks, and send to pingback services.
2070  *
2071  * @since 2.1.0
2072  * @uses $wpdb
2073  */
2074 function do_all_pings() {
2075         global $wpdb;
2076
2077         // Do pingbacks
2078         while ($ping = $wpdb->get_row("SELECT ID, post_content, meta_id FROM {$wpdb->posts}, {$wpdb->postmeta} WHERE {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id AND {$wpdb->postmeta}.meta_key = '_pingme' LIMIT 1")) {
2079                 delete_metadata_by_mid( 'post', $ping->meta_id );
2080                 pingback( $ping->post_content, $ping->ID );
2081         }
2082
2083         // Do Enclosures
2084         while ($enclosure = $wpdb->get_row("SELECT ID, post_content, meta_id FROM {$wpdb->posts}, {$wpdb->postmeta} WHERE {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id AND {$wpdb->postmeta}.meta_key = '_encloseme' LIMIT 1")) {
2085                 delete_metadata_by_mid( 'post', $enclosure->meta_id );
2086                 do_enclose( $enclosure->post_content, $enclosure->ID );
2087         }
2088
2089         // Do Trackbacks
2090         $trackbacks = $wpdb->get_col("SELECT ID FROM $wpdb->posts WHERE to_ping <> '' AND post_status = 'publish'");
2091         if ( is_array($trackbacks) )
2092                 foreach ( $trackbacks as $trackback )
2093                         do_trackbacks($trackback);
2094
2095         //Do Update Services/Generic Pings
2096         generic_ping();
2097 }
2098
2099 /**
2100  * Perform trackbacks.
2101  *
2102  * @since 1.5.0
2103  * @uses $wpdb
2104  *
2105  * @param int $post_id Post ID to do trackbacks on.
2106  */
2107 function do_trackbacks($post_id) {
2108         global $wpdb;
2109
2110         $post = get_post( $post_id );
2111         $to_ping = get_to_ping($post_id);
2112         $pinged  = get_pung($post_id);
2113         if ( empty($to_ping) ) {
2114                 $wpdb->update($wpdb->posts, array('to_ping' => ''), array('ID' => $post_id) );
2115                 return;
2116         }
2117
2118         if ( empty($post->post_excerpt) ) {
2119                 /** This filter is documented in wp-admin/post-template.php */
2120                 $excerpt = apply_filters( 'the_content', $post->post_content, $post->ID );
2121         } else {
2122                 /** This filter is documented in wp-admin/post-template.php */
2123                 $excerpt = apply_filters( 'the_excerpt', $post->post_excerpt );
2124         }
2125
2126         $excerpt = str_replace(']]>', ']]&gt;', $excerpt);
2127         $excerpt = wp_html_excerpt($excerpt, 252, '&#8230;');
2128
2129         /** This filter is documented in wp-includes/post-template.php */
2130         $post_title = apply_filters( 'the_title', $post->post_title, $post->ID );
2131         $post_title = strip_tags($post_title);
2132
2133         if ( $to_ping ) {
2134                 foreach ( (array) $to_ping as $tb_ping ) {
2135                         $tb_ping = trim($tb_ping);
2136                         if ( !in_array($tb_ping, $pinged) ) {
2137                                 trackback($tb_ping, $post_title, $excerpt, $post_id);
2138                                 $pinged[] = $tb_ping;
2139                         } else {
2140                                 $wpdb->query( $wpdb->prepare("UPDATE $wpdb->posts SET to_ping = TRIM(REPLACE(to_ping, %s, '')) WHERE ID = %d", $tb_ping, $post_id) );
2141                         }
2142                 }
2143         }
2144 }
2145
2146 /**
2147  * Sends pings to all of the ping site services.
2148  *
2149  * @since 1.2.0
2150  *
2151  * @param int $post_id Post ID. Not actually used.
2152  * @return int Same as Post ID from parameter
2153  */
2154 function generic_ping($post_id = 0) {
2155         $services = get_option('ping_sites');
2156
2157         $services = explode("\n", $services);
2158         foreach ( (array) $services as $service ) {
2159                 $service = trim($service);
2160                 if ( '' != $service )
2161                         weblog_ping($service);
2162         }
2163
2164         return $post_id;
2165 }
2166
2167 /**
2168  * Pings back the links found in a post.
2169  *
2170  * @since 0.71
2171  * @uses $wp_version
2172  * @uses IXR_Client
2173  *
2174  * @param string $content Post content to check for links.
2175  * @param int $post_ID Post ID.
2176  */
2177 function pingback($content, $post_ID) {
2178         global $wp_version;
2179         include_once(ABSPATH . WPINC . '/class-IXR.php');
2180         include_once(ABSPATH . WPINC . '/class-wp-http-ixr-client.php');
2181
2182         // original code by Mort (http://mort.mine.nu:8080)
2183         $post_links = array();
2184
2185         $pung = get_pung($post_ID);
2186
2187         // Step 1
2188         // Parsing the post, external links (if any) are stored in the $post_links array
2189         $post_links_temp = wp_extract_urls( $content );
2190
2191         // Step 2.
2192         // Walking thru the links array
2193         // first we get rid of links pointing to sites, not to specific files
2194         // Example:
2195         // http://dummy-weblog.org
2196         // http://dummy-weblog.org/
2197         // http://dummy-weblog.org/post.php
2198         // We don't wanna ping first and second types, even if they have a valid <link/>
2199
2200         foreach ( (array) $post_links_temp as $link_test ) :
2201                 if ( !in_array($link_test, $pung) && (url_to_postid($link_test) != $post_ID) // If we haven't pung it already and it isn't a link to itself
2202                                 && !is_local_attachment($link_test) ) : // Also, let's never ping local attachments.
2203                         if ( $test = @parse_url($link_test) ) {
2204                                 if ( isset($test['query']) )
2205                                         $post_links[] = $link_test;
2206                                 elseif ( isset( $test['path'] ) && ( $test['path'] != '/' ) && ( $test['path'] != '' ) )
2207                                         $post_links[] = $link_test;
2208                         }
2209                 endif;
2210         endforeach;
2211
2212         $post_links = array_unique( $post_links );
2213         /**
2214          * Fires just before pinging back links found in a post.
2215          *
2216          * @since 2.0.0
2217          *
2218          * @param array &$post_links An array of post links to be checked, passed by reference.
2219          * @param array &$pung       Whether a link has already been pinged, passed by reference.
2220          * @param int   $post_ID     The post ID.
2221          */
2222         do_action_ref_array( 'pre_ping', array( &$post_links, &$pung, $post_ID ) );
2223
2224         foreach ( (array) $post_links as $pagelinkedto ) {
2225                 $pingback_server_url = discover_pingback_server_uri( $pagelinkedto );
2226
2227                 if ( $pingback_server_url ) {
2228                         @ set_time_limit( 60 );
2229                         // Now, the RPC call
2230                         $pagelinkedfrom = get_permalink($post_ID);
2231
2232                         // using a timeout of 3 seconds should be enough to cover slow servers
2233                         $client = new WP_HTTP_IXR_Client($pingback_server_url);
2234                         $client->timeout = 3;
2235                         /**
2236                          * Filter the user agent sent when pinging-back a URL.
2237                          *
2238                          * @since 2.9.0
2239                          *
2240                          * @param string $concat_useragent    The user agent concatenated with ' -- WordPress/'
2241                          *                                    and the WordPress version.
2242                          * @param string $useragent           The useragent.
2243                          * @param string $pingback_server_url The server URL being linked to.
2244                          * @param string $pagelinkedto        URL of page linked to.
2245                          * @param string $pagelinkedfrom      URL of page linked from.
2246                          */
2247                         $client->useragent = apply_filters( 'pingback_useragent', $client->useragent . ' -- WordPress/' . $wp_version, $client->useragent, $pingback_server_url, $pagelinkedto, $pagelinkedfrom );
2248                         // when set to true, this outputs debug messages by itself
2249                         $client->debug = false;
2250
2251                         if ( $client->query('pingback.ping', $pagelinkedfrom, $pagelinkedto) || ( isset($client->error->code) && 48 == $client->error->code ) ) // Already registered
2252                                 add_ping( $post_ID, $pagelinkedto );
2253                 }
2254         }
2255 }
2256
2257 /**
2258  * Check whether blog is public before returning sites.
2259  *
2260  * @since 2.1.0
2261  *
2262  * @param mixed $sites Will return if blog is public, will not return if not public.
2263  * @return mixed Empty string if blog is not public, returns $sites, if site is public.
2264  */
2265 function privacy_ping_filter($sites) {
2266         if ( '0' != get_option('blog_public') )
2267                 return $sites;
2268         else
2269                 return '';
2270 }
2271
2272 /**
2273  * Send a Trackback.
2274  *
2275  * Updates database when sending trackback to prevent duplicates.
2276  *
2277  * @since 0.71
2278  * @uses $wpdb
2279  *
2280  * @param string $trackback_url URL to send trackbacks.
2281  * @param string $title Title of post.
2282  * @param string $excerpt Excerpt of post.
2283  * @param int $ID Post ID.
2284  * @return mixed Database query from update.
2285  */
2286 function trackback($trackback_url, $title, $excerpt, $ID) {
2287         global $wpdb;
2288
2289         if ( empty($trackback_url) )
2290                 return;
2291
2292         $options = array();
2293         $options['timeout'] = 4;
2294         $options['body'] = array(
2295                 'title' => $title,
2296                 'url' => get_permalink($ID),
2297                 'blog_name' => get_option('blogname'),
2298                 'excerpt' => $excerpt
2299         );
2300
2301         $response = wp_safe_remote_post( $trackback_url, $options );
2302
2303         if ( is_wp_error( $response ) )
2304                 return;
2305
2306         $wpdb->query( $wpdb->prepare("UPDATE $wpdb->posts SET pinged = CONCAT(pinged, '\n', %s) WHERE ID = %d", $trackback_url, $ID) );
2307         return $wpdb->query( $wpdb->prepare("UPDATE $wpdb->posts SET to_ping = TRIM(REPLACE(to_ping, %s, '')) WHERE ID = %d", $trackback_url, $ID) );
2308 }
2309
2310 /**
2311  * Send a pingback.
2312  *
2313  * @since 1.2.0
2314  * @uses $wp_version
2315  * @uses IXR_Client
2316  *
2317  * @param string $server Host of blog to connect to.
2318  * @param string $path Path to send the ping.
2319  */
2320 function weblog_ping($server = '', $path = '') {
2321         global $wp_version;
2322         include_once(ABSPATH . WPINC . '/class-IXR.php');
2323         include_once(ABSPATH . WPINC . '/class-wp-http-ixr-client.php');
2324
2325         // using a timeout of 3 seconds should be enough to cover slow servers
2326         $client = new WP_HTTP_IXR_Client($server, ((!strlen(trim($path)) || ('/' == $path)) ? false : $path));
2327         $client->timeout = 3;
2328         $client->useragent .= ' -- WordPress/'.$wp_version;
2329
2330         // when set to true, this outputs debug messages by itself
2331         $client->debug = false;
2332         $home = trailingslashit( home_url() );
2333         if ( !$client->query('weblogUpdates.extendedPing', get_option('blogname'), $home, get_bloginfo('rss2_url') ) ) // then try a normal ping
2334                 $client->query('weblogUpdates.ping', get_option('blogname'), $home);
2335 }
2336
2337 /**
2338  * Default filter attached to pingback_ping_source_uri to validate the pingback's Source URI
2339  *
2340  * @since 3.5.1
2341  * @see wp_http_validate_url()
2342  *
2343  * @param string $source_uri
2344  * @return string
2345  */
2346 function pingback_ping_source_uri( $source_uri ) {
2347         return (string) wp_http_validate_url( $source_uri );
2348 }
2349
2350 /**
2351  * Default filter attached to xmlrpc_pingback_error.
2352  *
2353  * Returns a generic pingback error code unless the error code is 48,
2354  * which reports that the pingback is already registered.
2355  *
2356  * @since 3.5.1
2357  * @link http://www.hixie.ch/specs/pingback/pingback#TOC3
2358  *
2359  * @param IXR_Error $ixr_error
2360  * @return IXR_Error
2361  */
2362 function xmlrpc_pingback_error( $ixr_error ) {
2363         if ( $ixr_error->code === 48 )
2364                 return $ixr_error;
2365         return new IXR_Error( 0, '' );
2366 }
2367
2368 //
2369 // Cache
2370 //
2371
2372 /**
2373  * Removes comment ID from the comment cache.
2374  *
2375  * @since 2.3.0
2376  * @package WordPress
2377  * @subpackage Cache
2378  *
2379  * @param int|array $ids Comment ID or array of comment IDs to remove from cache
2380  */
2381 function clean_comment_cache($ids) {
2382         foreach ( (array) $ids as $id )
2383                 wp_cache_delete($id, 'comment');
2384
2385         wp_cache_set( 'last_changed', microtime(), 'comment' );
2386 }
2387
2388 /**
2389  * Updates the comment cache of given comments.
2390  *
2391  * Will add the comments in $comments to the cache. If comment ID already exists
2392  * in the comment cache then it will not be updated. The comment is added to the
2393  * cache using the comment group with the key using the ID of the comments.
2394  *
2395  * @since 2.3.0
2396  * @package WordPress
2397  * @subpackage Cache
2398  *
2399  * @param array $comments Array of comment row objects
2400  */
2401 function update_comment_cache($comments) {
2402         foreach ( (array) $comments as $comment )
2403                 wp_cache_add($comment->comment_ID, $comment, 'comment');
2404 }
2405
2406 //
2407 // Internal
2408 //
2409
2410 /**
2411  * Close comments on old posts on the fly, without any extra DB queries. Hooked to the_posts.
2412  *
2413  * @access private
2414  * @since 2.7.0
2415  *
2416  * @param object $posts Post data object.
2417  * @param object $query Query object.
2418  * @return object
2419  */
2420 function _close_comments_for_old_posts( $posts, $query ) {
2421         if ( empty( $posts ) || ! $query->is_singular() || ! get_option( 'close_comments_for_old_posts' ) )
2422                 return $posts;
2423
2424         /**
2425          * Filter the list of post types to automatically close comments for.
2426          *
2427          * @since 3.2.0
2428          *
2429          * @param array $post_types An array of registered post types. Default array with 'post'.
2430          */
2431         $post_types = apply_filters( 'close_comments_for_post_types', array( 'post' ) );
2432         if ( ! in_array( $posts[0]->post_type, $post_types ) )
2433                 return $posts;
2434
2435         $days_old = (int) get_option( 'close_comments_days_old' );
2436         if ( ! $days_old )
2437                 return $posts;
2438
2439         if ( time() - strtotime( $posts[0]->post_date_gmt ) > ( $days_old * DAY_IN_SECONDS ) ) {
2440                 $posts[0]->comment_status = 'closed';
2441                 $posts[0]->ping_status = 'closed';
2442         }
2443
2444         return $posts;
2445 }
2446
2447 /**
2448  * Close comments on an old post. Hooked to comments_open and pings_open.
2449  *
2450  * @access private
2451  * @since 2.7.0
2452  *
2453  * @param bool $open Comments open or closed
2454  * @param int $post_id Post ID
2455  * @return bool $open
2456  */
2457 function _close_comments_for_old_post( $open, $post_id ) {
2458         if ( ! $open )
2459                 return $open;
2460
2461         if ( !get_option('close_comments_for_old_posts') )
2462                 return $open;
2463
2464         $days_old = (int) get_option('close_comments_days_old');
2465         if ( !$days_old )
2466                 return $open;
2467
2468         $post = get_post($post_id);
2469
2470         /** This filter is documented in wp-includes/comment.php */
2471         $post_types = apply_filters( 'close_comments_for_post_types', array( 'post' ) );
2472         if ( ! in_array( $post->post_type, $post_types ) )
2473                 return $open;
2474
2475         if ( time() - strtotime( $post->post_date_gmt ) > ( $days_old * DAY_IN_SECONDS ) )
2476                 return false;
2477
2478         return $open;
2479 }