root/trunk/wp-includes/comment.php

Revision 1530, 52.1 kB (checked in by donncha, 3 hours ago)

WP Merge with revision 9808

Line 
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("|(href\t*?=\t*?['\"]?)?(https?:)?//|i", 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 list of comment arguments are 'status', 'orderby', 'comment_date_gmt',
172  * 'order', 'number', 'offset', and 'post_id'.
173  *
174  * @since 2.7.0
175  * @uses $wpdb
176  *
177  * @param mixed $args Optional. Array or string of options to override defaults.
178  * @return array List of comments.
179  */
180 function get_comments( $args = '' ) {
181     global $wpdb;
182
183     $defaults = array('status' => '', 'orderby' => 'comment_date_gmt', 'order' => 'DESC', 'number' => '', 'offset' => '', 'post_id' => 0);
184
185     $args = wp_parse_args( $args, $defaults );
186     extract( $args, EXTR_SKIP );
187
188     // $args can be whatever, only use the args defined in defaults to compute the key
189     $key = md5( serialize( compact(array_keys($defaults)) )  );
190     $last_changed = wp_cache_get('last_changed', 'comment');
191     if ( !$last_changed ) {
192         $last_changed = time();
193         wp_cache_set('last_changed', $last_changed, 'comment');
194     }
195     $cache_key = "get_comments:$key:$last_changed";
196
197     if ( $cache = wp_cache_get( $cache_key, 'comment' ) ) {
198         return $cache;
199     }
200
201     $post_id = absint($post_id);
202
203     if ( 'hold' == $status )
204         $approved = "comment_approved = '0'";
205     elseif ( 'approve' == $status )
206         $approved = "comment_approved = '1'";
207     elseif ( 'spam' == $status )
208         $approved = "comment_approved = 'spam'";
209     else
210         $approved = "( comment_approved = '0' OR comment_approved = '1' )";
211
212     $order = ( 'ASC' == $order ) ? 'ASC' : 'DESC';
213
214     $orderby = 'comment_date_gmt'// Hard code for now
215
216     $number = absint($number);
217     $offset = absint($offset);
218
219     if ( !empty($number) ) {
220         if ( $offset )
221             $number = 'LIMIT ' . $offset . ',' . $number;
222         else
223             $number = 'LIMIT ' . $number;
224
225     } else {
226         $number = '';
227     }
228
229     if ( ! empty($post_id) )
230         $post_where = $wpdb->prepare( 'comment_post_ID = %d AND', $post_id );
231     else
232         $post_where = '';
233
234     $comments = $wpdb->get_results( "SELECT * FROM $wpdb->comments WHERE $post_where $approved ORDER BY $orderby $order $number" );
235     wp_cache_add( $cache_key, $comments, 'comment' );
236
237     return $comments;
238 }
239
240 /**
241  * Retrieve all of the WordPress supported comment statuses.
242  *
243  * Comments have a limited set of valid status values, this provides the comment
244  * status values and descriptions.
245  *
246  * @package WordPress
247  * @subpackage Post
248  * @since 2.7.0
249  *
250  * @return array List of comment statuses.
251  */
252 function get_comment_statuses( ) {
253     $status = array(
254         'hold'        => __('Unapproved'),
255         'approve'    => __('Approved'),
256         'spam'        => _c('Spam|adjective'),
257     );
258
259     return $status;
260 }
261
262
263 /**
264  * The date the last comment was modified.
265  *
266  * @since 1.5.0
267  * @uses $wpdb
268  * @global array $cache_lastcommentmodified
269  *
270  * @param string $timezone Which timezone to use in reference to 'gmt', 'blog',
271  *        or 'server' locations.
272  * @return string Last comment modified date.
273  */
274 function get_lastcommentmodified($timezone = 'server') {
275     global $cache_lastcommentmodified, $wpdb;
276
277     if ( isset($cache_lastcommentmodified[$timezone]) )
278         return $cache_lastcommentmodified[$timezone];
279
280     $add_seconds_server = date('Z');
281
282     switch ( strtolower($timezone)) {
283         case 'gmt':
284             $lastcommentmodified = $wpdb->get_var("SELECT comment_date_gmt FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1");
285             break;
286         case 'blog':
287             $lastcommentmodified = $wpdb->get_var("SELECT comment_date FROM $wpdb->comments WHERE comment_approved = '1' ORDER BY comment_date_gmt DESC LIMIT 1");
288             break;
289         case 'server':
290             $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));
291             break;
292     }
293
294     $cache_lastcommentmodified[$timezone] = $lastcommentmodified;
295
296     return $lastcommentmodified;
297 }
298
299 /**
300  * The amount of comments in a post or total comments.
301  *
302  * A lot like {@link wp_count_comments()}, in that they both return comment
303  * stats (albeit with different types). The {@link wp_count_comments()} actual
304  * caches, but this function does not.
305  *
306  * @since 2.0.0
307  * @uses $wpdb
308  *
309  * @param int $post_id Optional. Comment amount in post if > 0, else total comments blog wide.
310  * @return array The amount of spam, approved, awaiting moderation, and total comments.
311  */
312 function get_comment_count( $post_id = 0 ) {
313     global $wpdb;
314
315     $post_id = (int) $post_id;
316
317     $where = '';
318     if ( $post_id > 0 ) {
319         $where = $wpdb->prepare("WHERE comment_post_ID = %d", $post_id);
320     }
321
322     $totals = (array) $wpdb->get_results("
323         SELECT comment_approved, COUNT( * ) AS total
324         FROM {$wpdb->comments}
325         {$where}
326         GROUP BY comment_approved
327     ", ARRAY_A);
328
329     $comment_count = array(
330         "approved"              => 0,
331         "awaiting_moderation"   => 0,
332         "spam"                  => 0,
333         "total_comments"        => 0
334     );
335
336     foreach ( $totals as $row ) {
337         switch ( $row['comment_approved'] ) {
338             case 'spam':
339                 $comment_count['spam'] = $row['total'];
340                 $comment_count["total_comments"] += $row['total'];
341                 break;
342             case 1:
343                 $comment_count['approved'] = $row['total'];
344                 $comment_count['total_comments'] += $row['total'];
345                 break;
346             case 0:
347                 $comment_count['awaiting_moderation'] = $row['total'];
348                 $comment_count['total_comments'] += $row['total'];
349                 break;
350             default:
351                 break;
352         }
353     }
354
355     return $comment_count;
356 }
357
358 /**
359  * Sanitizes the cookies sent to the user already.
360  *
361  * Will only do anything if the cookies have already been created for the user.
362  * Mostly used after cookies had been sent to use elsewhere.
363  *
364  * @since 2.0.4
365  */
366 function sanitize_comment_cookies() {
367     if ( isset($_COOKIE['comment_author_'.COOKIEHASH]) ) {
368         $comment_author = apply_filters('pre_comment_author_name', $_COOKIE['comment_author_'.COOKIEHASH]);
369         $comment_author = stripslashes($comment_author);
370         $comment_author = attribute_escape($comment_author);
371         $_COOKIE['comment_author_'.COOKIEHASH] = $comment_author;
372     }
373
374     if ( isset($_COOKIE['comment_author_email_'.COOKIEHASH]) ) {
375         $comment_author_email = apply_filters('pre_comment_author_email', $_COOKIE['comment_author_email_'.COOKIEHASH]);
376         $comment_author_email = stripslashes($comment_author_email);
377         $comment_author_email = attribute_escape($comment_author_email);
378         $_COOKIE['comment_author_email_'.COOKIEHASH] = $comment_author_email;
379     }
380
381     if ( isset($_COOKIE['comment_author_url_'.COOKIEHASH]) ) {
382         $comment_author_url = apply_filters('pre_comment_author_url', $_COOKIE['comment_author_url_'.COOKIEHASH]);
383         $comment_author_url = stripslashes($comment_author_url);
384         $_COOKIE['comment_author_url_'.COOKIEHASH] = $comment_author_url;
385     }
386 }
387
388 /**
389  * Validates whether this comment is allowed to be made or not.
390  *
391  * @since 2.0.0
392  * @uses $wpdb
393  * @uses apply_filters() Calls 'pre_comment_approved' hook on the type of comment
394  * @uses do_action() Calls 'check_comment_flood' hook on $comment_author_IP, $comment_author_email, and $comment_date_gmt
395  *
396  * @param array $commentdata Contains information on the comment
397  * @return mixed Signifies the approval status (0|1|'spam')
398  */
399 function wp_allow_comment($commentdata) {
400     global $wpdb;
401     extract($commentdata, EXTR_SKIP);
402
403     // Simple duplicate check
404     // expected_slashed ($comment_post_ID, $comment_author, $comment_author_email, $comment_content)
405     $dupe = "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = '$comment_post_ID' AND ( comment_author = '$comment_author' ";
406     if ( $comment_author_email )
407         $dupe .= "OR comment_author_email = '$comment_author_email' ";
408     $dupe .= ") AND comment_content = '$comment_content' LIMIT 1";
409     if ( $wpdb->get_var($dupe) ) {
410         if ( defined('DOING_AJAX') )
411             die( __('Duplicate comment detected; it looks as though you\'ve already said that!') );
412
413         wp_die( __('Duplicate comment detected; it looks as though you\'ve already said that!') );
414     }
415
416     do_action( 'check_comment_flood', $comment_author_IP, $comment_author_email, $comment_date_gmt );
417
418     if ( $user_id ) {
419         $userdata = get_userdata($user_id);
420         $user = new WP_User($user_id);
421         $post_author = $wpdb->get_var($wpdb->prepare("SELECT post_author FROM $wpdb->posts WHERE ID = %d LIMIT 1", $comment_post_ID));
422     }
423
424     if ( $userdata && ( $user_id == $post_author || $user->has_cap('moderate_comments') ) ) {
425         // The author and the admins get respect.
426         $approved = 1;
427      } else {
428         // Everyone else's comments will be checked.
429         if ( check_comment($comment_author, $comment_author_email, $comment_author_url, $comment_content, $comment_author_IP, $comment_agent, $comment_type) )
430             $approved = 1;
431         else
432             $approved = 0;
433         if ( wp_blacklist_check($comment_author, $comment_author_email, $comment_author_url, $comment_content, $comment_author_IP, $comment_agent) )
434             $approved = 'spam';
435     }
436
437     $approved = apply_filters('pre_comment_approved', $approved);
438     return $approved;
439 }
440
441 /**
442  * Check whether comment flooding is occurring.
443  *
444  * Won't run, if current user can manage options, so to not block
445  * administrators.
446  *
447  * @since 2.3.0
448  * @uses $wpdb
449  * @uses apply_filters() Calls 'comment_flood_filter' filter with first
450  *        parameter false, last comment timestamp, new comment timestamp.
451  * @uses do_action() Calls 'comment_flood_trigger' action with parameters with
452  *        last comment timestamp and new comment timestamp.
453  *
454  * @param string $ip Comment IP.
455  * @param string $email Comment author email address.
456  * @param string $date MySQL time string.
457  */
458 function check_comment_flood_db( $ip, $email, $date ) {
459     global $wpdb;
460     if ( current_user_can( 'manage_options' ) )
461         return; // don't throttle admins
462     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) ) ) {
463         $time_lastcomment = mysql2date('U', $lasttime);
464         $time_newcomment  = mysql2date('U', $date);
465         $flood_die = apply_filters('comment_flood_filter', false, $time_lastcomment, $time_newcomment);
466         if ( $flood_die ) {
467             do_action('comment_flood_trigger', $time_lastcomment, $time_newcomment);
468
469             if ( defined('DOING_AJAX') )
470                 die( __('You are posting comments too quickly.  Slow down.') );
471
472             wp_die( __('You are posting comments too quickly.  Slow down.'), '', array('response' => 403) );
473         }
474     }
475 }
476
477 /**
478  * Separates an array of comments into an array keyed by comment_type.
479  *
480  * @since 2.7.0
481  *
482  * @param array $comments Array of comments
483  * @return array Array of comments keyed by comment_type.
484  */
485 function &separate_comments(&$comments) {
486     $comments_by_type = array('comment' => array(), 'trackback' => array(), 'pingback' => array(), 'pings' => array());
487     $count = count($comments);
488     for ( $i = 0; $i < $count; $i++ ) {
489         $type = $comments[$i]->comment_type;
490         if ( empty($type) )
491             $type = 'comment';
492         $comments_by_type[$type][] = &$comments[$i];
493         if ( 'trackback' == $type || 'pingback' == $type )
494             $comments_by_type['pings'][] = &$comments[$i];
495     }
496
497     return $comments_by_type;
498 }
499
500 /**
501  * Calculate the total number of comment pages.
502  *
503  * @since 2.7.0
504  * @uses get_query_var() Used to fill in the default for $per_page parameter.
505  * @uses get_option() Used to fill in defaults for parameters.
506  * @uses Walker_Comment
507  *
508  * @param array $comments Optional array of comment objects.  Defaults to $wp_query->comments
509  * @param int $per_page Optional comments per page.
510  * @param boolean $threaded Optional control over flat or threaded comments.
511  * @return int Number of comment pages.
512  */
513 function get_comment_pages_count( $comments = null, $per_page = null, $threaded = null ) {
514     global $wp_query;
515
516     if ( !$comments || !is_array($comments) )
517         $comments = $wp_query->comments;
518
519     if ( empty($comments) )
520         return 0;
521
522     if ( !isset($per_page) )
523         $per_page = (int) get_query_var('comments_per_page');
524     if ( 0 === $per_page )
525         $per_page = (int) get_option('comments_per_page');
526     if ( 0 === $per_page )
527         return 1;
528
529     if ( !isset($threaded) )
530         $threaded = get_option('thread_comments');
531
532     if ( $threaded ) {
533         $walker = new Walker_Comment;
534         $count = ceil( $walker->get_number_of_root_elements( $comments ) / $per_page );
535     } else {
536         $count = ceil( count( $comments ) / $per_page );
537     }
538
539     return $count;
540 }
541
542 /**
543  * Calculate what page number a comment will appear on for comment paging.
544  *
545  * @since 2.7.0
546  * @uses get_comment() Gets the full comment of the $comment_ID parameter.
547  * @uses get_option() Get various settings to control function and defaults.
548  * @uses get_page_of_comment() Used to loop up to top level comment.
549  *
550  * @param int $comment_ID Comment ID.
551  * @param array $args Optional args.
552  * @return int|null Comment page number or null on error.
553  */
554 function get_page_of_comment( $comment_ID, $args = array() ) {
555     global $wpdb;
556
557     if ( !$comment = get_comment( $comment_ID ) )
558         return;
559
560     $defaults = array( 'type' => 'all', 'page' => '', 'per_page' => '', 'max_depth' => '' );
561     $args = wp_parse_args( $args, $defaults );
562
563     if ( '' === $args[