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