add node : allowed to everyone
[plewww.git] / modules / comment.module
1 <?php
2 // $Id: comment.module 144 2007-03-28 07:52:20Z thierry $
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  * Constants to define a comment's published state
15  */
16 define('COMMENT_PUBLISHED', 0);
17 define('COMMENT_NOT_PUBLISHED', 1);
18
19 /**
20  * Constants to define the viewing modes for comment listings
21  */
22 define('COMMENT_MODE_FLAT_COLLAPSED', 1);
23 define('COMMENT_MODE_FLAT_EXPANDED', 2);
24 define('COMMENT_MODE_THREADED_COLLAPSED', 3);
25 define('COMMENT_MODE_THREADED_EXPANDED', 4);
26
27 /**
28  * Constants to define the viewing orders for comment listings
29  */
30 define('COMMENT_ORDER_NEWEST_FIRST', 1);
31 define('COMMENT_ORDER_OLDEST_FIRST', 2);
32
33 /**
34  * Constants to define the position of the comment controls
35  */
36 define('COMMENT_CONTROLS_ABOVE', 0);
37 define('COMMENT_CONTROLS_BELOW', 1);
38 define('COMMENT_CONTROLS_ABOVE_BELOW', 2);
39 define('COMMENT_CONTROLS_HIDDEN', 3);
40
41 /**
42  * Constants to define the anonymous poster contact handling
43  */
44 define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
45 define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
46 define('COMMENT_ANONYMOUS_MUST_CONTACT', 2);
47
48 /**
49  * Constants to define the comment form location
50  */
51 define('COMMENT_FORM_SEPARATE_PAGE', 0);
52 define('COMMENT_FORM_BELOW', 1);
53
54 /**
55  * Constants to define a node's comment state
56  */
57 define('COMMENT_NODE_DISABLED', 0);
58 define('COMMENT_NODE_READ_ONLY', 1);
59 define('COMMENT_NODE_READ_WRITE', 2);
60
61 /**
62  * Constants to define if comment preview is optional or required
63  */
64 define('COMMENT_PREVIEW_OPTIONAL', 0);
65 define('COMMENT_PREVIEW_REQUIRED', 1);
66
67 /**
68  * Implementation of hook_help().
69  */
70 function comment_help($section) {
71   switch ($section) {
72     case 'admin/help#comment':
73       $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>';
74       $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>';
75       $output .= t('<p>You can</p>
76 <ul>
77 <li>control access for various comment module functions through access permissions <a href="%admin-access">administer &gt;&gt; access control</a>.</li>
78 <li>administer comments <a href="%admin-comment-configure"> administer &gt;&gt; comments &gt;&gt; configure</a>.</li>
79 </ul>
80 ', array('%admin-access' => url('admin/access'), '%admin-comment-configure' => url('admin/comment/configure')));
81       $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>';
82       return $output;
83     case 'admin/modules#description':
84       return t('Allows users to comment on and discuss published content.');
85     case 'admin/comment':
86     case 'admin/comment/new':
87       return t("<p>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>");
88     case 'admin/comment/approval':
89       return t("<p>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>");
90     case 'admin/comment/configure':
91     case 'admin/comment/configure/settings':
92       return t("<p>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>");
93    }
94 }
95
96 /**
97  * Implementation of hook_menu().
98  */
99 function comment_menu($may_cache) {
100   $items = array();
101
102   if ($may_cache) {
103     $access = user_access('administer comments');
104     $items[] = array('path' => 'admin/comment', 'title' => t('comments'),
105       'callback' => 'comment_admin_overview', 'access' => $access);
106
107     // Tabs:
108     $items[] = array('path' => 'admin/comment/list', 'title' => t('list'),
109       'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
110     $items[] = array('path' => 'admin/comment/configure', 'title' => t('configure'),
111       'callback' => 'comment_configure', 'access' => $access, 'type' => MENU_LOCAL_TASK);
112
113     // Subtabs:
114     $items[] = array('path' => 'admin/comment/list/new', 'title' => t('published comments'),
115       'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
116     $items[] = array('path' => 'admin/comment/list/approval', 'title' => t('approval queue'),
117       'callback' => 'comment_admin_overview', 'access' => $access,
118       'callback arguments' => array('approval'),
119       'type' => MENU_LOCAL_TASK);
120
121     $items[] = array('path' => 'admin/comment/configure/settings', 'title' => t('settings'),
122       'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
123
124     $items[] = array('path' => 'comment/delete', 'title' => t('delete comment'),
125       'callback' => 'comment_delete', 'access' => $access, 'type' => MENU_CALLBACK);
126
127     $access = user_access('post comments');
128     $items[] = array('path' => 'comment/edit', 'title' => t('edit comment'),
129       'callback' => 'comment_edit', 'access' => $access, 'type' => MENU_CALLBACK);
130   }
131   else {
132     if (arg(0) == 'comment' && arg(1) == 'reply' && is_numeric(arg(2))) {
133       $node = node_load(arg(2));
134       if ($node->nid) {
135         $items[] = array('path' => 'comment/reply', 'title' => t('reply to comment'),
136           'callback' => 'comment_reply', 'access' => node_access('view', $node), 'type' => MENU_CALLBACK);
137       }
138     }
139     if ((arg(0) == 'node') && is_numeric(arg(1)) && is_numeric(arg(2))) {
140       $items[] = array('path' => ('node/'. arg(1) .'/'. arg(2)), 'title' => t('view'),
141         'callback' => 'node_page',
142         'type' => MENU_CALLBACK);
143     }
144   }
145
146   return $items;
147 }
148
149 /**
150  * Implementation of hook_perm().
151  */
152 function comment_perm() {
153   return array('access comments', 'post comments', 'administer comments', 'post comments without approval');
154 }
155
156 /**
157  * Implementation of hook_block().
158  *
159  * Generates a block with the most recent comments.
160  */
161 function comment_block($op = 'list', $delta = 0) {
162   if ($op == 'list') {
163     $blocks[0]['info'] = t('Recent comments');
164     return $blocks;
165   }
166   else if ($op == 'view' && user_access('access comments')) {
167     $block['subject'] = t('Recent comments');
168     $block['content'] = theme('comment_block');
169     return $block;
170   }
171 }
172
173 function theme_comment_block() {
174   $result = db_query_range(db_rewrite_sql('SELECT c.nid, c.subject, c.cid, c.timestamp FROM {comments} c INNER JOIN {node} n ON n.nid = c.nid WHERE n.status = 1 AND c.status = %d ORDER BY c.timestamp DESC', 'c'), COMMENT_PUBLISHED, 0, 10);
175   $items = array();
176   while ($comment = db_fetch_object($result)) {
177     $items[] = l($comment->subject, 'node/'. $comment->nid, NULL, NULL, 'comment-'. $comment->cid) .'<br />'. t('%time ago', array('%time' => format_interval(time() - $comment->timestamp)));
178   }
179   return theme('item_list', $items);
180 }
181
182 /**
183  * Implementation of hook_link().
184  */
185 function comment_link($type, $node = 0, $main = 0) {
186   $links = array();
187
188   if ($type == 'node' && $node->comment) {
189
190     if ($main) {
191       // Main page: display the number of comments that have been posted.
192
193       if (user_access('access comments')) {
194         $all = comment_num_all($node->nid);
195         $new = comment_num_new($node->nid);
196
197         if ($all) {
198           $links[] = l(format_plural($all, '1 comment', '%count comments'), "node/$node->nid", array('title' => t('Jump to the first comment of this posting.')), NULL, 'comment');
199
200           if ($new) {
201             $links[] = l(format_plural($new, '1 new comment', '%count new comments'), "node/$node->nid", array('title' => t('Jump to the first new comment of this posting.')), NULL, 'new');
202           }
203         }
204         else {
205           if ($node->comment == COMMENT_NODE_READ_WRITE) {
206             if (user_access('post comments')) {
207               $links[] = l(t('add new comment'), "comment/reply/$node->nid", array('title' => t('Add a new comment to this page.')), NULL, 'comment_form');
208             }
209             else {
210               $links[] = theme('comment_post_forbidden', $node->nid);
211             }
212           }
213         }
214       }
215     }
216     else {
217       // Node page: add a "post comment" link if the user is allowed to
218       // post comments, if this node is not read-only, and if the comment form isn't already shown
219
220       if ($node->comment == COMMENT_NODE_READ_WRITE) {
221         if (user_access('post comments')) {
222           if (variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
223             $links[] = l(t('add new comment'), "comment/reply/$node->nid", array('title' => t('Share your thoughts and opinions related to this posting.')), NULL, 'comment_form');
224           }
225         }
226         else {
227           $links[] = theme('comment_post_forbidden', $node->nid);
228         }
229       }
230     }
231   }
232
233   if ($type == 'comment') {
234     $links = comment_links($node, $main);
235   }
236
237   return $links;
238 }
239
240 function comment_form_alter($form_id, &$form) {
241   if (isset($form['type'])) {
242     if ($form['type']['#value'] .'_node_settings' == $form_id) {
243       $form['workflow']['comment_'. $form['type']['#value']] = array('#type' => 'radios', '#title' => t('Default comment setting'), '#default_value' => variable_get('comment_'. $form['type']['#value'], COMMENT_NODE_READ_WRITE), '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')), '#description' => t('Users with the <em>administer comments</em> permission will be able to override this setting.'));
244     }
245     if ($form['type']['#value'] .'_node_form' == $form_id) {
246       $node = $form['#node'];
247       if (user_access('administer comments')) {
248         $form['comment_settings'] = array(
249           '#type' => 'fieldset',
250           '#title' => t('Comment settings'),
251           '#collapsible' => TRUE,
252           '#collapsed' => TRUE,
253           '#weight' => 30,
254         );
255         $form['comment_settings']['comment'] = array(
256           '#type' => 'radios',
257           '#parents' => array('comment'),
258           '#default_value' => $node->comment,
259           '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')),
260         );
261       }
262       else {
263         $form['comment_settings']['comment'] = array(
264           '#type' => 'value',
265           '#value' => $node->comment,
266         );
267       }
268     }
269   }
270 }
271
272 /**
273  * Implementation of hook_nodeapi().
274  *
275  */
276 function comment_nodeapi(&$node, $op, $arg = 0) {
277   switch ($op) {
278     case 'load':
279       return db_fetch_array(db_query("SELECT last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid = %d", $node->nid));
280       break;
281
282     case 'prepare':
283       if (!isset($node->comment)) {
284         $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
285       }
286       break;
287
288     case 'insert':
289       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->created, $node->uid);
290       break;
291
292     case 'delete':
293       db_query('DELETE FROM {comments} WHERE nid = %d', $node->nid);
294       db_query('DELETE FROM {node_comment_statistics} WHERE nid = %d', $node->nid);
295       break;
296
297     case 'update index':
298       $text = '';
299       $comments = db_query('SELECT subject, comment, format FROM {comments} WHERE nid = %d AND status = %d', $node->nid, COMMENT_PUBLISHED);
300       while ($comment = db_fetch_object($comments)) {
301         $text .= '<h2>'. check_plain($comment->subject) .'</h2>'. check_markup($comment->comment, $comment->format, FALSE);
302       }
303       return $text;
304
305     case 'search result':
306       $comments = db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $node->nid));
307       return format_plural($comments, '1 comment', '%count comments');
308
309     case 'rss item':
310       return array(array('key' => 'comments', 'value' => url('node/'. $node->nid, NULL, 'comment', TRUE)));
311   }
312 }
313
314 /**
315  * Implementation of hook_user().
316  *
317  * Provides signature customization for the user's comments.
318  */
319 function comment_user($type, $edit, &$user, $category = NULL) {
320   if ($type == 'form' && $category == 'account') {
321     // when user tries to edit his own data
322     $form['comment_settings'] = array(
323       '#type' => 'fieldset',
324       '#title' => t('Comment settings'),
325       '#collapsible' => TRUE,
326       '#weight' => 4);
327     $form['comment_settings']['signature'] = array(
328       '#type' => 'textarea',
329       '#title' => t('Signature'),
330       '#default_value' => $edit['signature'],
331       '#description' => t('Your signature will be publicly displayed at the end of your comments.'));
332
333     return $form;
334   }
335   elseif ($type == 'delete') {
336     db_query('UPDATE {comments} SET uid = 0 WHERE uid = %d', $user->uid);
337     db_query('UPDATE {node_comment_statistics} SET last_comment_uid = 0 WHERE last_comment_uid = %d', $user->uid);
338   }
339 }
340
341 /**
342  * Menu callback; presents the comment settings page.
343  */
344 function comment_configure() {
345   $form['viewing_options'] = array(
346     '#type' => 'fieldset',
347     '#title' => t('Viewing options'),
348     '#collapsible' => TRUE,
349     '#collapsed' => TRUE,
350   );
351
352   $form['viewing_options']['comment_default_mode'] = array(
353     '#type' => 'radios',
354     '#title' => t('Default display mode'),
355     '#default_value' => variable_get('comment_default_mode', COMMENT_MODE_THREADED_EXPANDED),
356     '#options' => _comment_get_modes(),
357     '#description' => t('The default view for comments. Expanded views display the body of the comment. Threaded views keep replies together.'),
358   );
359
360   $form['viewing_options']['comment_default_order'] = array(
361     '#type' => 'radios',
362     '#title' => t('Default display order'),
363     '#default_value' => variable_get('comment_default_order', COMMENT_ORDER_NEWEST_FIRST),
364     '#options' => _comment_get_orders(),
365     '#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.'),
366   );
367
368   $form['viewing_options']['comment_default_per_page'] = array(
369     '#type' => 'select',
370     '#title' => t('Default comments per page'),
371     '#default_value' => variable_get('comment_default_per_page', 50),
372     '#options' => _comment_per_page(),
373     '#description' => t('Default number of comments for each page: more comments are distributed in several pages.'),
374   );
375
376   $form['viewing_options']['comment_controls'] = array(
377     '#type' => 'radios',
378     '#title' => t('Comment controls'),
379     '#default_value' => variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN),
380     '#options' => array(
381       t('Display above the comments'),
382       t('Display below the comments'),
383       t('Display above and below the comments'),
384       t('Do not display')),
385     '#description' => t('Position of the comment controls box.  The comment controls let the user change the default display mode and display order of comments.'),
386   );
387
388   $form['posting_settings'] = array(
389     '#type' => 'fieldset',
390     '#title' => t('Posting settings'),
391     '#collapsible' => TRUE,
392     '#collapsed' => TRUE,
393   );
394
395   $form['posting_settings']['comment_anonymous'] = array(
396     '#type' => 'radios',
397     '#title' => t('Anonymous commenting'),
398     '#default_value' => variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT),
399     '#options' => array(
400       COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
401       COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
402       COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information')),
403     '#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/access'))),
404   );
405   if (!user_access('post comments', user_load(array('uid' => 0)))) {
406     $form['posting_settings']['comment_anonymous']['#attributes'] = array('disabled' => 'disabled');
407   }
408
409   $form['posting_settings']['comment_subject_field'] = array(
410     '#type' => 'radios',
411     '#title' => t('Comment subject field'),
412     '#default_value' => variable_get('comment_subject_field', 1),
413     '#options' => array(t('Disabled'), t('Enabled')),
414     '#description' => t('Can users provide a unique subject for their comments?'),
415   );
416
417   $form['posting_settings']['comment_preview'] = array(
418     '#type' => 'radios',
419     '#title' => t('Preview comment'),
420     '#default_value' => variable_get('comment_preview', COMMENT_PREVIEW_REQUIRED),
421     '#options' => array(t('Optional'), t('Required')),
422   );
423
424   $form['posting_settings']['comment_form_location'] = array(
425     '#type' => 'radios',
426     '#title' => t('Location of comment submission form'),
427     '#default_value' => variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE),
428     '#options' => array(t('Display on separate page'), t('Display below post or comments')),
429   );
430
431   return system_settings_form('comment_settings_form', $form);
432 }
433
434 /**
435  * This is *not* a hook_access() implementation. This function is called
436  * to determine whether the current user has access to a particular comment.
437  *
438  * Authenticated users can edit their comments as long they have not been
439  * replied to. This prevents people from changing or revising their
440  * statements based on the replies to their posts.
441  */
442 function comment_access($op, $comment) {
443   global $user;
444
445   if ($op == 'edit') {
446     return ($user->uid && $user->uid == $comment->uid && comment_num_replies($comment->cid) == 0) || user_access('administer comments');
447   }
448 }
449
450 function comment_node_url() {
451   return arg(0) .'/'. arg(1);
452 }
453
454 function comment_edit($cid) {
455   global $user;
456
457   $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));
458   $comment = drupal_unpack($comment);
459   $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
460   if (comment_access('edit', $comment)) {
461     return comment_form((array)$comment);
462   }
463   else {
464     drupal_access_denied();
465   }
466 }
467
468 function comment_reply($nid, $pid = NULL) {
469   // set the breadcrumb trail
470   $node = node_load($nid);
471   menu_set_location(array(array('path' => "node/$nid", 'title' => $node->title), array('path' => "comment/reply/$nid")));
472
473   $op = isset($_POST['op']) ? $_POST['op'] : '';
474
475   $output = '';
476
477   // or are we merely showing the form?
478   if (user_access('access comments')) {
479
480     if ($op == t('Preview comment')) {
481       if (user_access('post comments')) {
482         $output .= comment_form(array('pid' => $pid, 'nid' => $nid), NULL);
483       }
484       else {
485         drupal_set_message(t('You are not authorized to post comments.'), 'error');
486         drupal_goto("node/$nid");
487       }
488     }
489     else {
490       // if this is a reply to another comment, show that comment first
491       // else, we'll just show the user the node they're commenting on.
492       if ($pid) {
493         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))) {
494           if ($comment->nid != $nid) {
495             // Attempting to reply to a comment not belonging to the current nid.
496             drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
497             drupal_goto("node/$nid");
498           }
499           $comment = drupal_unpack($comment);
500           $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
501           $output .= theme('comment_view', $comment);
502         }
503         else {
504           drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
505           drupal_goto("node/$nid");
506         }
507       }
508       else if (user_access('access content')) {
509         $output .= node_view($node);
510       }
511
512       // should we show the reply box?
513       if (node_comment_mode($nid) != COMMENT_NODE_READ_WRITE) {
514         drupal_set_message(t("This discussion is closed: you can't post new comments."), 'error');
515         drupal_goto("node/$nid");
516       }
517       else if (user_access('post comments')) {
518         $output .= comment_form(array('pid' => $pid, 'nid' => $nid), t('Reply'));
519       }
520       else {
521         drupal_set_message(t('You are not authorized to post comments.'), 'error');
522         drupal_goto("node/$nid");
523       }
524     }
525   }
526   else {
527     drupal_set_message(t('You are not authorized to view comments.'), 'error');
528     drupal_goto("node/$nid");
529   }
530
531   return $output;
532 }
533
534 /**
535  * Accepts a submission of new or changed comment content.
536  *
537  * @param $edit
538  *   A comment array.
539  *
540  * @return
541  *   If the comment is successfully saved the comment ID is returned.  If the comment
542  *   is not saved, FALSE is returned.
543  */
544 function comment_save($edit) {
545   global $user;
546   if (user_access('post comments') && (user_access('administer comments') || node_comment_mode($edit['nid']) == COMMENT_NODE_READ_WRITE)) {
547     if (!form_get_errors()) {
548       // Check for duplicate comments.  Note that we have to use the
549       // validated/filtered data to perform such check.
550       $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);
551       if ($duplicate != 0) {
552         watchdog('content', t('Comment: duplicate %subject.', array('%subject' => theme('placeholder', $edit['subject']))), WATCHDOG_WARNING);
553       }
554
555       if ($edit['cid']) {
556         // Update the comment in the database.
557         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']);
558
559         _comment_update_node_statistics($edit['nid']);
560
561         // Allow modules to respond to the updating of a comment.
562         comment_invoke_comment($edit, 'update');
563
564
565         // Add an entry to the watchdog log.
566         watchdog('content', t('Comment: updated %subject.', array('%subject' => theme('placeholder', $edit['subject']))), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], NULL, NULL, 'comment-'. $edit['cid']));
567       }
568       else {
569         // Add the comment to database.
570         $status = user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
571         $roles = variable_get('comment_roles', array());
572         $score = 0;
573
574         foreach (array_intersect(array_keys($roles), array_keys($user->roles)) as $rid) {
575           $score = max($roles[$rid], $score);
576         }
577
578         $users = serialize(array(0 => $score));
579
580         // Here we are building the thread field.  See the comment
581         // in comment_render().
582         if ($edit['pid'] == 0) {
583           // This is a comment with no parent comment (depth 0): we start
584           // by retrieving the maximum thread level.
585           $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $edit['nid']));
586
587           // Strip the "/" from the end of the thread.
588           $max = rtrim($max, '/');
589
590           // Finally, build the thread field for this new comment.
591           $thread = int2vancode(vancode2int($max) + 1) .'/';
592         }
593         else {
594           // This is comment with a parent comment: we increase
595           // the part of the thread value at the proper depth.
596
597           // Get the parent comment:
598           $parent = _comment_load($edit['pid']);
599
600           // Strip the "/" from the end of the parent thread.
601           $parent->thread = (string) rtrim((string) $parent->thread, '/');
602
603           // Get the max value in _this_ thread.
604           $max = db_result(db_query("SELECT MAX(thread) FROM {comments} WHERE thread LIKE '%s.%%' AND nid = %d", $parent->thread, $edit['nid']));
605
606           if ($max == '') {
607             // First child of this parent.
608             $thread = $parent->thread .'.'. int2vancode(0) .'/';
609           }
610           else {
611             // Strip the "/" at the end of the thread.
612             $max = rtrim($max, '/');
613
614             // We need to get the value at the correct depth.
615             $parts = explode('.', $max);
616             $parent_depth = count(explode('.', $parent->thread));
617             $last = $parts[$parent_depth];
618
619             // Finally, build the thread field for this new comment.
620             $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
621           }
622         }
623
624         $edit['cid'] = db_next_id('{comments}_cid');
625         $edit['timestamp'] = time();
626
627         if ($edit['uid'] == $user->uid) {
628           $edit['name'] = $user->name;
629         }
630
631         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'], $status, $score, $users, $thread, $edit['name'], $edit['mail'], $edit['homepage']);
632
633         _comment_update_node_statistics($edit['nid']);
634
635         // Tell the other modules a new comment has been submitted.
636         comment_invoke_comment($edit, 'insert');
637
638         // Add an entry to the watchdog log.
639         watchdog('content', t('Comment: added %subject.', array('%subject' => theme('placeholder', $edit['subject']))), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], NULL, NULL, 'comment-'. $edit['cid']));
640       }
641
642       // Clear the cache so an anonymous user can see his comment being added.
643       cache_clear_all();
644
645       // Explain the approval queue if necessary, and then
646       // redirect the user to the node he's commenting on.
647       if ($status == COMMENT_NOT_PUBLISHED) {
648         drupal_set_message(t('Your comment has been queued for moderation by site administrators and will be published after approval.'));
649       }
650       return $edit['cid'];
651     }
652     else {
653       return FALSE;
654     }
655   }
656   else {
657     $txt = t('Comment: unauthorized comment submitted or comment submitted to a closed node %subject.', array('%subject' => theme('placeholder', $edit['subject'])));
658     watchdog('content', $txt, WATCHDOG_WARNING);
659     drupal_set_message($txt, 'error');
660     return FALSE;
661   }
662 }
663
664 function comment_links($comment, $return = 1) {
665   global $user;
666
667   $links = array();
668
669   // If we are viewing just this comment, we link back to the node.
670   if ($return) {
671     $links[] = l(t('parent'), comment_node_url(), NULL, NULL, "comment-$comment->cid");
672   }
673
674   if (node_comment_mode($comment->nid) == COMMENT_NODE_READ_WRITE) {
675     if (user_access('administer comments') && user_access('post comments')) {
676       $links[] = l(t('delete'), "comment/delete/$comment->cid");
677       $links[] = l(t('edit'), "comment/edit/$comment->cid");
678       $links[] = l(t('reply'), "comment/reply/$comment->nid/$comment->cid");
679     }
680     else if (user_access('post comments')) {
681       if (comment_access('edit', $comment)) {
682         $links[] = l(t('edit'), "comment/edit/$comment->cid");
683       }
684       $links[] = l(t('reply'), "comment/reply/$comment->nid/$comment->cid");
685     }
686     else {
687       $links[] = theme('comment_post_forbidden', $comment->nid);
688     }
689   }
690
691   return $links;
692 }
693
694 function comment_render($node, $cid = 0) {
695   global $user;
696
697   $output = '';
698
699   if (user_access('access comments')) {
700     // Pre-process variables.
701     $nid = $node->nid;
702     if (empty($nid)) {
703       $nid = 0;
704     }
705
706     $mode = _comment_get_display_setting('mode');
707     $order = _comment_get_display_setting('sort');
708     $comments_per_page = _comment_get_display_setting('comments_per_page');
709
710     $output .= "<a id=\"comment\"></a>\n";
711
712     if ($cid) {
713       // Single comment view.
714       $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';
715       $query_args = array($cid);
716       if (!user_access('administer comments')) {
717         $query .= ' AND c.status = %d';
718         $query_args[] = COMMENT_PUBLISHED;
719       }
720       $query .= ' GROUP BY c.cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, u.picture, c.homepage, u.uid, u.name, u.picture, u.data, c.score, c.users, c.status';
721       $result = db_query($query, $query_args);
722
723       if ($comment = db_fetch_object($result)) {
724         $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
725         $output .= theme('comment_view', $comment, module_invoke_all('link', 'comment', $comment, 1));
726       }
727     }
728     else {
729       // Multiple comment view
730       $query_count = 'SELECT COUNT(*) FROM {comments} WHERE nid = %d';
731       $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';
732
733       $query_args = array($nid);
734       if (!user_access('administer comments')) {
735         $query .= ' AND c.status = %d';
736         $query_count .= ' AND status = %d';
737         $query_args[] = COMMENT_PUBLISHED;
738       }
739
740       $query .= ' GROUP BY c.cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, u.picture, c.homepage, u.uid, u.name, u.picture, u.data, c.score, c.users, c.thread, c.status';
741
742       /*
743       ** We want to use the standard pager, but threads would need every
744       ** comment to build the thread structure, so we need to store some
745       ** extra info.
746       **
747       ** We use a "thread" field to store this extra info. The basic idea
748       ** is to store a value and to order by that value. The "thread" field
749       ** keeps this data in a way which is easy to update and convenient
750       ** to use.
751       **
752       ** A "thread" value starts at "1". If we add a child (A) to this
753       ** comment, we assign it a "thread" = "1.1". A child of (A) will have
754       ** "1.1.1". Next brother of (A) will get "1.2". Next brother of the
755       ** parent of (A) will get "2" and so on.
756       **
757       ** First of all note that the thread field stores the depth of the
758       ** comment: depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
759       **
760       ** Now to get the ordering right, consider this example:
761       **
762       ** 1
763       ** 1.1
764       ** 1.1.1
765       ** 1.2
766       ** 2
767       **
768       ** If we "ORDER BY thread ASC" we get the above result, and this is
769       ** the natural order sorted by time.  However, if we "ORDER BY thread
770       ** DESC" we get:
771       **
772       ** 2
773       ** 1.2
774       ** 1.1.1
775       ** 1.1
776       ** 1
777       **
778       ** Clearly, this is not a natural way to see a thread, and users
779       ** will get confused. The natural order to show a thread by time
780       ** desc would be:
781       **
782       ** 2
783       ** 1
784       ** 1.2
785       ** 1.1
786       ** 1.1.1
787       **
788       ** which is what we already did before the standard pager patch. To
789       ** achieve this we simply add a "/" at the end of each "thread" value.
790       ** This way out thread fields will look like depicted below:
791       **
792       ** 1/
793       ** 1.1/
794       ** 1.1.1/
795       ** 1.2/
796       ** 2/
797       **
798       ** we add "/" since this char is, in ASCII, higher than every number,
799       ** so if now we "ORDER BY thread DESC" we get the correct order.  Try
800       ** it, it works ;).  However this would spoil the "ORDER BY thread ASC"
801       ** Here, we do not need to consider the trailing "/" so we use a
802       ** substring only.
803       */
804
805       if ($order == COMMENT_ORDER_NEWEST_FIRST) {
806         if ($mode == COMMENT_MODE_FLAT_COLLAPSED || $mode == COMMENT_MODE_FLAT_EXPANDED) {
807           $query .= ' ORDER BY c.timestamp DESC';
808         }
809         else {
810           $query .= ' ORDER BY c.thread DESC';
811         }
812       }
813       else if ($order == COMMENT_ORDER_OLDEST_FIRST) {
814         if ($mode == COMMENT_MODE_FLAT_COLLAPSED || $mode == COMMENT_MODE_FLAT_EXPANDED) {
815           $query .= ' ORDER BY c.timestamp';
816         }
817         else {
818
819           /*
820           ** See comment above.  Analysis learns that this doesn't cost
821           ** too much.  It scales much much better than having the whole
822           ** comment structure.
823           */
824
825           $query .= ' ORDER BY SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))';
826         }
827       }
828
829       // Start a form, for use with comment control.
830       $result = pager_query($query, $comments_per_page, 0, $query_count, $query_args);
831       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)) {
832         $output .= comment_controls($mode, $order, $comments_per_page);
833       }
834
835       while ($comment = db_fetch_object($result)) {
836         $comment = drupal_unpack($comment);
837         $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
838         $comment->depth = count(explode('.', $comment->thread)) - 1;
839
840         if ($mode == COMMENT_MODE_FLAT_COLLAPSED) {
841           $output .= theme('comment_flat_collapsed', $comment);
842         }
843         else if ($mode == COMMENT_MODE_FLAT_EXPANDED) {
844           $output .= theme('comment_flat_expanded', $comment);
845         }
846         else if ($mode == COMMENT_MODE_THREADED_COLLAPSED) {
847           $output .= theme('comment_thread_collapsed', $comment);
848         }
849         else if ($mode == COMMENT_MODE_THREADED_EXPANDED) {
850           $output .= theme('comment_thread_expanded', $comment);
851         }
852       }
853
854       $output .= theme('pager', NULL, $comments_per_page, 0);
855
856       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)) {
857         $output .= comment_controls($mode, $order, $comments_per_page);
858       }
859     }
860
861     // If enabled, show new comment form.
862     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)) {
863       $output .= comment_form(array('nid' => $nid), t('Post new comment'));
864     }
865   }
866   return $output;
867 }
868
869
870 /**
871  * Menu callback; delete a comment.
872  */
873 function comment_delete($cid) {
874   $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));
875   $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
876
877   $output = '';
878
879   // We'll only delete if the user has confirmed the
880   // deletion using the form in our else clause below.
881   if (is_object($comment) && is_numeric($comment->cid) && $_POST['edit']['confirm']) {
882     drupal_set_message(t('The comment and all its replies have been deleted.'));
883
884     // Delete comment and its replies.
885     _comment_delete_thread($comment);
886
887     _comment_update_node_statistics($comment->nid);
888
889     // Clear the cache so an anonymous user sees that his comment was deleted.
890     cache_clear_all();
891
892     drupal_goto("node/$comment->nid");
893   }
894   else if (is_object($comment) && is_numeric($comment->cid)) {
895     $output = confirm_form('comment_confirm_delete',
896                     array(),
897                     t('Are you sure you want to delete the comment %title?', array('%title' => theme('placeholder', $comment->subject))),
898                     'node/'. $comment->nid,
899                     t('Any replies to this comment will be lost. This action cannot be undone.'),
900                     t('Delete'),
901                     t('Cancel'));
902   }
903   else {
904     drupal_set_message(t('The comment no longer exists.'));
905   }
906
907   return $output;
908 }
909
910 /**
911  * Comment operations.  We offer different update operations depending on
912  * which comment administration page we're on.
913  */
914 function comment_operations($action = NULL) {
915   if ($action == 'publish') {
916     $operations = array(
917       'publish' => array(t('Publish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_PUBLISHED .' WHERE cid = %d'),
918       'delete' => array(t('Delete the selected comments'), '')
919     );
920   }
921   else if ($action == 'unpublish') {
922     $operations = array(
923       'unpublish' => array(t('Unpublish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_NOT_PUBLISHED .' WHERE cid = %d'),
924       'delete' => array(t('Delete the selected comments'), '')
925     );
926   }
927   else {
928     $operations = array(
929       'publish' => array(t('Publish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_PUBLISHED .' WHERE cid = %d'),
930       'unpublish' => array(t('Unpublish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_NOT_PUBLISHED .' WHERE cid = %d'),
931       'delete' => array(t('Delete the selected comments'), '')
932     );
933   }
934   return $operations;
935 }
936
937 /**
938  * Menu callback; present an administrative comment listing.
939  */
940 function comment_admin_overview($type = 'new') {
941   $edit = $_POST['edit'];
942
943   if ($edit['operation'] == 'delete') {
944     return comment_multiple_delete_confirm();
945   }
946
947   // build an 'Update options' form
948   $form['options'] = array(
949     '#type' => 'fieldset', '#title' => t('Update options'),
950     '#prefix' => '<div class="container-inline">', '#suffix' => '</div>'
951   );
952   $options = array();
953   foreach (comment_operations(arg(3) == 'approval' ? 'publish' : 'unpublish') as $key => $value) {
954     $options[$key] = $value[0];
955   }
956   $form['options']['operation'] = array('#type' => 'select', '#options' => $options, '#default_value' => 'publish');
957   $form['options']['submit'] = array('#type' => 'submit', '#value' => t('Update'));
958
959   // load the comments that we want to display
960   $status = ($type == 'approval') ? COMMENT_NOT_PUBLISHED : COMMENT_PUBLISHED;
961   $form['header'] = array('#type' => 'value', '#value' => array(
962     NULL,
963     array('data' => t('Subject'), 'field' => 'subject'),
964     array('data' => t('Author'), 'field' => 'name'),
965     array('data' => t('Time'), 'field' => 'timestamp', 'sort' => 'desc'),
966     array('data' => t('Operations'))
967   ));
968   $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);
969
970   // build a table listing the appropriate comments
971   $destination = drupal_get_destination();
972   while ($comment = db_fetch_object($result)) {
973     $comments[$comment->cid] = '';
974     $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
975     $form['subject'][$comment->cid] = array('#value' => l($comment->subject, 'node/'. $comment->nid, array('title' => truncate_utf8($comment->comment, 128)), NULL, 'comment-'. $comment->cid));
976     $form['username'][$comment->cid] = array('#value' => theme('username', $comment));
977     $form['timestamp'][$comment->cid] = array('#value' => format_date($comment->timestamp, 'small'));
978     $form['operations'][$comment->cid] = array('#value' => l(t('edit'), 'comment/edit/'. $comment->cid, array(), $destination));
979   }
980   $form['comments'] = array('#type' => 'checkboxes', '#options' => $comments);
981   $form['pager'] = array('#value' => theme('pager', NULL, 50, 0));
982   return drupal_get_form('comment_admin_overview', $form);
983 }
984
985 /**
986  * We can't execute any 'Update options' if no comments were selected.
987  */
988 function comment_admin_overview_validate($form_id, $edit) {
989   $edit['comments'] = array_diff($edit['comments'], array(0));
990   if (count($edit['comments']) == 0) {
991     form_set_error('', t('Please select one or more comments to perform the update on.'));
992     drupal_goto('admin/comment');
993   }
994 }
995
996 /**
997  * Execute the chosen 'Update option' on the selected comments, such as
998  * publishing, unpublishing or deleting.
999  */
1000 function comment_admin_overview_submit($form_id, $edit) {
1001   $operations = comment_operations();
1002   if ($operations[$edit['operation']][1]) {
1003     // extract the appropriate database query operation
1004     $query = $operations[$edit['operation']][1];
1005     foreach ($edit['comments'] as $cid => $value) {
1006       if ($value) {
1007         // perform the update action, then refresh node statistics
1008         db_query($query, $cid);
1009         $comment = _comment_load($cid);
1010         _comment_update_node_statistics($comment->nid);
1011         // Allow modules to respond to the updating of a comment.
1012         comment_invoke_comment($comment, $edit['operation']);
1013         // Add an entry to the watchdog log.
1014         watchdog('content', t('Comment: updated %subject.', array('%subject' => theme('placeholder', $comment->subject))), WATCHDOG_NOTICE, l(t('view'), 'node/'. $comment->nid, NULL, NULL, 'comment-'. $comment->cid));
1015       }
1016     }
1017     cache_clear_all();
1018     drupal_set_message(t('The update has been performed.'));
1019     drupal_goto('admin/comment');
1020   }
1021 }
1022
1023 function theme_comment_admin_overview($form) {
1024   $output = form_render($form['options']);
1025   if (isset($form['subject']) && is_array($form['subject'])) {
1026     foreach (element_children($form['subject']) as $key) {
1027       $row = array();
1028       $row[] = form_render($form['comments'][$key]);
1029       $row[] = form_render($form['subject'][$key]);
1030       $row[] = form_render($form['username'][$key]);
1031       $row[] = form_render($form['timestamp'][$key]);
1032       $row[] = form_render($form['operations'][$key]);
1033       $rows[] = $row;
1034     }
1035   }
1036   else {
1037     $rows[] = array(array('data' => t('No comments available.'), 'colspan' => '6'));
1038   }
1039
1040   $output .= theme('table', $form['header']['#value'], $rows);
1041   if ($form['pager']['#value']) {
1042     $output .= form_render($form['pager']);
1043   }
1044
1045   $output .= form_render($form);
1046
1047   return $output;
1048 }
1049
1050 /**
1051  * List the selected comments and verify that the admin really wants to delete
1052  * them.
1053  */
1054 function comment_multiple_delete_confirm() {
1055   $edit = $_POST['edit'];
1056
1057   $form['comments'] = array('#prefix' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE);
1058   // array_filter() returns only elements with actual values
1059   $comment_counter = 0;
1060   foreach (array_filter($edit['comments']) as $cid => $value) {
1061     $comment = _comment_load($cid);
1062     if (is_object($comment) && is_numeric($comment->cid)) {
1063       $subject = db_result(db_query('SELECT subject FROM {comments} WHERE cid = %d', $cid));
1064       $form['comments'][$cid] = array('#type' => 'hidden', '#value' => $cid, '#prefix' => '<li>', '#suffix' => check_plain($subject) .'</li>');
1065       $comment_counter++;
1066     }
1067   }
1068   $form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
1069
1070   if (!$comment_counter) {
1071     drupal_set_message(t('There do not appear to be any comments to delete or your selected comment was deleted by another administrator.'));
1072     drupal_goto('admin/comment');
1073   }
1074   else {
1075     return confirm_form('comment_multiple_delete_confirm', $form,
1076                         t('Are you sure you want to delete these comments and all their children?'),
1077                         'admin/comment', t('This action cannot be undone.'),
1078                         t('Delete comments'), t('Cancel'));
1079   }
1080 }
1081
1082 /**
1083  * Perform the actual comment deletion.
1084  */
1085 function comment_multiple_delete_confirm_submit($form_id, $edit) {
1086   if ($edit['confirm']) {
1087     foreach ($edit['comments'] as $cid => $value) {
1088       $comment = _comment_load($cid);
1089       _comment_delete_thread($comment);
1090       _comment_update_node_statistics($comment->nid);
1091       cache_clear_all();
1092     }
1093     drupal_set_message(t('The comments have been deleted.'));
1094   }
1095   drupal_goto('admin/comment');
1096 }
1097
1098 /**
1099 *** misc functions: helpers, privates, history
1100 **/
1101
1102 /**
1103  * Load the entire comment by cid.
1104  */
1105 function _comment_load($cid) {
1106   return db_fetch_object(db_query('SELECT * FROM {comments} WHERE cid = %d', $cid));
1107 }
1108
1109 function comment_num_all($nid) {
1110   static $cache;
1111
1112   if (!isset($cache[$nid])) {
1113     $cache[$nid] = db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $nid));
1114   }
1115   return $cache[$nid];
1116 }
1117
1118 function comment_num_replies($pid) {
1119   static $cache;
1120
1121   if (!isset($cache[$pid])) {
1122     $cache[$pid] = db_result(db_query('SELECT COUNT(cid) FROM {comments} WHERE pid = %d AND status = %d', $pid, COMMENT_PUBLISHED));
1123   }
1124
1125   return $cache[$pid];
1126 }
1127
1128 /**
1129  * get number of new comments for current user and specified node
1130  *
1131  * @param $nid node-id to count comments for
1132  * @param $timestamp time to count from (defaults to time of last user access
1133  *   to node)
1134  */
1135 function comment_num_new($nid, $timestamp = 0) {
1136   global $user;
1137
1138   if ($user->uid) {
1139     // Retrieve the timestamp at which the current user last viewed the
1140     // specified node.
1141     if (!$timestamp) {
1142       $timestamp = node_last_viewed($nid);
1143     }
1144     $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT);
1145
1146     // Use the timestamp to retrieve the number of new comments.
1147     $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));
1148
1149     return $result;
1150   }
1151   else {
1152     return 0;
1153   }
1154
1155 }
1156
1157 function comment_validate($edit) {
1158   global $user;
1159
1160   // Invoke other validation handlers
1161   comment_invoke_comment($edit, 'validate');
1162
1163   if (isset($edit['date'])) {
1164     // As of PHP 5.1.0, strtotime returns FALSE upon failure instead of -1.
1165     if (strtotime($edit['date']) <= 0) {
1166       form_set_error('date', t('You have to specify a valid date.'));
1167     }
1168   }
1169   if (isset($edit['author']) && !$account = user_load(array('name' => $edit['author']))) {
1170     form_set_error('author', t('You have to specify a valid author.'));
1171   }
1172
1173   // Check validity of name, mail and homepage (if given)
1174   if (!$user->uid || isset($edit['is_anonymous'])) {
1175     if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) > COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
1176       if ($edit['name']) {
1177         $taken = db_result(db_query("SELECT COUNT(uid) FROM {users} WHERE LOWER(name) = '%s'", $edit['name']), 0);
1178
1179         if ($taken != 0) {
1180           form_set_error('name', t('The name you used belongs to a registered user.'));
1181         }
1182
1183       }
1184       else if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
1185         form_set_error('name', t('You have to leave your name.'));
1186       }
1187
1188       if ($edit['mail']) {
1189         if (!valid_email_address($edit['mail'])) {
1190           form_set_error('mail', t('The e-mail address you specified is not valid.'));
1191         }
1192       }
1193       else if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
1194         form_set_error('mail', t('You have to leave an e-mail address.'));
1195       }
1196
1197       if ($edit['homepage']) {
1198         if (!valid_url($edit['homepage'], TRUE)) {
1199           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>.'));
1200         }
1201       }
1202     }
1203   }
1204
1205   return $edit;
1206 }
1207
1208 /*
1209 ** Generate the basic commenting form, for appending to a node or display on a separate page.
1210 ** This is rendered by theme_comment_form.
1211 */
1212
1213 function comment_form($edit, $title = NULL) {
1214   global $user;
1215
1216   $op = isset($_POST['op']) ? $_POST['op'] : '';
1217
1218   if ($user->uid) {
1219     if ($edit['cid'] && user_access('administer comments')) {
1220       if ($edit['author']) {
1221         $author = $edit['author'];
1222       }
1223       elseif ($edit['name']) {
1224         $author = $edit['name'];
1225       }
1226       else {
1227         $author = $edit['registered_name'];
1228       }
1229
1230       if ($edit['status']) {
1231         $status = $edit['status'];
1232       }
1233       else {
1234         $status = 0;
1235       }
1236
1237       if ($edit['date']) {
1238         $date = $edit['date'];
1239       }
1240       else {
1241         $date = format_date($edit['timestamp'], 'custom', 'Y-m-d H:i O');
1242       }
1243
1244       $form['admin'] = array(
1245         '#type' => 'fieldset',
1246         '#title' => t('Administration'),
1247         '#collapsible' => TRUE,
1248         '#collapsed' => TRUE,
1249         '#weight' => -2,
1250       );
1251
1252       if ($edit['registered_name'] != '') {
1253         // The comment is by a registered user
1254         $form['admin']['author'] = array(
1255           '#type' => 'textfield',
1256           '#title' => t('Authored by'),
1257           '#size' => 30,
1258           '#maxlength' => 60,
1259           '#autocomplete_path' => 'user/autocomplete',
1260           '#default_value' => $author,
1261           '#weight' => -1,
1262         );
1263       }
1264       else {
1265         // The comment is by an anonymous user
1266         $form['is_anonymous'] = array(
1267           '#type' => 'value',
1268           '#value' => TRUE,
1269         );
1270         $form['admin']['name'] = array(
1271           '#type' => 'textfield',
1272           '#title' => t('Authored by'),
1273           '#size' => 30,
1274           '#maxlength' => 60,
1275           '#default_value' => $author,
1276           '#weight' => -1,
1277         );
1278         $form['admin']['mail'] = array(
1279           '#type' => 'textfield',
1280           '#title' => t('E-mail'),
1281           '#maxlength' => 64,
1282           '#size' => 30,
1283           '#default_value' => $edit['mail'],
1284           '#description' => t('The content of this field is kept private and will not be shown publicly.'),
1285         );
1286
1287         $form['admin']['homepage'] = array(
1288           '#type' => 'textfield',
1289           '#title' => t('Homepage'),
1290           '#maxlength' => 255,
1291           '#size' => 30,
1292           '#default_value' => $edit['homepage'],
1293         );
1294       }
1295
1296       $form['admin']['date'] = array('#type' => 'textfield', '#parents' => array('date'), '#title' => t('Authored on'), '#size' => 20, '#maxlength' => 25, '#default_value' => $date, '#weight' => -1);
1297
1298       $form['admin']['status'] = array('#type' => 'radios', '#parents' => array('status'), '#title' => t('Status'), '#default_value' =>  $status, '#options' => array(t('Published'), t('Not published')), '#weight' => -1);
1299
1300     }
1301     else {
1302       $form['_author'] = array('#type' => 'item', '#title' => t('Your name'), '#value' => theme('username', $user)
1303       );
1304       $form['author'] = array('#type' => 'value', '#value' => $user->name);
1305     }
1306   }
1307   else if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MAY_CONTACT) {
1308     $form['name'] = array('#type' => 'textfield', '#title' => t('Your name'), '#maxlength' => 60, '#size' => 30, '#default_value' => $edit['name'] ? $edit['name'] : variable_get('anonymous', 'Anonymous')
1309     );
1310
1311     $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.')
1312     );
1313
1314     $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
1315   }
1316   else if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
1317     $form['name'] = array('#type' => 'textfield', '#title' => t('Your name'), '#maxlength' => 60, '#size' => 30, '#default_value' => $edit['name'] ? $edit['name'] : variable_get('anonymous', 'Anonymous'), '#required' => TRUE);
1318
1319     $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);
1320
1321     $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
1322   }
1323
1324   if (variable_get('comment_subject_field', 1) == 1) {
1325     $form['subject'] = array('#type' => 'textfield', '#title' => t('Subject'), '#maxlength' => 64, '#default_value' => $edit['subject']);
1326   }
1327
1328   $form['comment_filter']['comment'] = array('#type' => 'textarea', '#title' => t('Comment'), '#rows' => 15, '#default_value' => $edit['comment'] ? $edit['comment'] : $user->signature, '#required' => TRUE);
1329   $form['comment_filter']['format'] = filter_form($edit['format']);
1330
1331   $form['cid'] = array('#type' => 'value', '#value' => $edit['cid']);
1332   $form['pid'] = array('#type' => 'value', '#value' => $edit['pid']);
1333   $form['nid'] = array('#type' => 'value', '#value' => $edit['nid']);
1334   $form['uid'] = array('#type' => 'value', '#value' => $edit['uid']);
1335
1336   $form['preview'] = array('#type' => 'button', '#value' => t('Preview comment'), '#weight' => 19);
1337   $form['#token'] = 'comment' . $edit['nid'] . $edit['pid'];
1338
1339   // Only show post button if preview is optional or if we are in preview mode.
1340   // We show the post button in preview mode even if there are form errors so that
1341   // optional form elements (e.g., captcha) can be updated in preview mode.
1342   if (!form_get_errors() && ((variable_get('comment_preview', COMMENT_PREVIEW_REQUIRED) == COMMENT_PREVIEW_OPTIONAL) || ($op == t('Preview comment')) || ($op == t('Post comment')))) {
1343     $form['submit'] = array('#type' => 'submit', '#value' => t('Post comment'), '#weight' => 20);
1344   }
1345
1346   if ($op == t('Preview comment')) {
1347     $form['#after_build'] = array('comment_form_add_preview');
1348   }
1349
1350   if ($_REQUEST['destination']) {
1351     $form['#attributes']['destination'] = $_REQUEST['destination'];
1352   }
1353
1354   if (empty($edit['cid']) && empty($edit['pid'])) {
1355     $form['#action'] = url('comment/reply/'. $edit['nid']);
1356   }
1357
1358   // Graft in extra form additions
1359   $form = array_merge($form, comment_invoke_comment($form, 'form'));
1360
1361   return theme('box', $title, drupal_get_form('comment_form', $form));
1362 }
1363
1364 function comment_form_add_preview($form, $edit) {
1365   global $user;
1366
1367   drupal_set_title(t('Preview comment'));
1368
1369   $output = '';
1370
1371   comment_validate($edit);
1372   $comment = (object)_comment_form_submit($edit);
1373
1374   // Attach the user and time information.
1375   if ($edit['author']) {
1376     $account = user_load(array('name' => $edit['author']));
1377   }
1378   elseif ($user->uid && !isset($edit['is_anonymous'])) {
1379     $account = $user;
1380   }
1381   if ($account) {
1382     $comment->uid = $account->uid;
1383     $comment->name = check_plain($account->name);
1384   }
1385   $comment->timestamp = $edit['timestamp'] ? $edit['timestamp'] : time();
1386
1387   // Preview the comment with security check.
1388   if (!form_get_errors()) {
1389     $output .= theme('comment_view', $comment);
1390   }
1391   $form['comment_preview'] = array(
1392     '#value' => $output,
1393     '#weight' => -100,
1394     '#prefix' => '<div class="preview">',
1395     '#suffix' => '</div>',
1396   );
1397
1398   $output = '';
1399
1400   if ($edit['pid']) {
1401     $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));
1402     $comment = drupal_unpack($comment);
1403     $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1404     $output .= theme('comment_view', $comment);
1405   }
1406   else {
1407     $form['#suffix'] = node_view(node_load($edit['nid']));
1408     $edit['pid'] = 0;
1409   }
1410
1411   $form['comment_preview_below'] = array('#value' => $output, '#weight' => 100);
1412
1413   return $form;
1414 }
1415
1416 function comment_form_validate($form_id, $form_values) {
1417   comment_validate($form_values);
1418 }
1419
1420 function _comment_form_submit($form_values) {
1421   if (!isset($form_values['date'])) {
1422     $form_values['date'] = 'now';
1423   }
1424   $form_values['timestamp'] = strtotime($form_values['date']);
1425   if (isset($form_values['author'])) {
1426     $account = user_load(array('name' => $form_values['author']));
1427     $form_values['uid'] = $account->uid;
1428     $form_values['name'] = $form_values['author'];
1429   }
1430   // Validate the comment's subject.  If not specified, extract
1431   // one from the comment's body.
1432   if (trim($form_values['subject']) == '') {
1433     // The body may be in any format, so we:
1434     // 1) Filter it into HTML
1435     // 2) Strip out all HTML tags
1436     // 3) Convert entities back to plain-text.
1437   // Note: format is checked by check_markup().
1438     $form_values['subject'] = truncate_utf8(decode_entities(strip_tags(check_markup($form_values['comment'], $form_values['format']))), 29, TRUE);
1439   }
1440   return $form_values;
1441 }
1442
1443 function comment_form_submit($form_id, $form_values) {
1444   $form_values = _comment_form_submit($form_values);
1445   if ($cid = comment_save($form_values)) {
1446     return array('node/'. $form_values['nid'], NULL, "comment-$cid");
1447   }
1448 }
1449
1450 /*
1451 ** Renderer or visualization functions this can be optionally
1452 ** overridden by themes.
1453 */
1454
1455 function theme_comment_preview($comment, $links = array(), $visible = 1) {
1456   $output = '<div class="preview">';
1457   $output .= theme('comment_view', $comment, $links, $visible);
1458   $output .= '</div>';
1459   return $output;
1460 };
1461
1462 function theme_comment_view($comment, $links = array(), $visible = 1) {
1463
1464   // Emit selectors:
1465   $output = '';
1466   if (($comment->new = node_mark($comment->nid, $comment->timestamp)) != MARK_READ) {
1467     $output .= "<a id=\"new\"></a>\n";
1468   }
1469
1470   $output .= "<a id=\"comment-$comment->cid\"></a>\n";
1471
1472   // Switch to folded/unfolded view of the comment
1473   if ($visible) {
1474     $comment->comment = check_markup($comment->comment, $comment->format, FALSE);
1475
1476     // Comment API hook
1477     comment_invoke_comment($comment, 'view');
1478
1479     $output .= theme('comment', $comment, $links);
1480   }
1481   else {
1482     $output .= theme('comment_folded', $comment);
1483   }
1484
1485   return $output;
1486 }
1487
1488 function comment_controls($mode = COMMENT_MODE_THREADED_EXPANDED, $order = COMMENT_ORDER_NEWEST_FIRST, $comments_per_page = 50) {
1489   $form['mode'] = array('#type' => 'select',
1490     '#default_value' => $mode,
1491     '#options' => _comment_get_modes(),
1492     '#weight' => 1,
1493   );
1494   $form['order'] = array(
1495     '#type' => 'select',
1496     '#default_value' => $order,
1497     '#options' => _comment_get_orders(),
1498     '#weight' => 2,
1499   );
1500   foreach (_comment_per_page() as $i) {
1501     $options[$i] = t('%a comments per page', array('%a' => $i));
1502   }
1503   $form['comments_per_page'] = array('#type' => 'select',
1504     '#default_value' => $comments_per_page,
1505     '#options' => $options,
1506     '#weight' => 3,
1507   );
1508
1509   $form['submit'] = array('#type' => 'submit',
1510     '#value' => t('Save settings'),
1511     '#weight' => 20,
1512   );
1513
1514   return drupal_get_form('comment_controls', $form);
1515 }
1516
1517 function theme_comment_controls($form) {
1518   $output .= '<div class="container-inline">';
1519   $output .=  form_render($form);
1520   $output .= '</div>';
1521   $output .= '<div class="description">'. t('Select your preferred way to display the comments and click "Save settings" to activate your changes.') .'</div>';
1522   return theme('box', t('Comment viewing options'), $output);
1523 }
1524
1525 function comment_controls_submit($form_id, $form_values) {
1526   global $user;
1527
1528   $mode = $form_values['mode'];
1529   $order = $form_values['order'];
1530   $comments_per_page = $form_values['comments_per_page'];
1531
1532   if ($user->uid) {
1533     $user = user_save($user, array('mode' => $mode, 'sort' => $order, 'comments_per_page' => $comments_per_page));
1534   }
1535   else {
1536     $_SESSION['comment_mode'] = $mode;
1537     $_SESSION['comment_sort'] = $order;
1538     $_SESSION['comment_comments_per_page'] = $comments_per_page;
1539   }
1540 }
1541
1542 function theme_comment($comment, $links = array()) {
1543   $output  = '<div class="comment'. ($comment->status == COMMENT_NOT_PUBLISHED ? ' comment-unpublished' : '') .'">';
1544   $output .= '<div class="subject">'. l($comment->subject, $_GET['q'], NULL, NULL, "comment-$comment->cid") . ' ' . theme('mark', $comment->new) ."</div>\n";
1545   $output .= '<div class="credit">'. t('by %a on %b', array('%a' => theme('username', $comment), '%b' => format_date($comment->timestamp))) ."</div>\n";
1546   $output .= '<div class="body">'. $comment->comment .'</div>';
1547   $output .= '<div class="links">'. theme('links', $links) .'</div>';
1548   $output .= '</div>';
1549   return $output;
1550 }
1551
1552 function theme_comment_folded($comment) {
1553   $output  = "<div class=\"comment-folded\">\n";
1554   $output .= ' <span class="subject">'. l($comment->subject, comment_node_url() .'/'. $comment->cid, NULL, NULL, "comment-$comment->cid") . ' '. theme('mark', $comment->new) .'</span> ';
1555   $output .= '<span class="credit">'. t('by') .' '. theme('username', $comment) ."</span>\n";
1556   $output .= "</div>\n";
1557   return $output;
1558 }
1559
1560 function theme_comment_flat_collapsed($comment) {
1561   return theme('comment_view', $comment, '', 0);
1562   return '';
1563 }
1564
1565 function theme_comment_flat_expanded($comment) {
1566   return theme('comment_view', $comment, module_invoke_all('link', 'comment', $comment, 0));
1567 }
1568
1569 function theme_comment_thread_collapsed($comment) {
1570   $output  = '<div style="margin-left:'. ($comment->depth * 25) ."px;\">\n";
1571   $output .= theme('comment_view', $comment, '', 0);
1572   $output .= "</div>\n";
1573   return $output;
1574 }
1575
1576 function theme_comment_thread_expanded($comment) {
1577   $output = '';
1578   if ($comment->depth) {
1579     $output .= '<div style="margin-left:'. ($comment->depth * 25) ."px;\">\n";
1580   }
1581
1582   $output .= theme('comment_view', $comment, module_invoke_all('link', 'comment', $comment, 0));
1583
1584   if ($comment->depth) {
1585     $output .= "</div>\n";
1586   }
1587   return $output;
1588 }
1589
1590 function theme_comment_post_forbidden($nid) {
1591   global $user;
1592   if ($user->uid) {
1593     return t("you can't post comments");
1594   }
1595   else {
1596     // we cannot use drupal_get_destination() because these links sometimes appear on /node and taxo listing pages
1597     if (variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
1598       $destination = "destination=". drupal_urlencode("comment/reply/$nid#comment_form");
1599     }
1600     else {
1601       $destination = "destination=". drupal_urlencode("node/$nid#comment_form");
1602     }
1603
1604     if (variable_get('user_register', 1)) {
1605       return t('<a href="%login">login</a> or <a href="%register">register</a> to post comments', array('%login' => url('user/login', $destination), '%register' => check_url(url('user/register', $destination))));
1606     }
1607     else {
1608       return t('<a href="%login">login</a> to post comments', array('%login' => check_url(url('user/login', $destination))));
1609     }
1610   }
1611 }
1612
1613 function _comment_delete_thread($comment) {
1614   if (!is_object($comment) || !is_numeric($comment->cid)) {
1615     watchdog('content', t('Can not delete non-existent comment.'), WATCHDOG_WARNING);
1616     return;
1617   }
1618
1619   // Delete the comment:
1620   db_query('DELETE FROM {comments} WHERE cid = %d', $comment->cid);
1621   watchdog('content', t('Comment: deleted %subject.', array('%subject' => theme('placeholder', $comment->subject))));
1622
1623   comment_invoke_comment($comment, 'delete');
1624
1625   // Delete the comment's replies
1626   $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);
1627   while ($comment = db_fetch_object($result)) {
1628     $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
1629     _comment_delete_thread($comment);
1630   }
1631 }
1632
1633 /**
1634  * Return an array of viewing modes for comment listings.
1635  *
1636  * We can't use a global variable array because the locale system
1637  * is not initialized yet when the comment module is loaded.
1638  */
1639 function _comment_get_modes() {
1640   return array(
1641     COMMENT_MODE_FLAT_COLLAPSED => t('Flat list - collapsed'),
1642     COMMENT_MODE_FLAT_EXPANDED => t('Flat list - expanded'),
1643     COMMENT_MODE_THREADED_COLLAPSED => t('Threaded list - collapsed'),
1644     COMMENT_MODE_THREADED_EXPANDED => t('Threaded list - expanded')
1645   );
1646 }
1647
1648 /**
1649  * Return an array of viewing orders for comment listings.
1650  *
1651  * We can't use a global variable array because the locale system
1652  * is not initialized yet when the comment module is loaded.
1653  */
1654 function _comment_get_orders() {
1655   return array(
1656     COMMENT_ORDER_NEWEST_FIRST => t('Date - newest first'),
1657     COMMENT_ORDER_OLDEST_FIRST => t('Date - oldest first')
1658   );
1659 }
1660
1661 /**
1662  * Return an array of "comments per page" settings from which the user
1663  * can choose.
1664  */
1665 function _comment_per_page() {
1666   return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300));
1667 }
1668
1669 /**
1670  * Return a current comment display setting
1671  *
1672  * $setting can be one of these: 'mode', 'sort', 'comments_per_page'
1673  */
1674 function _comment_get_display_setting($setting) {
1675   global $user;
1676
1677   if ($_GET[$setting]) {
1678     $value = $_GET[$setting];
1679   }
1680   else {
1681     // get the setting's site default
1682     switch ($setting) {
1683       case 'mode':
1684         $default = variable_get('comment_default_mode', COMMENT_MODE_THREADED_EXPANDED);
1685         break;
1686       case 'sort':
1687         $default = variable_get('comment_default_order', COMMENT_ORDER_NEWEST_FIRST);
1688         break;
1689       case 'comments_per_page':
1690         $default = variable_get('comment_default_per_page', '50');
1691     }
1692     if (variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN) == COMMENT_CONTROLS_HIDDEN) {
1693       // if comment controls are disabled use site default
1694       $value = $default;
1695     }
1696     else {
1697       // otherwise use the user's setting if set
1698       if ($user->$setting) {
1699         $value = $user->$setting;
1700       }
1701       else if ($_SESSION['comment_'. $setting]) {
1702         $value = $_SESSION['comment_'. $setting];
1703       }
1704       else {
1705         $value = $default;
1706       }
1707     }
1708   }
1709   return $value;
1710 }
1711
1712 /**
1713  * Updates the comment statistics for a given node.  This should be called any
1714  * time a comment is added, deleted, or updated.
1715  *
1716  * The following fields are contained in the node_comment_statistics table.
1717  * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node.
1718  * - last_comment_name: the name of the anonymous poster for the last comment
1719  * - 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.
1720  * - comment_count: the total number of approved/published comments on this node.
1721  */
1722 function _comment_update_node_statistics($nid) {
1723   $count = db_result(db_query('SELECT COUNT(cid) FROM {comments} WHERE nid = %d AND status = %d', $nid, COMMENT_PUBLISHED));
1724
1725   // comments exist
1726   if ($count > 0) {
1727     $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));
1728     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);
1729   }
1730
1731   // no comments
1732   else {
1733     $node = db_fetch_object(db_query("SELECT uid, created FROM {node} WHERE nid = %d", $nid));
1734     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);
1735   }
1736 }
1737
1738 /**
1739  * Invoke a hook_comment() operation in all modules.
1740  *
1741  * @param &$comment
1742  *   A comment object.
1743  * @param $op
1744  *   A string containing the name of the comment operation.
1745  * @return
1746  *   The returned value of the invoked hooks.
1747  */
1748 function comment_invoke_comment(&$comment, $op) {
1749   $return = array();
1750   foreach (module_implements('comment') as $name) {
1751     $function = $name .'_comment';
1752     $result = $function($comment, $op);
1753     if (isset($result) && is_array($result)) {
1754       $return = array_merge($return, $result);
1755     }
1756     else if (isset($result)) {
1757       $return[] = $result;
1758     }
1759   }
1760   return $return;
1761 }
1762
1763 /**
1764  * Generate vancode.
1765  *
1766  * Consists of a leading character indicating length, followed by N digits
1767  * with a numerical value in base 36. Vancodes can be sorted as strings
1768  * without messing up numerical order.
1769  *
1770  * It goes:
1771  * 00, 01, 02, ..., 0y, 0z,
1772  * 110, 111, ... , 1zy, 1zz,
1773  * 2100, 2101, ..., 2zzy, 2zzz,
1774  * 31000, 31001, ...
1775  */
1776 function int2vancode($i = 0) {
1777   $num = base_convert((int)$i, 10, 36);
1778   $length = strlen($num);
1779   return chr($length + ord('0') - 1) . $num;
1780 }
1781
1782 /**
1783  * Decode vancode back to an integer.
1784  */
1785 function vancode2int($c = '00') {
1786   return base_convert(substr($c, 1), 36, 10);
1787 }