/[drupal]/drupal/modules/comment/comment.module
ViewVC logotype

Contents of /drupal/modules/comment/comment.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.800 - (show annotations) (download) (as text)
Tue Nov 3 06:47:22 2009 UTC (3 weeks, 5 days ago) by webchick
Branch: MAIN
Changes since 1.799: +1 -4 lines
File MIME type: text/x-php
#552478 by pwolanin, samj, dropcube, and sun: Improve link/header API and support on node/comment pages rel=canonical and rel=shortlink standards.
1 <?php
2 // $Id: comment.module,v 1.799 2009/11/02 23:20:28 webchick Exp $
3
4 /**
5 * @file
6 * Enables users to comment on published content.
7 *
8 * When enabled, the Drupal comment module creates a discussion
9 * board for each Drupal node. Users can post comments to discuss
10 * a forum topic, weblog post, story, collaborative book page, etc.
11 */
12
13 /**
14 * Comment is awaiting approval.
15 */
16 define('COMMENT_NOT_PUBLISHED', 0);
17
18 /**
19 * Comment is published.
20 */
21 define('COMMENT_PUBLISHED', 1);
22
23 /**
24 * Comments are displayed in a flat list - expanded.
25 */
26 define('COMMENT_MODE_FLAT', 0);
27
28 /**
29 * Comments are displayed as a threaded list - expanded.
30 */
31 define('COMMENT_MODE_THREADED', 1);
32
33 /**
34 * Anonymous posters cannot enter their contact information.
35 */
36 define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
37
38 /**
39 * Anonymous posters may leave their contact information.
40 */
41 define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
42
43 /**
44 * Anonymous posters are required to leave their contact information.
45 */
46 define('COMMENT_ANONYMOUS_MUST_CONTACT', 2);
47
48 /**
49 * Comment form should be displayed on a separate page.
50 */
51 define('COMMENT_FORM_SEPARATE_PAGE', 0);
52
53 /**
54 * Comment form should be shown below post or list of comments.
55 */
56 define('COMMENT_FORM_BELOW', 1);
57
58 /**
59 * Comments for this node are hidden.
60 */
61 define('COMMENT_NODE_HIDDEN', 0);
62
63 /**
64 * Comments for this node are closed.
65 */
66 define('COMMENT_NODE_CLOSED', 1);
67
68 /**
69 * Comments for this node are open.
70 */
71 define('COMMENT_NODE_OPEN', 2);
72
73 /**
74 * Implement hook_help().
75 */
76 function comment_help($path, $arg) {
77 switch ($path) {
78 case 'admin/help#comment':
79 $output = '<p>' . t('The comment module allows visitors to comment on your posts, creating ad hoc discussion boards. Any <a href="@content-type">content type</a> may have its <em>Default comment setting</em> set to <em>Open</em> to allow comments, <em>Hidden</em> to hide existing comments and prevent new comments or <em>Closed</em> to allow existing comments to be viewed but no new comments added. Comment display settings and other controls may also be customized for each content type.', array('@content-type' => url('admin/structure/types'))) . '</p>';
80 $output .= '<p>' . t('Comment permissions are assigned to user roles, and are used to determine whether anonymous users (or other roles) are allowed to comment on posts. If anonymous users are allowed to comment, their individual contact information may be retained in cookies stored on their local computer for use in later comment submissions. When a comment has no replies, it may be (optionally) edited by its author. The comment module uses the same text formats and HTML tags available when creating other forms of content.') . '</p>';
81 $output .= '<p>' . t('Change comment settings on the content type\'s <a href="@content-type">edit page</a>.', array('@content-type' => url('admin/structure/types'))) . '</p>';
82 $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@comment">Comment module</a>.', array('@comment' => 'http://drupal.org/handbook/modules/comment/')) . '</p>';
83
84 return $output;
85 }
86 }
87
88 /**
89 * Implement hook_entity_info().
90 */
91 function comment_entity_info() {
92 $return = array(
93 'comment' => array(
94 'label' => t('Comment'),
95 'base table' => 'comment',
96 'fieldable' => TRUE,
97 'controller class' => 'CommentController',
98 'object keys' => array(
99 'id' => 'cid',
100 'bundle' => 'node_type',
101 ),
102 'bundle keys' => array(
103 'bundle' => 'type',
104 ),
105 'bundles' => array(),
106 'static cache' => FALSE,
107 ),
108 );
109
110 foreach (node_type_get_names() as $type => $name) {
111 $return['comment']['bundles']['comment_node_' . $type] = array(
112 'label' => $name,
113 );
114 }
115
116 return $return;
117 }
118
119 /**
120 * Implement hook_theme().
121 */
122 function comment_theme() {
123 return array(
124 'comment_block' => array(
125 'variables' => array(),
126 ),
127 'comment_preview' => array(
128 'variables' => array('comment' => NULL),
129 ),
130 'comment' => array(
131 'template' => 'comment',
132 'render element' => 'elements',
133 ),
134 'comment_post_forbidden' => array(
135 'variables' => array('node' => NULL),
136 ),
137 'comment_wrapper' => array(
138 'template' => 'comment-wrapper',
139 'render element' => 'content',
140 ),
141 );
142 }
143
144 /**
145 * Implement hook_menu().
146 */
147 function comment_menu() {
148 $items['admin/content/comment'] = array(
149 'title' => 'Comments',
150 'description' => 'List and edit site comments and the comment approval queue.',
151 'page callback' => 'comment_admin',
152 'access arguments' => array('administer comments'),
153 'type' => MENU_LOCAL_TASK,
154 'file' => 'comment.admin.inc',
155 );
156 // Tabs begin here.
157 $items['admin/content/comment/new'] = array(
158 'title' => 'Published comments',
159 'type' => MENU_DEFAULT_LOCAL_TASK,
160 'weight' => -10,
161 );
162 $items['admin/content/comment/approval'] = array(
163 'title' => 'Unapproved comments',
164 'title callback' => 'comment_count_unpublished',
165 'page arguments' => array('approval'),
166 'access arguments' => array('administer comments'),
167 'type' => MENU_LOCAL_TASK,
168 );
169 $items['comment/%comment'] = array(
170 'title' => 'Comment permalink',
171 'page callback' => 'comment_permalink',
172 'page arguments' => array(1),
173 'access arguments' => array('access comments'),
174 'type' => MENU_CALLBACK,
175 );
176 $items['comment/%comment/view'] = array(
177 'title' => 'View comment',
178 'type' => MENU_DEFAULT_LOCAL_TASK,
179 'weight' => -10,
180 );
181 $items['comment/%comment/edit'] = array(
182 'title' => 'Edit',
183 'page callback' => 'drupal_get_form',
184 'page arguments' => array('comment_form', 1),
185 'access callback' => 'comment_access',
186 'access arguments' => array('edit', 1),
187 'type' => MENU_LOCAL_TASK,
188 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
189 'weight' => 0,
190 );
191 $items['comment/%comment/approve'] = array(
192 'title' => 'Approve',
193 'page callback' => 'comment_approve',
194 'page arguments' => array(1),
195 'access arguments' => array('administer comments'),
196 'type' => MENU_LOCAL_TASK,
197 'context' => MENU_CONTEXT_INLINE,
198 'file' => 'comment.pages.inc',
199 'weight' => 1,
200 );
201 $items['comment/%comment/delete'] = array(
202 'title' => 'Delete',
203 'page callback' => 'drupal_get_form',
204 'page arguments' => array('comment_confirm_delete', 1),
205 'access arguments' => array('administer comments'),
206 'type' => MENU_LOCAL_TASK,
207 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
208 'file' => 'comment.admin.inc',
209 'weight' => 2,
210 );
211 $items['comment/reply/%node'] = array(
212 'title' => 'Add new comment',
213 'page callback' => 'comment_reply',
214 'page arguments' => array(2),
215 'access callback' => 'node_access',
216 'access arguments' => array('view', 2),
217 'type' => MENU_CALLBACK,
218 'file' => 'comment.pages.inc',
219 );
220
221 return $items;
222 }
223
224 /**
225 * Returns a menu title which includes the number of unapproved comments.
226 */
227 function comment_count_unpublished() {
228 $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE status = :status', array(
229 ':status' => COMMENT_NOT_PUBLISHED,
230 ))->fetchField();
231 return t('Unapproved comments (@count)', array('@count' => $count));
232 }
233
234 /**
235 * Implement hook_node_type_insert().
236 */
237 function comment_node_type_insert($info) {
238 field_attach_create_bundle('comment', 'comment_node_' . $info->type);
239 }
240
241 /**
242 * Implement hook_node_type_update().
243 */
244 function comment_node_type_update($info) {
245 if (!empty($info->old_type) && $info->type != $info->old_type) {
246 field_attach_rename_bundle('comment', 'comment_node_' . $info->old_type, 'comment_node_' . $info->type);
247 }
248 }
249
250 /**
251 * Implement hook_node_type_delete().
252 */
253 function comment_node_type_delete($info) {
254 field_attach_delete_bundle('comment', 'comment_node_' . $info->type);
255 $settings = array(
256 'comment',
257 'comment_default_mode',
258 'comment_default_per_page',
259 'comment_anonymous',
260 'comment_subject_field',
261 'comment_preview',
262 'comment_form_location',
263 );
264 foreach ($settings as $setting) {
265 variable_del($setting . '_' . $info->type);
266 }
267 }
268
269 /**
270 * Implement hook_permission().
271 */
272 function comment_permission() {
273 return array(
274 'administer comments' => array(
275 'title' => t('Administer comments'),
276 'description' => t('Manage and approve comments, and configure comment administration settings.'),
277 ),
278 'access comments' => array(
279 'title' => t('Access comments'),
280 'description' => t('View comments attached to content.'),
281 ),
282 'post comments' => array(
283 'title' => t('Post comments'),
284 'description' => t('Add comments to content (approval required).'),
285 ),
286 'post comments without approval' => array(
287 'title' => t('Post comments without approval'),
288 'description' => t('Add comments to content (no approval required).'),
289 ),
290 );
291 }
292
293 /**
294 * Implement hook_block_info().
295 */
296 function comment_block_info() {
297 $blocks['recent']['info'] = t('Recent comments');
298
299 return $blocks;
300 }
301
302 /**
303 * Implement hook_block_configure().
304 */
305 function comment_block_configure($delta = '') {
306 $form['comment_block_count'] = array(
307 '#type' => 'select',
308 '#title' => t('Number of recent comments'),
309 '#default_value' => variable_get('comment_block_count', 10),
310 '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)),
311 );
312
313 return $form;
314 }
315
316 /**
317 * Implement hook_block_save().
318 */
319 function comment_block_save($delta = '', $edit = array()) {
320 variable_set('comment_block_count', (int)$edit['comment_block_count']);
321 }
322
323 /**
324 * Implement hook_block_view().
325 *
326 * Generates a block with the most recent comments.
327 */
328 function comment_block_view($delta = '') {
329 if (user_access('access comments')) {
330 $block['subject'] = t('Recent comments');
331 $block['content'] = theme('comment_block');
332
333 return $block;
334 }
335 }
336
337 /**
338 * Redirects comment links to the correct page depending on comment settings.
339 *
340 * Since comments are paged there is no way to guarantee which page a comment
341 * appears on. Comment paging and threading settings may be changed at any time.
342 * With threaded comments, an individual comment may move between pages as
343 * comments can be added either before or after it in the overall discussion.
344 * Therefore we use a central routing function for comment links, which
345 * calculates the page number based on current comment settings and returns
346 * the full comment view with the pager set dynamically.
347 *
348 * @param $comment
349 * A comment object.
350 * @return
351 * The comment listing set to the page on which the comment appears.
352 */
353 function comment_permalink($comment) {
354 $node = node_load($comment->nid);
355 if ($node && $comment) {
356
357 // Find the current display page for this comment.
358 $page = comment_get_display_page($comment->cid, $node->type);
359
360 // Set $_GET['q'] and $_GET['page'] ourselves so that the node callback
361 // behaves as it would when visiting the page directly.
362 $_GET['q'] = 'node/' . $node->nid;
363 $_GET['page'] = $page;
364
365 // Return the node view, this will show the correct comment in context.
366 return menu_execute_active_handler('node/' . $node->nid, FALSE);
367 }
368 drupal_not_found();
369 }
370
371 /**
372 * Find the most recent comments that are available to the current user.
373 *
374 * This is done in two steps:
375 * 1. Query the {node_comment_statistics} table to find n number of nodes that
376 * have the most recent comments. This table is indexed on
377 * last_comment_timestamp, thus making it a fast query.
378 * 2. Load the information from the comments table based on the nids found
379 * in step 1.
380 *
381 * @param integer $number
382 * (optional) The maximum number of comments to find.
383 * @return
384 * An array of comment objects each containing a nid, subject, cid, created
385 * and changed, or an empty array if there are no recent comments visible
386 * to the current user.
387 */
388 function comment_get_recent($number = 10) {
389 // Step 1: Select a $number of nodes which have new comments,
390 // and are visible to the current user.
391 $nids = db_query_range("SELECT nc.nid FROM {node_comment_statistics} nc WHERE nc.comment_count > 0 ORDER BY nc.last_comment_timestamp DESC", 0, $number)->fetchCol();
392
393 $comments = array();
394 if (!empty($nids)) {
395 // Step 2: From among the comments on the nodes selected in the first query,
396 // find the $number of most recent comments.
397 // Using Query Builder here for the IN-Statement.
398 $query = db_select('comment', 'c');
399 $query->innerJoin('node', 'n', 'n.nid = c.nid');
400 return $query
401 ->fields('c', array('nid', 'subject', 'cid', 'created', 'changed'))
402 ->condition('c.nid', $nids, 'IN')
403 ->condition('c.status', COMMENT_PUBLISHED)
404 ->condition('n.status', 1)
405 ->orderBy('c.cid', 'DESC')
406 ->range(0, $number)
407 ->execute()
408 ->fetchAll();
409 }
410
411 return $comments;
412 }
413
414 /**
415 * Calculate page number for first new comment.
416 *
417 * @param $num_comments
418 * Number of comments.
419 * @param $new_replies
420 * Number of new replies.
421 * @param $node
422 * The first new comment node.
423 * @return
424 * "page=X" if the page number is greater than zero; empty string otherwise.
425 */
426 function comment_new_page_count($num_comments, $new_replies, stdClass $node) {
427 $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
428 $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
429 $pagenum = NULL;
430 $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
431 if ($num_comments <= $comments_per_page) {
432 // Only one page of comments.
433 $pageno = 0;
434 }
435 elseif ($flat) {
436 // Flat comments.
437 $count = $num_comments - $new_replies;
438 $pageno = $count / $comments_per_page;
439 }
440 else {
441 // Threaded comments.
442 // Find the first thread with a new comment.
443 $result = db_query_range('SELECT thread FROM (SELECT thread
444 FROM {comment}
445 WHERE nid = :nid
446 AND status = 0
447 ORDER BY changed DESC) AS thread
448 ORDER BY SUBSTRING(thread, 1, (LENGTH(thread) - 1))', 0, $new_replies, array(':nid' => $node->nid))->fetchField();
449 $thread = substr($result, 0, -1);
450 $count = db_query('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND status = 0 AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array(
451 ':nid' => $node->nid,
452 ':thread' => $thread,
453 ))->fetchField();
454 $pageno = $count / $comments_per_page;
455 }
456
457 if ($pageno >= 1) {
458 $pagenum = array('page' => intval($pageno));
459 }
460
461 return $pagenum;
462 }
463
464 /**
465 * Returns a formatted list of recent comments to be displayed in the comment block.
466 *
467 * @return
468 * The comment list HTML.
469 * @ingroup themeable
470 */
471 function theme_comment_block() {
472 $items = array();
473 $number = variable_get('comment_block_count', 10);
474 foreach (comment_get_recent($number) as $comment) {
475 $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) . '<br />' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->changed)));
476 }
477
478 if ($items) {
479 return theme('item_list', array('items' => $items));
480 }
481 }
482
483 /**
484 * Implement hook_node_view().
485 */
486 function comment_node_view(stdClass $node, $build_mode) {
487 $links = array();
488
489 if ($node->comment) {
490 if ($build_mode == 'rss') {
491 if ($node->comment != COMMENT_NODE_HIDDEN) {
492 // Add a comments RSS element which is a URL to the comments of this node.
493 $node->rss_elements[] = array(
494 'key' => 'comments',
495 'value' => url('node/' . $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))
496 );
497 }
498 }
499 elseif ($build_mode == 'teaser') {
500 // Main page: display the number of comments that have been posted.
501 if (user_access('access comments')) {
502 if (!empty($node->comment_count)) {
503 $links['comment_comments'] = array(
504 'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
505 'href' => "node/$node->nid",
506 'attributes' => array('title' => t('Jump to the first comment of this posting.')),
507 'fragment' => 'comments',
508 'html' => TRUE,
509 );
510
511 $new = comment_num_new($node->nid);
512 if ($new) {
513 $links['comment_new_comments'] = array(
514 'title' => format_plural($new, '1 new comment', '@count new comments'),
515 'href' => "node/$node->nid",
516 'query' => comment_new_page_count($node->comment_count, $new, $node),
517 'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
518 'fragment' => 'new',
519 'html' => TRUE,
520 );
521 }
522 }
523 else {
524 if ($node->comment == COMMENT_NODE_OPEN) {
525 if (user_access('post comments')) {
526 $links['comment_add'] = array(
527 'title' => t('Add new comment'),
528 'href' => "comment/reply/$node->nid",
529 'attributes' => array('title' => t('Add a new comment to this page.')),
530 'fragment' => 'comment-form',
531 'html' => TRUE,
532 );
533 }
534 else {
535 $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
536 }
537 }
538 }
539 }
540 }
541 else {
542 // Node page: add a "post comment" link if the user is allowed to post
543 // comments and if this node is not read-only.
544 if ($node->comment == COMMENT_NODE_OPEN) {
545 if (user_access('post comments')) {
546 $links['comment_add'] = array(
547 'title' => t('Add new comment'),
548 'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
549 'fragment' => 'comment-form',
550 'html' => TRUE,
551 );
552 if (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_SEPARATE_PAGE) {
553 $links['comment_add']['href'] = "comment/reply/$node->nid";
554 }
555 else {
556 $links['comment_add']['href'] = "node/$node->nid";
557 }
558 }
559 else {
560 $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
561 }
562 }
563 }
564
565 if (isset($links['comment_forbidden'])) {
566 $links['comment_forbidden']['html'] = TRUE;
567 }
568
569 $node->content['links']['comment'] = array(
570 '#theme' => 'links',
571 '#links' => $links,
572 '#attributes' => array('class' => array('links', 'inline')),
573 );
574
575 // Only append comments when we are building a node on its own node detail
576 // page. We compare $node and $page_node to ensure that comments are not
577 // appended to other nodes shown on the page, for example a node_reference
578 // displayed in 'full' build mode within another node.
579 $page_node = menu_get_object();
580 if ($node->comment && isset($page_node->nid) && $page_node->nid == $node->nid && empty($node->in_preview) && user_access('access comments')) {
581 $node->content['comments'] = comment_node_page_additions($node);
582 }
583 }
584 }
585
586 /**
587 * Build the comment-related elements for node detail pages.
588 *
589 * @param $node
590 * A node object.
591 */
592 function comment_node_page_additions(stdClass $node) {
593 $additions = array();
594
595 // Only attempt to render comments if the node has visible comments.
596 // Unpublished comments are not included in $node->comment_count, so show
597 // comments unconditionally if the user is an administrator.
598 if ($node->comment_count || user_access('administer comments')) {
599 $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
600 $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
601 if ($cids = comment_get_thread($node, $mode, $comments_per_page)) {
602 $comments = comment_load_multiple($cids);
603 comment_prepare_thread($comments);
604 $build = comment_build_multiple($comments, $node);
605 $build['#attached']['css'][] = drupal_get_path('module', 'comment') . '/comment.css';
606 $build['pager']['#theme'] = 'pager';
607 $additions['comments'] = $build;
608 }
609 }
610
611 // Append comment form if needed.
612 if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) {
613 $build = drupal_get_form('comment_form', (object) array('nid' => $node->nid));
614 $additions['comment_form'] = $build;
615 }
616
617 if ($additions) {
618 $additions += array(
619 '#theme' => 'comment_wrapper',
620 '#node' => $node,
621 'comments' => array(),
622 'comment_form' => array(),
623 );
624 }
625
626 return $additions;
627 }
628
629 /**
630 * Retrieve comments for a thread.
631 *
632 * @param $node
633 * The node whose comment(s) needs rendering.
634 * @param $mode
635 * The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED.
636 * @param $comments_per_page
637 * The amount of comments to display per page.
638 *
639 * To display threaded comments in the correct order we keep a 'thread' field
640 * and order by that value. This field keeps this data in
641 * a way which is easy to update and convenient to use.
642 *
643 * A "thread" value starts at "1". If we add a child (A) to this comment,
644 * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
645 * brother of (A) will get "1.2". Next brother of the parent of (A) will get
646 * "2" and so on.
647 *
648 * First of all note that the thread field stores the depth of the comment:
649 * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
650 *
651 * Now to get the ordering right, consider this example:
652 *
653 * 1
654 * 1.1
655 * 1.1.1
656 * 1.2
657 * 2
658 *
659 * If we "ORDER BY thread ASC" we get the above result, and this is the
660 * natural order sorted by time. However, if we "ORDER BY thread DESC"
661 * we get:
662 *
663 * 2
664 * 1.2
665 * 1.1.1
666 * 1.1
667 * 1
668 *
669 * Clearly, this is not a natural way to see a thread, and users will get
670 * confused. The natural order to show a thread by time desc would be:
671 *
672 * 2
673 * 1
674 * 1.2
675 * 1.1
676 * 1.1.1
677 *
678 * which is what we already did before the standard pager patch. To achieve
679 * this we simply add a "/" at the end of each "thread" value. This way, the
680 * thread fields will look like this:
681 *
682 * 1/
683 * 1.1/
684 * 1.1.1/
685 * 1.2/
686 * 2/
687 *
688 * we add "/" since this char is, in ASCII, higher than every number, so if
689 * now we "ORDER BY thread DESC" we get the correct order. However this would
690 * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
691 * to consider the trailing "/" so we use a substring only.
692 */
693 function comment_get_thread(stdClass $node, $mode, $comments_per_page) {
694 $query = db_select('comment', 'c')->extend('PagerDefault');
695 $query->addField('c', 'cid');
696 $query
697 ->condition('c.nid', $node->nid)
698 ->addTag('node_access')
699 ->limit($comments_per_page);
700
701 $count_query = db_select('comment', 'c');
702 $count_query->addExpression('COUNT(*)');
703 $count_query
704 ->condition('c.nid', $node->nid)
705 ->addTag('node_access');
706
707 if (!user_access('administer comments')) {
708 $query->condition('c.status', COMMENT_PUBLISHED);
709 $count_query->condition('c.status', COMMENT_PUBLISHED);
710 }
711 if ($mode === COMMENT_MODE_FLAT) {
712 $query->orderBy('c.cid', 'ASC');
713 }
714 else {
715 // See comment above. Analysis reveals that this doesn't cost too
716 // much. It scales much much better than having the whole comment
717 // structure.
718 $query->orderBy('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'ASC');
719 }
720
721 $query->setCountQuery($count_query);
722 $cids = $query->execute()->fetchCol();
723
724 return $cids;
725 }
726
727 /**
728 * Loop over comment thread, noting indentation level.
729 *
730 * @param array $comments
731 * An array of comment objects, keyed by cid.
732 * @return
733 * The $comments argument is altered by reference with indentation information.
734 */
735 function comment_prepare_thread(&$comments) {
736 // A flag stating if we are still searching for first new comment on the thread.
737 $first_new = TRUE;
738
739 // A counter that helps track how indented we are.
740 $divs = 0;
741
742 foreach ($comments as $key => $comment) {
743 if ($first_new && $comment->new != MARK_READ) {
744 // Assign the anchor only for the first new comment. This avoids duplicate
745 // id attributes on a page.
746 $first_new = FALSE;
747 $comment->first_new = TRUE;
748 }
749
750 // The $divs element instructs #prefix whether to add an indent div or
751 // close existing divs (a negative value).
752 $comment->depth = count(explode('.', $comment->thread)) - 1;
753 if ($comment->depth > $divs) {
754 $comment->divs = 1;
755 $divs++;
756 }
757 else {
758 $comment->divs = $comment->depth - $divs;
759 while ($comment->depth < $divs) {
760 $divs--;
761 }
762 }
763 $comments[$key] = $comment;
764 }
765
766 // The final comment must close up some hanging divs
767 $comments[$key]->divs_final = $divs;
768 }
769
770 /**
771 * Generate an array for rendering the given comment.
772 *
773 * @param $comment
774 * A comment object.
775 * @param $node
776 * The node the comment is attached to.
777 * @param $build_mode
778 * Build mode, e.g. 'full', 'teaser'...
779 *
780 * @return
781 * An array as expected by drupal_render().
782 */
783 function comment_build($comment, stdClass $node, $build_mode = 'full') {
784 // Populate $comment->content with a render() array.
785 comment_build_content($comment, $node, $build_mode);
786
787 $build = $comment->content;
788 // We don't need duplicate rendering info in comment->content.
789 unset($comment->content);
790
791 $build += array(
792 '#theme' => 'comment',
793 '#comment' => $comment,
794 '#node' => $node,
795 '#build_mode' => $build_mode,
796 );
797 // Add contextual links for this comment.
798 $build['#contextual_links']['comment'] = menu_contextual_links('comment', array($comment->cid));
799
800 $prefix = '';
801 $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED;
802
803 // Add 'new' anchor if needed.
804 if (!empty($comment->first_new)) {
805 $prefix .= "<a id=\"new\"></a>\n";
806 }
807
808 // Add indentation div or close open divs as needed.
809 if ($is_threaded) {
810 $prefix .= $comment->divs <= 0 ? str_repeat('</div>', abs($comment->divs)) : "\n" . '<div class="indented">';
811 }
812
813 // Add anchor for each comment.
814 $prefix .= "<a id=\"comment-$comment->cid\"></a>\n";
815 $build['#prefix'] = $prefix;
816
817 // Close all open divs.
818 if ($is_threaded && !empty($comment->divs_final)) {
819 $build['#suffix'] = str_repeat('</div>', $comment->divs_final);
820 }
821
822 return $build;
823 }
824
825 /**
826 * Builds a structured array representing the comment's content.
827 *
828 * The content built for the comment (field values, comments, file attachments or
829 * other comment components) will vary depending on the $build_mode parameter.
830 *
831 * @param $comment
832 * A comment object.
833 * @param $node
834 * The node the comment is attached to.
835 * @param $build_mode
836 * Build mode, e.g. 'full', 'teaser'...
837 */
838 function comment_build_content($comment, stdClass $node, $build_mode = 'full') {
839 // Remove previously built content, if exists.
840 $comment->content = array();
841
842 // Build comment body.
843 $comment->content['comment_body'] = array(
844 '#markup' => check_markup($comment->comment, $comment->format, '', TRUE),
845 );
846
847 field_attach_prepare_view('comment', array($comment->cid => $comment), $build_mode);
848 $comment->content += field_attach_view('comment', $comment, $build_mode);
849
850 if (empty($comment->in_preview)) {
851 $comment->content['links']['comment'] = array(
852 '#theme' => 'links',
853 '#links' => comment_links($comment, $node),
854 '#attributes' => array('class' => array('links', 'inline')),
855 );
856 }
857
858 // Allow modules to make their own additions to the comment.
859 module_invoke_all('comment_view', $comment, $build_mode);
860
861 // Allow modules to modify the structured comment.
862 drupal_alter('comment_build', $comment, $build_mode);
863 }
864
865 /**
866 * Helper function, build links for an individual comment.
867 *
868 * Adds reply, edit, delete etc. depending on the current user permissions.
869 *
870 * @param $comment
871 * The comment object.
872 * @param $node
873 * The node the comment is attached to.
874 * @return
875 * A structured array of links.
876 */
877 function comment_links($comment, stdClass $node) {
878 $links = array();
879 if ($node->comment == COMMENT_NODE_OPEN) {
880 if (user_access('administer comments') && user_access('post comments')) {
881 $links['comment_delete'] = array(
882 'title' => t('delete'),
883 'href' => "comment/$comment->cid/delete",
884 'html' => TRUE,
885 );
886 $links['comment_edit'] = array(
887 'title' => t('edit'),
888 'href' => "comment/$comment->cid/edit",
889 'html' => TRUE,
890 );
891 $links['comment_reply'] = array(
892 'title' => t('reply'),
893 'href' => "comment/reply/$comment->nid/$comment->cid",
894 'html' => TRUE,
895 );
896 if ($comment->status == COMMENT_NOT_PUBLISHED) {
897 $links['comment_approve'] = array(
898 'title' => t('approve'),
899 'href' => "comment/$comment->cid/approve",
900 'html' => TRUE,
901 );
902 }
903 }
904 elseif (user_access('post comments')) {
905 if (comment_access('edit', $comment)) {
906 $links['comment_edit'] = array(
907 'title' => t('edit'),
908 'href' => "comment/$comment->cid/edit",
909 'html' => TRUE,
910 );
911 }
912 $links['comment_reply'] = array(
913 'title' => t('reply'),
914 'href' => "comment/reply/$comment->nid/$comment->cid",
915 'html' => TRUE,
916 );
917 }
918 else {
919 $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
920 $links['comment_forbidden']['html'] = TRUE;
921 }
922 }
923 return $links;
924 }
925
926 /**
927 * Construct a drupal_render() style array from an array of loaded comments.
928 *
929 * @param $comments
930 * An array of comments as returned by comment_load_multiple().
931 * @param $node
932 * The node the comments are attached to.
933 * @param $build_mode
934 * Build mode, e.g. 'full', 'teaser'...
935 * @param $weight
936 * An integer representing the weight of the first comment in the list.
937 * @return
938 * An array in the format expected by drupal_render().
939 */
940 function comment_build_multiple($comments, stdClass $node, $build_mode = 'full', $weight = 0) {
941 field_attach_prepare_view('comment', $comments, $build_mode);
942
943 $build = array(
944 '#sorted' => TRUE,
945 );
946 foreach ($comments as $comment) {
947 $build[$comment->cid] = comment_build($comment, $node, $build_mode);
948 $build[$comment->cid]['#weight'] = $weight;
949 $weight++;
950 }
951 return $build;
952 }
953
954 /**
955 * Implement hook_form_FORM_ID_alter().
956 */
957 function comment_form_node_type_form_alter(&$form, $form_state) {
958 if (isset($form['identity']['type'])) {
959 $form['comment'] = array(
960 '#type' => 'fieldset',
961 '#title' => t('Comment settings'),
962 '#collapsible' => TRUE,
963 '#collapsed' => TRUE,
964 '#group' => 'additional_settings',
965 '#attached' => array(
966 'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
967 ),
968 );
969 $form['comment']['comment_default_mode'] = array(
970 '#type' => 'checkbox',
971 '#title' => t('Threading'),
972 '#default_value' => variable_get('comment_default_mode_' . $form['#node_type']->type, COMMENT_MODE_THREADED),
973 '#description' => t('Show comment replies in a threaded list.'),
974 );
975 $form['comment']['comment_default_per_page'] = array(
976 '#type' => 'select',
977 '#title' => t('Comments per page'),
978 '#default_value' => variable_get('comment_default_per_page_' . $form['#node_type']->type, 50),
979 '#options' => _comment_per_page(),
980 );
981
982 $form['comment']['comment'] = array(
983 '#type' => 'select',
984 '#title' => t('Default comment setting for new content'),
985 '#default_value' => variable_get('comment_' . $form['#node_type']->type, COMMENT_NODE_OPEN),
986 '#options' => array(t('Hidden'), t('Closed'), t('Open')),
987 );
988 $form['comment']['comment_anonymous'] = array(
989 '#type' => 'select',
990 '#title' => t('Anonymous commenting'),
991 '#default_value' => variable_get('comment_anonymous_' . $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT),
992 '#options' => array(
993 COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
994 COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
995 COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'))
996 );
997
998 if (!user_access('post comments', drupal_anonymous_user())) {
999 $form['comment']['comment_anonymous']['#access'] = FALSE;
1000 }
1001
1002 $form['comment']['comment_subject_field'] = array(
1003 '#type' => 'checkbox',
1004 '#title' => t('Allow comment title'),
1005 '#default_value' => variable_get('comment_subject_field_' . $form['#node_type']->type, 1),
1006 );
1007 $form['comment']['comment_form_location'] = array(
1008 '#type' => 'checkbox',
1009 '#title' => t('Show reply form on the same page as comments'),
1010 '#default_value' => variable_get('comment_form_location_' . $form['#node_type']->type, COMMENT_FORM_BELOW),
1011 );
1012 $form['comment']['comment_preview'] = array(
1013 '#type' => 'radios',
1014 '#title' => t('Preview comment'),
1015 '#default_value' => variable_get('comment_preview_' . $form['#node_type']->type, DRUPAL_OPTIONAL),
1016 '#options' => array(
1017 DRUPAL_DISABLED => t('Disabled'),
1018 DRUPAL_OPTIONAL => t('Optional'),
1019 DRUPAL_REQUIRED => t('Required'),
1020 ),
1021 );
1022 }
1023 }
1024
1025 /**
1026 * Implement hook_form_alter().
1027 */
1028 function comment_form_alter(&$form, $form_state, $form_id) {
1029 if (!empty($form['#node_edit_form'])) {
1030 $node = $form['#node'];
1031 $form['comment_settings'] = array(
1032 '#type' => 'fieldset',
1033 '#access' => user_access('administer comments'),
1034 '#title' => t('Comment settings'),
1035 '#collapsible' => TRUE,
1036 '#collapsed' => TRUE,
1037 '#group' => 'additional_settings',
1038 '#attached' => array(
1039 'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
1040 ),
1041 '#weight' => 30,
1042 );
1043 $comment_count = isset($node->nid) ? db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->nid))->fetchField() : 0;
1044 $comment_settings = ($node->comment == COMMENT_NODE_HIDDEN && empty($comment_count)) ? COMMENT_NODE_CLOSED : $node->comment;
1045 $form['comment_settings']['comment'] = array(
1046 '#type' => 'radios',
1047 '#parents' => array('comment'),
1048 '#default_value' => $comment_settings,
1049 '#options' => array(
1050 COMMENT_NODE_OPEN => t('Open'),
1051 COMMENT_NODE_CLOSED => t('Closed'),
1052 COMMENT_NODE_HIDDEN => t('Hidden'),
1053 ),
1054 COMMENT_NODE_OPEN => array(
1055 '#type' => 'radio',
1056 '#title' => t('Open'),
1057 '#description' => t('Users with the "Post comments" permission can post comments.'),
1058 '#return_value' => COMMENT_NODE_OPEN,
1059 '#default_value' => $comment_settings,
1060 '#id' => 'edit-comment-2',
1061 '#parents' => array('comment'),
1062 ),
1063 COMMENT_NODE_CLOSED => array(
1064 '#type' => 'radio',
1065 '#title' => t('Closed'),
1066 '#description' => t('Users cannot post comments, but existing comments will be displayed.'),
1067 '#return_value' => COMMENT_NODE_CLOSED,
1068 '#default_value' => $comment_settings,
1069 '#id' => 'edit-comment-1',
1070 '#parents' => array('comment'),
1071 ),
1072 COMMENT_NODE_HIDDEN => array(
1073 '#type' => 'radio',
1074 '#title' => t('Hidden'),
1075 '#description' => t('Comments are hidden from view.'),
1076 '#return_value' => COMMENT_NODE_HIDDEN,
1077 '#default_value' => $comment_settings,
1078 '#id' => 'edit-comment-0',
1079 '#parents' => array('comment'),
1080 ),
1081 );
1082 // If the node doesn't have any comments, the "hidden" option makes no
1083 // sense, so don't even bother presenting it to the user.
1084 if (empty($comment_count)) {
1085 unset($form['comment_settings']['comment']['#options'][COMMENT_NODE_HIDDEN]);
1086 unset($form['comment_settings']['comment'][COMMENT_NODE_HIDDEN]);
1087 $form['comment_settings']['comment'][COMMENT_NODE_CLOSED]['#description'] = t('Users cannot post comments.');
1088 }
1089 }
1090 }
1091
1092 /**
1093 * Implement hook_node_load().
1094 */
1095 function comment_node_load($nodes, $types) {
1096 $comments_enabled = array();
1097
1098 // Check if comments are enabled for each node. If comments are disabled,
1099 // assign values without hitting the database.
1100 foreach ($nodes as $node) {
1101 // Store whether comments are enabled for this node.
1102 if ($node->comment != COMMENT_NODE_HIDDEN) {
1103 $comments_enabled[] = $node->nid;
1104 }
1105 else {
1106 $node->last_comment_timestamp = $node->created;
1107 $node->last_comment_name = '';
1108 $node->comment_count = 0;
1109 }
1110 }
1111
1112 // For nodes with comments enabled, fetch information from the database.
1113 if (!empty($comments_enabled)) {
1114 $result = db_query('SELECT nid, last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid IN(:comments_enabled)', array(':comments_enabled' => $comments_enabled));
1115 foreach ($result as $record) {
1116 $nodes[$record->nid]->last_comment_timestamp = $record->last_comment_timestamp;
1117 $nodes[$record->nid]->last_comment_name = $record->last_comment_name;
1118 $nodes[$record->nid]->comment_count = $record->comment_count;
1119 }
1120 }
1121 }
1122
1123 /**
1124 * Implement hook_node_prepare().
1125 */
1126 function comment_node_prepare(stdClass $node) {
1127 if (!isset($node->comment)) {
1128 $node->comment = variable_get("comment_$node->type", COMMENT_NODE_OPEN);
1129 }
1130 }
1131
1132 /**
1133 * Implement hook_node_insert().
1134 */
1135 function comment_node_insert(stdClass $node) {
1136 db_insert('node_comment_statistics')
1137 ->fields(array(
1138 'nid' => $node->nid,
1139 'last_comment_timestamp' => $node->changed,
1140 'last_comment_name' => NULL,
1141 'last_comment_uid' => $node->uid,
1142 'comment_count' => 0,
1143 ))
1144 ->execute();
1145 }
1146
1147 /**
1148 * Implement hook_node_delete().
1149 */
1150 function comment_node_delete(stdClass $node) {
1151 $cids = db_query('SELECT cid FROM {comment} WHERE nid = :nid', array(':nid' => $node->nid))->fetchCol();
1152 comment_delete_multiple($cids);
1153 db_delete('node_comment_statistics')
1154 ->condition('nid', $node->nid)
1155 ->execute();
1156 }
1157
1158 /**
1159 * Implement hook_node_update_index().
1160 */
1161 function comment_node_update_index(stdClass $node) {
1162 $text = '';
1163 if ($node->comment != COMMENT_NODE_HIDDEN) {
1164 $comments = db_query('SELECT subject, comment, format FROM {comment} WHERE nid = :nid AND status = :status', array(
1165 ':nid' => $node->nid,
1166 ':status' => COMMENT_PUBLISHED
1167 ));
1168 foreach ($comments as $comment) {
1169 $text .= '<h2>' . check_plain($comment->subject) . '</h2>' . check_markup($comment->comment, $comment->format, '', TRUE);
1170 }
1171 }
1172 return $text;
1173 }
1174
1175 /**
1176 * Implement hook_update_index().
1177 */
1178 function comment_update_index() {
1179 // Store the maximum possible comments per thread (used for ranking by reply count)
1180 variable_set('node_cron_comments_scale', 1.0 / max(1, db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}')->fetchField()));
1181 }
1182
1183 /**
1184 * Implement hook_node_search_result().
1185 */
1186 function comment_node_search_result(stdClass $node) {
1187 if ($node->comment != COMMENT_NODE_HIDDEN) {
1188 $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField();
1189 return format_plural($comments, '1 comment', '@count comments');
1190 }
1191 return '';
1192 }
1193
1194 /**
1195 * Implement hook_user_cancel().
1196 */
1197 function comment_user_cancel($edit, $account, $method) {
1198 switch ($method) {
1199 case 'user_cancel_block_unpublish':
1200 db_update('comment')
1201 ->fields(array('status' => 0))
1202 ->condition('uid', $account->uid)
1203 ->execute();