Wordpress 2.8.3
[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 is a trackback and part of the blogroll, then the trackback is
22  * automatically whitelisted. If the comment author was approved before, then
23  * the comment is automatically whitelisted.
24  *
25  * If none of the checks fail, then the failback is to set the check to pass
26  * (return true).
27  *
28  * @since 1.2.0
29  * @uses $wpdb
30  *
31  * @param string $author Comment Author's name
32  * @param string $email Comment Author's email
33  * @param string $url Comment Author's URL
34  * @param string $comment Comment contents
35  * @param string $user_ip Comment Author's IP address
36  * @param string $user_agent Comment Author's User Agent
37  * @param string $comment_type Comment type, either user submitted comment,
38  *              trackback, or pingback
39  * @return bool Whether the checks passed (true) and the comments should be
40  *              displayed or set to moderated
41  */
42 function check_comment($author, $email, $url, $comment, $user_ip, $user_agent, $comment_type) {
43         global $wpdb;
44
45         if ( 1 == get_option('comment_moderation') )
46                 return false; // If moderation is set to manual
47
48         if ( get_option('comment_max_links') && preg_match_all("/<[Aa][^>]*[Hh][Rr][Ee][Ff]=['\"]([^\"'>]+)[^>]*>/", apply_filters('comment_text',$comment), $out) >= get_option('comment_max_links') )
49                 return false; // Check # of external links
50
51         $mod_keys = trim(get_option('moderation_keys'));
52         if ( !empty($mod_keys) ) {
53                 $words = explode("\n", $mod_keys );
54
55                 foreach ( (array) $words as $word) {
56                         $word = trim($word);
57
58                         // Skip empty lines
59                         if ( empty($word) )
60                                 continue;
61
62                         // Do some escaping magic so that '#' chars in the
63                         // spam words don't break things:
64                         $word = preg_quote($word, '#');
65
66                         $pattern = "#$word#i";
67                         if ( preg_match($pattern, $author) ) return false;
68                         if ( preg_match($pattern, $email) ) return false;
69                         if ( preg_match($pattern, $url) ) return false;
70                         if ( preg_match($pattern, $comment) ) return false;
71                         if ( preg_match($pattern, $user_ip) ) return false;
72                         if ( preg_match($pattern, $user_agent) ) return false;
73                 }
74         }
75
76         // Comment whitelisting:
77         if ( 1 == get_option('comment_whitelist')) {
78                 if ( 'trackback' == $comment_type || 'pingback' == $comment_type ) { // check if domain is in blogroll
79                         $uri = parse_url($url);
80                         $domain = $uri['host'];
81                         $uri = parse_url( get_option('home') );
82                         $home_domain = $uri['host'];
83                         if ( $wpdb->get_var($wpdb->prepare("SELECT link_id FROM $wpdb->links WHERE link_url LIKE (%s) LIMIT 1", '%'.$domain.'%')) || $domain == $home_domain )
84                                 return true;
85                         else
86                                 return false;
87                 } elseif ( $author != '' && $email != '' ) {
88                         // expected_slashed ($author, $email)
89                         $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");
90                         if ( ( 1 == $ok_to_comment ) &&
91                                 ( empty($mod_keys) || false === strpos( $email, $mod_keys) ) )
92                                         return true;
93                         else
94                                 return false;
95                 } else {
96                         return false;
97                 }
98         }
99         return true;
100 }
101
102 /**
103  * Retrieve the approved comments for post $post_id.
104  *
105  * @since 2.0.0
106  * @uses $wpdb
107  *
108  * @param int $post_id The ID of the post
109  * @return array $comments The approved comments
110  */
111 function get_approved_comments($post_id) {
112         global $wpdb;
113         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));
114 }
115
116 /**
117  * Retrieves comment data given a comment ID or comment object.
118  *
119  * If an object is passed then the comment data will be cached and then returned
120  * after being passed through a filter. If the comment is empty, then the global
121  * comment variable will be used, if it is set.
122  *
123  * If the comment is empty, then the global comment variable will be used, if it
124  * is set.
125  *
126  * @since 2.0.0
127  * @uses $wpdb
128  *
129  * @param object|string|int $comment Comment to retrieve.
130  * @param string $output Optional. OBJECT or ARRAY_A or ARRAY_N constants.
131  * @return object|array|null Depends on $output value.
132  */
133 function &get_comment(&$comment, $output = OBJECT) {
134         global $wpdb;
135
136         if ( empty($comment) ) {
137                 if ( isset($GLOBALS['comment']) )
138                         $_comment = & $GLOBALS['comment'];
139                 else
140                         $_comment = null;
141         } elseif ( is_object($comment) ) {
142                 wp_cache_add($comment->comment_ID, $comment, 'comment');
143                 $_comment = $comment;
144         } else {
145                 if ( isset($GLOBALS['comment']) && ($GLOBALS['comment']->comment_ID == $comment) ) {
146                         $_comment = & $GLOBALS['comment'];
147                 } elseif ( ! $_comment = wp_cache_get($comment, 'comment') ) {
148                         $_comment = $wpdb->get_row($wpdb->prepare("SELECT * FROM $wpdb->comments WHERE comment_ID = %d LIMIT 1", $comment));
149                         wp_cache_add($_comment->comment_ID, $_comment, 'comment');
150                 }
151         }
152
153         $_comment = apply_filters('get_comment', $_comment);
154
155         if ( $output == OBJECT ) {
156                 return $_comment;
157         } elseif ( $output == ARRAY_A ) {
158                 $__comment = get_object_vars($_comment);
159                 return $__comment;
160         } elseif ( $output == ARRAY_N ) {
161                 $__comment = array_values(get_object_vars($_comment));
162                 return $__comment;
163         } else {
164                 return $_comment;
165         }
166 }
167
168 /**
169  * Retrieve a list of comments.
170  *
171  * The comment list can be for the blog as a whole or for an individual post.
172  *
173  * The list of comment arguments are 'status', 'orderby', 'comment_date_gmt',
174  * 'order', 'number', 'offset', and 'post_id'.
175  *
176  * @since 2.7.0
177  * @uses $wpdb
178  *
179  * @param mixed $args Optional. Array or string of options to override defaults.
180  * @return array List of comments.
181  */
182 function get_comments( $args = '' ) {
183         global $wpdb;
184
185         $defaults = array('status' => '', 'orderby' => 'comment_date_gmt', 'order' => 'DESC', 'number' => '', 'offset' => '', 'post_id' => 0);
186
187         $args = wp_parse_args( $args, $defaults );
188         extract( $args, EXTR_SKIP );
189
190         // $args can be whatever, only use the args defined in defaults to compute the key
191         $key = md5( serialize( compact(array_keys($defaults)) )  );
192         $last_changed = wp_cache_get('last_changed', 'comment');
193         if ( !$last_changed ) {
194                 $last_changed = time();
195                 wp_cache_set('last_changed', $last_changed, 'comment');
196         }
197         $cache_key = "get_comments:$key:$last_changed";
198
199         if ( $cache = wp_cache_get( $cache_key, 'comment' ) ) {
200                 return $cache;
201         }
202
203         $post_id = absint($post_id);
204
205         if ( 'hold' == $status )
206                 $approved = "comment_approved = '0'";
207         elseif ( 'approve' == $status )
208                 $approved = "comment_approved = '1'";
209         elseif ( 'spam' == $status )
210                 $approved = "comment_approved = 'spam'";
211         else
212                 $approved = "( comment_approved = '0' OR comment_approved = '1' )";
213
214         $order = ( 'ASC' == $order ) ? 'ASC' : 'DESC';
215
216         $orderby = 'comment_date_gmt';  // Hard code for now
217
218         $number = absint($number);
219         $offset = absint($offset);
220
221         if ( !empty($number) ) {
222                 if ( $offset )
223                         $number = 'LIMIT ' . $offset . ',' . $number;
224                 else
225                         $number = 'LIMIT ' . $number;
226
227         } else {
228                 $number = '';
229         }
230
231         if ( ! empty($post_id) )
232                 $post_where = $wpdb->prepare( 'comment_post_ID = %d AND', $post_id );
233         else
234                 $post_where = '';
235
236         $comments = $wpdb->get_results( "SELECT * FROM $wpdb->comments WHERE $post_where $approved ORDER BY $orderby $order $number" );
237         wp_cache_add( $cache_key, $comments, 'comment' );
238
239         return $comments;
240 }
241
242 /**
243  * Retrieve all of the WordPress supported comment statuses.
244  *
245  * Comments have a limited set of valid status values, this provides the comment
246  * status values and descriptions.
247  *
248  * @package WordPress
249  * @subpackage Post
250  * @since 2.7.0
251  *
252  * @return array List of comment statuses.
253  */
254 function get_comment_statuses( ) {
255         $status = array(
256                 'hold'          => __('Unapproved'),
257                 /* translators: comment status  */
258                 'approve'       => _x('Approved', 'adjective'),
259                 /* translators: comment status */
260                 'spam'          => _x('Spam', 'adjective'),
261         );
262
263         return $status;
264 }
265
266
267 /**
268  * The date the last comment was modified.
269  *
270  * @since 1.5.0
271  * @uses $wpdb
272  * @global array $cache_lastcommentmodified
273  *
274  * @param string $timezone Which timezone to use in reference to 'gmt', 'blog',
275  *              or 'server' locations.
276  * @return string Last comment modified date.
277  */
278 function get_lastcommentmodified($timezone = 'server') {
279         global $cache_lastcommentmodified, $wpdb;
280
281         if ( isset($cache_lastcommentmodified[$timezone]) )
282                 return $cache_lastcommentmodified[$timezone];
283
284         $add_seconds_server = date('Z');
285
286         switch ( strtolower($timezone)) {
287                 case 'gmt':
288                         $lastcommentmodified = $wpdb->get_var("SELECT comment_date_gmt FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1");
289                         break;
290                 case 'blog':
291                         $lastcommentmodified = $wpdb->get_var("SELECT comment_date FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1");
292                         break;
293                 case 'server':
294                         $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));
295                         break;
296         }
297
298         $cache_lastcommentmodified[$timezone] = $lastcommentmodified;
299
300         return $lastcommentmodified;
301 }
302
303 /**
304  * The amount of comments in a post or total comments.
305  *
306  * A lot like {@link wp_count_comments()}, in that they both return comment
307  * stats (albeit with different types). The {@link wp_count_comments()} actual
308  * caches, but this function does not.
309  *
310  * @since 2.0.0
311  * @uses $wpdb
312  *
313  * @param int $post_id Optional. Comment amount in post if > 0, else total comments blog wide.
314  * @return array The amount of spam, approved, awaiting moderation, and total comments.
315  */
316 function get_comment_count( $post_id = 0 ) {
317         global $wpdb;
318
319         $post_id = (int) $post_id;
320
321         $where = '';
322         if ( $post_id > 0 ) {
323                 $where = $wpdb->prepare("WHERE comment_post_ID = %d", $post_id);
324         }
325
326         $totals = (array) $wpdb->get_results("
327                 SELECT comment_approved, COUNT( * ) AS total
328                 FROM {$wpdb->comments}
329                 {$where}
330                 GROUP BY comment_approved
331         ", ARRAY_A);
332
333         $comment_count = array(
334                 "approved"              => 0,
335                 "awaiting_moderation"   => 0,
336                 "spam"                  => 0,
337                 "total_comments"        => 0
338         );
339
340         foreach ( $totals as $row ) {
341                 switch ( $row['comment_approved'] ) {
342                         case 'spam':
343                                 $comment_count['spam'] = $row['total'];
344                                 $comment_count["total_comments"] += $row['total'];
345                                 break;
346                         case 1:
347                                 $comment_count['approved'] = $row['total'];
348                                 $comment_count['total_comments'] += $row['total'];
349                                 break;
350                         case 0:
351                                 $comment_count['awaiting_moderation'] = $row['total'];
352                                 $comment_count['total_comments'] += $row['total'];
353                                 break;
354                         default:
355                                 break;
356                 }
357         }
358
359         return $comment_count;
360 }
361
362 /**
363  * Sanitizes the cookies sent to the user already.
364  *
365  * Will only do anything if the cookies have already been created for the user.
366  * Mostly used after cookies had been sent to use elsewhere.
367  *
368  * @since 2.0.4
369  */
370 function sanitize_comment_cookies() {
371         if ( isset($_COOKIE['comment_author_'.COOKIEHASH]) ) {
372                 $comment_author = apply_filters('pre_comment_author_name', $_COOKIE['comment_author_'.COOKIEHASH]);
373                 $comment_author = stripslashes($comment_author);
374                 $comment_author = esc_attr($comment_author);
375                 $_COOKIE['comment_author_'.COOKIEHASH] = $comment_author;
376         }
377
378         if ( isset($_COOKIE['comment_author_email_'.COOKIEHASH]) ) {
379                 $comment_author_email = apply_filters('pre_comment_author_email', $_COOKIE['comment_author_email_'.COOKIEHASH]);
380                 $comment_author_email = stripslashes($comment_author_email);
381                 $comment_author_email = esc_attr($comment_author_email);
382                 $_COOKIE['comment_author_email_'.COOKIEHASH] = $comment_author_email;
383         }
384
385         if ( isset($_COOKIE['comment_author_url_'.COOKIEHASH]) ) {
386                 $comment_author_url = apply_filters('pre_comment_author_url', $_COOKIE['comment_author_url_'.COOKIEHASH]);
387                 $comment_author_url = stripslashes($comment_author_url);
388                 $_COOKIE['comment_author_url_'.COOKIEHASH] = $comment_author_url;
389         }
390 }
391
392 /**
393  * Validates whether this comment is allowed to be made or not.
394  *
395  * @since 2.0.0
396  * @uses $wpdb
397  * @uses apply_filters() Calls 'pre_comment_approved' hook on the type of comment
398  * @uses do_action() Calls 'check_comment_flood' hook on $comment_author_IP, $comment_author_email, and $comment_date_gmt
399  *
400  * @param array $commentdata Contains information on the comment
401  * @return mixed Signifies the approval status (0|1|'spam')
402  */
403 function wp_allow_comment($commentdata) {
404         global $wpdb;
405         extract($commentdata, EXTR_SKIP);
406
407         // Simple duplicate check
408         // expected_slashed ($comment_post_ID, $comment_author, $comment_author_email, $comment_content)
409         $dupe = "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = '$comment_post_ID' AND ( comment_author = '$comment_author' ";
410         if ( $comment_author_email )
411                 $dupe .= "OR comment_author_email = '$comment_author_email' ";
412         $dupe .= ") AND comment_content = '$comment_content' LIMIT 1";
413         if ( $wpdb->get_var($dupe) ) {
414                 if ( defined('DOING_AJAX') )
415                         die( __('Duplicate comment detected; it looks as though you&#8217;ve already said that!') );
416
417                 wp_die( __('Duplicate comment detected; it looks as though you&#8217;ve already said that!') );
418         }
419
420         do_action( 'check_comment_flood', $comment_author_IP, $comment_author_email, $comment_date_gmt );
421
422         if ( $user_id ) {
423                 $userdata = get_userdata($user_id);
424                 $user = new WP_User($user_id);
425                 $post_author = $wpdb->get_var($wpdb->prepare("SELECT post_author FROM $wpdb->posts WHERE ID = %d LIMIT 1", $comment_post_ID));
426         }
427
428         if ( isset($userdata) && ( $user_id == $post_author || $user->has_cap('moderate_comments') ) ) {
429                 // The author and the admins get respect.
430                 $approved = 1;
431          } else {
432                 // Everyone else's comments will be checked.
433                 if ( check_comment($comment_author, $comment_author_email, $comment_author_url, $comment_content, $comment_author_IP, $comment_agent, $comment_type) )
434                         $approved = 1;
435                 else
436                         $approved = 0;
437                 if ( wp_blacklist_check($comment_author, $comment_author_email, $comment_author_url, $comment_content, $comment_author_IP, $comment_agent) )
438                         $approved = 'spam';
439         }
440
441         $approved = apply_filters('pre_comment_approved', $approved);
442         return $approved;
443 }
444
445 /**
446  * Check whether comment flooding is occurring.
447  *
448  * Won't run, if current user can manage options, so to not block
449  * administrators.
450  *
451  * @since 2.3.0
452  * @uses $wpdb
453  * @uses apply_filters() Calls 'comment_flood_filter' filter with first
454  *              parameter false, last comment timestamp, new comment timestamp.
455  * @uses do_action() Calls 'comment_flood_trigger' action with parameters with
456  *              last comment timestamp and new comment timestamp.
457  *
458  * @param string $ip Comment IP.
459  * @param string $email Comment author email address.
460  * @param string $date MySQL time string.
461  */
462 function check_comment_flood_db( $ip, $email, $date ) {
463         global $wpdb;
464         if ( current_user_can( 'manage_options' ) )
465                 return; // don't throttle admins
466         if ( $lasttime = $wpdb->get_var( $wpdb->prepare("SELECT comment_date_gmt FROM $wpdb->comments WHERE comment_author_IP = %s OR comment_author_email = %s ORDER BY comment_date DESC LIMIT 1", $ip, $email) ) ) {
467                 $time_lastcomment = mysql2date('U', $lasttime, false);
468                 $time_newcomment  = mysql2date('U', $date, false);
469                 $flood_die = apply_filters('comment_flood_filter', false, $time_lastcomment, $time_newcomment);
470                 if ( $flood_die ) {
471                         do_action('comment_flood_trigger', $time_lastcomment, $time_newcomment);
472
473                         if ( defined('DOING_AJAX') )
474                                 die( __('You are posting comments too quickly.  Slow down.') );
475
476                         wp_die( __('You are posting comments too quickly.  Slow down.'), '', array('response' => 403) );
477                 }
478         }
479 }
480
481 /**
482  * Separates an array of comments into an array keyed by comment_type.
483  *
484  * @since 2.7.0
485  *
486  * @param array $comments Array of comments
487  * @return array Array of comments keyed by comment_type.
488  */
489 function &separate_comments(&$comments) {
490         $comments_by_type = array('comment' => array(), 'trackback' => array(), 'pingback' => array(), 'pings' => array());
491         $count = count($comments);
492         for ( $i = 0; $i < $count; $i++ ) {
493                 $type = $comments[$i]->comment_type;
494                 if ( empty($type) )
495                         $type = 'comment';
496                 $comments_by_type[$type][] = &$comments[$i];
497                 if ( 'trackback' == $type || 'pingback' == $type )
498                         $comments_by_type['pings'][] = &$comments[$i];
499         }
500
501         return $comments_by_type;
502 }
503
504 /**
505  * Calculate the total number of comment pages.
506  *
507  * @since 2.7.0
508  * @uses get_query_var() Used to fill in the default for $per_page parameter.
509  * @uses get_option() Used to fill in defaults for parameters.
510  * @uses Walker_Comment
511  *
512  * @param array $comments Optional array of comment objects.  Defaults to $wp_query->comments
513  * @param int $per_page Optional comments per page.
514  * @param boolean $threaded Optional control over flat or threaded comments.
515  * @return int Number of comment pages.
516  */
517 function get_comment_pages_count( $comments = null, $per_page = null, $threaded = null ) {
518         global $wp_query;
519
520         if ( null === $comments && null === $per_page && null === $threaded && !empty($wp_query->max_num_comment_pages) )
521                 return $wp_query->max_num_comment_pages;
522
523         if ( !$comments || !is_array($comments) )
524                 $comments = $wp_query->comments;
525
526         if ( empty($comments) )
527                 return 0;
528
529         if ( !isset($per_page) )
530                 $per_page = (int) get_query_var('comments_per_page');
531         if ( 0 === $per_page )
532                 $per_page = (int) get_option('comments_per_page');
533         if ( 0 === $per_page )
534                 return 1;
535
536         if ( !isset($threaded) )
537                 $threaded = get_option('thread_comments');
538
539         if ( $threaded ) {
540                 $walker = new Walker_Comment;
541                 $count = ceil( $walker->get_number_of_root_elements( $comments ) / $per_page );
542         } else {
543                 $count = ceil( count( $comments ) / $per_page );
544         }
545
546         return $count;
547 }
548
549 /**
550  * Calculate what page number a comment will appear on for comment paging.
551  *
552  * @since 2.7.0
553  * @uses get_comment() Gets the full comment of the $comment_ID parameter.
554  * @uses get_option() Get various settings to control function and defaults.
555  * @uses get_page_of_comment() Used to loop up to top level comment.
556  *
557  * @param int $comment_ID Comment ID.
558  * @param array $args Optional args.
559  * @return int|null Comment page number or null on error.
560  */
561 function get_page_of_comment( $comment_ID, $args = array() ) {
562         global $wpdb;
563
564         if ( !$comment = get_comment( $comment_ID ) )
565                 return;
566
567         $defaults = array( 'type' => 'all', 'page' => '', 'per_page' => '', 'max_depth' => '' );
568         $args = wp_parse_args( $args, $defaults );
569
570         if ( '' === $args['per_page'] && get_option('page_comments') )
571                 $args['per_page'] = get_query_var('comments_per_page');
572         if ( empty($args['per_page']) ) {
573                 $args['per_page'] = 0;
574                 $args['page'] = 0;
575         }
576         if ( $args['per_page'] < 1 )
577                 return 1;
578
579         if ( '' === $args['max_depth'] ) {
580                 if ( get_option('thread_comments') )
581                         $args['max_depth'] = get_option('thread_comments_depth');
582                 else
583                         $args['max_depth'] = -1;
584         }
585
586         // Find this comment's top level parent if threading is enabled
587         if ( $args['max_depth'] > 1 && 0 != $comment->comment_parent )
588                 return get_page_of_comment( $comment->comment_parent, $args );
589
590         $allowedtypes = array(
591                 'comment' => '',
592                 'pingback' => 'pingback',
593                 'trackback' => 'trackback',
594         );
595
596         $comtypewhere = ( 'all' != $args['type'] && isset($allowedtypes[$args['type']]) ) ? " AND comment_type = '" . $allowedtypes[$args['type']] . "'" : '';
597
598         // Count comments older than this one
599         $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 ) );
600
601         // No older comments? Then it's page #1.
602         if ( 0 == $oldercoms )
603                 return 1;
604
605         // Divide comments older than this one by comments per page to get this comment's page number
606         return ceil( ( $oldercoms + 1 ) / $args['per_page'] );
607 }
608
609 /**
610  * Does comment contain blacklisted characters or words.
611  *
612  * @since 1.5.0
613  * @uses do_action() Calls 'wp_blacklist_check' hook for all parameters.
614  *
615  * @param string $author The author of the comment
616  * @param string $email The email of the comment
617  * @param string $url The url used in the comment
618  * @param string $comment The comment content
619  * @param string $user_ip The comment author IP address
620  * @param string $user_agent The author's browser user agent
621  * @return bool True if comment contains blacklisted content, false if comment does not
622  */
623 function wp_blacklist_check($author, $email, $url, $comment, $user_ip, $user_agent) {
624         do_action('wp_blacklist_check', $author, $email, $url, $comment, $user_ip, $user_agent);
625
626         if ( preg_match_all('/&#(\d+);/', $comment . $author . $url, $chars) ) {
627                 foreach ( (array) $chars[1] as $char ) {
628                         // If it's an encoded char in the normal ASCII set, reject
629                         if ( 38 == $char )
630                                 continue; // Unless it's &
631                         if ( $char < 128 )
632                                 return true;
633                 }
634         }
635
636         $mod_keys = trim( get_option('blacklist_keys') );
637         if ( '' == $mod_keys )
638                 return false; // If moderation keys are empty
639         $words = explode("\n", $mod_keys );
640
641         foreach ( (array) $words as $word ) {
642                 $word = trim($word);
643
644                 // Skip empty lines
645                 if ( empty($word) ) { continue; }
646
647                 // Do some escaping magic so that '#' chars in the
648                 // spam words don't break things:
649                 $word = preg_quote($word, '#');
650
651                 $pattern = "#$word#i";
652                 if (
653                            preg_match($pattern, $author)
654                         || preg_match($pattern, $email)
655                         || preg_match($pattern, $url)
656                         || preg_match($pattern, $comment)
657                         || preg_match($pattern, $user_ip)
658                         || preg_match($pattern, $user_agent)
659                  )
660                         return true;
661         }
662         return false;
663 }
664
665 /**
666  * Retrieve total comments for blog or single post.
667  *
668  * The properties of the returned object contain the 'moderated', 'approved',
669  * and spam comments for either the entire blog or single post. Those properties
670  * contain the amount of comments that match the status. The 'total_comments'
671  * property contains the integer of total comments.
672  *
673  * The comment stats are cached and then retrieved, if they already exist in the
674  * cache.
675  *
676  * @since 2.5.0
677  *
678  * @param int $post_id Optional. Post ID.
679  * @return object Comment stats.
680  */
681 function wp_count_comments( $post_id = 0 ) {
682         global $wpdb;
683
684         $post_id = (int) $post_id;
685
686         $stats = apply_filters('wp_count_comments', array(), $post_id);
687         if ( !empty($stats) )
688                 return $stats;
689
690         $count = wp_cache_get("comments-{$post_id}", 'counts');
691
692         if ( false !== $count )
693                 return $count;
694
695         $where = '';
696         if( $post_id > 0 )
697                 $where = $wpdb->prepare( "WHERE comment_post_ID = %d", $post_id );
698
699         $count = $wpdb->get_results( "SELECT comment_approved, COUNT( * ) AS num_comments FROM {$wpdb->comments} {$where} GROUP BY comment_approved", ARRAY_A );
700
701         $total = 0;
702         $approved = array('0' => 'moderated', '1' => 'approved', 'spam' => 'spam');
703         $known_types = array_keys( $approved );
704         foreach( (array) $count as $row_num => $row ) {
705                 $total += $row['num_comments'];
706                 if ( in_array( $row['comment_approved'], $known_types ) )
707                         $stats[$approved[$row['comment_approved']]] = $row['num_comments'];
708         }
709
710         $stats['total_comments'] = $total;
711         foreach ( $approved as $key ) {
712                 if ( empty($stats[$key]) )
713                         $stats[$key] = 0;
714         }
715
716         $stats = (object) $stats;
717         wp_cache_set("comments-{$post_id}", $stats, 'counts');
718
719         return $stats;
720 }
721
722 /**
723  * Removes comment ID and maybe updates post comment count.
724  *
725  * The post comment count will be updated if the comment was approved and has a
726  * post ID available.
727  *
728  * @since 2.0.0
729  * @uses $wpdb
730  * @uses do_action() Calls 'delete_comment' hook on comment ID
731  * @uses do_action() Calls 'wp_set_comment_status' hook on comment ID with 'delete' set for the second parameter
732  * @uses wp_transition_comment_status() Passes new and old comment status along with $comment object
733  *
734  * @param int $comment_id Comment ID
735  * @return bool False if delete comment query failure, true on success.
736  */
737 function wp_delete_comment($comment_id) {
738         global $wpdb;
739         do_action('delete_comment', $comment_id);
740
741         $comment = get_comment($comment_id);
742
743         if ( ! $wpdb->query( $wpdb->prepare("DELETE FROM $wpdb->comments WHERE comment_ID = %d LIMIT 1", $comment_id) ) )
744                 return false;
745
746         // Move children up a level.
747         $children = $wpdb->get_col( $wpdb->prepare("SELECT comment_ID FROM $wpdb->comments WHERE comment_parent = %d", $comment_id) );
748         if ( !empty($children) ) {
749                 $wpdb->update($wpdb->comments, array('comment_parent' => $comment->comment_parent), array('comment_parent' => $comment_id));
750                 clean_comment_cache($children);
751         }
752
753         $post_id = $comment->comment_post_ID;
754         if ( $post_id && $comment->comment_approved == 1 )
755                 wp_update_comment_count($post_id);
756
757         clean_comment_cache($comment_id);
758
759         do_action('wp_set_comment_status', $comment_id, 'delete');
760         wp_transition_comment_status('delete', $comment->comment_approved, $comment);
761         return true;
762 }
763
764 /**
765  * The status of a comment by ID.
766  *
767  * @since 1.0.0
768  *
769  * @param int $comment_id Comment ID
770  * @return string|bool Status might be 'deleted', 'approved', 'unapproved', 'spam'. False on failure.
771  */
772 function wp_get_comment_status($comment_id) {
773         $comment = get_comment($comment_id);
774         if ( !$comment )
775                 return false;
776
777         $approved = $comment->comment_approved;
778
779         if ( $approved == NULL )
780                 return 'deleted';
781         elseif ( $approved == '1' )
782                 return 'approved';
783         elseif ( $approved == '0' )
784                 return 'unapproved';
785         elseif ( $approved == 'spam' )
786                 return 'spam';
787         else
788                 return false;
789 }
790
791 /**
792  * Call hooks for when a comment status transition occurs.
793  *
794  * Calls hooks for comment status transitions. If the new comment status is not the same
795  * as the previous comment status, then two hooks will be ran, the first is
796  * 'transition_comment_status' with new status, old status, and comment data. The
797  * next action called is 'comment_OLDSTATUS_to_NEWSTATUS' the NEWSTATUS is the
798  * $new_status parameter and the OLDSTATUS is $old_status parameter; it has the
799  * comment data.
800  *
801  * The final action will run whether or not the comment statuses are the same. The
802  * action is named 'comment_NEWSTATUS_COMMENTTYPE', NEWSTATUS is from the $new_status
803  * parameter and COMMENTTYPE is comment_type comment data.
804  *
805  * @since 2.7.0
806  *
807  * @param string $new_status New comment status.
808  * @param string $old_status Previous comment status.
809  * @param object $comment Comment data.
810  */
811 function wp_transition_comment_status($new_status, $old_status, $comment) {
812         // Translate raw statuses to human readable formats for the hooks
813         // This is not a complete list of comment status, it's only the ones that need to be renamed
814         $comment_statuses = array(
815                 0         => 'unapproved',
816                 'hold'    => 'unapproved', // wp_set_comment_status() uses "hold"
817                 1         => 'approved',
818                 'approve' => 'approved', // wp_set_comment_status() uses "approve"
819         );
820         if ( isset($comment_statuses[$new_status]) ) $new_status = $comment_statuses[$new_status];
821         if ( isset($comment_statuses[$old_status]) ) $old_status = $comment_statuses[$old_status];
822
823         // Call the hooks
824         if ( $new_status != $old_status ) {
825                 do_action('transition_comment_status', $new_status, $old_status, $comment);
826                 do_action("comment_${old_status}_to_$new_status", $comment);
827         }
828         do_action("comment_${new_status}_$comment->comment_type", $comment->comment_ID, $comment);
829 }
830
831 /**
832  * Get current commenter's name, email, and URL.
833  *
834  * Expects cookies content to already be sanitized. User of this function might
835  * wish to recheck the returned array for validity.
836  *
837  * @see sanitize_comment_cookies() Use to sanitize cookies
838  *
839  * @since 2.0.4
840  *
841  * @return array Comment author, email, url respectively.
842  */
843 function wp_get_current_commenter() {
844         // Cookies should already be sanitized.
845
846         $comment_author = '';
847         if ( isset($_COOKIE['comment_author_'.COOKIEHASH]) )
848                 $comment_author = $_COOKIE['comment_author_'.COOKIEHASH];
849
850         $comment_author_email = '';
851         if ( isset($_COOKIE['comment_author_email_'.COOKIEHASH]) )
852                 $comment_author_email = $_COOKIE['comment_author_email_'.COOKIEHASH];
853
854         $comment_author_url = '';
855         if ( isset($_COOKIE['comment_author_url_'.COOKIEHASH]) )
856                 $comment_author_url = $_COOKIE['comment_author_url_'.COOKIEHASH];
857
858         return compact('comment_author', 'comment_author_email', 'comment_author_url');
859 }
860
861 /**
862  * Inserts a comment to the database.
863  *
864  * The available comment data key names are 'comment_author_IP', 'comment_date',
865  * 'comment_date_gmt', 'comment_parent', 'comment_approved', and 'user_id'.
866  *
867  * @since 2.0.0
868  * @uses $wpdb
869  *
870  * @param array $commentdata Contains information on the comment.
871  * @return int The new comment's ID.
872  */
873 function wp_insert_comment($commentdata) {
874         global $wpdb;
875         extract(stripslashes_deep($commentdata), EXTR_SKIP);
876
877         if ( ! isset($comment_author_IP) )
878                 $comment_author_IP = '';
879         if ( ! isset($comment_date) )
880                 $comment_date = current_time('mysql');
881         if ( ! isset($comment_date_gmt) )
882                 $comment_date_gmt = get_gmt_from_date($comment_date);
883         if ( ! isset($comment_parent) )
884                 $comment_parent = 0;
885         if ( ! isset($comment_approved) )
886                 $comment_approved = 1;
887         if ( ! isset($comment_karma) )
888                 $comment_karma = 0;
889         if ( ! isset($user_id) )
890                 $user_id = 0;
891         if ( ! isset($comment_type) )
892                 $comment_type = '';
893
894         $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');
895         $wpdb->insert($wpdb->comments, $data);
896
897         $id = (int) $wpdb->insert_id;
898
899         if ( $comment_approved == 1 )
900                 wp_update_comment_count($comment_post_ID);
901
902         $comment = get_comment($id);
903         do_action('wp_insert_comment', $id, $comment);
904
905         return $id;
906 }
907
908 /**
909  * Filters and sanitizes comment data.
910  *
911  * Sets the comment data 'filtered' field to true when finished. This can be
912  * checked as to whether the comment should be filtered and to keep from
913  * filtering the same comment more than once.
914  *
915  * @since 2.0.0
916  * @uses apply_filters() Calls 'pre_user_id' hook on comment author's user ID
917  * @uses apply_filters() Calls 'pre_comment_user_agent' hook on comment author's user agent
918  * @uses apply_filters() Calls 'pre_comment_author_name' hook on comment author's name
919  * @uses apply_filters() Calls 'pre_comment_content' hook on the comment's content
920  * @uses apply_filters() Calls 'pre_comment_user_ip' hook on comment author's IP
921  * @uses apply_filters() Calls 'pre_comment_author_url' hook on comment author's URL
922  * @uses apply_filters() Calls 'pre_comment_author_email' hook on comment author's email address
923  *
924  * @param array $commentdata Contains information on the comment.
925  * @return array Parsed comment information.
926  */
927 function wp_filter_comment($commentdata) {
928         $commentdata['user_id']              = apply_filters('pre_user_id', $commentdata['user_ID']);
929         $commentdata['comment_agent']        = apply_filters('pre_comment_user_agent', $commentdata['comment_agent']);
930         $commentdata['comment_author']       = apply_filters('pre_comment_author_name', $commentdata['comment_author']);
931         $commentdata['comment_content']      = apply_filters('pre_comment_content', $commentdata['comment_content']);
932         $commentdata['comment_author_IP']    = apply_filters('pre_comment_user_ip', $commentdata['comment_author_IP']);
933         $commentdata['comment_author_url']   = apply_filters('pre_comment_author_url', $commentdata['comment_author_url']);
934         $commentdata['comment_author_email'] = apply_filters('pre_comment_author_email', $commentdata['comment_author_email']);
935         $commentdata['filtered'] = true;
936         return $commentdata;
937 }
938
939 /**
940  * Whether comment should be blocked because of comment flood.
941  *
942  * @since 2.1.0
943  *
944  * @param bool $block Whether plugin has already blocked comment.
945  * @param int $time_lastcomment Timestamp for last comment.
946  * @param int $time_newcomment Timestamp for new comment.
947  * @return bool Whether comment should be blocked.
948  */
949 function wp_throttle_comment_flood($block, $time_lastcomment, $time_newcomment) {
950         if ( $block ) // a plugin has already blocked... we'll let that decision stand
951                 return $block;
952         if ( ($time_newcomment - $time_lastcomment) < 15 )
953                 return true;
954         return false;
955 }
956
957 /**
958  * Adds a new comment to the database.
959  *
960  * Filters new comment to ensure that the fields are sanitized and valid before
961  * inserting comment into database. Calls 'comment_post' action with comment ID
962  * and whether comment is approved by WordPress. Also has 'preprocess_comment'
963  * filter for processing the comment data before the function handles it.
964  *
965  * @since 1.5.0
966  * @uses apply_filters() Calls 'preprocess_comment' hook on $commentdata parameter array before processing
967  * @uses do_action() Calls 'comment_post' hook on $comment_ID returned from adding the comment and if the comment was approved.
968  * @uses wp_filter_comment() Used to filter comment before adding comment.
969  * @uses wp_allow_comment() checks to see if comment is approved.
970  * @uses wp_insert_comment() Does the actual comment insertion to the database.
971  *
972  * @param array $commentdata Contains information on the comment.
973  * @return int The ID of the comment after adding.
974  */
975 function wp_new_comment( $commentdata ) {
976         $commentdata = apply_filters('preprocess_comment', $commentdata);
977
978         $commentdata['comment_post_ID'] = (int) $commentdata['comment_post_ID'];
979         $commentdata['user_ID']         = (int) $commentdata['user_ID'];
980
981         $commentdata['comment_parent'] = absint($commentdata['comment_parent']);
982         $parent_status = ( 0 < $commentdata['comment_parent'] ) ? wp_get_comment_status($commentdata['comment_parent']) : '';
983         $commentdata['comment_parent'] = ( 'approved' == $parent_status || 'unapproved' == $parent_status ) ? $commentdata['comment_parent'] : 0;
984
985         $commentdata['comment_author_IP'] = preg_replace( '/[^0-9a-fA-F:., ]/', '',$_SERVER['REMOTE_ADDR'] );
986         $commentdata['comment_agent']     = $_SERVER['HTTP_USER_AGENT'];
987
988         $commentdata['comment_date']     = current_time('mysql');
989         $commentdata['comment_date_gmt'] = current_time('mysql', 1);
990
991         $commentdata = wp_filter_comment($commentdata);
992
993         $commentdata['comment_approved'] = wp_allow_comment($commentdata);
994
995         $comment_ID = wp_insert_comment($commentdata);
996
997         do_action('comment_post', $comment_ID, $commentdata['comment_approved']);
998
999         if ( 'spam' !== $commentdata['comment_approved'] ) { // If it's spam save it silently for later crunching
1000                 if ( '0' == $commentdata['comment_approved'] )
1001                         wp_notify_moderator($comment_ID);
1002
1003                 $post = &get_post($commentdata['comment_post_ID']); // Don't notify if it's your own comment
1004
1005                 if ( get_option('comments_notify') && $commentdata['comment_approved'] && $post->post_author != $commentdata['user_ID'] )
1006                         wp_notify_postauthor($comment_ID, $commentdata['comment_type']);
1007         }
1008
1009         return $comment_ID;
1010 }
1011
1012 /**
1013  * Sets the status of a comment.
1014  *
1015  * The 'wp_set_comment_status' action is called after the comment is handled and
1016  * will only be called, if the comment status is either 'hold', 'approve', or
1017  * 'spam'. If the comment status is not in the list, then false is returned and
1018  * if the status is 'delete', then the comment is deleted without calling the
1019  * action.
1020  *
1021  * @since 1.0.0
1022  * @uses wp_transition_comment_status() Passes new and old comment status along with $comment object
1023  *
1024  * @param int $comment_id Comment ID.
1025  * @param string $comment_status New comment status, either 'hold', 'approve', 'spam', or 'delete'.
1026  * @param bool $wp_error Whether to return a WP_Error object if there is a failure. Default is false.
1027  * @return bool False on failure or deletion and true on success.
1028  */
1029 function wp_set_comment_status($comment_id, $comment_status, $wp_error = false) {
1030         global $wpdb;
1031
1032         $status = '0';
1033         switch ( $comment_status ) {
1034                 case 'hold':
1035                         $status = '0';
1036                         break;
1037                 case 'approve':
1038                         $status = '1';
1039                         if ( get_option('comments_notify') ) {
1040                                 $comment = get_comment($comment_id);
1041                                 wp_notify_postauthor($comment_id, $comment->comment_type);
1042                         }
1043                         break;
1044                 case 'spam':
1045                         $status = 'spam';
1046                         break;
1047                 case 'delete':
1048                         return wp_delete_comment($comment_id);
1049                         break;
1050                 default:
1051                         return false;
1052         }
1053
1054         if ( !$wpdb->update( $wpdb->comments, array('comment_approved' => $status), array('comment_ID' => $comment_id) ) ) {
1055                 if ( $wp_error )
1056                         return new WP_Error('db_update_error', __('Could not update comment status'), $wpdb->last_error);
1057                 else
1058                         return false;
1059         }
1060
1061         clean_comment_cache($comment_id);
1062
1063         $comment = get_comment($comment_id);
1064
1065         do_action('wp_set_comment_status', $comment_id, $comment_status);
1066         wp_transition_comment_status($comment_status, $comment->comment_approved, $comment);
1067
1068         wp_update_comment_count($comment->comment_post_ID);
1069
1070         return true;
1071 }
1072
1073 /**
1074  * Updates an existing comment in the database.
1075  *
1076  * Filters the comment and makes sure certain fields are valid before updating.
1077  *
1078  * @since 2.0.0
1079  * @uses $wpdb
1080  * @uses wp_transition_comment_status() Passes new and old comment status along with $comment object
1081  *
1082  * @param array $commentarr Contains information on the comment.
1083  * @return int Comment was updated if value is 1, or was not updated if value is 0.
1084  */
1085 function wp_update_comment($commentarr) {
1086         global $wpdb;
1087
1088         // First, get all of the original fields
1089         $comment = get_comment($commentarr['comment_ID'], ARRAY_A);
1090
1091         // Escape data pulled from DB.
1092         $comment = $wpdb->escape($comment);
1093
1094         $old_status = $comment['comment_approved'];
1095
1096         // Merge old and new fields with new fields overwriting old ones.
1097         $commentarr = array_merge($comment, $commentarr);
1098
1099         $commentarr = wp_filter_comment( $commentarr );
1100
1101         // Now extract the merged array.
1102         extract(stripslashes_deep($commentarr), EXTR_SKIP);
1103
1104         $comment_content = apply_filters('comment_save_pre', $comment_content);
1105
1106         $comment_date_gmt = get_gmt_from_date($comment_date);
1107
1108         if ( !isset($comment_approved) )
1109                 $comment_approved = 1;
1110         else if ( 'hold' == $comment_approved )
1111                 $comment_approved = 0;
1112         else if ( 'approve' == $comment_approved )
1113                 $comment_approved = 1;
1114
1115         $data = compact('comment_content', 'comment_author', 'comment_author_email', 'comment_approved', 'comment_karma', 'comment_author_url', 'comment_date', 'comment_date_gmt');
1116         $wpdb->update($wpdb->comments, $data, compact('comment_ID'));
1117
1118         $rval = $wpdb->rows_affected;
1119
1120         clean_comment_cache($comment_ID);
1121         wp_update_comment_count($comment_post_ID);
1122         do_action('edit_comment', $comment_ID);
1123         $comment = get_comment($comment_ID);
1124         wp_transition_comment_status($comment->comment_approved, $old_status, $comment);
1125         return $rval;
1126 }
1127
1128 /**
1129  * Whether to defer comment counting.
1130  *
1131  * When setting $defer to true, all post comment counts will not be updated
1132  * until $defer is set to false. When $defer is set to false, then all
1133  * previously deferred updated post comment counts will then be automatically
1134  * updated without having to call wp_update_comment_count() after.
1135  *
1136  * @since 2.5.0
1137  * @staticvar bool $_defer
1138  *
1139  * @param bool $defer
1140  * @return unknown
1141  */
1142 function wp_defer_comment_counting($defer=null) {
1143         static $_defer = false;
1144
1145         if ( is_bool($defer) ) {
1146                 $_defer = $defer;
1147                 // flush any deferred counts
1148                 if ( !$defer )
1149                         wp_update_comment_count( null, true );
1150         }
1151
1152         return $_defer;
1153 }
1154
1155 /**
1156  * Updates the comment count for post(s).
1157  *
1158  * When $do_deferred is false (is by default) and the comments have been set to
1159  * be deferred, the post_id will be added to a queue, which will be updated at a
1160  * later date and only updated once per post ID.
1161  *
1162  * If the comments have not be set up to be deferred, then the post will be
1163  * updated. When $do_deferred is set to true, then all previous deferred post
1164  * IDs will be updated along with the current $post_id.
1165  *
1166  * @since 2.1.0
1167  * @see wp_update_comment_count_now() For what could cause a false return value
1168  *
1169  * @param int $post_id Post ID
1170  * @param bool $do_deferred Whether to process previously deferred post comment counts
1171  * @return bool True on success, false on failure
1172  */
1173 function wp_update_comment_count($post_id, $do_deferred=false) {
1174         static $_deferred = array();
1175
1176         if ( $do_deferred ) {
1177                 $_deferred = array_unique($_deferred);
1178                 foreach ( $_deferred as $i => $_post_id ) {
1179                         wp_update_comment_count_now($_post_id);
1180                         unset( $_deferred[$i] ); /** @todo Move this outside of the foreach and reset $_deferred to an array instead */
1181                 }
1182         }
1183
1184         if ( wp_defer_comment_counting() ) {
1185                 $_deferred[] = $post_id;
1186                 return true;
1187         }
1188         elseif ( $post_id ) {
1189                 return wp_update_comment_count_now($post_id);
1190         }
1191
1192 }
1193
1194 /**
1195  * Updates the comment count for the post.
1196  *
1197  * @since 2.5.0
1198  * @uses $wpdb
1199  * @uses do_action() Calls 'wp_update_comment_count' hook on $post_id, $new, and $old
1200  * @uses do_action() Calls 'edit_posts' hook on $post_id and $post
1201  *
1202  * @param int $post_id Post ID
1203  * @return bool False on '0' $post_id or if post with ID does not exist. True on success.
1204  */
1205 function wp_update_comment_count_now($post_id) {
1206         global $wpdb;
1207         $post_id = (int) $post_id;
1208         if ( !$post_id )
1209                 return false;
1210         if ( !$post = get_post($post_id) )
1211                 return false;
1212
1213         $old = (int) $post->comment_count;
1214         $new = (int) $wpdb->get_var( $wpdb->prepare("SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1'", $post_id) );
1215         $wpdb->update( $wpdb->posts, array('comment_count' => $new), array('ID' => $post_id) );
1216
1217         if ( 'page' == $post->post_type )
1218                 clean_page_cache( $post_id );
1219         else
1220                 clean_post_cache( $post_id );
1221
1222         do_action('wp_update_comment_count', $post_id, $new, $old);
1223         do_action('edit_post', $post_id, $post);
1224
1225         return true;
1226 }
1227
1228 //
1229 // Ping and trackback functions.
1230 //
1231
1232 /**
1233  * Finds a pingback server URI based on the given URL.
1234  *
1235  * Checks the HTML for the rel="pingback" link and x-pingback headers. It does
1236  * a check for the x-pingback headers first and returns that, if available. The
1237  * check for the rel="pingback" has more overhead than just the header.
1238  *
1239  * @since 1.5.0
1240  *
1241  * @param string $url URL to ping.
1242  * @param int $deprecated Not Used.
1243  * @return bool|string False on failure, string containing URI on success.
1244  */
1245 function discover_pingback_server_uri($url, $deprecated = 2048) {
1246
1247         $pingback_str_dquote = 'rel="pingback"';
1248         $pingback_str_squote = 'rel=\'pingback\'';
1249
1250         /** @todo Should use Filter Extension or custom preg_match instead. */
1251         $parsed_url = parse_url($url);
1252
1253         if ( ! isset( $parsed_url['host'] ) ) // Not an URL. This should never happen.
1254                 return false;
1255
1256         //Do not search for a pingback server on our own uploads
1257         $uploads_dir = wp_upload_dir();
1258         if ( 0 === strpos($url, $uploads_dir['baseurl']) )
1259                 return false;
1260
1261         $response = wp_remote_head( $url, array( 'timeout' => 2, 'httpversion' => '1.0' ) );
1262
1263         if ( is_wp_error( $response ) )
1264                 return false;
1265
1266         if ( isset( $response['headers']['x-pingback'] ) )
1267                 return $response['headers']['x-pingback'];
1268
1269         // Not an (x)html, sgml, or xml page, no use going further.
1270         if ( isset( $response['headers']['content-type'] ) && preg_match('#(image|audio|video|model)/#is', $response['headers']['content-type']) )
1271                 return false;
1272
1273         // Now do a GET since we're going to look in the html headers (and we're sure its not a binary file)
1274         $response = wp_remote_get( $url, array( 'timeout' => 2, 'httpversion' => '1.0' ) );
1275
1276         if ( is_wp_error( $response ) )
1277                 return false;
1278
1279         $contents = $response['body'];
1280
1281         $pingback_link_offset_dquote = strpos($contents, $pingback_str_dquote);
1282         $pingback_link_offset_squote = strpos($contents, $pingback_str_squote);
1283         if ( $pingback_link_offset_dquote || $pingback_link_offset_squote ) {
1284                 $quote = ($pingback_link_offset_dquote) ? '"' : '\'';
1285                 $pingback_link_offset = ($quote=='"') ? $pingback_link_offset_dquote : $pingback_link_offset_squote;
1286                 $pingback_href_pos = @strpos($contents, 'href=', $pingback_link_offset);
1287                 $pingback_href_start = $pingback_href_pos+6;
1288                 $pingback_href_end = @strpos($contents, $quote, $pingback_href_start);
1289                 $pingback_server_url_len = $pingback_href_end - $pingback_href_start;
1290                 $pingback_server_url = substr($contents, $pingback_href_start, $pingback_server_url_len);
1291
1292                 // We may find rel="pingback" but an incomplete pingback URL
1293                 if ( $pingback_server_url_len > 0 ) { // We got it!
1294                         return $pingback_server_url;
1295                 }
1296         }
1297
1298         return false;
1299 }
1300
1301 /**
1302  * Perform all pingbacks, enclosures, trackbacks, and send to pingback services.
1303  *
1304  * @since 2.1.0
1305  * @uses $wpdb
1306  */
1307 function do_all_pings() {
1308         global $wpdb;
1309
1310         // Do pingbacks
1311         while ($ping = $wpdb->get_row("SELECT * FROM {$wpdb->posts}, {$wpdb->postmeta} WHERE {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id AND {$wpdb->postmeta}.meta_key = '_pingme' LIMIT 1")) {
1312                 $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE post_id = {$ping->ID} AND meta_key = '_pingme';");
1313                 pingback($ping->post_content, $ping->ID);
1314         }
1315
1316         // Do Enclosures
1317         while ($enclosure = $wpdb->get_row("SELECT * FROM {$wpdb->posts}, {$wpdb->postmeta} WHERE {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id AND {$wpdb->postmeta}.meta_key = '_encloseme' LIMIT 1")) {
1318                 $wpdb->query( $wpdb->prepare("DELETE FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_encloseme';", $enclosure->ID) );
1319                 do_enclose($enclosure->post_content, $enclosure->ID);
1320         }
1321
1322         // Do Trackbacks
1323         $trackbacks = $wpdb->get_col("SELECT ID FROM $wpdb->posts WHERE to_ping <> '' AND post_status = 'publish'");
1324         if ( is_array($trackbacks) )
1325                 foreach ( $trackbacks as $trackback )
1326                         do_trackbacks($trackback);
1327
1328         //Do Update Services/Generic Pings
1329         generic_ping();
1330 }
1331
1332 /**
1333  * Perform trackbacks.
1334  *
1335  * @since 1.5.0
1336  * @uses $wpdb
1337  *
1338  * @param int $post_id Post ID to do trackbacks on.
1339  */
1340 function do_trackbacks($post_id) {
1341         global $wpdb;
1342
1343         $post = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id) );
1344         $to_ping = get_to_ping($post_id);
1345         $pinged  = get_pung($post_id);
1346         if ( empty($to_ping) ) {
1347                 $wpdb->update($wpdb->posts, array('to_ping' => ''), array('ID' => $post_id) );
1348                 return;
1349         }
1350
1351         if ( empty($post->post_excerpt) )
1352                 $excerpt = apply_filters('the_content', $post->post_content);
1353         else
1354                 $excerpt = apply_filters('the_excerpt', $post->post_excerpt);
1355         $excerpt = str_replace(']]>', ']]&gt;', $excerpt);
1356         $excerpt = wp_html_excerpt($excerpt, 252) . '...';
1357
1358         $post_title = apply_filters('the_title', $post->post_title);
1359         $post_title = strip_tags($post_title);
1360
1361         if ( $to_ping ) {
1362                 foreach ( (array) $to_ping as $tb_ping ) {
1363                         $tb_ping = trim($tb_ping);
1364                         if ( !in_array($tb_ping, $pinged) ) {
1365                                 trackback($tb_ping, $post_title, $excerpt, $post_id);
1366                                 $pinged[] = $tb_ping;
1367                         } else {
1368                                 $wpdb->query( $wpdb->prepare("UPDATE $wpdb->posts SET to_ping = TRIM(REPLACE(to_ping, '$tb_ping', '')) WHERE ID = %d", $post_id) );
1369                         }
1370                 }
1371         }
1372 }
1373
1374 /**
1375  * Sends pings to all of the ping site services.
1376  *
1377  * @since 1.2.0
1378  *
1379  * @param int $post_id Post ID. Not actually used.
1380  * @return int Same as Post ID from parameter
1381  */
1382 function generic_ping($post_id = 0) {
1383         $services = get_option('ping_sites');
1384
1385         $services = explode("\n", $services);
1386         foreach ( (array) $services as $service ) {
1387                 $service = trim($service);
1388                 if ( '' != $service )
1389                         weblog_ping($service);
1390         }
1391
1392         return $post_id;
1393 }
1394
1395 /**
1396  * Pings back the links found in a post.
1397  *
1398  * @since 0.71
1399  * @uses $wp_version
1400  * @uses IXR_Client
1401  *
1402  * @param string $content Post content to check for links.
1403  * @param int $post_ID Post ID.
1404  */
1405 function pingback($content, $post_ID) {
1406         global $wp_version;
1407         include_once(ABSPATH . WPINC . '/class-IXR.php');
1408
1409         // original code by Mort (http://mort.mine.nu:8080)
1410         $post_links = array();
1411
1412         $pung = get_pung($post_ID);
1413
1414         // Variables
1415         $ltrs = '\w';
1416         $gunk = '/#~:.?+=&%@!\-';
1417         $punc = '.:?\-';
1418         $any = $ltrs . $gunk . $punc;
1419
1420         // Step 1
1421         // Parsing the post, external links (if any) are stored in the $post_links array
1422         // This regexp comes straight from phpfreaks.com
1423         // http://www.phpfreaks.com/quickcode/Extract_All_URLs_on_a_Page/15.php
1424         preg_match_all("{\b http : [$any] +? (?= [$punc] * [^$any] | $)}x", $content, $post_links_temp);
1425
1426         // Step 2.
1427         // Walking thru the links array
1428         // first we get rid of links pointing to sites, not to specific files
1429         // Example:
1430         // http://dummy-weblog.org
1431         // http://dummy-weblog.org/
1432         // http://dummy-weblog.org/post.php
1433         // We don't wanna ping first and second types, even if they have a valid <link/>
1434
1435         foreach ( (array) $post_links_temp[0] as $link_test ) :
1436                 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
1437                                 && !is_local_attachment($link_test) ) : // Also, let's never ping local attachments.
1438                         if ( $test = @parse_url($link_test) ) {
1439                                 if ( isset($test['query']) )
1440                                         $post_links[] = $link_test;
1441                                 elseif ( ($test['path'] != '/') && ($test['path'] != '') )
1442                                         $post_links[] = $link_test;
1443                         }
1444                 endif;
1445         endforeach;
1446
1447         do_action_ref_array('pre_ping', array(&$post_links, &$pung));
1448
1449         foreach ( (array) $post_links as $pagelinkedto ) {
1450                 $pingback_server_url = discover_pingback_server_uri($pagelinkedto, 2048);
1451
1452                 if ( $pingback_server_url ) {
1453                         @ set_time_limit( 60 );
1454                          // Now, the RPC call
1455                         $pagelinkedfrom = get_permalink($post_ID);
1456
1457                         // using a timeout of 3 seconds should be enough to cover slow servers
1458                         $client = new IXR_Client($pingback_server_url);
1459                         $client->timeout = 3;
1460                         $client->useragent .= ' -- WordPress/' . $wp_version;
1461
1462                         // when set to true, this outputs debug messages by itself
1463                         $client->debug = false;
1464
1465                         if ( $client->query('pingback.ping', $pagelinkedfrom, $pagelinkedto) || ( isset($client->error->code) && 48 == $client->error->code ) ) // Already registered
1466                                 add_ping( $post_ID, $pagelinkedto );
1467                 }
1468         }
1469 }
1470
1471 /**
1472  * Check whether blog is public before returning sites.
1473  *
1474  * @since 2.1.0
1475  *
1476  * @param mixed $sites Will return if blog is public, will not return if not public.
1477  * @return mixed Empty string if blog is not public, returns $sites, if site is public.
1478  */
1479 function privacy_ping_filter($sites) {
1480         if ( '0' != get_option('blog_public') )
1481                 return $sites;
1482         else
1483                 return '';
1484 }
1485
1486 /**
1487  * Send a Trackback.
1488  *
1489  * Updates database when sending trackback to prevent duplicates.
1490  *
1491  * @since 0.71
1492  * @uses $wpdb
1493  *
1494  * @param string $trackback_url URL to send trackbacks.
1495  * @param string $title Title of post.
1496  * @param string $excerpt Excerpt of post.
1497  * @param int $ID Post ID.
1498  * @return mixed Database query from update.
1499  */
1500 function trackback($trackback_url, $title, $excerpt, $ID) {
1501         global $wpdb;
1502
1503         if ( empty($trackback_url) )
1504                 return;
1505
1506         $options = array();
1507         $options['timeout'] = 4;
1508         $options['body'] = array(
1509                 'title' => $title,
1510                 'url' => get_permalink($ID),
1511                 'blog_name' => get_option('blogname'),
1512                 'excerpt' => $excerpt
1513         );
1514
1515         $response = wp_remote_post($trackback_url, $options);
1516
1517         if ( is_wp_error( $response ) )
1518                 return;
1519
1520         $tb_url = addslashes( $trackback_url );
1521         $wpdb->query( $wpdb->prepare("UPDATE $wpdb->posts SET pinged = CONCAT(pinged, '\n', '$tb_url') WHERE ID = %d", $ID) );
1522         return $wpdb->query( $wpdb->prepare("UPDATE $wpdb->posts SET to_ping = TRIM(REPLACE(to_ping, '$tb_url', '')) WHERE ID = %d", $ID) );
1523 }
1524
1525 /**
1526  * Send a pingback.
1527  *
1528  * @since 1.2.0
1529  * @uses $wp_version
1530  * @uses IXR_Client
1531  *
1532  * @param string $server Host of blog to connect to.
1533  * @param string $path Path to send the ping.
1534  */
1535 function weblog_ping($server = '', $path = '') {
1536         global $wp_version;
1537         include_once(ABSPATH . WPINC . '/class-IXR.php');
1538
1539         // using a timeout of 3 seconds should be enough to cover slow servers
1540         $client = new IXR_Client($server, ((!strlen(trim($path)) || ('/' == $path)) ? false : $path));
1541         $client->timeout = 3;
1542         $client->useragent .= ' -- WordPress/'.$wp_version;
1543
1544         // when set to true, this outputs debug messages by itself
1545         $client->debug = false;
1546         $home = trailingslashit( get_option('home') );
1547         if ( !$client->query('weblogUpdates.extendedPing', get_option('blogname'), $home, get_bloginfo('rss2_url') ) ) // then try a normal ping
1548                 $client->query('weblogUpdates.ping', get_option('blogname'), $home);
1549 }
1550
1551 //
1552 // Cache
1553 //
1554
1555 /**
1556  * Removes comment ID from the comment cache.
1557  *
1558  * @since 2.3.0
1559  * @package WordPress
1560  * @subpackage Cache
1561  *
1562  * @param int|array $id Comment ID or array of comment IDs to remove from cache
1563  */
1564 function clean_comment_cache($ids) {
1565         foreach ( (array) $ids as $id )
1566                 wp_cache_delete($id, 'comment');
1567 }
1568
1569 /**
1570  * Updates the comment cache of given comments.
1571  *
1572  * Will add the comments in $comments to the cache. If comment ID already exists
1573  * in the comment cache then it will not be updated. The comment is added to the
1574  * cache using the comment group with the key using the ID of the comments.
1575  *
1576  * @since 2.3.0
1577  * @package WordPress
1578  * @subpackage Cache
1579  *
1580  * @param array $comments Array of comment row objects
1581  */
1582 function update_comment_cache($comments) {
1583         foreach ( (array) $comments as $comment )
1584                 wp_cache_add($comment->comment_ID, $comment, 'comment');
1585 }
1586
1587 //
1588 // Internal
1589 //
1590
1591 /**
1592  * Close comments on old posts on the fly, without any extra DB queries.  Hooked to the_posts.
1593  *
1594  * @access private
1595  * @since 2.7.0
1596  *
1597  * @param object $posts Post data object.
1598  * @return object
1599  */
1600 function _close_comments_for_old_posts( $posts ) {
1601         if ( empty($posts) || !is_singular() || !get_option('close_comments_for_old_posts') )
1602                 return $posts;
1603
1604         $days_old = (int) get_option('close_comments_days_old');
1605         if ( !$days_old )
1606                 return $posts;
1607
1608         if ( time() - strtotime( $posts[0]->post_date_gmt ) > ( $days_old * 24 * 60 * 60 ) ) {
1609                 $posts[0]->comment_status = 'closed';
1610                 $posts[0]->ping_status = 'closed';
1611         }
1612
1613         return $posts;
1614 }
1615
1616 /**
1617  * Close comments on an old post.  Hooked to comments_open and pings_open.
1618  *
1619  * @access private
1620  * @since 2.7.0
1621  *
1622  * @param bool $open Comments open or closed
1623  * @param int $post_id Post ID
1624  * @return bool $open
1625  */
1626 function _close_comments_for_old_post( $open, $post_id ) {
1627         if ( ! $open )
1628                 return $open;
1629
1630         if ( !get_option('close_comments_for_old_posts') )
1631                 return $open;
1632
1633         $days_old = (int) get_option('close_comments_days_old');
1634         if ( !$days_old )
1635                 return $open;
1636
1637         $post = get_post($post_id);
1638
1639         if ( time() - strtotime( $post->post_date_gmt ) > ( $days_old * 24 * 60 * 60 ) )
1640                 return false;
1641
1642         return $open;
1643 }
1644
1645 ?>