Spike PHPCoverage Details: comment.module

Line #FrequencySource Line
1 <?php
2 // $Id: comment.module,v 1.621 2008/02/23 08:02:48 dries Exp $
3 
4 /**
5  * @file
6  * Enables users to comment on published content.
7  *
8  * When enabled, the Drupal comment module creates a discussion
9  * board for each Drupal node. Users can post comments to discuss
10  * a forum topic, weblog post, story, collaborative book page, etc.
11  */
12 
13 /**
14  * Comment is published.
15  */
16 define('COMMENT_PUBLISHED', 0);
17 
18 /**
19  * Comment is awaiting approval.
20  */
21 define('COMMENT_NOT_PUBLISHED', 1);
22 
23 /**
24  * Comments are displayed in a flat list - collapsed.
25  */
26 define('COMMENT_MODE_FLAT_COLLAPSED', 1);
27 
28 /**
29  * Comments are displayed in a flat list - expanded.
30  */
31 define('COMMENT_MODE_FLAT_EXPANDED', 2);
32 
33 /**
34  * Comments are displayed as a threaded list - collapsed.
35  */
36 define('COMMENT_MODE_THREADED_COLLAPSED', 3);
37 
38 /**
39  * Comments are displayed as a threaded list - expanded.
40  */
41 define('COMMENT_MODE_THREADED_EXPANDED', 4);
42 
43 /**
44  * Comments are ordered by date - newest first.
45  */
46 define('COMMENT_ORDER_NEWEST_FIRST', 1);
47 
48 /**
49  * Comments are ordered by date - oldest first.
50  */
51 define('COMMENT_ORDER_OLDEST_FIRST', 2);
52 
53 /**
54  * Comment controls should be shown above the comment list.
55  */
56 define('COMMENT_CONTROLS_ABOVE', 0);
57 
58 /**
59  * Comment controls should be shown below the comment list.
60  */
61 define('COMMENT_CONTROLS_BELOW', 1);
62 
63 /**
64  * Comment controls should be shown both above and below the comment list.
65  */
66 define('COMMENT_CONTROLS_ABOVE_BELOW', 2);
67 
68 /**
69  * Comment controls are hidden.
70  */
71 define('COMMENT_CONTROLS_HIDDEN', 3);
72 
73 /**
74  * Anonymous posters may not enter their contact information.
75  */
76 define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
77 
78 /**
79  * Anonymous posters may leave their contact information.
80  */
81 define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
82 
83 /**
84  * Anonymous posters must leave their contact information.
85  */
86 define('COMMENT_ANONYMOUS_MUST_CONTACT', 2);
87 
88 /**
89  * Comment form should be displayed on a separate page.
90  */
91 define('COMMENT_FORM_SEPARATE_PAGE', 0);
92 
93 /**
94  * Comment form should be shown below post or list of comments.
95  */
96 define('COMMENT_FORM_BELOW', 1);
97 
98 /**
99  * Comments for this node are disabled.
100  */
101 define('COMMENT_NODE_DISABLED', 0);
102 
103 /**
104  * Comments for this node are locked.
105  */
106 define('COMMENT_NODE_READ_ONLY', 1);
107 
108 /**
109  * Comments are enabled on this node.
110  */
111 define('COMMENT_NODE_READ_WRITE', 2);
112 
113 /**
114  * Comment preview is optional.
115  */
116 define('COMMENT_PREVIEW_OPTIONAL', 0);
117 
118 /**
119  * Comment preview is required.
120  */
121 define('COMMENT_PREVIEW_REQUIRED', 1);
122 
123 /**
124  * Implementation of hook_help().
125  */
126 function comment_help($path, $arg) {
127   switch ($path) {
128     case 'admin/help#comment':
129       $output = '<p>'. t('The comment module allows visitors to comment on your posts, creating ad hoc discussion boards. Any <a href="@content-type">content type</a> may have its <em>Default comment setting</em> set to <em>Read/Write</em> to allow comments, or <em>Disabled</em>, to prevent comments. Comment display settings and other controls may also be customized for each content type (some display settings are customizable by individual users).', array('@content-type' => url('admin/content/types'))) .'</p>';
130       $output .= '<p>'. t('Comment permissions are assigned to user roles, and are used to determine whether anonymous users (or other roles) are allowed to comment on posts. If anonymous users are allowed to comment, their individual contact information may be retained in cookies stored on their local computer for use in later comment submissions. When a comment has no replies, it may be (optionally) edited by its author. The comment module uses the same input formats and HTML tags available when creating other forms of content.') .'</p>';
131       $output .= '<p>'. t('For more information, see the online handbook entry for <a href="@comment">Comment module</a>.', array('@comment' => 'http://drupal.org/handbook/modules/comment/')) .'</p>';
132       return $output;
133     case 'admin/content/comment':
134       return '<p>'. t("Below is a list of the latest comments posted to your site. Click on a subject to see the comment, the author's name to edit the author's user information, 'edit' to modify the text, and 'delete' to remove their submission.") .'</p>';
135     case 'admin/content/comment/approval':
136       return '<p>'. t("Below is a list of the comments posted to your site that need approval. To approve a comment, click on 'edit' and then change its 'moderation status' to Approved. Click on a subject to see the comment, the author's name to edit the author's user information, 'edit' to modify the text, and 'delete' to remove their submission.") .'</p>';
137   }
138 }
139 
140 /**
141  * Implementation of hook_theme().
142  */
143 function comment_theme() {
144   return array(
145     'comment_block' => array(
146       'arguments' => array(),
147     ),
148     'comment_admin_overview' => array(
149       'arguments' => array('form' => NULL),
150     ),
151     'comment_preview' => array(
152       'arguments' => array('comment' => NULL, 'node' => NULL, 'links' => array(), 'visible' => 1),
153     ),
154     'comment_view' => array(
155       'arguments' => array('comment' => NULL, 'node' => NULL, 'links' => array(), 'visible' => 1),
156     ),
157     'comment_controls' => array(
158       'arguments' => array('form' => NULL),
159     ),
160     'comment' => array(
161       'template' => 'comment',
162       'arguments' => array('comment' => NULL, 'node' => NULL, 'links' => array()),
163     ),
164     'comment_folded' => array(
165       'template' => 'comment-folded',
166       'arguments' => array('comment' => NULL),
167     ),
168     'comment_flat_collapsed' => array(
169       'arguments' => array('comment' => NULL, 'node' => NULL),
170     ),
171     'comment_flat_expanded' => array(
172       'arguments' => array('comment' => NULL, 'node' => NULL),
173     ),
174     'comment_thread_collapsed' => array(
175       'arguments' => array('comment' => NULL, 'node' => NULL),
176     ),
177     'comment_thread_expanded' => array(
178       'arguments' => array('comment' => NULL, 'node' => NULL),
179     ),
180     'comment_post_forbidden' => array(
181       'arguments' => array('nid' => NULL),
182     ),
183     'comment_wrapper' => array(
184       'template' => 'comment-wrapper',
185       'arguments' => array('content' => NULL, 'node' => NULL),
186     ),
187     'comment_submitted' => array(
188       'arguments' => array('comment' => NULL),
189     ),
190   );
191 }
192 
193 /**
194  * Implementation of hook_menu().
195  */
196 function comment_menu() {
197   $items['admin/content/comment'] = array(
1981    'title' => 'Comments',
199     'description' => 'List and edit site comments and the comment moderation queue.',
200     'page callback' => 'comment_admin',
201     'access arguments' => array('administer comments'),
202     'file' => 'comment.admin.inc',
203   );
204 
205   // Tabs:
206   $items['admin/content/comment/new'] = array(
2071    'title' => 'Published comments',
208     'type' => MENU_DEFAULT_LOCAL_TASK,
209     'weight' => -10,
210   );
211   $items['admin/content/comment/approval'] = array(
2121    'title' => 'Approval queue',
213     'page arguments' => array('approval'),
214     'type' => MENU_LOCAL_TASK,
215     'file' => 'comment.admin.inc',
216   );
217 
218   $items['comment/delete'] = array(
2191    'title' => 'Delete comment',
220     'page callback' => 'comment_delete',
221     'access arguments' => array('administer comments'),
222     'type' => MENU_CALLBACK,
223     'file' => 'comment.admin.inc',
224   );
225 
226   $items['comment/edit'] = array(
2271    'title' => 'Edit comment',
228     'page callback' => 'comment_edit',
229     'access arguments' => array('post comments'),
230     'type' => MENU_CALLBACK,
231     'file' => 'comment.pages.inc',
232   );
233   $items['comment/reply/%node'] = array(
2341    'title' => 'Reply to comment',
235     'page callback' => 'comment_reply',
236     'page arguments' => array(2),
237     'access callback' => 'node_access',
238     'access arguments' => array('view', 2),
239     'type' => MENU_CALLBACK,
240     'file' => 'comment.pages.inc',
241   );
242 
2431  return $items;
244 }
245 
246 /**
247  * Implementation of hook_node_type().
248  */
249 function comment_node_type($op, $info) {
250   $settings = array(
2511    'comment',
252     'comment_default_mode',
253     'comment_default_order',
254     'comment_default_per_page',
255     'comment_controls',
256     'comment_anonymous',
257     'comment_subject_field',
258     'comment_preview',
259     'comment_form_location',
260   );
261   switch ($op) {
2621    case 'delete':
263       foreach ($settings as $setting) {
264         variable_del($setting .'_'. $info->type);
265       }
266       break;
267   }
268 }
269 
270 /**
271  * Implementation of hook_perm().
272  */
273 function comment_perm() {
274   return array(
275     'access comments' => t('View comments attached to content.'),
276     'post comments' => t('Add comments to content (approval required).'),
277     'post comments without approval' => t('Add comments to content (no approval required).'),
278     'administer comments' => t('Manage and approve comments, and configure comment administration settings.'),
279   );
280 }
281 
282 /**
283  * Implementation of hook_block().
284  *
285  * Generates a block with the most recent comments.
286  */
287 function comment_block($op = 'list', $delta = 0, $edit = array()) {
288   switch ($op) {
289     case 'list':
290       $blocks[0]['info'] = t('Recent comments');
291       return $blocks;
292 
293     case 'configure':
294       $form['comment_block_count'] = array(
295         '#type' => 'select',
296         '#title' => t('Number of recent comments'),
297         '#default_value' => variable_get('comment_block_count', 10),
298         '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)),
299         '#description' => t('Number of comments displayed in the <em>Recent comments</em> block.'),
300       );
301       return $form;
302 
303     case 'save':
304       variable_set('comment_block_count', (int)$edit['comment_block_count']);
305       break;
306 
307     case 'view':
308       if (user_access('access comments')) {
309         $block['subject'] = t('Recent comments');
310         $block['content'] = theme('comment_block');
311         return $block;
312       }
313   }
314 }
315 
316 /**
317  * Find a number of recent comments. This is done in two steps.
318  *   1. Find the n (specified by $number) nodes that have the most recent
319  *      comments.  This is done by querying node_comment_statistics which has
320  *      an index on last_comment_timestamp, and is thus a fast query.
321  *   2. Loading the information from the comments table based on the nids found
322  *      in step 1.
323  *
324  * @param $number
325  *   (optional) The maximum number of comments to find.
326  * @return
327  *   An array of comment objects each containing a nid,
328  *   subject, cid, and timestamp, or an empty array if there are no recent
329  *   comments visible to the current user.
330  */
331 function comment_get_recent($number = 10) {
332   // Select the $number nodes (visible to the current user) with the most
333   // recent comments. This is efficient due to the index on
334   // last_comment_timestamp.
335   $result = db_query_range(db_rewrite_sql("SELECT nc.nid FROM {node_comment_statistics} nc WHERE nc.comment_count > 0 ORDER BY nc.last_comment_timestamp DESC", 'nc'), 0, $number);
336 
337   $nids = array();
338   while ($row = db_fetch_object($result)) {
339     $nids[] = $row->nid;
340   }
341 
342   $comments = array();
343   if (!empty($nids)) {
344     // From among the comments on the nodes selected in the first query,
345     // find the $number most recent comments.
346     $result = db_query_range('SELECT c.nid, c.subject, c.cid, c.timestamp FROM {comments} c INNER JOIN {node} n ON n.nid = c.nid WHERE c.nid IN ('. implode(',', $nids) .') AND n.status = 1 AND c.status = %d ORDER BY c.cid DESC', COMMENT_PUBLISHED, 0, $number);
347     while ($comment = db_fetch_object($result)) {
348       $comments[] = $comment;
349     }
350   }
351 
352   return $comments;
353 }
354 
355 /**
356  * Calculate page number for first new comment.
357  *
358  * @param $num_comments
359  *   Number of comments.
360  * @param $new_replies
361  *   Number of new replies.
362  * @param $node
363  *   The first new comment node.
364  * @return
365  *   "page=X" if the page number is greater than zero; empty string otherwise.
366  */
367 function comment_new_page_count($num_comments, $new_replies, $node) {
368   $comments_per_page = _comment_get_display_setting('comments_per_page', $node);
369   $mode = _comment_get_display_setting('mode', $node);
370   $order = _comment_get_display_setting('sort', $node);
371   $pagenum = NULL;
372   $flat = in_array($mode, array(COMMENT_MODE_FLAT_COLLAPSED, COMMENT_MODE_FLAT_EXPANDED));
373   if ($num_comments <= $comments_per_page || ($flat && $order == COMMENT_ORDER_NEWEST_FIRST)) {
374     // Only one page of comments or flat forum and newest first.
375     // First new comment will always be on first page.
376     $pageno = 0;
377   }
378   else {
379     if ($flat) {
380       // Flat comments and oldest first.
381       $count = $num_comments - $new_replies;
382     }
383     else {
384       // Threaded comments. See the documentation for comment_render().
385       if ($order == COMMENT_ORDER_NEWEST_FIRST) {
386         // Newest first: find the last thread with new comment
387         $result = db_query('(SELECT thread FROM {comments} WHERE nid = %d  AND status = 0 ORDER BY timestamp DESC LIMIT %d) ORDER BY thread DESC LIMIT 1', $node->nid, $new_replies);
388         $thread = db_result($result);
389         $result_count = db_query("SELECT COUNT(*) FROM {comments} WHERE nid = %d AND status = 0 AND thread > '". $thread ."'", $node->nid);
390       }
391       else {
392         // Oldest first: find the first thread with new comment
393         $result = db_query('(SELECT thread FROM {comments} WHERE nid = %d  AND status = 0 ORDER BY timestamp DESC LIMIT %d) ORDER BY SUBSTRING(thread, 1, (LENGTH(thread) - 1)) LIMIT 1', $node->nid, $new_replies);
394         $thread = substr(db_result($result), 0, -1);
395         $result_count = db_query("SELECT COUNT(*) FROM {comments} WHERE nid = %d AND status = 0 AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < '". $thread ."'", $node->nid);
396       }
397       $count = db_result($result_count);
398     }
399     $pageno =  $count / $comments_per_page;
400   }
401   if ($pageno >= 1) {
402     $pagenum = "page=". intval($pageno);
403   }
404   return $pagenum;
405 }
406 
407 /**
408  * Returns a formatted list of recent comments to be displayed in the comment block.
409  *
410  * @return
411  *   The comment list HTML.
412  * @ingroup themeable
413  */
414 function theme_comment_block() {
415   $items = array();
416   $number = variable_get('comment_block_count', 10);
417   foreach (comment_get_recent($number) as $comment) {
418     $items[] = l($comment->subject, 'node/'. $comment->nid, array('fragment' => 'comment-'. $comment->cid)) .'<br />'. t('@time ago', array('@time' => format_interval(time() - $comment->timestamp)));
419   }
420   if ($items) {
421     return theme('item_list', $items);
422   }
423 }
424 
425 /**
426  * Implementation of hook_link().
427  */
428 function comment_link($type, $node = NULL, $teaser = FALSE) {
429   $links = array();
430 
431   if ($type == 'node' && $node->comment) {
432 
433     if ($teaser) {
434       // Main page: display the number of comments that have been posted.
435 
436       if (user_access('access comments')) {
437         $all = comment_num_all($node->nid);
438 
439         if ($all) {
440           $links['comment_comments'] = array(
441             'title' => format_plural($all, '1 comment', '@count comments'),
442             'href' => "node/$node->nid",
443             'attributes' => array('title' => t('Jump to the first comment of this posting.')),
444             'fragment' => 'comments'
445           );
446 
447           $new = comment_num_new($node->nid);
448 
449           if ($new) {
450             $links['comment_new_comments'] = array(
451               'title' => format_plural($new, '1 new comment', '@count new comments'),
452               'href' => "node/$node->nid",
453               'query' => comment_new_page_count($all, $new, $node),
454               'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
455               'fragment' => 'new'
456             );
457           }
458         }
459         else {
460           if ($node->comment == COMMENT_NODE_READ_WRITE) {
461             if (user_access('post comments')) {
462               $links['comment_add'] = array(
463                 'title' => t('Add new comment'),
464                 'href' => "comment/reply/$node->nid",
465                 'attributes' => array('title' => t('Add a new comment to this page.')),
466                 'fragment' => 'comment-form'
467               );
468             }
469             else {
470               $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
471             }
472           }
473         }
474       }
475     }
476     else {
477       // Node page: add a "post comment" link if the user is allowed to
478       // post comments, if this node is not read-only, and if the comment form isn't already shown
479 
480       if ($node->comment == COMMENT_NODE_READ_WRITE) {
481         if (user_access('post comments')) {
482           if (variable_get('comment_form_location_'. $node->type, COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
483             $links['comment_add'] = array(
484               'title' => t('Add new comment'),
485               'href' => "comment/reply/$node->nid",
486               'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
487               'fragment' => 'comment-form'
488             );
489           }
490         }
491         else {
492           $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
493         }
494       }
495     }
496   }
497 
498   if ($type == 'comment') {
499     $links = comment_links($node, $teaser);
500   }
501   if (isset($links['comment_forbidden'])) {
502     $links['comment_forbidden']['html'] = TRUE;
503   }
504 
505   return $links;
506 }
507 
508 /**
509  * Implementation of hook_form_alter().
510  */
511 function comment_form_alter(&$form, $form_state, $form_id) {
512   if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
513     $form['comment'] = array(
514       '#type' => 'fieldset',
515       '#title' => t('Comment settings'),
516       '#collapsible' => TRUE,
517       '#collapsed' => TRUE,
518     );
519     $form['comment']['comment'] = array(
520       '#type' => 'radios',
521       '#title' => t('Default comment setting'),
522       '#default_value' => variable_get('comment_'. $form['#node_type']->type, COMMENT_NODE_READ_WRITE),
523       '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')),
524       '#description' => t('Users with the <em>administer comments</em> permission will be able to override this setting.'),
525     );
526     $form['comment']['comment_default_mode'] = array(
527       '#type' => 'radios',
528       '#title' => t('Default display mode'),
529       '#default_value' => variable_get('comment_default_mode_'. $form['#node_type']->type, COMMENT_MODE_THREADED_EXPANDED),
530       '#options' => _comment_get_modes(),
531       '#description' => t('The default view for comments. Expanded views display the body of the comment. Threaded views keep replies together.'),
532     );
533     $form['comment']['comment_default_order'] = array(
534       '#type' => 'radios',
535       '#title' => t('Default display order'),
536       '#default_value' => variable_get('comment_default_order_'. $form['#node_type']->type, COMMENT_ORDER_NEWEST_FIRST),
537       '#options' => _comment_get_orders(),
538       '#description' => t('The default sorting for new users and anonymous users while viewing comments. These users may change their view using the comment control panel. For registered users, this change is remembered as a persistent user preference.'),
539     );
540     $form['comment']['comment_default_per_page'] = array(
541       '#type' => 'select',
542       '#title' => t('Default comments per page'),
543       '#default_value' => variable_get('comment_default_per_page_'. $form['#node_type']->type, 50),
544       '#options' => _comment_per_page(),
545       '#description' => t('Default number of comments for each page: more comments are distributed in several pages.'),
546     );
547     $form['comment']['comment_controls'] = array(
548       '#type' => 'radios',
549       '#title' => t('Comment controls'),
550       '#default_value' => variable_get('comment_controls_'. $form['#node_type']->type, COMMENT_CONTROLS_HIDDEN),
551       '#options' => array(
552         t('Display above the comments'),
553         t('Display below the comments'),
554         t('Display above and below the comments'),
555         t('Do not display')),
556       '#description' => t('Position of the comment controls box. The comment controls let the user change the default display mode and display order of comments.'),
557     );
558     $form['comment']['comment_anonymous'] = array(
559       '#type' => 'radios',
560       '#title' => t('Anonymous commenting'),
561       '#default_value' => variable_get('comment_anonymous_'. $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT),
562       '#options' => array(
563         COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
564         COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
565         COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information')),
566       '#description' => t('This option is enabled when anonymous users have permission to post comments on the <a href="@url">permissions page</a>.', array('@url' => url('admin/user/permissions', array('fragment' => 'module-comment')))),
567     );
568     if (!user_access('post comments', drupal_anonymous_user())) {
569       $form['comment']['comment_anonymous']['#disabled'] = TRUE;
570     }
571     $form['comment']['comment_subject_field'] = array(
572       '#type' => 'radios',
573       '#title' => t('Comment subject field'),
574       '#default_value' => variable_get('comment_subject_field_'. $form['#node_type']->type, 1),
575       '#options' => array(t('Disabled'), t('Enabled')),
576       '#description' => t('Can users provide a unique subject for their comments?'),
577     );
578     $form['comment']['comment_preview'] = array(
579       '#type' => 'radios',
580       '#title' => t('Preview comment'),
581       '#default_value' => variable_get('comment_preview_'. $form['#node_type']->type, COMMENT_PREVIEW_REQUIRED),
582       '#options' => array(t('Optional'), t('Required')),
583       '#description' => t("Forces a user to look at their comment by clicking on a 'Preview' button before they can actually add the comment"),
584     );
585     $form['comment']['comment_form_location'] = array(
586       '#type' => 'radios',
587       '#title' => t('Location of comment submission form'),
588       '#default_value' => variable_get('comment_form_location_'. $form['#node_type']->type, COMMENT_FORM_SEPARATE_PAGE),
589       '#options' => array(t('Display on separate page'), t('Display below post or comments')),
590     );
591   }
592   elseif (isset($form['type']) && isset($form['#node'])) {
593     if ($form['type']['#value'] .'_node_form' == $form_id) {
594       $node = $form['#node'];
595       $form['comment_settings'] = array(
596         '#type' => 'fieldset',
597         '#access' => user_access('administer comments'),
598         '#title' => t('Comment settings'),
599         '#collapsible' => TRUE,
600         '#collapsed' => TRUE,
601         '#weight' => 30,
602       );
603       $form['comment_settings']['comment'] = array(
604         '#type' => 'radios',
605         '#parents' => array('comment'),
606         '#default_value' => $node->comment,
607         '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')),
608       );
609     }
610   }
611 }
612 
613 /**
614  * Implementation of hook_nodeapi().
615  */
616 function comment_nodeapi(&$node, $op, $arg = 0) {
617   switch ($op) {
618     case 'load':
619       return db_fetch_array(db_query("SELECT last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid = %d", $node->nid));
620       break;
621 
622     case 'prepare':
623       if (!isset($node->comment)) {
624         $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
625       }
626       break;
627 
628     case 'insert':
629       db_query('INSERT INTO {node_comment_statistics} (nid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count) VALUES (%d, %d, NULL, %d, 0)', $node->nid, $node->changed, $node->uid);
630       break;
631 
632     case 'delete':
633       db_query('DELETE FROM {comments} WHERE nid = %d', $node->nid);
634       db_query('DELETE FROM {node_comment_statistics} WHERE nid = %d', $node->nid);
635       break;
636 
637     case 'update index':
638       $text = '';
639       $comments = db_query('SELECT subject, comment, format FROM {comments} WHERE nid = %d AND status = %d', $node->nid, COMMENT_PUBLISHED);
640       while ($comment = db_fetch_object($comments)) {
641         $text .= '<h2>'. check_plain($comment->subject) .'</h2>'. check_markup($comment->comment, $comment->format, FALSE);
642       }
643       return $text;
644 
645     case 'search result':
646       $comments = db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $node->nid));
647       return format_plural($comments, '1 comment', '@count comments');
648 
649     case 'rss item':
650       if ($node->comment != COMMENT_NODE_DISABLED) {
651         return array(array('key' => 'comments', 'value' => url('node/'. $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))));
652       }
653       else {
654         return array();
655       }
656   }
657 }
658 
659 /**
660  * Implementation of hook_user().
661  */
662 function comment_user($type, $edit, &$user, $category = NULL) {
6631  if ($type == 'delete') {
664     db_query('UPDATE {comments} SET uid = 0 WHERE uid = %d', $user->uid);
665     db_query('UPDATE {node_comment_statistics} SET last_comment_uid = 0 WHERE last_comment_uid = %d', $user->uid);
666   }
667 }
668 
669 /**
670  * This is *not* a hook_access() implementation. This function is called
671  * to determine whether the current user has access to a particular comment.
672  *
673  * Authenticated users can edit their comments as long they have not been
674  * replied to. This prevents people from changing or revising their
675  * statements based on the replies to their posts.
676  *
677  * @param $op
678  *   The operation that is to be performed on the comment. Only 'edit' is recognized now.
679  * @param $comment
680  *   The comment object.
681  * @return
682  *   TRUE if the current user has acces to the comment, FALSE otherwise.
683  */
684 function comment_access($op, $comment) {
685   global $user;
686 
687   if ($op == 'edit') {
688     return ($user->uid && $user->uid == $comment->uid && comment_num_replies($comment->cid) == 0) || user_access('administer comments');
689   }
690 }
691 
692 /**
693  * A simple helper function.
694  *
695  * @return
696  *   The 0th and the 1st path components joined by a slash.
697  */
698 function comment_node_url() {
699   return arg(0) .'/'. arg(1);
700 }
701 
702 /**
703  * Accepts a submission of new or changed comment content.
704  *
705  * @param $edit
706  *   A comment array.
707  *
708  * @return
709  *   If the comment is successfully saved the comment ID is returned. If the comment
710  *   is not saved, FALSE is returned.
711  */
712 function comment_save($edit) {
713   global $user;
714   if (user_access('post comments') && (user_access('administer comments') || node_comment_mode($edit['nid']) == COMMENT_NODE_READ_WRITE)) {
715     if (!form_get_errors()) {
716       $edit += array(
717         'mail' => '',
718         'homepage' => '',
719         'name' => '',
720         'status' => user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED,
721       );
722       if ($edit['cid']) {
723         // Update the comment in the database.
724         db_query("UPDATE {comments} SET status = %d, timestamp = %d, subject = '%s', comment = '%s', format = %d, uid = %d, name = '%s', mail = '%s', homepage = '%s' WHERE cid = %d", $edit['status'], $edit['timestamp'], $edit['subject'], $edit['comment'], $edit['format'], $edit['uid'], $edit['name'], $edit['mail'], $edit['homepage'], $edit['cid']);
725 
726         // Allow modules to respond to the updating of a comment.
727         comment_invoke_comment($edit, 'update');
728 
729         // Add an entry to the watchdog log.
730         watchdog('content', 'Comment: updated %subject.', array('%subject' => $edit['subject']), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], array('fragment' => 'comment-'. $edit['cid'])));
731       }
732       else {
733         // Add the comment to database.
734         // Here we are building the thread field. See the documentation for
735         // comment_render().
736         if ($edit['pid'] == 0) {
737           // This is a comment with no parent comment (depth 0): we start
738           // by retrieving the maximum thread level.
739           $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $edit['nid']));
740 
741           // Strip the "/" from the end of the thread.
742           $max = rtrim($max, '/');
743 
744           // Finally, build the thread field for this new comment.
745           $thread = int2vancode(vancode2int($max) + 1) .'/';
746         }
747         else {
748           // This is comment with a parent comment: we increase
749           // the part of the thread value at the proper depth.
750 
751           // Get the parent comment:
752           $parent = _comment_load($edit['pid']);
753 
754           // Strip the "/" from the end of the parent thread.
755           $parent->thread = (string) rtrim((string) $parent->thread, '/');
756 
757           // Get the max value in _this_ thread.
758           $max = db_result(db_query("SELECT MAX(thread) FROM {comments} WHERE thread LIKE '%s.%%' AND nid = %d", $parent->thread, $edit['nid']));
759 
760           if ($max == '') {
761             // First child of this parent.
762             $thread = $parent->thread .'.'. int2vancode(0) .'/';
763           }
764           else {
765             // Strip the "/" at the end of the thread.
766             $max = rtrim($max, '/');
767 
768             // We need to get the value at the correct depth.
769             $parts = explode('.', $max);
770             $parent_depth = count(explode('.', $parent->thread));
771             $last = $parts[$parent_depth];
772 
773             // Finally, build the thread field for this new comment.
774             $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
775           }
776         }
777 
778         $edit['timestamp'] = time();
779 
780         if ($edit['uid'] === $user->uid) { // '===' because we want to modify anonymous users too
781           $edit['name'] = $user->name;
782         }
783 
784         db_query("INSERT INTO {comments} (nid, pid, uid, subject, comment, format, hostname, timestamp, status, thread, name, mail, homepage) VALUES (%d, %d, %d, '%s', '%s', %d, '%s', %d, %d, '%s', '%s', '%s', '%s')", $edit['nid'], $edit['pid'], $edit['uid'], $edit['subject'], $edit['comment'], $edit['format'], ip_address(), $edit['timestamp'], $edit['status'], $thread, $edit['name'], $edit['mail'], $edit['homepage']);
785         $edit['cid'] = db_last_insert_id('comments', 'cid');
786 
787         // Tell the other modules a new comment has been submitted.
788         comment_invoke_comment($edit, 'insert');
789 
790         // Add an entry to the watchdog log.
791         watchdog('content', 'Comment: added %subject.', array('%subject' => $edit['subject']), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], array('fragment' => 'comment-'. $edit['cid'])));
792       }
793       _comment_update_node_statistics($edit['nid']);
794 
795       // Clear the cache so an anonymous user can see his comment being added.
796       cache_clear_all();
797 
798       // Explain the approval queue if necessary, and then
799       // redirect the user to the node he's commenting on.
800       if ($edit['status'] == COMMENT_NOT_PUBLISHED) {
801         drupal_set_message(t('Your comment has been queued for moderation by site administrators and will be published after approval.'));
802       }
803       else {
804         comment_invoke_comment($edit, 'publish');
805       }
806       return $edit['cid'];
807     }
808     else {
809       return FALSE;
810     }
811   }
812   else {
813     watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $edit['subject']), WATCHDOG_WARNING);
814     drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $edit['subject'])), 'error');
815     return FALSE;
816   }
817 }
818 
819 /**
820  * Build command links for a comment (e.g.\ edit, reply, delete) with respect to the current user's access permissions.
821  *
822  * @param $comment
823  *   The comment to which the links will be related.
824  * @param $return
825  *   Not used.
826  * @return
827  *   An associative array containing the links.
828  */
829 function comment_links($comment, $return = 1) {
830   global $user;
831 
832   $links = array();
833 
834   // If we are viewing just this comment, we link back to the node.
835   if ($return) {
836     $links['comment_parent'] = array(
837       'title' => t('parent'),
838       'href' => comment_node_url(),
839       'fragment' => "comment-$comment->cid"
840     );
841   }
842 
843   if (node_comment_mode($comment->nid) == COMMENT_NODE_READ_WRITE) {
844     if (user_access('administer comments') && user_access('post comments')) {
845       $links['comment_delete'] = array(
846         'title' => t('delete'),
847         'href' => "comment/delete/$comment->cid"
848       );
849       $links['comment_edit'] = array(
850         'title' => t('edit'),
851         'href' => "comment/edit/$comment->cid"
852       );
853       $links['comment_reply'] = array(
854         'title' => t('reply'),
855         'href' => "comment/reply/$comment->nid/$comment->cid"
856       );
857     }
858     else if (user_access('post comments')) {
859       if (comment_access('edit', $comment)) {
860         $links['comment_edit'] = array(
861           'title' => t('edit'),
862           'href' => "comment/edit/$comment->cid"
863         );
864       }
865       $links['comment_reply'] = array(
866         'title' => t('reply'),
867         'href' => "comment/reply/$comment->nid/$comment->cid"
868       );
869     }
870     else {
871       $node = node_load($comment->nid);
872       $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
873     }
874   }
875 
876   return $links;
877 }
878 
879 /**
880  * Renders comment(s).
881  *
882  * @param $node
883  *   The node which comment(s) needs rendering.
884  * @param $cid
885  *   Optional, if given, only one comment is rendered.
886  *
887  * To display threaded comments in the correct order we keep a 'thread' field
888  * and order by that value. This field keeps this data in
889  * a way which is easy to update and convenient to use.
890  *
891  * A "thread" value starts at "1". If we add a child (A) to this comment,
892  * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
893  * brother of (A) will get "1.2". Next brother of the parent of (A) will get
894  * "2" and so on.
895  *
896  * First of all note that the thread field stores the depth of the comment:
897  * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
898  *
899  * Now to get the ordering right, consider this example:
900  *
901  * 1
902  * 1.1
903  * 1.1.1
904  * 1.2
905  * 2
906  *
907  * If we "ORDER BY thread ASC" we get the above result, and this is the
908  * natural order sorted by time. However, if we "ORDER BY thread DESC"
909  * we get:
910  *
911  * 2
912  * 1.2
913  * 1.1.1
914  * 1.1
915  * 1
916  *
917  * Clearly, this is not a natural way to see a thread, and users will get
918  * confused. The natural order to show a thread by time desc would be:
919  *
920  * 2
921  * 1
922  * 1.2
923  * 1.1
924  * 1.1.1
925  *
926  * which is what we already did before the standard pager patch. To achieve
927  * this we simply add a "/" at the end of each "thread" value. This way out
928  * thread fields will look like depicted below:
929  *
930  * 1/
931  * 1.1/
932  * 1.1.1/
933  * 1.2/
934  * 2/
935  *
936  * we add "/" since this char is, in ASCII, higher than every number, so if
937  * now we "ORDER BY thread DESC" we get the correct order. However this would
938  * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
939  * to consider the trailing "/" so we use a substring only.
940  */
941 function comment_render($node, $cid = 0) {
942   global $user;
943 
944   $output = '';
945 
946   if (user_access('access comments')) {
947     // Pre-process variables.
948     $nid = $node->nid;
949     if (empty($nid)) {
950       $nid = 0;
951     }
952 
953     $mode = _comment_get_display_setting('mode', $node);
954     $order = _comment_get_display_setting('sort', $node);
955     $comments_per_page = _comment_get_display_setting('comments_per_page', $node);
956 
957     if ($cid && is_numeric($cid)) {
958       // Single comment view.
959       $query = 'SELECT c.cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.picture, u.data, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d';
960       $query_args = array($cid);
961       if (!user_access('administer comments')) {
962         $query .= ' AND c.status = %d';
963         $query_args[] = COMMENT_PUBLISHED;
964       }
965 
966       $query = db_rewrite_sql($query, 'c', 'cid');
967       $result = db_query($query, $query_args);
968 
969       if ($comment = db_fetch_object($result)) {
970         $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
971         $links = module_invoke_all('link', 'comment', $comment, 1);
972         drupal_alter('link', $links, $node);
973 
974         $output .= theme('comment_view', $comment, $node, $links);
975       }
976     }
977     else {
978       // Multiple comment view
979       $query_count = 'SELECT COUNT(*) FROM {comments} WHERE nid = %d';
980       $query = 'SELECT c.cid as cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.picture, u.data, c.thread, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.nid = %d';
981 
982       $query_args = array($nid);
983       if (!user_access('administer comments')) {
984         $query .= ' AND c.status = %d';
985         $query_count .= ' AND status = %d';
986         $query_args[] = COMMENT_PUBLISHED;
987       }
988 
989       if ($order == COMMENT_ORDER_NEWEST_FIRST) {
990         if ($mode == COMMENT_MODE_FLAT_COLLAPSED || $mode == COMMENT_MODE_FLAT_EXPANDED) {
991           $query .= ' ORDER BY c.cid DESC';
992         }
993         else {
994           $query .= ' ORDER BY c.thread DESC';
995         }
996       }
997       else if ($order == COMMENT_ORDER_OLDEST_FIRST) {
998         if ($mode == COMMENT_MODE_FLAT_COLLAPSED || $mode == COMMENT_MODE_FLAT_EXPANDED) {
999           $query .= ' ORDER BY c.cid';
1000         }
1001         else {
1002           // See comment above. Analysis reveals that this doesn't cost too
1003           // much. It scales much much better than having the whole comment
1004           // structure.
1005           $query .= ' ORDER BY SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))';
1006         }
1007       }
1008       $query = db_rewrite_sql($query, 'c', 'cid');
1009       $query_count = db_rewrite_sql($query_count, 'c', 'cid');
1010 
1011       // Start a form, for use with comment control.
1012       $result = pager_query($query, $comments_per_page, 0, $query_count, $query_args);
1013 
1014       $divs = 0;
1015       $num_rows = FALSE;
1016       $comments = '';
1017       drupal_add_css(drupal_get_path('module', 'comment') .'/comment.css');
1018       while ($comment = db_fetch_object($result)) {
1019         $comment = drupal_unpack($comment);
1020         $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1021         $comment->depth = count(explode('.', $comment->thread)) - 1;
1022 
1023         if ($mode == COMMENT_MODE_THREADED_COLLAPSED || $mode == COMMENT_MODE_THREADED_EXPANDED) {
1024           if ($comment->depth > $divs) {
1025             $divs++;
1026             $comments .= '<div class="indented">';
1027           }
1028           else {
1029             while ($comment->depth < $divs) {
1030               $divs--;
1031               $comments .= '</div>';
1032             }
1033           }
1034         }
1035 
1036         if ($mode == COMMENT_MODE_FLAT_COLLAPSED) {
1037           $comments .= theme('comment_flat_collapsed', $comment, $node);
1038         }
1039         else if ($mode == COMMENT_MODE_FLAT_EXPANDED) {
1040           $comments .= theme('comment_flat_expanded', $comment, $node);
1041         }
1042         else if ($mode == COMMENT_MODE_THREADED_COLLAPSED) {
1043           $comments .= theme('comment_thread_collapsed', $comment, $node);
1044         }
1045         else if ($mode == COMMENT_MODE_THREADED_EXPANDED) {
1046           $comments .= theme('comment_thread_expanded', $comment, $node);
1047         }
1048 
1049         $num_rows = TRUE;
1050       }
1051       while ($divs-- > 0) {
1052         $comments .= '</div>';
1053       }
1054 
1055       $comment_controls = variable_get('comment_controls_'. $node->type, COMMENT_CONTROLS_HIDDEN);
1056       if ($num_rows && ($comment_controls == COMMENT_CONTROLS_ABOVE || $comment_controls == COMMENT_CONTROLS_ABOVE_BELOW)) {
1057         $output .= drupal_get_form('comment_controls', $mode, $order, $comments_per_page);
1058       }
1059 
1060       $output .= $comments;
1061       $output .= theme('pager', NULL, $comments_per_page, 0);
1062 
1063       if ($num_rows && ($comment_controls == COMMENT_CONTROLS_BELOW || $comment_controls == COMMENT_CONTROLS_ABOVE_BELOW)) {
1064         $output .= drupal_get_form('comment_controls', $mode, $order, $comments_per_page);
1065       }
1066     }
1067 
1068     // If enabled, show new comment form if it's not already being displayed.
1069     $reply = arg(0) == 'comment' && arg(1) == 'reply';
1070     if (user_access('post comments') && node_comment_mode($nid) == COMMENT_NODE_READ_WRITE && (variable_get('comment_form_location_'. $node->type, COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_BELOW) && !$reply) {
1071       $output .= comment_form_box(array('nid' => $nid), t('Post new comment'));
1072     }
1073 
1074     $output = theme('comment_wrapper', $output, $node);
1075   }
1076 
1077   return $output;
1078 }
1079 
1080 /**
1081  * Comment operations. We offer different update operations depending on
1082  * which comment administration page we're on.
1083  *
1084  * @param $action
1085  *   The comment administration page.
1086  * @return
1087  *   An associative array containing the offered operations.
1088  */
1089 function comment_operations($action = NULL) {
1090   if ($action == 'publish') {
1091     $operations = array(
1092       'publish' => array(t('Publish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_PUBLISHED .' WHERE cid = %d'),
1093       'delete' => array(t('Delete the selected comments'), '')
1094     );
1095   }
1096   else if ($action == 'unpublish') {
1097     $operations = array(
1098       'unpublish' => array(t('Unpublish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_NOT_PUBLISHED .' WHERE cid = %d'),
1099       'delete' => array(t('Delete the selected comments'), '')
1100     );
1101   }
1102   else {
1103     $operations = array(
1104       'publish' => array(t('Publish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_PUBLISHED .' WHERE cid = %d'),
1105       'unpublish' => array(t('Unpublish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_NOT_PUBLISHED .' WHERE cid = %d'),
1106       'delete' => array(t('Delete the selected comments'), '')
1107     );
1108   }
1109   return $operations;
1110 }
1111 
1112 /**
1113  * Misc functions: helpers, privates, history
1114  */
1115 
1116 /**
1117  * Load the entire comment by cid.
1118  *
1119  * @param $cid
1120  *   The identifying comment id.
1121  * @return
1122  *   The comment object.
1123  */
1124 function _comment_load($cid) {
1125   return db_fetch_object(db_query('SELECT * FROM {comments} WHERE cid = %d', $cid));
1126 }
1127 
1128 /**
1129  * Get comment count for a node.
1130  *
1131  * @param $nid
1132  *   The node id.
1133  * @return
1134  *   The comment count.
1135  */
1136 function comment_num_all($nid) {
1137   static $cache;
1138 
1139   if (!isset($cache[$nid])) {
1140     $cache[$nid] = db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $nid));
1141   }
1142   return $cache[$nid];
1143 }
1144 
1145 /**
1146  * Get replies count for a comment.
1147  *
1148  * @param $pid
1149  *   The comment id.
1150  * @return
1151  *   The replies count.
1152  */
1153 function comment_num_replies($pid) {
1154   static $cache;
1155 
1156   if (!isset($cache[$pid])) {
1157     $cache[$pid] = db_result(db_query('SELECT COUNT(cid) FROM {comments} WHERE pid = %d AND status = %d', $pid, COMMENT_PUBLISHED));
1158   }
1159 
1160   return $cache[$pid];
1161 }
1162 
1163 /**
1164  * Get number of new comments for current user and specified node.
1165  *
1166  * @param $nid
1167  *   node-id to count comments for
1168  * @param $timestamp
1169  *   time to count from (defaults to time of last user access
1170  *   to node)
1171  */
1172 function comment_num_new($nid, $timestamp = 0) {
1173   global $user;
1174 
1175   if ($user->uid) {
1176     // Retrieve the timestamp at which the current user last viewed the
1177     // specified node.
1178     if (!$timestamp) {
1179       $timestamp = node_last_viewed($nid);
1180     }
1181     $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT);
1182 
1183     // Use the timestamp to retrieve the number of new comments.
1184     $result = db_result(db_query('SELECT COUNT(c.cid) FROM {node} n INNER JOIN {comments} c ON n.nid = c.nid WHERE n.nid = %d AND timestamp > %d AND c.status = %d', $nid, $timestamp, COMMENT_PUBLISHED));
1185 
1186     return $result;
1187   }
1188   else {
1189     return 0;
1190   }
1191 
1192 }
1193 
1194 /**
1195  * Validate comment data.
1196  *
1197  * @param $edit
1198  *   An associative array containig the comment data.
1199  * @return
1200  *   The original $edit.
1201  */
1202 function comment_validate($edit) {
1203   global $user;
1204 
1205   // Invoke other validation handlers
1206   comment_invoke_comment($edit, 'validate');
1207 
1208   if (isset($edit['date'])) {
1209     // As of PHP 5.1.0, strtotime returns FALSE upon failure instead of -1.
1210     if (strtotime($edit['date']) <= 0) {
1211       form_set_error('date', t('You have to specify a valid date.'));
1212     }
1213   }
1214   if (isset($edit['author']) && !$account = user_load(array('name' => $edit['author']))) {
1215     form_set_error('author', t('You have to specify a valid author.'));
1216   }
1217 
1218   // Check validity of name, mail and homepage (if given)
1219   if (!$user->uid || isset($edit['is_anonymous'])) {
1220     $node = node_load($edit['nid']);
1221     if (variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) > COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
1222       if ($edit['name']) {
1223         $taken = db_result(db_query("SELECT COUNT(uid) FROM {users} WHERE LOWER(name) = '%s'", $edit['name']));
1224 
1225         if ($taken != 0) {
1226           form_set_error('name', t('The name you used belongs to a registered user.'));
1227         }
1228 
1229       }
1230       else if (variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
1231         form_set_error('name', t('You have to leave your name.'));
1232       }
1233 
1234       if ($edit['mail']) {
1235         if (!valid_email_address($edit['mail'])) {
1236           form_set_error('mail', t('The e-mail address you specified is not valid.'));
1237         }
1238       }
1239       else if (variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
1240         form_set_error('mail', t('You have to leave an e-mail address.'));
1241       }
1242 
1243       if ($edit['homepage']) {
1244         if (!valid_url($edit['homepage'], TRUE)) {
1245           form_set_error('homepage', t('The URL of your homepage is not valid. Remember that it must be fully qualified, i.e. of the form <code>http://example.com/directory</code>.'));
1246         }
1247       }
1248     }
1249   }
1250 
1251   return $edit;
1252 }
1253 
1254 /**
1255  * Generate the basic commenting form, for appending to a node or display on a separate page.
1256  *
1257  * @param $title
1258  *   Not used.
1259  * @ingroup forms
1260  * @see comment_form_validate()
1261  * @see comment_form_submit()
1262  */
1263 function comment_form(&$form_state, $edit, $title = NULL) {
1264   global $user;
1265 
1266   $op = isset($_POST['op']) ? $_POST['op'] : '';
1267   $node = node_load($edit['nid']);
1268 
1269   if (!$user->uid && variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
1270     drupal_add_js(drupal_get_path('module', 'comment') .'/comment.js');
1271   }
1272   $edit += array('name' => '', 'mail' => '', 'homepage' => '');
1273   if ($user->uid) {
1274     if (!empty($edit['cid']) && user_access('administer comments')) {
1275       if (!empty($edit['author'])) {
1276         $author = $edit['author'];
1277       }
1278       elseif (!empty($edit['name'])) {
1279         $author = $edit['name'];
1280       }
1281       else {
1282         $author = $edit['registered_name'];
1283       }
1284 
1285       if (!empty($edit['status'])) {
1286         $status = $edit['status'];
1287       }
1288       else {
1289         $status = 0;
1290       }
1291 
1292       if (!empty($edit['date'])) {
1293         $date = $edit['date'];
1294       }
1295       else {
1296         $date = format_date($edit['timestamp'], 'custom', 'Y-m-d H:i O');
1297       }
1298 
1299       $form['admin'] = array(
1300         '#type' => 'fieldset',
1301         '#title' => t('Administration'),
1302         '#collapsible' => TRUE,
1303         '#collapsed' => TRUE,
1304         '#weight' => -2,
1305       );
1306 
1307       if ($edit['registered_name'] != '') {
1308         // The comment is by a registered user
1309         $form['admin']['author'] = array(
1310           '#type' => 'textfield',
1311           '#title' => t('Authored by'),
1312           '#size' => 30,
1313           '#maxlength' => 60,
1314           '#autocomplete_path' => 'user/autocomplete',
1315           '#default_value' => $author,
1316           '#weight' => -1,
1317         );
1318       }
1319       else {
1320         // The comment is by an anonymous user
1321         $form['is_anonymous'] = array(
1322           '#type' => 'value',
1323           '#value' => TRUE,
1324         );
1325         $form['admin']['name'] = array(
1326           '#type' => 'textfield',
1327           '#title' => t('Authored by'),
1328           '#size' => 30,
1329           '#maxlength' => 60,
1330           '#default_value' => $author,
1331           '#weight' => -1,
1332         );
1333         $form['admin']['mail'] = array(
1334           '#type' => 'textfield',
1335           '#title' => t('E-mail'),
1336           '#maxlength' => 64,
1337           '#size' => 30,
1338           '#default_value' => $edit['mail'],
1339           '#description' => t('The content of this field is kept private and will not be shown publicly.'),
1340         );
1341 
1342         $form['admin']['homepage'] = array(
1343           '#type' => 'textfield',
1344           '#title' => t('Homepage'),
1345           '#maxlength' => 255,
1346           '#size' => 30,
1347           '#default_value' => $edit['homepage'],
1348         );
1349       }
1350 
1351       $form['admin']['date'] = array('#type' => 'textfield', '#parents' => array('date'), '#title' => t('Authored on'), '#size' => 20, '#maxlength' => 25, '#default_value' => $date, '#weight' => -1);
1352 
1353       $form['admin']['status'] = array('#type' => 'radios', '#parents' => array('status'), '#title' => t('Status'), '#default_value' =>  $status, '#options' => array(t('Published'), t('Not published')), '#weight' => -1);
1354 
1355     }
1356     else {
1357       $form['_author'] = array('#type' => 'item', '#title' => t('Your name'), '#value' => theme('username', $user)
1358       );
1359       $form['author'] = array('#type' => 'value', '#value' => $user->name);
1360     }
1361   }
1362   else if (variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MAY_CONTACT) {
1363     $form['name'] = array('#type' => 'textfield', '#title' => t('Your name'), '#maxlength' => 60, '#size' => 30, '#default_value' => $edit['name'] ? $edit['name'] : variable_get('anonymous', t('Anonymous'))
1364     );
1365 
1366     $form['mail'] = array('#type' => 'textfield', '#title' => t('E-mail'), '#maxlength' => 64, '#size' => 30, '#default_value' => $edit['mail'], '#description' => t('The content of this field is kept private and will not be shown publicly.')
1367     );
1368 
1369     $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
1370   }
1371   else if (variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
1372     $form['name'] = array('#type' => 'textfield', '#title' => t('Your name'), '#maxlength' => 60, '#size' => 30, '#default_value' => $edit['name'] ? $edit['name'] : variable_get('anonymous', t('Anonymous')), '#required' => TRUE);
1373 
1374     $form['mail'] = array('#type' => 'textfield', '#title' => t('E-mail'), '#maxlength' => 64, '#size' => 30, '#default_value' => $edit['mail'], '#description' => t('The content of this field is kept private and will not be shown publicly.'), '#required' => TRUE);
1375 
1376     $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
1377   }
1378 
1379   if (variable_get('comment_subject_field_'. $node->type, 1) == 1) {
1380     $form['subject'] = array('#type' => 'textfield', '#title' => t('Subject'), '#maxlength' => 64, '#default_value' => !empty($edit['subject']) ? $edit['subject'] : '');
1381   }
1382 
1383   if (!empty($edit['comment'])) {
1384     $default = $edit['comment'];
1385   }
1386   else {
1387     $default = '';
1388   }
1389 
1390   $form['comment_filter']['comment'] = array(
1391     '#type' => 'textarea',
1392     '#title' => t('Comment'),
1393     '#rows' => 15,
1394     '#default_value' => $default,
1395     '#required' => TRUE,
1396   );
1397   if (!isset($edit['format'])) {
1398     $edit['format'] = FILTER_FORMAT_DEFAULT;
1399   }
1400   $form['comment_filter']['format'] = filter_form($edit['format']);
1401 
1402   $form['cid'] = array('#type' => 'value', '#value' => !empty($edit['cid']) ? $edit['cid'] : NULL);
1403   $form['pid'] = array('#type' => 'value', '#value' => !empty($edit['pid']) ? $edit['pid'] : NULL);
1404   $form['nid'] = array('#type' => 'value', '#value' => $edit['nid']);
1405   $form['uid'] = array('#type' => 'value', '#value' => !empty($edit['uid']) ? $edit['uid'] : NULL);
1406 
1407   // Only show save button if preview is optional or if we are in preview mode.
1408   // We show the save button in preview mode even if there are form errors so that
1409   // optional form elements (e.g., captcha) can be updated in preview mode.
1410   if (!form_get_errors() && ((variable_get('comment_preview_'. $node->type, COMMENT_PREVIEW_REQUIRED) == COMMENT_PREVIEW_OPTIONAL) || ($op == t('Preview')) || ($op == t('Save')))) {
1411     $form['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#weight' => 19);
1412   }
1413 
1414   $form['preview'] = array('#type' => 'button', '#value' => t('Preview'), '#weight' => 20);
1415   $form['#token'] = 'comment'. $edit['nid'] . (isset($edit['pid']) ? $edit['pid'] : '');
1416 
1417   if ($op == t('Preview')) {
1418     $form['#after_build'] = array('comment_form_add_preview');
1419   }
1420 
1421   if (empty($edit['cid']) && empty($edit['pid'])) {
1422     $form['#action'] = url('comment/reply/'. $edit['nid']);
1423   }
1424 
1425   return $form;
1426 }
1427 
1428 /**
1429  * Theme the comment form box.
1430  *
1431  * @param $edit
1432  *   The form structure.
1433  * @param $title
1434  *   The form title.
1435  */
1436 function comment_form_box($edit, $title = NULL) {
1437   return theme('box', $title, drupal_get_form('comment_form', $edit, $title));
1438 }
1439 
1440 /**
1441  * Form builder; Generate and validate a comment preview form.
1442  *
1443  * @ingroup forms
1444  */
1445 function comment_form_add_preview($form, &$form_state) {
1446   global $user;
1447   $edit = $form_state['values'];
1448   drupal_set_title(t('Preview comment'));
1449 
1450   $output = '';
1451   $node = node_load($edit['nid']);
1452 
1453   // Invoke full validation for the form, to protect against cross site
1454   // request forgeries (CSRF) and setting arbitrary values for fields such as
1455   // the input format. Preview the comment only when form validation does not
1456   // set any errors.
1457   drupal_validate_form($form['form_id']['#value'], $form, $form_state);
1458   if (!form_get_errors()) {
1459     _comment_form_submit($edit);
1460     $comment = (object)$edit;
1461 
1462     // Attach the user and time information.
1463     if (!empty($edit['author'])) {
1464       $account = user_load(array('name' => $edit['author']));
1465     }
1466     elseif ($user->uid && !isset($edit['is_anonymous'])) {
1467       $account = $user;
1468     }
1469     if (!empty($account)) {
1470       $comment->uid = $account->uid;
1471       $comment->name = check_plain($account->name);
1472     }
1473     elseif (empty($comment->name)) {
1474       $comment->name = variable_get('anonymous', t('Anonymous'));
1475     }
1476     $comment->timestamp = !empty($edit['timestamp']) ? $edit['timestamp'] : time();
1477     $output .= theme('comment_view', $comment, $node);
1478   }
1479   $form['comment_preview'] = array(
1480     '#value' => $output,
1481     '#weight' => -100,
1482     '#prefix' => '<div class="preview">',
1483     '#suffix' => '</div>',
1484   );
1485 
1486   $output = '';
1487 
1488   if ($edit['pid']) {
1489     $comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.signature, u.picture, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d AND c.status = %d', $edit['pid'], COMMENT_PUBLISHED));
1490     $comment = drupal_unpack($comment);
1491     $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1492     $output .= theme('comment_view', $comment, $node);
1493   }
1494   else {
1495     $suffix = empty($form['#suffix']) ? '' : $form['#suffix'];
1496     $form['#suffix'] = $suffix . node_view($node);
1497     $edit['pid'] = 0;
1498   }
1499 
1500   $form['comment_preview_below'] = array('#value' => $output, '#weight' => 100);
1501 
1502   return $form;
1503 }
1504 
1505 /**
1506  * Validate comment form submissions.
1507  */
1508 function comment_form_validate($form, &$form_state) {
1509   global $user;
1510   if ($user->uid === 0) {
1511     foreach (array('name', 'homepage', 'mail') as $field) {
1512       // Set cookie for 365 days.
1513       if (isset($form_state['values'][$field])) {
1514         setcookie('comment_info_'. $field, $form_state['values'][$field], time() + 31536000, '/');
1515       }
1516     }
1517   }
1518   comment_validate($form_state['values']);
1519 }
1520 
1521 /**
1522  * Prepare a comment for submission.
1523  *
1524  * @param $comment_values
1525  *   An associative array containing the comment data.
1526  */
1527 function _comment_form_submit(&$comment_values) {
1528   $comment_values += array('subject' => '');
1529   if (!isset($comment_values['date'])) {
1530     $comment_values['date'] = 'now';
1531   }
1532   $comment_values['timestamp'] = strtotime($comment_values['date']);
1533   if (isset($comment_values['author'])) {
1534     $account = user_load(array('name' => $comment_values['author']));
1535     $comment_values['uid'] = $account->uid;
1536     $comment_values['name'] = $comment_values['author'];
1537   }
1538   // Validate the comment's subject. If not specified, extract
1539   // one from the comment's body.
1540   if (trim($comment_values['subject']) == '') {
1541     // The body may be in any format, so we:
1542     // 1) Filter it into HTML
1543     // 2) Strip out all HTML tags
1544     // 3) Convert entities back to plain-text.
1545     // Note: format is checked by check_markup().
1546     $comment_values['subject'] = trim(truncate_utf8(decode_entities(strip_tags(check_markup($comment_values['comment'], $comment_values['format']))), 29, TRUE));
1547     // Edge cases where the comment body is populated only by HTML tags will
1548     // require a default subject.
1549     if ($comment_values['subject'] == '') {
1550       $comment_values['subject'] = t('(No subject)');
1551     }
1552   }
1553 }
1554 
1555 /**
1556  * Process comment form submissions; prepare the comment, store it, and set a redirection target.
1557  */
1558 function comment_form_submit($form, &$form_state) {
1559   _comment_form_submit($form_state['values']);
1560   if ($cid = comment_save($form_state['values'])) {
1561     $node = node_load($form_state['values']['nid']);
1562     $page = comment_new_page_count($node->comment_count, 1, $node);
1563     $form_state['redirect'] = array('node/'. $node->nid, $page, "comment-$cid");
1564     return;
1565   }
1566 }
1567 
1568 /**
1569  * Theme a single comment block.
1570  *
1571  * @param $comment
1572  *   The comment object.
1573  * @param $node
1574  *   The comment node.
1575  * @param $links
1576  *   An associative array containing control links.
1577  * @param $visible
1578  *   Switches between folded/unfolded view.
1579  * @ingroup themeable
1580  */
1581 function theme_comment_view($comment, $node, $links = array(), $visible = TRUE) {
1582   static $first_new = TRUE;
1583 
1584   $output = '';
1585   $comment->new = node_mark($comment->nid, $comment->timestamp);
1586   if ($first_new && $comment->new != MARK_READ) {
1587     // Assign the anchor only for the first new comment. This avoids duplicate
1588     // id attributes on a page.
1589     $first_new = FALSE;
1590     $output .= "<a id=\"new\"></a>\n";
1591   }
1592 
1593   $output .= "<a id=\"comment-$comment->cid\"></a>\n";
1594 
1595   // Switch to folded/unfolded view of the comment
1596   if ($visible) {
1597     $comment->comment = check_markup($comment->comment, $comment->format, FALSE);
1598 
1599     // Comment API hook
1600     comment_invoke_comment($comment, 'view');
1601 
1602     $output .= theme('comment', $comment, $node, $links);
1603   }
1604   else {
1605     $output .= theme('comment_folded', $comment);
1606   }
1607 
1608   return $output;
1609 }
1610 
1611 
1612 /**
1613  * Build a comment control form.
1614  *
1615  * @param $mode
1616  *   Comment display mode.
1617  * @param $order
1618  *   Comment order mode.
1619  * @param $comments_per_page
1620  *   Comments per page.
1621  * @ingroup forms
1622  */
1623 function comment_controls($mode = COMMENT_MODE_THREADED_EXPANDED, $order = COMMENT_ORDER_NEWEST_FIRST, $comments_per_page = 50) {
1624   $form['mode'] = array('#type' => 'select',
1625     '#default_value' => $mode,
1626     '#options' => _comment_get_modes(),
1627     '#weight' => 1,
1628   );
1629   $form['order'] = array(
1630     '#type' => 'select',
1631     '#default_value' => $order,
1632     '#options' => _comment_get_orders(),
1633     '#weight' => 2,
1634   );
1635   foreach (_comment_per_page() as $i) {
1636     $options[$i] = t('!a comments per page', array('!a' => $i));
1637   }
1638   $form['comments_per_page'] = array('#type' => 'select',
1639     '#default_value' => $comments_per_page,
1640     '#options' => $options,
1641     '#weight' => 3,
1642   );
1643 
1644   $form['submit'] = array('#type' => 'submit',
1645     '#value' => t('Save settings'),
1646     '#weight' => 20,
1647   );
1648 
1649   return $form;
1650 }
1651 
1652 /**
1653  * Theme comment controls box where the user can change the default display mode and display order of comments.
1654  *
1655  * @param $form
1656  *   The form structure.
1657  * @ingroup themeable
1658  */
1659 function theme_comment_controls($form) {
1660   $output = '<div class="container-inline">';
1661   $output .=  drupal_render($form);
1662   $output .= '</div>';
1663   $output .= '<div class="description">'. t('Select your preferred way to display the comments and click "Save settings" to activate your changes.') .'</div>';
1664   return theme('box', t('Comment viewing options'), $output);
1665 }
1666 
1667 /**
1668  * Process comment_controls form submissions.
1669  */
1670 function comment_controls_submit($form, &$form_state) {
1671   global $user;
1672 
1673   $mode = $form_state['values']['mode'];
1674   $order = $form_state['values']['order'];
1675   $comments_per_page = $form_state['values']['comments_per_page'];
1676 
1677   if ($user->uid) {
1678     $account = user_save($user, array('mode' => $mode, 'sort' => $order, 'comments_per_page' => $comments_per_page));
1679     // Terminate if an error occured during user_save().
1680     if (!$account) {
1681       drupal_set_message(t("Error saving user account."), 'error');
1682       return;
1683     }
1684     $user = $account;
1685   }
1686   else {
1687     $_SESSION['comment_mode'] = $mode;
1688     $_SESSION['comment_sort'] = $order;
1689     $_SESSION['comment_comments_per_page'] = $comments_per_page;
1690   }
1691 }
1692 
1693 /**
1694  * Process variables for comment.tpl.php.
1695  *
1696  * @see comment.tpl.php
1697  * @see theme_comment()
1698  */
1699 function template_preprocess_comment(&$variables) {
1700   $comment = $variables['comment'];
1701   $node = $variables['node'];
1702   $variables['author']    = theme('username', $comment);
1703   $variables['content']   = $comment->comment;
1704   $variables['date']      = format_date($comment->timestamp);
1705   $variables['links']     = isset($variables['links']) ? theme('links', $variables['links']) : '';
1706   $variables['new']       = $comment->new ? t('new') : '';
1707   $variables['picture']   = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', $comment) : '';
1708   $variables['signature'] = $comment->signature;
1709   $variables['submitted'] = theme('comment_submitted', $comment);
1710   $variables['title']     = l($comment->subject, $_GET['q'], array('fragment' => "comment-$comment->cid"));
1711   $variables['template_files'][] = 'comment-'. $node->type;
1712   // set status to a string representation of comment->status.
1713   if (isset($comment->preview)) {
1714     $variables['status']  = 'comment-preview';
1715   }
1716   else {
1717     $variables['status']  = ($comment->status == COMMENT_NOT_PUBLISHED) ? 'comment-unpublished' : 'comment-published';
1718   }
1719 }
1720 
1721 /**
1722  * Process variables for comment-folded.tpl.php.
1723  *
1724  * @see comment-folded.tpl.php
1725  * @see theme_comment_folded()
1726  */
1727 function template_preprocess_comment_folded(&$variables) {
1728   $comment = $variables['comment'];
1729   $variables['author'] = theme('username', $comment);
1730   $variables['date']   = format_date($comment->timestamp);
1731   $variables['new']    = $comment->new ? t('new') : '';
1732   $variables['title']  = l($comment->subject, comment_node_url() .'/'. $comment->cid, array('fragment' => "comment-$comment->cid"));
1733 }
1734 
1735 /**
1736  * Theme comment flat collapsed view.
1737  *
1738  * @param $comment
1739  *   The comment to be themed.
1740  * @param $node
1741  *   The comment node.
1742  * @ingroup themeable
1743  */
1744 function theme_comment_flat_collapsed($comment, $node) {
1745   return theme('comment_view', $comment, $node, '', 0);
1746 }
1747 
1748 /**
1749  * Theme comment flat expanded view.
1750  *
1751  * @param $comment
1752  *   The comment to be themed.
1753  * @param $node
1754  *   The comment node.
1755  * @ingroup themeable
1756  */
1757 function theme_comment_flat_expanded($comment, $node) {
1758   return theme('comment_view', $comment, $node, module_invoke_all('link', 'comment', $comment, 0));
1759 }
1760 
1761 /**
1762  * Theme comment thread collapsed view.
1763  *
1764  * @param $comment
1765  *   The comment to be themed.
1766  * @param $node
1767  *   The comment node.
1768  * @ingroup themeable
1769  */
1770 function theme_comment_thread_collapsed($comment, $node) {
1771   return theme('comment_view', $comment, $node, '', 0);
1772 }
1773 
1774 /**
1775  * Theme comment thread expanded view.
1776  *
1777  * @param $comment
1778  *   The comment to be themed.
1779  * @param $node
1780  *   The comment node.
1781  * @ingroup themeable
1782  */
1783 function theme_comment_thread_expanded($comment, $node) {
1784   return theme('comment_view', $comment, $node, module_invoke_all('link', 'comment', $comment, 0));
1785 }
1786 
1787 /**
1788  * Theme a "you can't post comments" notice.
1789  *
1790  * @param $node
1791  *   The comment node.
1792  * @ingroup themeable
1793  */
1794 function theme_comment_post_forbidden($node) {
1795   global $user;
1796   static $authenticated_post_comments;
1797 
1798   if (!$user->uid) {
1799     if (!isset($authenticated_post_comments)) {
1800       // We only output any link if we are certain, that users get permission
1801       // to post comments by logging in. We also locally cache this information.
1802       $authenticated_post_comments = array_key_exists(DRUPAL_AUTHENTICATED_RID, user_roles(TRUE, 'post comments') + user_roles(TRUE, 'post comments without approval'));
1803     }
1804 
1805     if ($authenticated_post_comments) {
1806       // We cannot use drupal_get_destination() because these links
1807       // sometimes appear on /node and taxonomy listing pages.
1808       if (variable_get('comment_form_location_'. $node->type, COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
1809         $destination = 'destination='. drupal_urlencode("comment/reply/$node->nid#comment-form");
1810       }
1811       else {
1812         $destination = 'destination='. drupal_urlencode("node/$node->nid#comment-form");
1813       }
1814 
1815       if (variable_get('user_register', 1)) {
1816         // Users can register themselves.
1817         return t('<a href="@login">Login</a> or <a href="@register">register</a> to post comments', array('@login' => url('user/login', array('query' => $destination)), '@register' => url('user/register', array('query' => $destination))));
1818       }
1819       else {
1820         // Only admins can add new users, no public registration.
1821         return t('<a href="@login">Login</a> to post comments', array('@login' => url('user/login', array('query' => $destination))));
1822       }
1823     }
1824   }
1825 }
1826 
1827 /**
1828  * Process variables for comment-wrapper.tpl.php.
1829  *
1830  * @see comment-wrapper.tpl.php
1831  * @see theme_comment_wrapper()
1832  */
1833 function template_preprocess_comment_wrapper(&$variables) {
1834   // Provide contextual information.
1835   $variables['display_mode']  = _comment_get_display_setting('mode', $variables['node']);
1836   $variables['display_order'] = _comment_get_display_setting('sort', $variables['node']);
1837   $variables['comment_controls_state'] = variable_get('comment_controls_'. $variables['node']->type, COMMENT_CONTROLS_HIDDEN);
1838   $variables['template_files'][] = 'comment-wrapper-'. $variables['node']->type;
1839 }
1840 
1841 /**
1842  * Theme a "Submitted by ..." notice.
1843  *
1844  * @param $comment
1845  *   The comment.
1846  * @ingroup themeable
1847  */
1848 function theme_comment_submitted($comment) {
1849   return t('Submitted by !username on @datetime.',
1850     array(
1851       '!username' => theme('username', $comment),
1852       '@datetime' => format_date($comment->timestamp)
1853     ));
1854 }
1855 
1856 /**
1857  * Return an array of viewing modes for comment listings.
1858  *
1859  * We can't use a global variable array because the locale system
1860  * is not initialized yet when the comment module is loaded.
1861  */
1862 function _comment_get_modes() {
1863   return array(
1864     COMMENT_MODE_FLAT_COLLAPSED => t('Flat list - collapsed'),
1865     COMMENT_MODE_FLAT_EXPANDED => t('Flat list - expanded'),
1866     COMMENT_MODE_THREADED_COLLAPSED => t('Threaded list - collapsed'),
1867     COMMENT_MODE_THREADED_EXPANDED => t('Threaded list - expanded')
1868   );
1869 }
1870 
1871 /**
1872  * Return an array of viewing orders for comment listings.
1873  *
1874  * We can't use a global variable array because the locale system
1875  * is not initialized yet when the comment module is loaded.
1876  */
1877 function _comment_get_orders() {
1878   return array(
1879     COMMENT_ORDER_NEWEST_FIRST => t('Date - newest first'),
1880     COMMENT_ORDER_OLDEST_FIRST => t('Date - oldest first')
1881   );
1882 }
1883 
1884 /**
1885  * Return an array of "comments per page" settings from which the user
1886  * can choose.
1887  */
1888 function _comment_per_page() {
1889   return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300));
1890 }
1891 
1892 /**
1893  * Return a current comment display setting
1894  *
1895  * @param $setting
1896  *   can be one of these: 'mode', 'sort', 'comments_per_page'
1897  * @param $node
1898  *   The comment node in question.
1899  */
1900 function _comment_get_display_setting($setting, $node) {
1901   global $user;
1902 
1903   if (isset($_GET[$setting])) {
1904     $value = $_GET[$setting];
1905   }
1906   else {
1907     // get the setting's site default
1908     switch ($setting) {
1909       case 'mode':
1910         $default = variable_get('comment_default_mode_'. $node->type, COMMENT_MODE_THREADED_EXPANDED);
1911         break;
1912       case 'sort':
1913         $default = variable_get('comment_default_order_'. $node->type, COMMENT_ORDER_NEWEST_FIRST);
1914         break;
1915       case 'comments_per_page':
1916         $default = variable_get('comment_default_per_page_'. $node->type, 50);
1917     }
1918     if (variable_get('comment_controls_'. $node->type, COMMENT_CONTROLS_HIDDEN) == COMMENT_CONTROLS_HIDDEN) {
1919       // if comment controls are disabled use site default
1920       $value = $default;
1921     }
1922     else {
1923       // otherwise use the user's setting if set
1924       if (isset($user->$setting) && $user->$setting) {
1925         $value = $user->$setting;
1926       }
1927       else if (isset($_SESSION['comment_'. $setting]) && $_SESSION['comment_'. $setting]) {
1928         $value = $_SESSION['comment_'. $setting];
1929       }
1930       else {
1931         $value = $default;
1932       }
1933     }
1934   }
1935   return $value;
1936 }
1937 
1938 /**
1939  * Updates the comment statistics for a given node. This should be called any
1940  * time a comment is added, deleted, or updated.
1941  *
1942  * The following fields are contained in the node_comment_statistics table.
1943  * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node.
1944  * - last_comment_name: the name of the anonymous poster for the last comment
1945  * - last_comment_uid: the uid of the poster for the last comment for this node or the node authors uid if no comments exists for the node.
1946  * - comment_count: the total number of approved/published comments on this node.
1947  */
1948 function _comment_update_node_statistics($nid) {
1949   $count = db_result(db_query('SELECT COUNT(cid) FROM {comments} WHERE nid = %d AND status = %d', $nid, COMMENT_PUBLISHED));
1950 
1951   // comments exist
1952   if ($count > 0) {
1953     $last_reply = db_fetch_object(db_query_range('SELECT cid, name, timestamp, uid FROM {comments} WHERE nid = %d AND status = %d ORDER BY cid DESC', $nid, COMMENT_PUBLISHED, 0, 1));
1954     db_query("UPDATE {node_comment_statistics} SET comment_count = %d, last_comment_timestamp = %d, last_comment_name = '%s', last_comment_uid = %d WHERE nid = %d", $count, $last_reply->timestamp, $last_reply->uid ? '' : $last_reply->name, $last_reply->uid, $nid);
1955   }
1956 
1957   // no comments
1958   else {
1959     $node = db_fetch_object(db_query("SELECT uid, created FROM {node} WHERE nid = %d", $nid));
1960     db_query("UPDATE {node_comment_statistics} SET comment_count = 0, last_comment_timestamp = %d, last_comment_name = '', last_comment_uid = %d WHERE nid = %d", $node->created, $node->uid, $nid);
1961   }
1962 }
1963 
1964 /**
1965  * Invoke a hook_comment() operation in all modules.
1966  *
1967  * @param &$comment
1968  *   A comment object.
1969  * @param $op
1970  *   A string containing the name of the comment operation.
1971  * @return
1972  *   The returned value of the invoked hooks.
1973  */
1974 function comment_invoke_comment(&$comment, $op) {
1975   $return = array();
1976   foreach (module_implements('comment') as $name) {
1977     $function = $name .'_comment';
1978     $result = $function($comment, $op);
1979     if (isset($result) && is_array($result)) {
1980       $return = array_merge($return, $result);
1981     }
1982     else if (isset($result)) {
1983       $return[] = $result;
1984     }
1985   }
1986   return $return;
1987 }
1988 
1989 /**
1990  * Generate vancode.
1991  *
1992  * Consists of a leading character indicating length, followed by N digits
1993  * with a numerical value in base 36. Vancodes can be sorted as strings
1994  * without messing up numerical order.
1995  *
1996  * It goes:
1997  * 00, 01, 02, ..., 0y, 0z,
1998  * 110, 111, ... , 1zy, 1zz,
1999  * 2100, 2101, ..., 2zzy, 2zzz,
2000  * 31000, 31001, ...
2001  */
2002 function int2vancode($i = 0) {
2003   $num = base_convert((int)$i, 10, 36);
2004   $length = strlen($num);
2005   return chr($length + ord('0') - 1) . $num;
2006 }
2007 
2008 /**
2009  * Decode vancode back to an integer.
2010  */
2011 function vancode2int($c = '00') {
2012   return base_convert(substr($c, 1), 36, 10);
2013 }
2014 
2015 /**
2016  * Implementation of hook_hook_info().
2017  */
2018 function comment_hook_info() {
2019   return array(
2020     'comment' => array(
2021       'comment' => array(
2022         'insert' => array(
2023           'runs when' => t('After saving a new comment'),
2024         ),
2025         'update' => array(
2026           'runs when' => t('After saving an updated comment'),
2027         ),
2028         'delete' => array(
2029           'runs when' => t('After deleting a comment')
2030         ),
2031         'view' => array(
2032           'runs when' => t('When a comment is being viewed by an authenticated user')
2033         ),
2034       ),
2035     ),
2036   );
2037 }
2038 
2039 /**
2040  * Implementation of hook_action_info().
2041  */
2042 function comment_action_info() {
2043   return array(
2044     'comment_unpublish_action' => array(
20451      'description' => t('Unpublish comment'),
2046       'type' => 'comment',
2047       'configurable' => FALSE,
2048       'hooks' => array(
2049         'comment' => array('insert', 'update'),
2050       )
2051     ),
2052     'comment_unpublish_by_keyword_action' => array(
2053       'description' => t('Unpublish comment containing keyword(s)'),
2054       'type' => 'comment',
2055       'configurable' => TRUE,
2056       'hooks' => array(
2057         'comment' => array('insert', 'update'),
2058       )
2059     )
2060   );
2061 }
2062 
2063 /**
2064  * Drupal action to unpublish a comment.
2065  *
2066  * @param $context
2067  *   Keyed array. Must contain the id of the comment if $comment is not passed.
2068  * @param $comment
2069  *   An optional comment object.
2070  */
2071 function comment_unpublish_action($comment, $context = array()) {
2072   if (isset($comment->cid)) {
2073     $cid = $comment->cid;
2074     $subject = $comment->subject;
2075   }
2076   else {
2077     $cid = $context['cid'];
2078     $subject = db_result(db_query("SELECT subject FROM {comments} WHERE cid = %d", $cid));
2079   }
2080   db_query('UPDATE {comments} SET status = %d WHERE cid = %d', COMMENT_NOT_PUBLISHED, $cid);
2081   watchdog('action', 'Unpublished comment %subject.', array('%subject' => $subject));
2082 }
2083 
2084 /**
2085  * Form builder; Prepare a form for blacklisted keywords.
2086  *
2087  * @ingroup forms
2088  */
2089 function comment_unpublish_by_keyword_action_form($context) {
2090   $form['keywords'] = array(
2091     '#title' => t('Keywords'),
2092     '#type' => 'textarea',
2093     '#description' => t('The comment will be unpublished if it contains any of the character sequences above. Use a comma-separated list of character sequences. Example: funny, bungee jumping, "Company, Inc.". Character sequences are case-sensitive.'),
2094     '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '',
2095   );
2096   return $form;
2097 }
2098 
2099 /**
2100  * Process comment_unpublish_by_keyword_action_form form submissions.
2101  */
2102 function comment_unpublish_by_keyword_action_submit($form, $form_state) {
2103   return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
2104 }
2105 
2106 /**
2107  * Implementation of a configurable Drupal action.
2108  * Unpublish a comment if it contains a certain string.
2109  *
2110  * @param $context
2111  *   An array providing more information about the context of the call to this action.
2112  *   Unused here since this action currently only supports the insert and update ops of
2113  *   the comment hook, both of which provide a complete $comment object.
2114  * @param $comment
2115  *   A comment object.
2116  */
2117 function comment_unpublish_by_keyword_action($comment, $context) {
2118   foreach ($context['keywords'] as $keyword) {
2119     if (strstr($comment->comment, $keyword) || strstr($comment->subject, $keyword)) {
2120       db_query('UPDATE {comments} SET status = %d WHERE cid = %d', COMMENT_NOT_PUBLISHED, $comment->cid);
2121       watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject));
2122       break;
2123     }
2124   }
2125 }