Imported drupal-5.5
[drupal.git] / modules / comment / comment.module
blob2593195ecdd22f8df0bbeeafb33132dff0ce6bfc
1 <?php
2 // $Id: comment.module,v 1.520.2.12 2007/11/07 08:03:30 drumm Exp $
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  */
13 /**
14  * Comment is published.
15  */
16 define('COMMENT_PUBLISHED', 0);
18 /**
19  * Comment is awaiting approval.
20  */
21 define('COMMENT_NOT_PUBLISHED', 1);
23 /**
24  * Comments are displayed in a flat list - collapsed.
25  */
26 define('COMMENT_MODE_FLAT_COLLAPSED', 1);
28 /**
29  * Comments are displayed in a flat list - expanded.
30  */
31 define('COMMENT_MODE_FLAT_EXPANDED', 2);
33 /**
34  * Comments are displayed as a threaded list - collapsed.
35  */
36 define('COMMENT_MODE_THREADED_COLLAPSED', 3);
38 /**
39  * Comments are displayed as a threaded list - expanded.
40  */
41 define('COMMENT_MODE_THREADED_EXPANDED', 4);
43 /**
44  * Comments are ordered by date - newest first.
45  */
46 define('COMMENT_ORDER_NEWEST_FIRST', 1);
48 /**
49  * Comments are ordered by date - oldest first.
50  */
51 define('COMMENT_ORDER_OLDEST_FIRST', 2);
53 /**
54  * Comment controls should be shown above the comment list.
55  */
56 define('COMMENT_CONTROLS_ABOVE', 0);
58 /**
59  * Comment controls should be shown below the comment list.
60  */
61 define('COMMENT_CONTROLS_BELOW', 1);
63 /**
64  * Comment controls should be shown both above and below the comment list.
65  */
66 define('COMMENT_CONTROLS_ABOVE_BELOW', 2);
68 /**
69  * Comment controls are hidden.
70  */
71 define('COMMENT_CONTROLS_HIDDEN', 3);
73 /**
74  * Anonymous posters may not enter their contact information.
75  */
76 define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
78 /**
79  * Anonymous posters may leave their contact information.
80  */
81 define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
83 /**
84  * Anonymous posters must leave their contact information.
85  */
86 define('COMMENT_ANONYMOUS_MUST_CONTACT', 2);
88 /**
89  * Comment form should be displayed on a separate page.
90  */
91 define('COMMENT_FORM_SEPARATE_PAGE', 0);
93 /**
94  * Comment form should be shown below post or list of comments.
95  */
96 define('COMMENT_FORM_BELOW', 1);
98 /**
99  * Comments for this node are disabled.
100  */
101 define('COMMENT_NODE_DISABLED', 0);
104  * Comments for this node are locked.
105  */
106 define('COMMENT_NODE_READ_ONLY', 1);
109  * Comments are enabled on this node.
110  */
111 define('COMMENT_NODE_READ_WRITE', 2);
114  * Comment preview is optional.
115  */
116 define('COMMENT_PREVIEW_OPTIONAL', 0);
119  * Comment preview is required.
120  */
121 define('COMMENT_PREVIEW_REQUIRED', 1);
124  * Implementation of hook_help().
125  */
126 function comment_help($section) {
127   switch ($section) {
128     case 'admin/help#comment':
129       $output = '<p>'. t('The comment module creates a discussion board for each post. Users can post comments to discuss a forum topic, weblog post, story, collaborative book page, etc. The ability to comment is an important part of involving members in a community dialogue.') .'</p>';
130       $output .= '<p>'. t('An administrator can give comment permissions to user groups, and users can (optionally) edit their last comment, assuming no others have been posted since. Attached to each comment board is a control panel for customizing the way that comments are displayed. Users can control the chronological ordering of posts (newest or oldest first) and the number of posts to display on each page. Comments behave like other user submissions. Filters, smileys and HTML that work in nodes will also work with comments. The comment module provides specific features to inform site members when new comments have been posted.') .'</p>';
131       $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="@comment">Comment page</a>.', array('@comment' => 'http://drupal.org/handbook/modules/comment/')) .'</p>';
132       return $output;
133     case 'admin/content/comment':
134     case 'admin/content/comment/new':
135       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>';
136     case 'admin/content/comment/approval':
137       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>';
138     case 'admin/content/comment/settings':
139       return '<p>'. t("Comments can be attached to any node, and their settings are below. The display comes in two types: a 'flat list' where everything is flush to the left side, and comments come in chronological order, and a 'threaded list' where replies to other comments are placed immediately below and slightly indented, forming an outline. They also come in two styles: 'expanded', where you see both the title and the contents, and 'collapsed' where you only see the title. Preview comment forces a user to look at their comment by clicking on a 'Preview' button before they can actually add the comment.") .'</p>';
140    }
144  * Implementation of hook_menu().
145  */
146 function comment_menu($may_cache) {
147   $items = array();
149   if ($may_cache) {
150     $access = user_access('administer comments');
151     $items[] = array(
152       'path' => 'admin/content/comment',
153       'title' => t('Comments'),
154       'description' => t('List and edit site comments and the comment moderation queue.'),
155       'callback' => 'comment_admin',
156       'access' => $access
157     );
159     // Tabs:
160     $items[] = array('path' => 'admin/content/comment/list', 'title' => t('List'),
161       'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
163     // Subtabs:
164     $items[] = array('path' => 'admin/content/comment/list/new', 'title' => t('Published comments'),
165       'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
166     $items[] = array('path' => 'admin/content/comment/list/approval', 'title' => t('Approval queue'),
167       'callback' => 'comment_admin',
168       'callback arguments' => array('approval'),
169       'access' => $access,
170       'type' => MENU_LOCAL_TASK);
172     $items[] = array(
173       'path' => 'admin/content/comment/settings',
174       'title' => t('Settings'),
175       'callback' => 'drupal_get_form',
176       'callback arguments' => array('comment_admin_settings'),
177       'access' => $access,
178       'weight' => 10,
179       'type' => MENU_LOCAL_TASK);
181     $items[] = array('path' => 'comment/delete', 'title' => t('Delete comment'),
182       'callback' => 'comment_delete', 'access' => $access, 'type' => MENU_CALLBACK);
184     $access = user_access('post comments');
185     $items[] = array('path' => 'comment/edit', 'title' => t('Edit comment'),
186       'callback' => 'comment_edit',
187       'access' => $access, 'type' => MENU_CALLBACK);
188   }
189   else {
190     if (arg(0) == 'comment' && arg(1) == 'reply' && is_numeric(arg(2))) {
191       $node = node_load(arg(2));
192       if ($node->nid) {
193         $items[] = array('path' => 'comment/reply', 'title' => t('Reply to comment'),
194           'callback' => 'comment_reply', 'access' => node_access('view', $node), 'type' => MENU_CALLBACK);
195       }
196     }
197     if ((arg(0) == 'node') && is_numeric(arg(1)) && is_numeric(arg(2))) {
198       $items[] = array(
199         'path' => ('node/'. arg(1) .'/'. arg(2)),
200         'title' => t('View'),
201         'callback' => 'node_page_view',
202         'callback arguments' => array(node_load(arg(1)), arg(2)),
203         'type' => MENU_CALLBACK,
204       );
205     }
206   }
208   return $items;
212  * Implementation of hook_perm().
213  */
214 function comment_perm() {
215   return array('access comments', 'post comments', 'administer comments', 'post comments without approval');
219  * Implementation of hook_block().
221  * Generates a block with the most recent comments.
222  */
223 function comment_block($op = 'list', $delta = 0) {
224   if ($op == 'list') {
225     $blocks[0]['info'] = t('Recent comments');
226     return $blocks;
227   }
228   else if ($op == 'view' && user_access('access comments')) {
229     $block['subject'] = t('Recent comments');
230     $block['content'] = theme('comment_block');
231     return $block;
232   }
236  * Find a number of recent comments. This is done in two steps.
237  *   1. Find the n (specified by $number) nodes that have the most recent
238  *      comments.  This is done by querying node_comment_statistics which has
239  *      an index on last_comment_timestamp, and is thus a fast query.
240  *   2. Loading the information from the comments table based on the nids found
241  *      in step 1.
243  * @param $number (optional) The maximum number of comments to find.
244  * @return $comments An array of comment objects each containing a nid,
245  *   subject, cid, and timstamp, or an empty array if there are no recent
246  *   comments visible to the current user.
247  */
248 function comment_get_recent($number = 10) {
249   // Select the $number nodes (visible to the current user) with the most
250   // recent comments. This is efficient due to the index on
251   // last_comment_timestamp.
252   $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);
254   $nids = array();
255   while ($row = db_fetch_object($result)) {
256     $nids[] = $row->nid;
257   }
259   $comments = array();
260   if (!empty($nids)) {
261     // From among the comments on the nodes selected in the first query,
262     // find the $number most recent comments.
263     $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);
264     while ($comment = db_fetch_object($result)) {
265       $comments[] = $comment;
266     }
267   }
269   return $comments;
273  * Returns a formatted list of recent comments to be displayed in the comment
274  * block.
276  * @ingroup themeable
277  */
278 function theme_comment_block() {
279   $items = array();
280   foreach (comment_get_recent() as $comment) {
281     $items[] = l($comment->subject, 'node/'. $comment->nid, NULL, NULL, 'comment-'. $comment->cid) .'<br />'. t('@time ago', array('@time' => format_interval(time() - $comment->timestamp)));
282   }
283   if ($items) {
284     return theme('item_list', $items);
285   }
289  * Implementation of hook_link().
290  */
291 function comment_link($type, $node = NULL, $teaser = FALSE) {
292   $links = array();
294   if ($type == 'node' && $node->comment) {
296     if ($teaser) {
297       // Main page: display the number of comments that have been posted.
299       if (user_access('access comments')) {
300         $all = comment_num_all($node->nid);
302         if ($all) {
303           $links['comment_comments'] = array(
304             'title' => format_plural($all, '1 comment', '@count comments'),
305             'href' => "node/$node->nid",
306             'attributes' => array('title' => t('Jump to the first comment of this posting.')),
307             'fragment' => 'comments'
308           );
310           $new = comment_num_new($node->nid);
312           if ($new) {
313             $links['comment_new_comments'] = array(
314               'title' => format_plural($new, '1 new comment', '@count new comments'),
315               'href' => "node/$node->nid",
316               'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
317               'fragment' => 'new'
318             );
319           }
320         }
321         else {
322           if ($node->comment == COMMENT_NODE_READ_WRITE) {
323             if (user_access('post comments')) {
324               $links['comment_add'] = array(
325                 'title' => t('Add new comment'),
326                 'href' => "comment/reply/$node->nid",
327                 'attributes' => array('title' => t('Add a new comment to this page.')),
328                 'fragment' => 'comment-form'
329               );
330             }
331             else {
332               $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node->nid);
333             }
334           }
335         }
336       }
337     }
338     else {
339       // Node page: add a "post comment" link if the user is allowed to
340       // post comments, if this node is not read-only, and if the comment form isn't already shown
342       if ($node->comment == COMMENT_NODE_READ_WRITE) {
343         if (user_access('post comments')) {
344           if (variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
345             $links['comment_add'] = array(
346               'title' => t('Add new comment'),
347               'href' => "comment/reply/$node->nid",
348               'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
349               'fragment' => 'comment-form'
350             );
351           }
352         }
353         else {
354           $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node->nid);
355         }
356       }
357     }
358   }
360   if ($type == 'comment') {
361     $links = comment_links($node, $teaser);
362   }
363   if (isset($links['comment_forbidden'])) {
364     $links['comment_forbidden']['html'] = TRUE;
365   }
367   return $links;
370 function comment_form_alter($form_id, &$form) {
371   if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
372     $form['workflow']['comment'] = array(
373       '#type' => 'radios',
374       '#title' => t('Default comment setting'),
375       '#default_value' => variable_get('comment_'. $form['#node_type']->type, COMMENT_NODE_READ_WRITE),
376       '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')),
377       '#description' => t('Users with the <em>administer comments</em> permission will be able to override this setting.'),
378     );
379   }
380   elseif (isset($form['type'])) {
381     if ($form['type']['#value'] .'_node_form' == $form_id) {
382       $node = $form['#node'];
383       $form['comment_settings'] = array(
384         '#type' => 'fieldset',
385         '#access' => user_access('administer comments'),
386         '#title' => t('Comment settings'),
387         '#collapsible' => TRUE,
388         '#collapsed' => TRUE,
389         '#weight' => 30,
390       );
391       $form['comment_settings']['comment'] = array(
392         '#type' => 'radios',
393         '#parents' => array('comment'),
394         '#default_value' => $node->comment,
395         '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')),
396       );
397     }
398   }
402  * Implementation of hook_nodeapi().
404  */
405 function comment_nodeapi(&$node, $op, $arg = 0) {
406   switch ($op) {
407     case 'load':
408       return db_fetch_array(db_query("SELECT last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid = %d", $node->nid));
409       break;
411     case 'prepare':
412       if (!isset($node->comment)) {
413         $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
414       }
415       break;
417     case 'insert':
418       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);
419       break;
421     case 'delete':
422       db_query('DELETE FROM {comments} WHERE nid = %d', $node->nid);
423       db_query('DELETE FROM {node_comment_statistics} WHERE nid = %d', $node->nid);
424       break;
426     case 'update index':
427       $text = '';
428       $comments = db_query('SELECT subject, comment, format FROM {comments} WHERE nid = %d AND status = %d', $node->nid, COMMENT_PUBLISHED);
429       while ($comment = db_fetch_object($comments)) {
430         $text .= '<h2>'. check_plain($comment->subject) .'</h2>'. check_markup($comment->comment, $comment->format, FALSE);
431       }
432       return $text;
434     case 'search result':
435       $comments = db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $node->nid));
436       return format_plural($comments, '1 comment', '@count comments');
438     case 'rss item':
439       if ($node->comment != COMMENT_NODE_DISABLED) {
440         return array(array('key' => 'comments', 'value' => url('node/'. $node->nid, NULL, 'comments', TRUE)));
441       }
442       else {
443         return array();
444       }
445   }
449  * Implementation of hook_user().
451  * Provides signature customization for the user's comments.
452  */
453 function comment_user($type, $edit, &$user, $category = NULL) {
454   if ($type == 'form' && $category == 'account') {
455     // when user tries to edit his own data
456     $form['comment_settings'] = array(
457       '#type' => 'fieldset',
458       '#title' => t('Comment settings'),
459       '#collapsible' => TRUE,
460       '#weight' => 4);
461     $form['comment_settings']['signature'] = array(
462       '#type' => 'textarea',
463       '#title' => t('Signature'),
464       '#default_value' => $edit['signature'],
465       '#description' => t('Your signature will be publicly displayed at the end of your comments.'));
467     return $form;
468   }
469   elseif ($type == 'delete') {
470     db_query('UPDATE {comments} SET uid = 0 WHERE uid = %d', $user->uid);
471     db_query('UPDATE {node_comment_statistics} SET last_comment_uid = 0 WHERE last_comment_uid = %d', $user->uid);
472   }
476  * Menu callback; presents the comment settings page.
477  */
478 function comment_admin_settings() {
479   $form['viewing_options'] = array(
480     '#type' => 'fieldset',
481     '#title' => t('Viewing options'),
482     '#collapsible' => TRUE,
483   );
485   $form['viewing_options']['comment_default_mode'] = array(
486     '#type' => 'radios',
487     '#title' => t('Default display mode'),
488     '#default_value' => variable_get('comment_default_mode', COMMENT_MODE_THREADED_EXPANDED),
489     '#options' => _comment_get_modes(),
490     '#description' => t('The default view for comments. Expanded views display the body of the comment. Threaded views keep replies together.'),
491   );
493   $form['viewing_options']['comment_default_order'] = array(
494     '#type' => 'radios',
495     '#title' => t('Default display order'),
496     '#default_value' => variable_get('comment_default_order', COMMENT_ORDER_NEWEST_FIRST),
497     '#options' => _comment_get_orders(),
498     '#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.'),
499   );
501   $form['viewing_options']['comment_default_per_page'] = array(
502     '#type' => 'select',
503     '#title' => t('Default comments per page'),
504     '#default_value' => variable_get('comment_default_per_page', 50),
505     '#options' => _comment_per_page(),
506     '#description' => t('Default number of comments for each page: more comments are distributed in several pages.'),
507   );
509   $form['viewing_options']['comment_controls'] = array(
510     '#type' => 'radios',
511     '#title' => t('Comment controls'),
512     '#default_value' => variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN),
513     '#options' => array(
514       t('Display above the comments'),
515       t('Display below the comments'),
516       t('Display above and below the comments'),
517       t('Do not display')),
518     '#description' => t('Position of the comment controls box. The comment controls let the user change the default display mode and display order of comments.'),
519   );
521   $form['posting_settings'] = array(
522     '#type' => 'fieldset',
523     '#title' => t('Posting settings'),
524     '#collapsible' => TRUE,
525   );
527   $form['posting_settings']['comment_anonymous'] = array(
528     '#type' => 'radios',
529     '#title' => t('Anonymous commenting'),
530     '#default_value' => variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT),
531     '#options' => array(
532       COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
533       COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
534       COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information')),
535     '#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/access', NULL, 'module-comment'))),
536   );
537   if (!user_access('post comments', user_load(array('uid' => 0)))) {
538     $form['posting_settings']['comment_anonymous']['#disabled'] = TRUE;
539   }
541   $form['posting_settings']['comment_subject_field'] = array(
542     '#type' => 'radios',
543     '#title' => t('Comment subject field'),
544     '#default_value' => variable_get('comment_subject_field', 1),
545     '#options' => array(t('Disabled'), t('Enabled')),
546     '#description' => t('Can users provide a unique subject for their comments?'),
547   );
549   $form['posting_settings']['comment_preview'] = array(
550     '#type' => 'radios',
551     '#title' => t('Preview comment'),
552     '#default_value' => variable_get('comment_preview', COMMENT_PREVIEW_REQUIRED),
553     '#options' => array(t('Optional'), t('Required')),
554   );
556   $form['posting_settings']['comment_form_location'] = array(
557     '#type' => 'radios',
558     '#title' => t('Location of comment submission form'),
559     '#default_value' => variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE),
560     '#options' => array(t('Display on separate page'), t('Display below post or comments')),
561   );
563   return system_settings_form($form);
567  * This is *not* a hook_access() implementation. This function is called
568  * to determine whether the current user has access to a particular comment.
570  * Authenticated users can edit their comments as long they have not been
571  * replied to. This prevents people from changing or revising their
572  * statements based on the replies to their posts.
573  */
574 function comment_access($op, $comment) {
575   global $user;
577   if ($op == 'edit') {
578     return ($user->uid && $user->uid == $comment->uid && comment_num_replies($comment->cid) == 0) || user_access('administer comments');
579   }
582 function comment_node_url() {
583   return arg(0) .'/'. arg(1);
586 function comment_edit($cid) {
587   global $user;
589   $comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d', $cid));
590   $comment = drupal_unpack($comment);
591   $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
592   if (comment_access('edit', $comment)) {
593     return comment_form_box((array)$comment);
594   }
595   else {
596     drupal_access_denied();
597   }
601  * This function is responsible for generating a comment reply form.
602  * There are several cases that have to be handled, including:
603  *   - replies to comments
604  *   - replies to nodes
605  *   - attempts to reply to nodes that can no longer accept comments
606  *   - respecting access permissions ('access comments', 'post comments', etc.)
608  * The node or comment that is being replied to must appear above the comment
609  * form to provide the user context while authoring the comment.
611  * @param $nid
612  *   Every comment belongs to a node. This is that node's id.
613  * @param $pid
614  *   Some comments are replies to other comments. In those cases, $pid is the parent
615  *   comment's cid.
617  * @return $output
618  *   The rendered parent node or comment plus the new comment form.
619  */
620 function comment_reply($nid, $pid = NULL) {
621   // Load the parent node.
622   $node = node_load($nid);
624   // Set the breadcrumb trail.
625   menu_set_location(array(array('path' => "node/$nid", 'title' => $node->title), array('path' => "comment/reply/$nid")));
627   $op = isset($_POST['op']) ? $_POST['op'] : '';
629   $output = '';
631   if (user_access('access comments')) {
632     // The user is previewing a comment prior to submitting it.
633     if ($op == t('Preview comment')) {
634       if (user_access('post comments')) {
635         $output .= comment_form_box(array('pid' => $pid, 'nid' => $nid), NULL);
636       }
637       else {
638         drupal_set_message(t('You are not authorized to post comments.'), 'error');
639         drupal_goto("node/$nid");
640       }
641     }
642     else {
643       // $pid indicates that this is a reply to a comment.
644       if ($pid) {
645         // load the comment whose cid = $pid
646         if ($comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.picture, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d AND c.status = %d', $pid, COMMENT_PUBLISHED))) {
647           // If that comment exists, make sure that the current comment and the parent comment both
648           // belong to the same parent node.
649           if ($comment->nid != $nid) {
650             // Attempting to reply to a comment not belonging to the current nid.
651             drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
652             drupal_goto("node/$nid");
653           }
654           // Display the parent comment
655           $comment = drupal_unpack($comment);
656           $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
657           $output .= theme('comment_view', $comment);
658         }
659         else {
660           drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
661           drupal_goto("node/$nid");
662         }
663       }
664       // This is the case where the comment is in response to a node. Display the node.
665       else if (user_access('access content')) {
666         $output .= node_view($node);
667       }
669       // Should we show the reply box?
670       if (node_comment_mode($nid) != COMMENT_NODE_READ_WRITE) {
671         drupal_set_message(t("This discussion is closed: you can't post new comments."), 'error');
672         drupal_goto("node/$nid");
673       }
674       else if (user_access('post comments')) {
675         $output .= comment_form_box(array('pid' => $pid, 'nid' => $nid), t('Reply'));
676       }
677       else {
678         drupal_set_message(t('You are not authorized to post comments.'), 'error');
679         drupal_goto("node/$nid");
680       }
681     }
682   }
683   else {
684     drupal_set_message(t('You are not authorized to view comments.'), 'error');
685     drupal_goto("node/$nid");
686   }
688   return $output;
692  * Accepts a submission of new or changed comment content.
694  * @param $edit
695  *   A comment array.
697  * @return
698  *   If the comment is successfully saved the comment ID is returned. If the comment
699  *   is not saved, FALSE is returned.
700  */
701 function comment_save($edit) {
702   global $user;
703   if (user_access('post comments') && (user_access('administer comments') || node_comment_mode($edit['nid']) == COMMENT_NODE_READ_WRITE)) {
704     if (!form_get_errors()) {
705       if ($edit['cid']) {
706         // Update the comment in the database.
707         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']);
709         _comment_update_node_statistics($edit['nid']);
711         // Allow modules to respond to the updating of a comment.
712         comment_invoke_comment($edit, 'update');
714         // Add an entry to the watchdog log.
715         watchdog('content', t('Comment: updated %subject.', array('%subject' => $edit['subject'])), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], NULL, NULL, 'comment-'. $edit['cid']));
716       }
717       else {
718         // Check for duplicate comments. Note that we have to use the
719         // validated/filtered data to perform such check.
720         $duplicate = db_result(db_query("SELECT COUNT(cid) FROM {comments} WHERE pid = %d AND nid = %d AND subject = '%s' AND comment = '%s'", $edit['pid'], $edit['nid'], $edit['subject'], $edit['comment']), 0);
721         if ($duplicate != 0) {
722           watchdog('content', t('Comment: duplicate %subject.', array('%subject' => $edit['subject'])), WATCHDOG_WARNING);
723         }
725         // Add the comment to database.
726         $edit['status'] = user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
727         $roles = variable_get('comment_roles', array());
728         $score = 0;
730         foreach (array_intersect(array_keys($roles), array_keys($user->roles)) as $rid) {
731           $score = max($roles[$rid], $score);
732         }
734         $users = serialize(array(0 => $score));
736         // Here we are building the thread field. See the documentation for
737         // comment_render().
738         if ($edit['pid'] == 0) {
739           // This is a comment with no parent comment (depth 0): we start
740           // by retrieving the maximum thread level.
741           $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $edit['nid']));
743           // Strip the "/" from the end of the thread.
744           $max = rtrim($max, '/');
746           // Finally, build the thread field for this new comment.
747           $thread = int2vancode(vancode2int($max) + 1) .'/';
748         }
749         else {
750           // This is comment with a parent comment: we increase
751           // the part of the thread value at the proper depth.
753           // Get the parent comment:
754           $parent = _comment_load($edit['pid']);
756           // Strip the "/" from the end of the parent thread.
757           $parent->thread = (string) rtrim((string) $parent->thread, '/');
759           // Get the max value in _this_ thread.
760           $max = db_result(db_query("SELECT MAX(thread) FROM {comments} WHERE thread LIKE '%s.%%' AND nid = %d", $parent->thread, $edit['nid']));
762           if ($max == '') {
763             // First child of this parent.
764             $thread = $parent->thread .'.'. int2vancode(0) .'/';
765           }
766           else {
767             // Strip the "/" at the end of the thread.
768             $max = rtrim($max, '/');
770             // We need to get the value at the correct depth.
771             $parts = explode('.', $max);
772             $parent_depth = count(explode('.', $parent->thread));
773             $last = $parts[$parent_depth];
775             // Finally, build the thread field for this new comment.
776             $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
777           }
778         }
780         $edit['cid'] = db_next_id('{comments}_cid');
781         $edit['timestamp'] = time();
783         if ($edit['uid'] === $user->uid) { // '===' because we want to modify anonymous users too
784           $edit['name'] = $user->name;
785         }
787         db_query("INSERT INTO {comments} (cid, nid, pid, uid, subject, comment, format, hostname, timestamp, status, score, users, thread, name, mail, homepage) VALUES (%d, %d, %d, %d, '%s', '%s', %d, '%s', %d, %d, %d, '%s', '%s', '%s', '%s', '%s')", $edit['cid'], $edit['nid'], $edit['pid'], $edit['uid'], $edit['subject'], $edit['comment'], $edit['format'], $_SERVER['REMOTE_ADDR'], $edit['timestamp'], $edit['status'], $score, $users, $thread, $edit['name'], $edit['mail'], $edit['homepage']);
789         _comment_update_node_statistics($edit['nid']);
791         // Tell the other modules a new comment has been submitted.
792         comment_invoke_comment($edit, 'insert');
794         // Add an entry to the watchdog log.
795         watchdog('content', t('Comment: added %subject.', array('%subject' => $edit['subject'])), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], NULL, NULL, 'comment-'. $edit['cid']));
796       }
798       // Clear the cache so an anonymous user can see his comment being added.
799       cache_clear_all();
801       // Explain the approval queue if necessary, and then
802       // redirect the user to the node he's commenting on.
803       if ($edit['status'] == COMMENT_NOT_PUBLISHED) {
804         drupal_set_message(t('Your comment has been queued for moderation by site administrators and will be published after approval.'));
805       }
806       return $edit['cid'];
807     }
808     else {
809       return FALSE;
810     }
811   }
812   else {
813     $txt = t('Comment: unauthorized comment submitted or comment submitted to a closed node %subject.', array('%subject' => $edit['subject']));
814     watchdog('content', $txt, WATCHDOG_WARNING);
815     drupal_set_message($txt, 'error');
816     return FALSE;
817   }
820 function comment_links($comment, $return = 1) {
821   global $user;
823   $links = array();
825   // If we are viewing just this comment, we link back to the node.
826   if ($return) {
827     $links['comment_parent'] = array(
828       'title' => t('parent'),
829       'href' => comment_node_url(),
830       'fragment' => "comment-$comment->cid"
831     );
832   }
834   if (node_comment_mode($comment->nid) == COMMENT_NODE_READ_WRITE) {
835     if (user_access('administer comments') && user_access('post comments')) {
836       $links['comment_delete'] = array(
837         'title' => t('delete'),
838         'href' => "comment/delete/$comment->cid"
839       );
840       $links['comment_edit'] = array(
841         'title' => t('edit'),
842         'href' => "comment/edit/$comment->cid"
843       );
844       $links['comment_reply'] = array(
845         'title' => t('reply'),
846         'href' => "comment/reply/$comment->nid/$comment->cid"
847       );
848     }
849     else if (user_access('post comments')) {
850       if (comment_access('edit', $comment)) {
851         $links['comment_edit'] = array(
852           'title' => t('edit'),
853           'href' => "comment/edit/$comment->cid"
854         );
855       }
856       $links['comment_reply'] = array(
857         'title' => t('reply'),
858         'href' => "comment/reply/$comment->nid/$comment->cid"
859       );
860     }
861     else {
862       $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $comment->nid);
863     }
864   }
866   return $links;
870  * Renders comment(s).
872  * @param $node
873  *   The node which comment(s) needs rendering.
874  * @param $cid
875  *   Optional, if given, only one comment is rendered.
877  * To display threaded comments in the correct order we keep a 'thread' field
878  * and order by that value. This field keeps this data in
879  * a way which is easy to update and convenient to use.
881  * A "thread" value starts at "1". If we add a child (A) to this comment,
882  * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
883  * brother of (A) will get "1.2". Next brother of the parent of (A) will get
884  * "2" and so on.
886  * First of all note that the thread field stores the depth of the comment:
887  * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
889  * Now to get the ordering right, consider this example:
891  * 1
892  * 1.1
893  * 1.1.1
894  * 1.2
895  * 2
897  * If we "ORDER BY thread ASC" we get the above result, and this is the
898  * natural order sorted by time. However, if we "ORDER BY thread DESC"
899  * we get:
901  * 2
902  * 1.2
903  * 1.1.1
904  * 1.1
905  * 1
907  * Clearly, this is not a natural way to see a thread, and users will get
908  * confused. The natural order to show a thread by time desc would be:
910  * 2
911  * 1
912  * 1.2
913  * 1.1
914  * 1.1.1
916  * which is what we already did before the standard pager patch. To achieve
917  * this we simply add a "/" at the end of each "thread" value. This way out
918  * thread fields will look like depicted below:
920  * 1/
921  * 1.1/
922  * 1.1.1/
923  * 1.2/
924  * 2/
926  * we add "/" since this char is, in ASCII, higher than every number, so if
927  * now we "ORDER BY thread DESC" we get the correct order. However this would
928  * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
929  * to consider the trailing "/" so we use a substring only.
930  */
931 function comment_render($node, $cid = 0) {
932   global $user;
934   $output = '';
936   if (user_access('access comments')) {
937     // Pre-process variables.
938     $nid = $node->nid;
939     if (empty($nid)) {
940       $nid = 0;
941     }
943     $mode = _comment_get_display_setting('mode');
944     $order = _comment_get_display_setting('sort');
945     $comments_per_page = _comment_get_display_setting('comments_per_page');
947     if ($cid) {
948       // Single comment view.
949       $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.picture, u.data, c.score, c.users, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d';
950       $query_args = array($cid);
951       if (!user_access('administer comments')) {
952         $query .= ' AND c.status = %d';
953         $query_args[] = COMMENT_PUBLISHED;
954       }
956       $result = db_query($query, $query_args);
958       if ($comment = db_fetch_object($result)) {
959         $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
960         $links = module_invoke_all('link', 'comment', $comment, 1);
962         foreach (module_implements('link_alter') as $module) {
963           $function = $module .'_link_alter';
964           $function($node, $links);
965         }
967         $output .= theme('comment_view', $comment, $links);
968       }
969     }
970     else {
971       // Multiple comment view
972       $query_count = 'SELECT COUNT(*) FROM {comments} WHERE nid = %d';
973       $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.picture, u.data, c.score, c.users, c.thread, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.nid = %d';
975       $query_args = array($nid);
976       if (!user_access('administer comments')) {
977         $query .= ' AND c.status = %d';
978         $query_count .= ' AND status = %d';
979         $query_args[] = COMMENT_PUBLISHED;
980       }
982       if ($order == COMMENT_ORDER_NEWEST_FIRST) {
983         if ($mode == COMMENT_MODE_FLAT_COLLAPSED || $mode == COMMENT_MODE_FLAT_EXPANDED) {
984           $query .= ' ORDER BY c.cid DESC';
985         }
986         else {
987           $query .= ' ORDER BY c.thread DESC';
988         }
989       }
990       else if ($order == COMMENT_ORDER_OLDEST_FIRST) {
991         if ($mode == COMMENT_MODE_FLAT_COLLAPSED || $mode == COMMENT_MODE_FLAT_EXPANDED) {
992           $query .= ' ORDER BY c.cid';
993         }
994         else {
996           /*
997           ** See comment above. Analysis learns that this doesn't cost
998           ** too much. It scales much much better than having the whole
999           ** comment structure.
1000           */
1002           $query .= ' ORDER BY SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))';
1003         }
1004       }
1006       // Start a form, for use with comment control.
1007       $result = pager_query($query, $comments_per_page, 0, $query_count, $query_args);
1008       if (db_num_rows($result) && (variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN) == COMMENT_CONTROLS_ABOVE || variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN) == COMMENT_CONTROLS_ABOVE_BELOW)) {
1009         $output .= drupal_get_form('comment_controls', $mode, $order, $comments_per_page);
1010       }
1012       $divs = 0;
1013       $last_depth = 0;
1014       drupal_add_css(drupal_get_path('module', 'comment') .'/comment.css');
1015       while ($comment = db_fetch_object($result)) {
1016         $comment = drupal_unpack($comment);
1017         $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1018         $comment->depth = count(explode('.', $comment->thread)) - 1;
1020         if ($mode == COMMENT_MODE_THREADED_COLLAPSED || $mode == COMMENT_MODE_THREADED_EXPANDED) {
1021           if ($comment->depth > $last_depth) {
1022             $divs++;
1023             $output .= '<div class="indented">';
1024             $last_depth++;
1025           }
1026           else {
1027             while ($comment->depth < $last_depth) {
1028               $divs--;
1029               $output .= '</div>';
1030               $last_depth--;
1031             }
1032           }
1033         }
1035         if ($mode == COMMENT_MODE_FLAT_COLLAPSED) {
1036           $output .= theme('comment_flat_collapsed', $comment);
1037         }
1038         else if ($mode == COMMENT_MODE_FLAT_EXPANDED) {
1039           $output .= theme('comment_flat_expanded', $comment);
1040         }
1041         else if ($mode == COMMENT_MODE_THREADED_COLLAPSED) {
1042           $output .= theme('comment_thread_collapsed', $comment);
1043         }
1044         else if ($mode == COMMENT_MODE_THREADED_EXPANDED) {
1045           $output .= theme('comment_thread_expanded', $comment);
1046         }
1047       }
1048       for ($i = 0; $i < $divs; $i++) {
1049         $output .= '</div>';
1050       }
1051       $output .= theme('pager', NULL, $comments_per_page, 0);
1053       if (db_num_rows($result) && (variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN) == COMMENT_CONTROLS_BELOW || variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN) == COMMENT_CONTROLS_ABOVE_BELOW)) {
1054         $output .= drupal_get_form('comment_controls', $mode, $order, $comments_per_page);
1055       }
1056     }
1058     // If enabled, show new comment form if it's not already being displayed.
1059     $reply = arg(0) == 'comment' && arg(1) == 'reply';
1060     if (user_access('post comments') && node_comment_mode($nid) == COMMENT_NODE_READ_WRITE && (variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_BELOW) && !$reply) {
1061       $output .= comment_form_box(array('nid' => $nid), t('Post new comment'));
1062     }
1064     $output = theme('comment_wrapper', $output);
1065   }
1067   return $output;
1071  * Menu callback; delete a comment.
1072  */
1073 function comment_delete($cid = NULL) {
1074   $comment = db_fetch_object(db_query('SELECT c.*, u.name AS registered_name, u.uid FROM {comments} c INNER JOIN {users} u ON u.uid = c.uid WHERE c.cid = %d', $cid));
1075   $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1077   $output = '';
1079   if (is_object($comment) && is_numeric($comment->cid)) {
1080     $output = drupal_get_form('comment_confirm_delete', $comment);
1081   }
1082   else {
1083     drupal_set_message(t('The comment no longer exists.'));
1084   }
1086   return $output;
1089 function comment_confirm_delete($comment) {
1091   $form = array();
1092   $form['comment'] = array(
1093     '#type' => 'value',
1094     '#value' => $comment,
1095   );
1097   return confirm_form(
1098     $form,
1099     t('Are you sure you want to delete the comment %title?', array('%title' => $comment->subject)),
1100     'node/'. $comment->nid,
1101     t('Any replies to this comment will be lost. This action cannot be undone.'),
1102     t('Delete'),
1103     t('Cancel'));
1106 function comment_confirm_delete_submit($form_id, $form_values) {
1107   $comment = $form_values['comment'];
1109   // Delete comment and its replies.
1110   _comment_delete_thread($comment);
1111   _comment_update_node_statistics($comment->nid);
1112  // Clear the cache so an anonymous user sees that his comment was deleted.
1113   cache_clear_all();
1115   drupal_set_message(t('The comment and all its replies have been deleted.'));
1117   return "node/$comment->nid";
1122  * Comment operations. We offer different update operations depending on
1123  * which comment administration page we're on.
1124  */
1125 function comment_operations($action = NULL) {
1126   if ($action == 'publish') {
1127     $operations = array(
1128       'publish' => array(t('Publish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_PUBLISHED .' WHERE cid = %d'),
1129       'delete' => array(t('Delete the selected comments'), '')
1130     );
1131   }
1132   else if ($action == 'unpublish') {
1133     $operations = array(
1134       'unpublish' => array(t('Unpublish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_NOT_PUBLISHED .' WHERE cid = %d'),
1135       'delete' => array(t('Delete the selected comments'), '')
1136     );
1137   }
1138   else {
1139     $operations = array(
1140       'publish' => array(t('Publish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_PUBLISHED .' WHERE cid = %d'),
1141       'unpublish' => array(t('Unpublish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_NOT_PUBLISHED .' WHERE cid = %d'),
1142       'delete' => array(t('Delete the selected comments'), '')
1143     );
1144   }
1145   return $operations;
1149  * Menu callback; present an administrative comment listing.
1150  */
1151 function comment_admin($type = 'new') {
1152   $edit = $_POST;
1154   if ($edit['operation'] == 'delete' && $edit['comments']) {
1155     return drupal_get_form('comment_multiple_delete_confirm');
1156   }
1157   else {
1158     return drupal_get_form('comment_admin_overview', $type, arg(4));
1159   }
1162 function comment_admin_overview($type = 'new', $arg) {
1163   // build an 'Update options' form
1164   $form['options'] = array(
1165     '#type' => 'fieldset', '#title' => t('Update options'),
1166     '#prefix' => '<div class="container-inline">', '#suffix' => '</div>'
1167   );
1168   $options = array();
1169   foreach (comment_operations($arg == 'approval' ? 'publish' : 'unpublish') as $key => $value) {
1170     $options[$key] = $value[0];
1171   }
1172   $form['options']['operation'] = array('#type' => 'select', '#options' => $options, '#default_value' => 'publish');
1173   $form['options']['submit'] = array('#type' => 'submit', '#value' => t('Update'));
1175   // load the comments that we want to display
1176   $status = ($type == 'approval') ? COMMENT_NOT_PUBLISHED : COMMENT_PUBLISHED;
1177   $form['header'] = array('#type' => 'value', '#value' => array(
1178     theme('table_select_header_cell'),
1179     array('data' => t('Subject'), 'field' => 'subject'),
1180     array('data' => t('Author'), 'field' => 'name'),
1181     array('data' => t('Time'), 'field' => 'timestamp', 'sort' => 'desc'),
1182     array('data' => t('Operations'))
1183   ));
1184   $result = pager_query('SELECT c.subject, c.nid, c.cid, c.comment, c.timestamp, c.status, c.name, c.homepage, u.name AS registered_name, u.uid FROM {comments} c INNER JOIN {users} u ON u.uid = c.uid WHERE c.status = %d'. tablesort_sql($form['header']['#value']), 50, 0, NULL, $status);
1186   // build a table listing the appropriate comments
1187   $destination = drupal_get_destination();
1188   while ($comment = db_fetch_object($result)) {
1189     $comments[$comment->cid] = '';
1190     $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1191     $form['subject'][$comment->cid] = array('#value' => l($comment->subject, 'node/'. $comment->nid, array('title' => truncate_utf8($comment->comment, 128)), NULL, 'comment-'. $comment->cid));
1192     $form['username'][$comment->cid] = array('#value' => theme('username', $comment));
1193     $form['timestamp'][$comment->cid] = array('#value' => format_date($comment->timestamp, 'small'));
1194     $form['operations'][$comment->cid] = array('#value' => l(t('edit'), 'comment/edit/'. $comment->cid, array(), $destination));
1195   }
1196   $form['comments'] = array('#type' => 'checkboxes', '#options' => $comments);
1197   $form['pager'] = array('#value' => theme('pager', NULL, 50, 0));
1198   return $form;
1202  * We can't execute any 'Update options' if no comments were selected.
1203  */
1204 function comment_admin_overview_validate($form_id, $form_values) {
1205   $form_values['comments'] = array_diff($form_values['comments'], array(0));
1206   if (count($form_values['comments']) == 0) {
1207     form_set_error('', t('Please select one or more comments to perform the update on.'));
1208     drupal_goto('admin/content/comment');
1209   }
1213  * Execute the chosen 'Update option' on the selected comments, such as
1214  * publishing, unpublishing or deleting.
1215  */
1216 function comment_admin_overview_submit($form_id, $form_values) {
1217   $operations = comment_operations();
1218   if ($operations[$form_values['operation']][1]) {
1219     // extract the appropriate database query operation
1220     $query = $operations[$form_values['operation']][1];
1221     foreach ($form_values['comments'] as $cid => $value) {
1222       if ($value) {
1223         // perform the update action, then refresh node statistics
1224         db_query($query, $cid);
1225         $comment = _comment_load($cid);
1226         _comment_update_node_statistics($comment->nid);
1227         // Allow modules to respond to the updating of a comment.
1228         comment_invoke_comment($comment, $form_values['operation']);
1229         // Add an entry to the watchdog log.
1230         watchdog('content', t('Comment: updated %subject.', array('%subject' => $comment->subject)), WATCHDOG_NOTICE, l(t('view'), 'node/'. $comment->nid, NULL, NULL, 'comment-'. $comment->cid));
1231       }
1232     }
1233     cache_clear_all();
1234     drupal_set_message(t('The update has been performed.'));
1235     return 'admin/content/comment';
1236   }
1239 function theme_comment_admin_overview($form) {
1240   $output = drupal_render($form['options']);
1241   if (isset($form['subject']) && is_array($form['subject'])) {
1242     foreach (element_children($form['subject']) as $key) {
1243       $row = array();
1244       $row[] = drupal_render($form['comments'][$key]);
1245       $row[] = drupal_render($form['subject'][$key]);
1246       $row[] = drupal_render($form['username'][$key]);
1247       $row[] = drupal_render($form['timestamp'][$key]);
1248       $row[] = drupal_render($form['operations'][$key]);
1249       $rows[] = $row;
1250     }
1251   }
1252   else {
1253     $rows[] = array(array('data' => t('No comments available.'), 'colspan' => '6'));
1254   }
1256   $output .= theme('table', $form['header']['#value'], $rows);
1257   if ($form['pager']['#value']) {
1258     $output .= drupal_render($form['pager']);
1259   }
1261   $output .= drupal_render($form);
1263   return $output;
1267  * List the selected comments and verify that the admin really wants to delete
1268  * them.
1269  */
1270 function comment_multiple_delete_confirm() {
1271   $edit = $_POST;
1273   $form['comments'] = array('#prefix' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE);
1274   // array_filter() returns only elements with actual values
1275   $comment_counter = 0;
1276   foreach (array_filter($edit['comments']) as $cid => $value) {
1277     $comment = _comment_load($cid);
1278     if (is_object($comment) && is_numeric($comment->cid)) {
1279       $subject = db_result(db_query('SELECT subject FROM {comments} WHERE cid = %d', $cid));
1280       $form['comments'][$cid] = array('#type' => 'hidden', '#value' => $cid, '#prefix' => '<li>', '#suffix' => check_plain($subject) .'</li>');
1281       $comment_counter++;
1282     }
1283   }
1284   $form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
1286   if (!$comment_counter) {
1287     drupal_set_message(t('There do not appear to be any comments to delete or your selected comment was deleted by another administrator.'));
1288     drupal_goto('admin/content/comment');
1289   }
1290   else {
1291     return confirm_form($form,
1292                         t('Are you sure you want to delete these comments and all their children?'),
1293                         'admin/content/comment', t('This action cannot be undone.'),
1294                         t('Delete comments'), t('Cancel'));
1295   }
1299  * Perform the actual comment deletion.
1300  */
1301 function comment_multiple_delete_confirm_submit($form_id, $form_values) {
1302   if ($form_values['confirm']) {
1303     foreach ($form_values['comments'] as $cid => $value) {
1304       $comment = _comment_load($cid);
1305       _comment_delete_thread($comment);
1306       _comment_update_node_statistics($comment->nid);
1307     }
1308     cache_clear_all();
1309     drupal_set_message(t('The comments have been deleted.'));
1310   }
1311   drupal_goto('admin/content/comment');
1315 *** misc functions: helpers, privates, history
1319  * Load the entire comment by cid.
1320  */
1321 function _comment_load($cid) {
1322   return db_fetch_object(db_query('SELECT * FROM {comments} WHERE cid = %d', $cid));
1325 function comment_num_all($nid) {
1326   static $cache;
1328   if (!isset($cache[$nid])) {
1329     $cache[$nid] = db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $nid));
1330   }
1331   return $cache[$nid];
1334 function comment_num_replies($pid) {
1335   static $cache;
1337   if (!isset($cache[$pid])) {
1338     $cache[$pid] = db_result(db_query('SELECT COUNT(cid) FROM {comments} WHERE pid = %d AND status = %d', $pid, COMMENT_PUBLISHED));
1339   }
1341   return $cache[$pid];
1345  * get number of new comments for current user and specified node
1347  * @param $nid node-id to count comments for
1348  * @param $timestamp time to count from (defaults to time of last user access
1349  *   to node)
1350  */
1351 function comment_num_new($nid, $timestamp = 0) {
1352   global $user;
1354   if ($user->uid) {
1355     // Retrieve the timestamp at which the current user last viewed the
1356     // specified node.
1357     if (!$timestamp) {
1358       $timestamp = node_last_viewed($nid);
1359     }
1360     $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT);
1362     // Use the timestamp to retrieve the number of new comments.
1363     $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));
1365     return $result;
1366   }
1367   else {
1368     return 0;
1369   }
1373 function comment_validate($edit) {
1374   global $user;
1376   // Invoke other validation handlers
1377   comment_invoke_comment($edit, 'validate');
1379   if (isset($edit['date'])) {
1380     // As of PHP 5.1.0, strtotime returns FALSE upon failure instead of -1.
1381     if (strtotime($edit['date']) <= 0) {
1382       form_set_error('date', t('You have to specify a valid date.'));
1383     }
1384   }
1385   if (isset($edit['author']) && !$account = user_load(array('name' => $edit['author']))) {
1386     form_set_error('author', t('You have to specify a valid author.'));
1387   }
1389   // Check validity of name, mail and homepage (if given)
1390   if (!$user->uid || isset($edit['is_anonymous'])) {
1391     if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) > COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
1392       if ($edit['name']) {
1393         $taken = db_result(db_query("SELECT COUNT(uid) FROM {users} WHERE LOWER(name) = '%s'", $edit['name']), 0);
1395         if ($taken != 0) {
1396           form_set_error('name', t('The name you used belongs to a registered user.'));
1397         }
1399       }
1400       else if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
1401         form_set_error('name', t('You have to leave your name.'));
1402       }
1404       if ($edit['mail']) {
1405         if (!valid_email_address($edit['mail'])) {
1406           form_set_error('mail', t('The e-mail address you specified is not valid.'));
1407         }
1408       }
1409       else if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
1410         form_set_error('mail', t('You have to leave an e-mail address.'));
1411       }
1413       if ($edit['homepage']) {
1414         if (!valid_url($edit['homepage'], TRUE)) {
1415           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>.'));
1416         }
1417       }
1418     }
1419   }
1421   return $edit;
1425 ** Generate the basic commenting form, for appending to a node or display on a separate page.
1426 ** This is rendered by theme_comment_form.
1429 function comment_form($edit, $title = NULL) {
1430   global $user;
1432   $op = isset($_POST['op']) ? $_POST['op'] : '';
1434   if ($user->uid) {
1435     if ($edit['cid'] && user_access('administer comments')) {
1436       if ($edit['author']) {
1437         $author = $edit['author'];
1438       }
1439       elseif ($edit['name']) {
1440         $author = $edit['name'];
1441       }
1442       else {
1443         $author = $edit['registered_name'];
1444       }
1446       if ($edit['status']) {
1447         $status = $edit['status'];
1448       }
1449       else {
1450         $status = 0;
1451       }
1453       if ($edit['date']) {
1454         $date = $edit['date'];
1455       }
1456       else {
1457         $date = format_date($edit['timestamp'], 'custom', 'Y-m-d H:i O');
1458       }
1460       $form['admin'] = array(
1461         '#type' => 'fieldset',
1462         '#title' => t('Administration'),
1463         '#collapsible' => TRUE,
1464         '#collapsed' => TRUE,
1465         '#weight' => -2,
1466       );
1468       if ($edit['registered_name'] != '') {
1469         // The comment is by a registered user
1470         $form['admin']['author'] = array(
1471           '#type' => 'textfield',
1472           '#title' => t('Authored by'),
1473           '#size' => 30,
1474           '#maxlength' => 60,
1475           '#autocomplete_path' => 'user/autocomplete',
1476           '#default_value' => $author,
1477           '#weight' => -1,
1478         );
1479       }
1480       else {
1481         // The comment is by an anonymous user
1482         $form['is_anonymous'] = array(
1483           '#type' => 'value',
1484           '#value' => TRUE,
1485         );
1486         $form['admin']['name'] = array(
1487           '#type' => 'textfield',
1488           '#title' => t('Authored by'),
1489           '#size' => 30,
1490           '#maxlength' => 60,
1491           '#default_value' => $author,
1492           '#weight' => -1,
1493         );
1494         $form['admin']['mail'] = array(
1495           '#type' => 'textfield',
1496           '#title' => t('E-mail'),
1497           '#maxlength' => 64,
1498           '#size' => 30,
1499           '#default_value' => $edit['mail'],
1500           '#description' => t('The content of this field is kept private and will not be shown publicly.'),
1501         );
1503         $form['admin']['homepage'] = array(
1504           '#type' => 'textfield',
1505           '#title' => t('Homepage'),
1506           '#maxlength' => 255,
1507           '#size' => 30,
1508           '#default_value' => $edit['homepage'],
1509         );
1510       }
1512       $form['admin']['date'] = array('#type' => 'textfield', '#parents' => array('date'), '#title' => t('Authored on'), '#size' => 20, '#maxlength' => 25, '#default_value' => $date, '#weight' => -1);
1514       $form['admin']['status'] = array('#type' => 'radios', '#parents' => array('status'), '#title' => t('Status'), '#default_value' =>  $status, '#options' => array(t('Published'), t('Not published')), '#weight' => -1);
1516     }
1517     else {
1518       $form['_author'] = array('#type' => 'item', '#title' => t('Your name'), '#value' => theme('username', $user)
1519       );
1520       $form['author'] = array('#type' => 'value', '#value' => $user->name);
1521     }
1522   }
1523   else if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MAY_CONTACT) {
1524     $form['name'] = array('#type' => 'textfield', '#title' => t('Your name'), '#maxlength' => 60, '#size' => 30, '#default_value' => $edit['name'] ? $edit['name'] : variable_get('anonymous', t('Anonymous'))
1525     );
1527     $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.')
1528     );
1530     $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
1531   }
1532   else if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
1533     $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);
1535     $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);
1537     $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
1538   }
1540   if (variable_get('comment_subject_field', 1) == 1) {
1541     $form['subject'] = array('#type' => 'textfield', '#title' => t('Subject'), '#maxlength' => 64, '#default_value' => $edit['subject']);
1542   }
1544   $form['comment_filter']['comment'] = array('#type' => 'textarea', '#title' => t('Comment'), '#rows' => 15, '#default_value' => $edit['comment'] ? $edit['comment'] : $user->signature, '#required' => TRUE);
1545   if (!isset($edit['format'])) {
1546     $edit['format'] = FILTER_FORMAT_DEFAULT;
1547   }
1548   $form['comment_filter']['format'] = filter_form($edit['format']);
1550   $form['cid'] = array('#type' => 'value', '#value' => $edit['cid']);
1551   $form['pid'] = array('#type' => 'value', '#value' => $edit['pid']);
1552   $form['nid'] = array('#type' => 'value', '#value' => $edit['nid']);
1553   $form['uid'] = array('#type' => 'value', '#value' => $edit['uid']);
1555   $form['preview'] = array('#type' => 'button', '#value' => t('Preview comment'), '#weight' => 19);
1556   $form['#token'] = 'comment'. $edit['nid'] . $edit['pid'];
1558   // Only show post button if preview is optional or if we are in preview mode.
1559   // We show the post button in preview mode even if there are form errors so that
1560   // optional form elements (e.g., captcha) can be updated in preview mode.
1561   if (!form_get_errors() && ((variable_get('comment_preview', COMMENT_PREVIEW_REQUIRED) == COMMENT_PREVIEW_OPTIONAL) || ($op == t('Preview comment')) || ($op == t('Post comment')))) {
1562     $form['submit'] = array('#type' => 'submit', '#value' => t('Post comment'), '#weight' => 20);
1563   }
1565   if ($op == t('Preview comment')) {
1566     $form['#after_build'] = array('comment_form_add_preview');
1567   }
1569   if (empty($edit['cid']) && empty($edit['pid'])) {
1570     $form['#action'] = url('comment/reply/'. $edit['nid']);
1571   }
1573   // Graft in extra form additions
1574   $form = array_merge($form, comment_invoke_comment($form, 'form'));
1575   return $form;
1578 function comment_form_box($edit, $title = NULL) {
1579   return theme('box', $title, drupal_get_form('comment_form', $edit, $title));
1582 function comment_form_add_preview($form, $edit) {
1583   global $user;
1585   drupal_set_title(t('Preview comment'));
1587   $output = '';
1589   // Invoke full validation for the form, to protect against cross site
1590   // request forgeries (CSRF) and setting arbitrary values for fields such as
1591   // the input format. Preview the comment only when form validation does not
1592   // set any errors.
1593   drupal_validate_form($form['form_id']['#value'], $form);
1594   if (!form_get_errors()) {
1595     $comment = (object)_comment_form_submit($edit);
1597     // Attach the user and time information.
1598     if ($edit['author']) {
1599       $account = user_load(array('name' => $edit['author']));
1600     }
1601     elseif ($user->uid && !isset($edit['is_anonymous'])) {
1602       $account = $user;
1603     }
1604     if ($account) {
1605       $comment->uid = $account->uid;
1606       $comment->name = check_plain($account->name);
1607     }
1608     $comment->timestamp = $edit['timestamp'] ? $edit['timestamp'] : time();
1609     $output .= theme('comment_view', $comment);
1610   }
1611   $form['comment_preview'] = array(
1612     '#value' => $output,
1613     '#weight' => -100,
1614     '#prefix' => '<div class="preview">',
1615     '#suffix' => '</div>',
1616   );
1618   $output = '';
1620   if ($edit['pid']) {
1621     $comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, 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));
1622     $comment = drupal_unpack($comment);
1623     $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1624     $output .= theme('comment_view', $comment);
1625   }
1626   else {
1627     $suffix = empty($form['#suffix']) ? '' : $form['#suffix'];
1628     $form['#suffix'] = $suffix . node_view(node_load($edit['nid']));
1629     $edit['pid'] = 0;
1630   }
1632   $form['comment_preview_below'] = array('#value' => $output, '#weight' => 100);
1634   return $form;
1637 function comment_form_validate($form_id, $form_values) {
1638   comment_validate($form_values);
1641 function _comment_form_submit($form_values) {
1642   if (!isset($form_values['date'])) {
1643     $form_values['date'] = 'now';
1644   }
1645   $form_values['timestamp'] = strtotime($form_values['date']);
1646   if (isset($form_values['author'])) {
1647     $account = user_load(array('name' => $form_values['author']));
1648     $form_values['uid'] = $account->uid;
1649     $form_values['name'] = $form_values['author'];
1650   }
1651   // Validate the comment's subject. If not specified, extract
1652   // one from the comment's body.
1653   if (trim($form_values['subject']) == '') {
1654     // The body may be in any format, so we:
1655     // 1) Filter it into HTML
1656     // 2) Strip out all HTML tags
1657     // 3) Convert entities back to plain-text.
1658     // Note: format is checked by check_markup().
1659     $form_values['subject'] = trim(truncate_utf8(decode_entities(strip_tags(check_markup($form_values['comment'], $form_values['format']))), 29, TRUE));
1660     // Edge cases where the comment body is populated only by HTML tags will
1661     // require a default subject.
1662     if ($form_values['subject'] == '') {
1663       $form_values['subject'] = t('(No subject)');
1664     }
1665   }
1667   return $form_values;
1670 function comment_form_submit($form_id, $form_values) {
1671   $form_values = _comment_form_submit($form_values);
1672   if ($cid = comment_save($form_values)) {
1673     return array('node/'. $form_values['nid'], NULL, "comment-$cid");
1674   }
1678 ** Renderer or visualization functions this can be optionally
1679 ** overridden by themes.
1682 function theme_comment_preview($comment, $links = array(), $visible = 1) {
1683   $output = '<div class="preview">';
1684   $output .= theme('comment_view', $comment, $links, $visible);
1685   $output .= '</div>';
1686   return $output;
1689 function theme_comment_view($comment, $links = array(), $visible = 1) {
1690   static $first_new = TRUE;
1692   $output = '';
1693   $comment->new = node_mark($comment->nid, $comment->timestamp);
1694   if ($first_new && $comment->new != MARK_READ) {
1695     // Assign the anchor only for the first new comment. This avoids duplicate
1696     // id attributes on a page.
1697     $first_new = FALSE;
1698     $output .= "<a id=\"new\"></a>\n";
1699   }
1701   $output .= "<a id=\"comment-$comment->cid\"></a>\n";
1703   // Switch to folded/unfolded view of the comment
1704   if ($visible) {
1705     $comment->comment = check_markup($comment->comment, $comment->format, FALSE);
1707     // Comment API hook
1708     comment_invoke_comment($comment, 'view');
1710     $output .= theme('comment', $comment, $links);
1711   }
1712   else {
1713     $output .= theme('comment_folded', $comment);
1714   }
1716   return $output;
1719 function comment_controls($mode = COMMENT_MODE_THREADED_EXPANDED, $order = COMMENT_ORDER_NEWEST_FIRST, $comments_per_page = 50) {
1720   $form['mode'] = array('#type' => 'select',
1721     '#default_value' => $mode,
1722     '#options' => _comment_get_modes(),
1723     '#weight' => 1,
1724   );
1725   $form['order'] = array(
1726     '#type' => 'select',
1727     '#default_value' => $order,
1728     '#options' => _comment_get_orders(),
1729     '#weight' => 2,
1730   );
1731   foreach (_comment_per_page() as $i) {
1732     $options[$i] = t('!a comments per page', array('!a' => $i));
1733   }
1734   $form['comments_per_page'] = array('#type' => 'select',
1735     '#default_value' => $comments_per_page,
1736     '#options' => $options,
1737     '#weight' => 3,
1738   );
1740   $form['submit'] = array('#type' => 'submit',
1741     '#value' => t('Save settings'),
1742     '#weight' => 20,
1743   );
1745   return $form;
1748 function theme_comment_controls($form) {
1749   $output .= '<div class="container-inline">';
1750   $output .=  drupal_render($form);
1751   $output .= '</div>';
1752   $output .= '<div class="description">'. t('Select your preferred way to display the comments and click "Save settings" to activate your changes.') .'</div>';
1753   return theme('box', t('Comment viewing options'), $output);
1756 function comment_controls_submit($form_id, $form_values) {
1757   global $user;
1759   $mode = $form_values['mode'];
1760   $order = $form_values['order'];
1761   $comments_per_page = $form_values['comments_per_page'];
1763   if ($user->uid) {
1764     $user = user_save($user, array('mode' => $mode, 'sort' => $order, 'comments_per_page' => $comments_per_page));
1765   }
1766   else {
1767     $_SESSION['comment_mode'] = $mode;
1768     $_SESSION['comment_sort'] = $order;
1769     $_SESSION['comment_comments_per_page'] = $comments_per_page;
1770   }
1773 function theme_comment($comment, $links = array()) {
1774   $output  = '<div class="comment'. ($comment->status == COMMENT_NOT_PUBLISHED ? ' comment-unpublished' : '') .'">';
1775   $output .= '<div class="subject">'. l($comment->subject, $_GET['q'], NULL, NULL, "comment-$comment->cid") .' '. theme('mark', $comment->new) ."</div>\n";
1776   $output .= '<div class="credit">'. t('by %a on %b', array('%a' => theme('username', $comment), '%b' => format_date($comment->timestamp))) ."</div>\n";
1777   $output .= '<div class="body">'. $comment->comment .'</div>';
1778   $output .= '<div class="links">'. theme('links', $links) .'</div>';
1779   $output .= '</div>';
1780   return $output;
1783 function theme_comment_folded($comment) {
1784   $output  = "<div class=\"comment-folded\">\n";
1785   $output .= ' <span class="subject">'. l($comment->subject, comment_node_url() .'/'. $comment->cid, NULL, NULL, "comment-$comment->cid") .' '. theme('mark', $comment->new) .'</span> ';
1786   $output .= '<span class="credit">'. t('by') .' '. theme('username', $comment) ."</span>\n";
1787   $output .= "</div>\n";
1788   return $output;
1791 function theme_comment_flat_collapsed($comment) {
1792   return theme('comment_view', $comment, '', 0);
1795 function theme_comment_flat_expanded($comment) {
1796   return theme('comment_view', $comment, module_invoke_all('link', 'comment', $comment, 0));
1799 function theme_comment_thread_collapsed($comment) {
1800   $output .= theme('comment_view', $comment, '', 0);
1801   return $output;
1804 function theme_comment_thread_expanded($comment) {
1805   $output = '';
1806   $output .= theme('comment_view', $comment, module_invoke_all('link', 'comment', $comment, 0));
1807   return $output;
1810 function theme_comment_post_forbidden($nid) {
1811   global $user;
1812   if ($user->uid) {
1813     return t("you can't post comments");
1814   }
1815   else {
1816     // we cannot use drupal_get_destination() because these links sometimes appear on /node and taxo listing pages
1817     if (variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
1818       $destination = "destination=". drupal_urlencode("comment/reply/$nid#comment-form");
1819     }
1820     else {
1821       $destination = "destination=". drupal_urlencode("node/$nid#comment-form");
1822     }
1824     if (variable_get('user_register', 1)) {
1825       return t('<a href="@login">Login</a> or <a href="@register">register</a> to post comments', array('@login' => url('user/login', $destination), '@register' => url('user/register', $destination)));
1826     }
1827     else {
1828       return t('<a href="@login">Login</a> to post comments', array('@login' => url('user/login', $destination)));
1829     }
1830   }
1834  * Allow themable wrapping of all comments.
1835  */
1836 function theme_comment_wrapper($content) {
1837   return '<div id="comments">'. $content .'</div>';
1840 function _comment_delete_thread($comment) {
1841   if (!is_object($comment) || !is_numeric($comment->cid)) {
1842     watchdog('content', t('Can not delete non-existent comment.'), WATCHDOG_WARNING);
1843     return;
1844   }
1846   // Delete the comment:
1847   db_query('DELETE FROM {comments} WHERE cid = %d', $comment->cid);
1848   watchdog('content', t('Comment: deleted %subject.', array('%subject' => $comment->subject)));
1850   comment_invoke_comment($comment, 'delete');
1852   // Delete the comment's replies
1853   $result = db_query('SELECT c.*, u.name AS registered_name, u.uid FROM {comments} c INNER JOIN {users} u ON u.uid = c.uid WHERE pid = %d', $comment->cid);
1854   while ($comment = db_fetch_object($result)) {
1855     $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1856     _comment_delete_thread($comment);
1857   }
1861  * Return an array of viewing modes for comment listings.
1863  * We can't use a global variable array because the locale system
1864  * is not initialized yet when the comment module is loaded.
1865  */
1866 function _comment_get_modes() {
1867   return array(
1868     COMMENT_MODE_FLAT_COLLAPSED => t('Flat list - collapsed'),
1869     COMMENT_MODE_FLAT_EXPANDED => t('Flat list - expanded'),
1870     COMMENT_MODE_THREADED_COLLAPSED => t('Threaded list - collapsed'),
1871     COMMENT_MODE_THREADED_EXPANDED => t('Threaded list - expanded')
1872   );
1876  * Return an array of viewing orders for comment listings.
1878  * We can't use a global variable array because the locale system
1879  * is not initialized yet when the comment module is loaded.
1880  */
1881 function _comment_get_orders() {
1882   return array(
1883     COMMENT_ORDER_NEWEST_FIRST => t('Date - newest first'),
1884     COMMENT_ORDER_OLDEST_FIRST => t('Date - oldest first')
1885   );
1889  * Return an array of "comments per page" settings from which the user
1890  * can choose.
1891  */
1892 function _comment_per_page() {
1893   return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300));
1897  * Return a current comment display setting
1899  * $setting can be one of these: 'mode', 'sort', 'comments_per_page'
1900  */
1901 function _comment_get_display_setting($setting) {
1902   global $user;
1904   if (isset($_GET[$setting])) {
1905     $value = $_GET[$setting];
1906   }
1907   else {
1908     // get the setting's site default
1909     switch ($setting) {
1910       case 'mode':
1911         $default = variable_get('comment_default_mode', COMMENT_MODE_THREADED_EXPANDED);
1912         break;
1913       case 'sort':
1914         $default = variable_get('comment_default_order', COMMENT_ORDER_NEWEST_FIRST);
1915         break;
1916       case 'comments_per_page':
1917         $default = variable_get('comment_default_per_page', '50');
1918     }
1919     if (variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN) == COMMENT_CONTROLS_HIDDEN) {
1920       // if comment controls are disabled use site default
1921       $value = $default;
1922     }
1923     else {
1924       // otherwise use the user's setting if set
1925       if ($user->$setting) {
1926         $value = $user->$setting;
1927       }
1928       else if ($_SESSION['comment_'. $setting]) {
1929         $value = $_SESSION['comment_'. $setting];
1930       }
1931       else {
1932         $value = $default;
1933       }
1934     }
1935   }
1936   return $value;
1940  * Updates the comment statistics for a given node. This should be called any
1941  * time a comment is added, deleted, or updated.
1943  * The following fields are contained in the node_comment_statistics table.
1944  * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node.
1945  * - last_comment_name: the name of the anonymous poster for the last comment
1946  * - 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.
1947  * - comment_count: the total number of approved/published comments on this node.
1948  */
1949 function _comment_update_node_statistics($nid) {
1950   $count = db_result(db_query('SELECT COUNT(cid) FROM {comments} WHERE nid = %d AND status = %d', $nid, COMMENT_PUBLISHED));
1952   // comments exist
1953   if ($count > 0) {
1954     $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));
1955     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);
1956   }
1958   // no comments
1959   else {
1960     $node = db_fetch_object(db_query("SELECT uid, created FROM {node} WHERE nid = %d", $nid));
1961     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);
1962   }
1966  * Invoke a hook_comment() operation in all modules.
1968  * @param &$comment
1969  *   A comment object.
1970  * @param $op
1971  *   A string containing the name of the comment operation.
1972  * @return
1973  *   The returned value of the invoked hooks.
1974  */
1975 function comment_invoke_comment(&$comment, $op) {
1976   $return = array();
1977   foreach (module_implements('comment') as $name) {
1978     $function = $name .'_comment';
1979     $result = $function($comment, $op);
1980     if (isset($result) && is_array($result)) {
1981       $return = array_merge($return, $result);
1982     }
1983     else if (isset($result)) {
1984       $return[] = $result;
1985     }
1986   }
1987   return $return;
1991  * Generate vancode.
1993  * Consists of a leading character indicating length, followed by N digits
1994  * with a numerical value in base 36. Vancodes can be sorted as strings
1995  * without messing up numerical order.
1997  * It goes:
1998  * 00, 01, 02, ..., 0y, 0z,
1999  * 110, 111, ... , 1zy, 1zz,
2000  * 2100, 2101, ..., 2zzy, 2zzz,
2001  * 31000, 31001, ...
2002  */
2003 function int2vancode($i = 0) {
2004   $num = base_convert((int)$i, 10, 36);
2005   $length = strlen($num);
2006   return chr($length + ord('0') - 1) . $num;
2010  * Decode vancode back to an integer.
2011  */
2012 function vancode2int($c = '00') {
2013   return base_convert(substr($c, 1), 36, 10);