2 // $Id: comment.module 144 2007-03-28 07:52:20Z thierry $
6 * Enables users to comment on published content.
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.
14 * Constants to define a comment's published state
16 define('COMMENT_PUBLISHED', 0);
17 define('COMMENT_NOT_PUBLISHED', 1);
20 * Constants to define the viewing modes for comment listings
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);
28 * Constants to define the viewing orders for comment listings
30 define('COMMENT_ORDER_NEWEST_FIRST', 1);
31 define('COMMENT_ORDER_OLDEST_FIRST', 2);
34 * Constants to define the position of the comment controls
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);
42 * Constants to define the anonymous poster contact handling
44 define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
45 define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
46 define('COMMENT_ANONYMOUS_MUST_CONTACT', 2);
49 * Constants to define the comment form location
51 define('COMMENT_FORM_SEPARATE_PAGE', 0);
52 define('COMMENT_FORM_BELOW', 1);
55 * Constants to define a node's comment state
57 define('COMMENT_NODE_DISABLED', 0);
58 define('COMMENT_NODE_READ_ONLY', 1);
59 define('COMMENT_NODE_READ_WRITE', 2);
62 * Constants to define if comment preview is optional or required
64 define('COMMENT_PREVIEW_OPTIONAL', 0);
65 define('COMMENT_PREVIEW_REQUIRED', 1);
68 * Implementation of hook_help().
70 function comment_help($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>
77 <li>control access for various comment module functions through access permissions <a href="%admin-access">administer >> access control</a>.</li>
78 <li>administer comments <a href="%admin-comment-configure"> administer >> comments >> configure</a>.</li>
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>';
83 case 'admin/modules#description':
84 return t('Allows users to comment on and discuss published content.');
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>");
97 * Implementation of hook_menu().
99 function comment_menu($may_cache) {
103 $access = user_access('administer comments');
104 $items[] = array('path' => 'admin/comment', 'title' => t('comments'),
105 'callback' => 'comment_admin_overview', 'access' => $access);
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);
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);
121 $items[] = array('path' => 'admin/comment/configure/settings', 'title' => t('settings'),
122 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
124 $items[] = array('path' => 'comment/delete', 'title' => t('delete comment'),
125 'callback' => 'comment_delete', 'access' => $access, 'type' => MENU_CALLBACK);
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);
132 if (arg(0) == 'comment' && arg(1) == 'reply' && is_numeric(arg(2))) {
133 $node = node_load(arg(2));
135 $items[] = array('path' => 'comment/reply', 'title' => t('reply to comment'),
136 'callback' => 'comment_reply', 'access' => node_access('view', $node), 'type' => MENU_CALLBACK);
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);
150 * Implementation of hook_perm().
152 function comment_perm() {
153 return array('access comments', 'post comments', 'administer comments', 'post comments without approval');
157 * Implementation of hook_block().
159 * Generates a block with the most recent comments.
161 function comment_block($op = 'list', $delta = 0) {
163 $blocks[0]['info'] = t('Recent comments');
166 else if ($op == 'view' && user_access('access comments')) {
167 $block['subject'] = t('Recent comments');
168 $block['content'] = theme('comment_block');
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);
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)));
179 return theme('item_list', $items);
183 * Implementation of hook_link().
185 function comment_link($type, $node = 0, $main = 0) {
188 if ($type == 'node' && $node->comment) {
191 // Main page: display the number of comments that have been posted.
193 if (user_access('access comments')) {
194 $all = comment_num_all($node->nid);
195 $new = comment_num_new($node->nid);
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');
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');
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');
210 $links[] = theme('comment_post_forbidden', $node->nid);
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
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');
227 $links[] = theme('comment_post_forbidden', $node->nid);
233 if ($type == 'comment') {
234 $links = comment_links($node, $main);
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.'));
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,
255 $form['comment_settings']['comment'] = array(
257 '#parents' => array('comment'),
258 '#default_value' => $node->comment,
259 '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')),
263 $form['comment_settings']['comment'] = array(
265 '#value' => $node->comment,
273 * Implementation of hook_nodeapi().
276 function comment_nodeapi(&$node, $op, $arg = 0) {
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));
283 if (!isset($node->comment)) {
284 $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
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);
293 db_query('DELETE FROM {comments} WHERE nid = %d', $node->nid);
294 db_query('DELETE FROM {node_comment_statistics} WHERE nid = %d', $node->nid);
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);
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');
310 return array(array('key' => 'comments', 'value' => url('node/'. $node->nid, NULL, 'comment', TRUE)));
315 * Implementation of hook_user().
317 * Provides signature customization for the user's comments.
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,
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.'));
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);
342 * Menu callback; presents the comment settings page.
344 function comment_configure() {
345 $form['viewing_options'] = array(
346 '#type' => 'fieldset',
347 '#title' => t('Viewing options'),
348 '#collapsible' => TRUE,
349 '#collapsed' => TRUE,
352 $form['viewing_options']['comment_default_mode'] = array(
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.'),
360 $form['viewing_options']['comment_default_order'] = array(
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.'),
368 $form['viewing_options']['comment_default_per_page'] = array(
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.'),
376 $form['viewing_options']['comment_controls'] = array(
378 '#title' => t('Comment controls'),
379 '#default_value' => variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN),
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.'),
388 $form['posting_settings'] = array(
389 '#type' => 'fieldset',
390 '#title' => t('Posting settings'),
391 '#collapsible' => TRUE,
392 '#collapsed' => TRUE,
395 $form['posting_settings']['comment_anonymous'] = array(
397 '#title' => t('Anonymous commenting'),
398 '#default_value' => variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT),
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'))),
405 if (!user_access('post comments', user_load(array('uid' => 0)))) {
406 $form['posting_settings']['comment_anonymous']['#attributes'] = array('disabled' => 'disabled');
409 $form['posting_settings']['comment_subject_field'] = array(
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?'),
417 $form['posting_settings']['comment_preview'] = array(
419 '#title' => t('Preview comment'),
420 '#default_value' => variable_get('comment_preview', COMMENT_PREVIEW_REQUIRED),
421 '#options' => array(t('Optional'), t('Required')),
424 $form['posting_settings']['comment_form_location'] = array(
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')),
431 return system_settings_form('comment_settings_form', $form);
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.
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.
442 function comment_access($op, $comment) {
446 return ($user->uid && $user->uid == $comment->uid && comment_num_replies($comment->cid) == 0) || user_access('administer comments');
450 function comment_node_url() {
451 return arg(0) .'/'. arg(1);
454 function comment_edit($cid) {
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);
464 drupal_access_denied();
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")));
473 $op = isset($_POST['op']) ? $_POST['op'] : '';
477 // or are we merely showing the form?
478 if (user_access('access comments')) {
480 if ($op == t('Preview comment')) {
481 if (user_access('post comments')) {
482 $output .= comment_form(array('pid' => $pid, 'nid' => $nid), NULL);
485 drupal_set_message(t('You are not authorized to post comments.'), 'error');
486 drupal_goto("node/$nid");
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.
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");
499 $comment = drupal_unpack($comment);
500 $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
501 $output .= theme('comment_view', $comment);
504 drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
505 drupal_goto("node/$nid");
508 else if (user_access('access content')) {
509 $output .= node_view($node);
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");
517 else if (user_access('post comments')) {
518 $output .= comment_form(array('pid' => $pid, 'nid' => $nid), t('Reply'));
521 drupal_set_message(t('You are not authorized to post comments.'), 'error');
522 drupal_goto("node/$nid");
527 drupal_set_message(t('You are not authorized to view comments.'), 'error');
528 drupal_goto("node/$nid");
535 * Accepts a submission of new or changed comment content.
541 * If the comment is successfully saved the comment ID is returned. If the comment
542 * is not saved, FALSE is returned.
544 function comment_save($edit) {
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);
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']);
559 _comment_update_node_statistics($edit['nid']);
561 // Allow modules to respond to the updating of a comment.
562 comment_invoke_comment($edit, 'update');
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']));
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());
574 foreach (array_intersect(array_keys($roles), array_keys($user->roles)) as $rid) {
575 $score = max($roles[$rid], $score);
578 $users = serialize(array(0 => $score));
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']));
587 // Strip the "/" from the end of the thread.
588 $max = rtrim($max, '/');
590 // Finally, build the thread field for this new comment.
591 $thread = int2vancode(vancode2int($max) + 1) .'/';
594 // This is comment with a parent comment: we increase
595 // the part of the thread value at the proper depth.
597 // Get the parent comment:
598 $parent = _comment_load($edit['pid']);
600 // Strip the "/" from the end of the parent thread.
601 $parent->thread = (string) rtrim((string) $parent->thread, '/');
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']));
607 // First child of this parent.
608 $thread = $parent->thread .'.'. int2vancode(0) .'/';
611 // Strip the "/" at the end of the thread.
612 $max = rtrim($max, '/');
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];
619 // Finally, build the thread field for this new comment.
620 $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
624 $edit['cid'] = db_next_id('{comments}_cid');
625 $edit['timestamp'] = time();
627 if ($edit['uid'] == $user->uid) {
628 $edit['name'] = $user->name;
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']);
633 _comment_update_node_statistics($edit['nid']);
635 // Tell the other modules a new comment has been submitted.
636 comment_invoke_comment($edit, 'insert');
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']));
642 // Clear the cache so an anonymous user can see his comment being added.
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.'));
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');
664 function comment_links($comment, $return = 1) {
669 // If we are viewing just this comment, we link back to the node.
671 $links[] = l(t('parent'), comment_node_url(), NULL, NULL, "comment-$comment->cid");
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");
680 else if (user_access('post comments')) {
681 if (comment_access('edit', $comment)) {
682 $links[] = l(t('edit'), "comment/edit/$comment->cid");
684 $links[] = l(t('reply'), "comment/reply/$comment->nid/$comment->cid");
687 $links[] = theme('comment_post_forbidden', $comment->nid);
694 function comment_render($node, $cid = 0) {
699 if (user_access('access comments')) {
700 // Pre-process variables.
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');
710 $output .= "<a id=\"comment\"></a>\n";
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;
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);
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));
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';
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;
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';
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
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
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.
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.
760 ** Now to get the ordering right, consider this example:
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
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
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:
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
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';
810 $query .= ' ORDER BY c.thread DESC';
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';
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.
825 $query .= ' ORDER BY SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))';
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);
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;
840 if ($mode == COMMENT_MODE_FLAT_COLLAPSED) {
841 $output .= theme('comment_flat_collapsed', $comment);
843 else if ($mode == COMMENT_MODE_FLAT_EXPANDED) {
844 $output .= theme('comment_flat_expanded', $comment);
846 else if ($mode == COMMENT_MODE_THREADED_COLLAPSED) {
847 $output .= theme('comment_thread_collapsed', $comment);
849 else if ($mode == COMMENT_MODE_THREADED_EXPANDED) {
850 $output .= theme('comment_thread_expanded', $comment);
854 $output .= theme('pager', NULL, $comments_per_page, 0);
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);
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'));
871 * Menu callback; delete a comment.
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;
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.'));
884 // Delete comment and its replies.
885 _comment_delete_thread($comment);
887 _comment_update_node_statistics($comment->nid);
889 // Clear the cache so an anonymous user sees that his comment was deleted.
892 drupal_goto("node/$comment->nid");
894 else if (is_object($comment) && is_numeric($comment->cid)) {
895 $output = confirm_form('comment_confirm_delete',
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.'),
904 drupal_set_message(t('The comment no longer exists.'));
911 * Comment operations. We offer different update operations depending on
912 * which comment administration page we're on.
914 function comment_operations($action = NULL) {
915 if ($action == 'publish') {
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'), '')
921 else if ($action == 'unpublish') {
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'), '')
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'), '')
938 * Menu callback; present an administrative comment listing.
940 function comment_admin_overview($type = 'new') {
941 $edit = $_POST['edit'];
943 if ($edit['operation'] == 'delete') {
944 return comment_multiple_delete_confirm();
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>'
953 foreach (comment_operations(arg(3) == 'approval' ? 'publish' : 'unpublish') as $key => $value) {
954 $options[$key] = $value[0];
956 $form['options']['operation'] = array('#type' => 'select', '#options' => $options, '#default_value' => 'publish');
957 $form['options']['submit'] = array('#type' => 'submit', '#value' => t('Update'));
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(
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'))
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);
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));
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);
986 * We can't execute any 'Update options' if no comments were selected.
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');
997 * Execute the chosen 'Update option' on the selected comments, such as
998 * publishing, unpublishing or deleting.
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) {
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));
1018 drupal_set_message(t('The update has been performed.'));
1019 drupal_goto('admin/comment');
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) {
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]);
1037 $rows[] = array(array('data' => t('No comments available.'), 'colspan' => '6'));
1040 $output .= theme('table', $form['header']['#value'], $rows);
1041 if ($form['pager']['#value']) {
1042 $output .= form_render($form['pager']);
1045 $output .= form_render($form);
1051 * List the selected comments and verify that the admin really wants to delete
1054 function comment_multiple_delete_confirm() {
1055 $edit = $_POST['edit'];
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>');
1068 $form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
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');
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'));
1083 * Perform the actual comment deletion.
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);
1093 drupal_set_message(t('The comments have been deleted.'));
1095 drupal_goto('admin/comment');
1099 *** misc functions: helpers, privates, history
1103 * Load the entire comment by cid.
1105 function _comment_load($cid) {
1106 return db_fetch_object(db_query('SELECT * FROM {comments} WHERE cid = %d', $cid));
1109 function comment_num_all($nid) {
1112 if (!isset($cache[$nid])) {
1113 $cache[$nid] = db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $nid));
1115 return $cache[$nid];
1118 function comment_num_replies($pid) {
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));
1125 return $cache[$pid];
1129 * get number of new comments for current user and specified node
1131 * @param $nid node-id to count comments for
1132 * @param $timestamp time to count from (defaults to time of last user access
1135 function comment_num_new($nid, $timestamp = 0) {
1139 // Retrieve the timestamp at which the current user last viewed the
1142 $timestamp = node_last_viewed($nid);
1144 $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT);
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));
1157 function comment_validate($edit) {
1160 // Invoke other validation handlers
1161 comment_invoke_comment($edit, 'validate');
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.'));
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.'));
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);
1180 form_set_error('name', t('The name you used belongs to a registered user.'));
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.'));
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.'));
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.'));
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>.'));
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.
1213 function comment_form($edit, $title = NULL) {
1216 $op = isset($_POST['op']) ? $_POST['op'] : '';
1219 if ($edit['cid'] && user_access('administer comments')) {
1220 if ($edit['author']) {
1221 $author = $edit['author'];
1223 elseif ($edit['name']) {
1224 $author = $edit['name'];
1227 $author = $edit['registered_name'];
1230 if ($edit['status']) {
1231 $status = $edit['status'];
1237 if ($edit['date']) {
1238 $date = $edit['date'];
1241 $date = format_date($edit['timestamp'], 'custom', 'Y-m-d H:i O');
1244 $form['admin'] = array(
1245 '#type' => 'fieldset',
1246 '#title' => t('Administration'),
1247 '#collapsible' => TRUE,
1248 '#collapsed' => TRUE,
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'),
1259 '#autocomplete_path' => 'user/autocomplete',
1260 '#default_value' => $author,
1265 // The comment is by an anonymous user
1266 $form['is_anonymous'] = array(
1270 $form['admin']['name'] = array(
1271 '#type' => 'textfield',
1272 '#title' => t('Authored by'),
1275 '#default_value' => $author,
1278 $form['admin']['mail'] = array(
1279 '#type' => 'textfield',
1280 '#title' => t('E-mail'),
1283 '#default_value' => $edit['mail'],
1284 '#description' => t('The content of this field is kept private and will not be shown publicly.'),
1287 $form['admin']['homepage'] = array(
1288 '#type' => 'textfield',
1289 '#title' => t('Homepage'),
1290 '#maxlength' => 255,
1292 '#default_value' => $edit['homepage'],
1296 $form['admin']['date'] = array('#type' => 'textfield', '#parents' => array('date'), '#title' => t('Authored on'), '#size' => 20, '#maxlength' => 25, '#default_value' => $date, '#weight' => -1);
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);
1302 $form['_author'] = array('#type' => 'item', '#title' => t('Your name'), '#value' => theme('username', $user)
1304 $form['author'] = array('#type' => 'value', '#value' => $user->name);
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')
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.')
1314 $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
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);
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);
1321 $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
1324 if (variable_get('comment_subject_field', 1) == 1) {
1325 $form['subject'] = array('#type' => 'textfield', '#title' => t('Subject'), '#maxlength' => 64, '#default_value' => $edit['subject']);
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']);
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']);
1336 $form['preview'] = array('#type' => 'button', '#value' => t('Preview comment'), '#weight' => 19);
1337 $form['#token'] = 'comment' . $edit['nid'] . $edit['pid'];
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);
1346 if ($op == t('Preview comment')) {
1347 $form['#after_build'] = array('comment_form_add_preview');
1350 if ($_REQUEST['destination']) {
1351 $form['#attributes']['destination'] = $_REQUEST['destination'];
1354 if (empty($edit['cid']) && empty($edit['pid'])) {
1355 $form['#action'] = url('comment/reply/'. $edit['nid']);
1358 // Graft in extra form additions
1359 $form = array_merge($form, comment_invoke_comment($form, 'form'));
1361 return theme('box', $title, drupal_get_form('comment_form', $form));
1364 function comment_form_add_preview($form, $edit) {
1367 drupal_set_title(t('Preview comment'));
1371 comment_validate($edit);
1372 $comment = (object)_comment_form_submit($edit);
1374 // Attach the user and time information.
1375 if ($edit['author']) {
1376 $account = user_load(array('name' => $edit['author']));
1378 elseif ($user->uid && !isset($edit['is_anonymous'])) {
1382 $comment->uid = $account->uid;
1383 $comment->name = check_plain($account->name);
1385 $comment->timestamp = $edit['timestamp'] ? $edit['timestamp'] : time();
1387 // Preview the comment with security check.
1388 if (!form_get_errors()) {
1389 $output .= theme('comment_view', $comment);
1391 $form['comment_preview'] = array(
1392 '#value' => $output,
1394 '#prefix' => '<div class="preview">',
1395 '#suffix' => '</div>',
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);
1407 $form['#suffix'] = node_view(node_load($edit['nid']));
1411 $form['comment_preview_below'] = array('#value' => $output, '#weight' => 100);
1416 function comment_form_validate($form_id, $form_values) {
1417 comment_validate($form_values);
1420 function _comment_form_submit($form_values) {
1421 if (!isset($form_values['date'])) {
1422 $form_values['date'] = 'now';
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'];
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);
1440 return $form_values;
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");
1451 ** Renderer or visualization functions this can be optionally
1452 ** overridden by themes.
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>';
1462 function theme_comment_view($comment, $links = array(), $visible = 1) {
1466 if (($comment->new = node_mark($comment->nid, $comment->timestamp)) != MARK_READ) {
1467 $output .= "<a id=\"new\"></a>\n";
1470 $output .= "<a id=\"comment-$comment->cid\"></a>\n";
1472 // Switch to folded/unfolded view of the comment
1474 $comment->comment = check_markup($comment->comment, $comment->format, FALSE);
1477 comment_invoke_comment($comment, 'view');
1479 $output .= theme('comment', $comment, $links);
1482 $output .= theme('comment_folded', $comment);
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(),
1494 $form['order'] = array(
1495 '#type' => 'select',
1496 '#default_value' => $order,
1497 '#options' => _comment_get_orders(),
1500 foreach (_comment_per_page() as $i) {
1501 $options[$i] = t('%a comments per page', array('%a' => $i));
1503 $form['comments_per_page'] = array('#type' => 'select',
1504 '#default_value' => $comments_per_page,
1505 '#options' => $options,
1509 $form['submit'] = array('#type' => 'submit',
1510 '#value' => t('Save settings'),
1514 return drupal_get_form('comment_controls', $form);
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);
1525 function comment_controls_submit($form_id, $form_values) {
1528 $mode = $form_values['mode'];
1529 $order = $form_values['order'];
1530 $comments_per_page = $form_values['comments_per_page'];
1533 $user = user_save($user, array('mode' => $mode, 'sort' => $order, 'comments_per_page' => $comments_per_page));
1536 $_SESSION['comment_mode'] = $mode;
1537 $_SESSION['comment_sort'] = $order;
1538 $_SESSION['comment_comments_per_page'] = $comments_per_page;
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>';
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";
1560 function theme_comment_flat_collapsed($comment) {
1561 return theme('comment_view', $comment, '', 0);
1565 function theme_comment_flat_expanded($comment) {
1566 return theme('comment_view', $comment, module_invoke_all('link', 'comment', $comment, 0));
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";
1576 function theme_comment_thread_expanded($comment) {
1578 if ($comment->depth) {
1579 $output .= '<div style="margin-left:'. ($comment->depth * 25) ."px;\">\n";
1582 $output .= theme('comment_view', $comment, module_invoke_all('link', 'comment', $comment, 0));
1584 if ($comment->depth) {
1585 $output .= "</div>\n";
1590 function theme_comment_post_forbidden($nid) {
1593 return t("you can't post comments");
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");
1601 $destination = "destination=". drupal_urlencode("node/$nid#comment_form");
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))));
1608 return t('<a href="%login">login</a> to post comments', array('%login' => check_url(url('user/login', $destination))));
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);
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))));
1623 comment_invoke_comment($comment, 'delete');
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);
1634 * Return an array of viewing modes for comment listings.
1636 * We can't use a global variable array because the locale system
1637 * is not initialized yet when the comment module is loaded.
1639 function _comment_get_modes() {
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')
1649 * Return an array of viewing orders for comment listings.
1651 * We can't use a global variable array because the locale system
1652 * is not initialized yet when the comment module is loaded.
1654 function _comment_get_orders() {
1656 COMMENT_ORDER_NEWEST_FIRST => t('Date - newest first'),
1657 COMMENT_ORDER_OLDEST_FIRST => t('Date - oldest first')
1662 * Return an array of "comments per page" settings from which the user
1665 function _comment_per_page() {
1666 return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300));
1670 * Return a current comment display setting
1672 * $setting can be one of these: 'mode', 'sort', 'comments_per_page'
1674 function _comment_get_display_setting($setting) {
1677 if ($_GET[$setting]) {
1678 $value = $_GET[$setting];
1681 // get the setting's site default
1684 $default = variable_get('comment_default_mode', COMMENT_MODE_THREADED_EXPANDED);
1687 $default = variable_get('comment_default_order', COMMENT_ORDER_NEWEST_FIRST);
1689 case 'comments_per_page':
1690 $default = variable_get('comment_default_per_page', '50');
1692 if (variable_get('comment_controls', COMMENT_CONTROLS_HIDDEN) == COMMENT_CONTROLS_HIDDEN) {
1693 // if comment controls are disabled use site default
1697 // otherwise use the user's setting if set
1698 if ($user->$setting) {
1699 $value = $user->$setting;
1701 else if ($_SESSION['comment_'. $setting]) {
1702 $value = $_SESSION['comment_'. $setting];
1713 * Updates the comment statistics for a given node. This should be called any
1714 * time a comment is added, deleted, or updated.
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.
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));
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);
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);
1739 * Invoke a hook_comment() operation in all modules.
1744 * A string containing the name of the comment operation.
1746 * The returned value of the invoked hooks.
1748 function comment_invoke_comment(&$comment, $op) {
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);
1756 else if (isset($result)) {
1757 $return[] = $result;
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.
1771 * 00, 01, 02, ..., 0y, 0z,
1772 * 110, 111, ... , 1zy, 1zz,
1773 * 2100, 2101, ..., 2zzy, 2zzz,
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;
1783 * Decode vancode back to an integer.
1785 function vancode2int($c = '00') {
1786 return base_convert(substr($c, 1), 36, 10);