70218a52e600f354ee631d625a70b199c7302523
[project/drupal.git] / core / modules / comment / comment.module
1 <?php
2
3 /**
4 * @file
5 * Enables users to comment on published content.
6 *
7 * When enabled, the Comment module creates a discussion board for each Drupal
8 * node. Users can post comments to discuss a forum topic, story, collaborative
9 * book page, etc.
10 */
11
12 /**
13 * Comment is awaiting approval.
14 */
15 const COMMENT_NOT_PUBLISHED = 0;
16
17 /**
18 * Comment is published.
19 */
20 const COMMENT_PUBLISHED = 1;
21
22 /**
23 * Comments are displayed in a flat list - expanded.
24 */
25 const COMMENT_MODE_FLAT = 0;
26
27 /**
28 * Comments are displayed as a threaded list - expanded.
29 */
30 const COMMENT_MODE_THREADED = 1;
31
32 /**
33 * Anonymous posters cannot enter their contact information.
34 */
35 const COMMENT_ANONYMOUS_MAYNOT_CONTACT = 0;
36
37 /**
38 * Anonymous posters may leave their contact information.
39 */
40 const COMMENT_ANONYMOUS_MAY_CONTACT = 1;
41
42 /**
43 * Anonymous posters are required to leave their contact information.
44 */
45 const COMMENT_ANONYMOUS_MUST_CONTACT = 2;
46
47 /**
48 * Comment form should be displayed on a separate page.
49 */
50 const COMMENT_FORM_SEPARATE_PAGE = 0;
51
52 /**
53 * Comment form should be shown below post or list of comments.
54 */
55 const COMMENT_FORM_BELOW = 1;
56
57 /**
58 * Comments for this node are hidden.
59 */
60 const COMMENT_NODE_HIDDEN = 0;
61
62 /**
63 * Comments for this node are closed.
64 */
65 const COMMENT_NODE_CLOSED = 1;
66
67 /**
68 * Comments for this node are open.
69 */
70 const COMMENT_NODE_OPEN = 2;
71
72 /**
73 * Implements hook_help().
74 */
75 function comment_help($path, $arg) {
76 switch ($path) {
77 case 'admin/help#comment':
78 $output = '<h3>' . t('About') . '</h3>';
79 $output .= '<p>' . t('The Comment module allows users to comment on site content, set commenting defaults and permissions, and moderate comments. For more information, see the online handbook entry for <a href="@comment">Comment module</a>.', array('@comment' => 'http://drupal.org/handbook/modules/comment/')) . '</p>';
80 $output .= '<h3>' . t('Uses') . '</h3>';
81 $output .= '<dl>';
82 $output .= '<dt>' . t('Default and custom settings') . '</dt>';
83 $output .= '<dd>' . t("Each <a href='@content-type'>content type</a> can have its own default comment settings configured as: <em>Open</em> to allow new comments, <em>Hidden</em> to hide existing comments and prevent new comments, or <em>Closed</em> to view existing comments, but prevent new comments. These defaults will apply to all new content created (changes to the settings on existing content must be done manually). Other comment settings can also be customized per content type, and can be overridden for any given item of content. When a comment has no replies, it remains editable by its author, as long as the author has a user account and is logged in.", array('@content-type' => url('admin/structure/types'))) . '</dd>';
84 $output .= '<dt>' . t('Comment approval') . '</dt>';
85 $output .= '<dd>' . t("Comments from users who have the <em>Skip comment approval</em> permission are published immediately. All other comments are placed in the <a href='@comment-approval'>Unapproved comments</a> queue, until a user who has permission to <em>Administer comments</em> publishes or deletes them. Published comments can be bulk managed on the <a href='@admin-comment'>Published comments</a> administration page.", array('@comment-approval' => url('admin/content/comment/approval'), '@admin-comment' => url('admin/content/comment'))) . '</dd>';
86 $output .= '</dl>';
87 return $output;
88 }
89 }
90
91 /**
92 * Implements hook_entity_info().
93 */
94 function comment_entity_info() {
95 $return = array(
96 'comment' => array(
97 'label' => t('Comment'),
98 'base table' => 'comment',
99 'uri callback' => 'comment_uri',
100 'fieldable' => TRUE,
101 'controller class' => 'CommentStorageController',
102 'entity class' => 'Comment',
103 'entity keys' => array(
104 'id' => 'cid',
105 'bundle' => 'node_type',
106 'label' => 'subject',
107 ),
108 'bundles' => array(),
109 'view modes' => array(
110 'full' => array(
111 'label' => t('Full comment'),
112 'custom settings' => FALSE,
113 ),
114 ),
115 'static cache' => FALSE,
116 ),
117 );
118
119 foreach (node_type_get_names() as $type => $name) {
120 $return['comment']['bundles']['comment_node_' . $type] = array(
121 'label' => t('@node_type comment', array('@node_type' => $name)),
122 // Provide the node type/bundle name for other modules, so it does not
123 // have to be extracted manually from the bundle name.
124 'node bundle' => $type,
125 'admin' => array(
126 // Place the Field UI paths for comments one level below the
127 // corresponding paths for nodes, so that they appear in the same set
128 // of local tasks. Note that the paths use a different placeholder name
129 // and thus a different menu loader callback, so that Field UI page
130 // callbacks get a comment bundle name from the node type in the URL.
131 // See comment_node_type_load() and comment_menu_alter().
132 'path' => 'admin/structure/types/manage/%comment_node_type/comment',
133 'bundle argument' => 4,
134 'real path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type) . '/comment',
135 'access arguments' => array('administer content types'),
136 ),
137 );
138 }
139
140 return $return;
141 }
142
143 /**
144 * Loads the comment bundle name corresponding a given content type.
145 *
146 * This function is used as a menu loader callback in comment_menu().
147 *
148 * @param $name
149 * The URL-formatted machine name of the node type whose comment fields are
150 * to be edited. 'URL-formatted' means that underscores are replaced by
151 * hyphens.
152 *
153 * @return
154 * The comment bundle name corresponding to the node type.
155 *
156 * @see comment_menu_alter()
157 */
158 function comment_node_type_load($name) {
159 if ($type = node_type_get_type(strtr($name, array('-' => '_')))) {
160 return 'comment_node_' . $type->type;
161 }
162 }
163
164 /**
165 * Entity uri callback.
166 */
167 function comment_uri($comment) {
168 return array(
169 'path' => 'comment/' . $comment->cid,
170 'options' => array('fragment' => 'comment-' . $comment->cid),
171 );
172 }
173
174 /**
175 * Implements hook_field_extra_fields().
176 */
177 function comment_field_extra_fields() {
178 $return = array();
179
180 foreach (node_type_get_types() as $type) {
181 if (variable_get('comment_subject_field_' . $type->type, 1) == 1) {
182 $return['comment']['comment_node_' . $type->type] = array(
183 'form' => array(
184 'author' => array(
185 'label' => t('Author'),
186 'description' => t('Author textfield'),
187 'weight' => -2,
188 ),
189 'subject' => array(
190 'label' => t('Subject'),
191 'description' => t('Subject textfield'),
192 'weight' => -1,
193 ),
194 ),
195 );
196 }
197 }
198
199 return $return;
200 }
201
202 /**
203 * Implements hook_theme().
204 */
205 function comment_theme() {
206 return array(
207 'comment_block' => array(
208 'variables' => array(),
209 ),
210 'comment_preview' => array(
211 'variables' => array('comment' => NULL),
212 ),
213 'comment' => array(
214 'template' => 'comment',
215 'render element' => 'elements',
216 ),
217 'comment_post_forbidden' => array(
218 'variables' => array('node' => NULL),
219 ),
220 'comment_wrapper' => array(
221 'template' => 'comment-wrapper',
222 'render element' => 'content',
223 ),
224 );
225 }
226
227 /**
228 * Implements hook_menu().
229 */
230 function comment_menu() {
231 $items['admin/content/comment'] = array(
232 'title' => 'Comments',
233 'description' => 'List and edit site comments and the comment approval queue.',
234 'page callback' => 'comment_admin',
235 'access arguments' => array('administer comments'),
236 'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
237 'file' => 'comment.admin.inc',
238 );
239 // Tabs begin here.
240 $items['admin/content/comment/new'] = array(
241 'title' => 'Published comments',
242 'type' => MENU_DEFAULT_LOCAL_TASK,
243 'weight' => -10,
244 );
245 $items['admin/content/comment/approval'] = array(
246 'title' => 'Unapproved comments',
247 'title callback' => 'comment_count_unpublished',
248 'page arguments' => array('approval'),
249 'access arguments' => array('administer comments'),
250 'type' => MENU_LOCAL_TASK,
251 );
252 $items['comment/%'] = array(
253 'title' => 'Comment permalink',
254 'page callback' => 'comment_permalink',
255 'page arguments' => array(1),
256 'access arguments' => array('access comments'),
257 );
258 $items['comment/%/view'] = array(
259 'title' => 'View comment',
260 'type' => MENU_DEFAULT_LOCAL_TASK,
261 'weight' => -10,
262 );
263 // Every other comment path uses %, but this one loads the comment directly,
264 // so we don't end up loading it twice (in the page and access callback).
265 $items['comment/%comment/edit'] = array(
266 'title' => 'Edit',
267 'page callback' => 'comment_edit_page',
268 'page arguments' => array(1),
269 'access callback' => 'comment_access',
270 'access arguments' => array('edit', 1),
271 'type' => MENU_LOCAL_TASK,
272 'weight' => 0,
273 );
274 $items['comment/%/approve'] = array(
275 'title' => 'Approve',
276 'page callback' => 'comment_approve',
277 'page arguments' => array(1),
278 'access arguments' => array('administer comments'),
279 'file' => 'comment.pages.inc',
280 'weight' => 1,
281 );
282 $items['comment/%/delete'] = array(
283 'title' => 'Delete',
284 'page callback' => 'comment_confirm_delete_page',
285 'page arguments' => array(1),
286 'access arguments' => array('administer comments'),
287 'type' => MENU_LOCAL_TASK,
288 'file' => 'comment.admin.inc',
289 'weight' => 2,
290 );
291 $items['comment/reply/%node'] = array(
292 'title' => 'Add new comment',
293 'page callback' => 'comment_reply',
294 'page arguments' => array(2),
295 'access callback' => 'node_access',
296 'access arguments' => array('view', 2),
297 'file' => 'comment.pages.inc',
298 );
299
300 return $items;
301 }
302
303 /**
304 * Implements hook_menu_alter().
305 */
306 function comment_menu_alter(&$items) {
307 // Add comments to the description for admin/content.
308 $items['admin/content']['description'] = 'Administer content and comments.';
309
310 // Adjust the Field UI tabs on admin/structure/types/manage/[node-type].
311 // See comment_entity_info().
312 $items['admin/structure/types/manage/%comment_node_type/comment/fields']['title'] = 'Comment fields';
313 $items['admin/structure/types/manage/%comment_node_type/comment/fields']['weight'] = 3;
314 $items['admin/structure/types/manage/%comment_node_type/comment/display']['title'] = 'Comment display';
315 $items['admin/structure/types/manage/%comment_node_type/comment/display']['weight'] = 4;
316 }
317
318 /**
319 * Returns a menu title which includes the number of unapproved comments.
320 */
321 function comment_count_unpublished() {
322 $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE status = :status', array(
323 ':status' => COMMENT_NOT_PUBLISHED,
324 ))->fetchField();
325 return t('Unapproved comments (@count)', array('@count' => $count));
326 }
327
328 /**
329 * Implements hook_node_type_insert().
330 *
331 * Creates a comment body field for a node type created while the Comment module
332 * is enabled. For node types created before the Comment module is enabled,
333 * hook_modules_enabled() serves to create the body fields.
334 *
335 * @see comment_modules_enabled()
336 */
337 function comment_node_type_insert($info) {
338 _comment_body_field_create($info);
339 }
340
341 /**
342 * Implements hook_node_type_update().
343 */
344 function comment_node_type_update($info) {
345 if (!empty($info->old_type) && $info->type != $info->old_type) {
346 field_attach_rename_bundle('comment', 'comment_node_' . $info->old_type, 'comment_node_' . $info->type);
347 }
348 }
349
350 /**
351 * Implements hook_node_type_delete().
352 */
353 function comment_node_type_delete($info) {
354 field_attach_delete_bundle('comment', 'comment_node_' . $info->type);
355 $settings = array(
356 'comment',
357 'comment_default_mode',
358 'comment_default_per_page',
359 'comment_anonymous',
360 'comment_subject_field',
361 'comment_preview',
362 'comment_form_location',
363 );
364 foreach ($settings as $setting) {
365 variable_del($setting . '_' . $info->type);
366 }
367 }
368
369 /**
370 * Creates a comment_body field instance for a given node type.
371 *
372 * @param $info
373 * An object representing the content type. The only property that is
374 * currently used is $info->type, which is the machine name of the content
375 * type for which the body field (instance) is to be created.
376 */
377 function _comment_body_field_create($info) {
378 // Create the field if needed.
379 if (!field_read_field('comment_body', array('include_inactive' => TRUE))) {
380 $field = array(
381 'field_name' => 'comment_body',
382 'type' => 'text_long',
383 'entity_types' => array('comment'),
384 );
385 field_create_field($field);
386 }
387 // Create the instance if needed.
388 if (!field_read_instance('comment', 'comment_body', 'comment_node_' . $info->type, array('include_inactive' => TRUE))) {
389 field_attach_create_bundle('comment', 'comment_node_' . $info->type);
390 // Attaches the body field by default.
391 $instance = array(
392 'field_name' => 'comment_body',
393 'label' => 'Comment',
394 'entity_type' => 'comment',
395 'bundle' => 'comment_node_' . $info->type,
396 'settings' => array('text_processing' => 1),
397 'required' => TRUE,
398 'display' => array(
399 'default' => array(
400 'label' => 'hidden',
401 'type' => 'text_default',
402 'weight' => 0,
403 ),
404 ),
405 );
406 field_create_instance($instance);
407 }
408 }
409
410 /**
411 * Implements hook_permission().
412 */
413 function comment_permission() {
414 return array(
415 'administer comments' => array(
416 'title' => t('Administer comments and comment settings'),
417 ),
418 'access comments' => array(
419 'title' => t('View comments'),
420 ),
421 'post comments' => array(
422 'title' => t('Post comments'),
423 ),
424 'skip comment approval' => array(
425 'title' => t('Skip comment approval'),
426 ),
427 'edit own comments' => array(
428 'title' => t('Edit own comments'),
429 ),
430 );
431 }
432
433 /**
434 * Implements hook_block_info().
435 */
436 function comment_block_info() {
437 $blocks['recent']['info'] = t('Recent comments');
438 $blocks['recent']['properties']['administrative'] = TRUE;
439
440 return $blocks;
441 }
442
443 /**
444 * Implements hook_block_configure().
445 */
446 function comment_block_configure($delta = '') {
447 $form['comment_block_count'] = array(
448 '#type' => 'select',
449 '#title' => t('Number of recent comments'),
450 '#default_value' => variable_get('comment_block_count', 10),
451 '#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)),
452 );
453
454 return $form;
455 }
456
457 /**
458 * Implements hook_block_save().
459 */
460 function comment_block_save($delta = '', $edit = array()) {
461 variable_set('comment_block_count', (int) $edit['comment_block_count']);
462 }
463
464 /**
465 * Implements hook_block_view().
466 *
467 * Generates a block with the most recent comments.
468 */
469 function comment_block_view($delta = '') {
470 if (user_access('access comments')) {
471 $block['subject'] = t('Recent comments');
472 $block['content'] = theme('comment_block');
473
474 return $block;
475 }
476 }
477
478 /**
479 * Redirects comment links to the correct page depending on comment settings.
480 *
481 * Since comments are paged there is no way to guarantee which page a comment
482 * appears on. Comment paging and threading settings may be changed at any time.
483 * With threaded comments, an individual comment may move between pages as
484 * comments can be added either before or after it in the overall discussion.
485 * Therefore we use a central routing function for comment links, which
486 * calculates the page number based on current comment settings and returns
487 * the full comment view with the pager set dynamically.
488 *
489 * @param $cid
490 * A comment identifier.
491 *
492 * @return
493 * The comment listing set to the page on which the comment appears.
494 */
495 function comment_permalink($cid) {
496 if (($comment = comment_load($cid)) && ($node = node_load($comment->nid))) {
497
498 // Find the current display page for this comment.
499 $page = comment_get_display_page($comment->cid, $node->type);
500
501 // Set $_GET['q'] and $_GET['page'] ourselves so that the node callback
502 // behaves as it would when visiting the page directly.
503 $_GET['q'] = 'node/' . $node->nid;
504 $_GET['page'] = $page;
505
506 // Return the node view, this will show the correct comment in context.
507 return menu_execute_active_handler('node/' . $node->nid, FALSE);
508 }
509 drupal_not_found();
510 }
511
512 /**
513 * Finds the most recent comments that are available to the current user.
514 *
515 * @param integer $number
516 * (optional) The maximum number of comments to find. Defaults to 10.
517 *
518 * @return
519 * An array of comment objects or an empty array if there are no recent
520 * comments visible to the current user.
521 */
522 function comment_get_recent($number = 10) {
523 $query = db_select('comment', 'c');
524 $query->innerJoin('node', 'n', 'n.nid = c.nid');
525 $query->addTag('node_access');
526 $comments = $query
527 ->fields('c')
528 ->condition('c.status', COMMENT_PUBLISHED)
529 ->condition('n.status', NODE_PUBLISHED)
530 ->orderBy('c.created', 'DESC')
531 // Additionally order by cid to ensure that comments with the same timestamp
532 // are returned in the exact order posted.
533 ->orderBy('c.cid', 'DESC')
534 ->range(0, $number)
535 ->execute()
536 ->fetchAll();
537
538 return $comments ? $comments : array();
539 }
540
541 /**
542 * Calculates the page number for the first new comment.
543 *
544 * @param $num_comments
545 * Number of comments.
546 * @param $new_replies
547 * Number of new replies.
548 * @param $node
549 * The first new comment node.
550 *
551 * @return
552 * "page=X" if the page number is greater than zero; empty string otherwise.
553 */
554 function comment_new_page_count($num_comments, $new_replies, $node) {
555 $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
556 $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
557 $pagenum = NULL;
558 $flat = $mode == COMMENT_MODE_FLAT ? TRUE : FALSE;
559 if ($num_comments <= $comments_per_page) {
560 // Only one page of comments.
561 $pageno = 0;
562 }
563 elseif ($flat) {
564 // Flat comments.
565 $count = $num_comments - $new_replies;
566 $pageno = $count / $comments_per_page;
567 }
568 else {
569 // Threaded comments: we build a query with a subquery to find the first
570 // thread with a new comment.
571
572 // 1. Find all the threads with a new comment.
573 $unread_threads_query = db_select('comment')
574 ->fields('comment', array('thread'))
575 ->condition('nid', $node->nid)
576 ->condition('status', COMMENT_PUBLISHED)
577 ->orderBy('created', 'DESC')
578 ->orderBy('cid', 'DESC')
579 ->range(0, $new_replies);
580
581 // 2. Find the first thread.
582 $first_thread = db_select($unread_threads_query, 'thread')
583 ->fields('thread', array('thread'))
584 ->orderBy('SUBSTRING(thread, 1, (LENGTH(thread) - 1))')
585 ->range(0, 1)
586 ->execute()
587 ->fetchField();
588
589 // Remove the final '/'.
590 $first_thread = substr($first_thread, 0, -1);
591
592 // Find the number of the first comment of the first unread thread.
593 $count = db_query('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND status = :status AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread', array(
594 ':status' => COMMENT_PUBLISHED,
595 ':nid' => $node->nid,
596 ':thread' => $first_thread,
597 ))->fetchField();
598
599 $pageno = $count / $comments_per_page;
600 }
601
602 if ($pageno >= 1) {
603 $pagenum = array('page' => intval($pageno));
604 }
605
606 return $pagenum;
607 }
608
609 /**
610 * Returns HTML for a list of recent comments.
611 *
612 * @ingroup themeable
613 */
614 function theme_comment_block() {
615 $items = array();
616 $number = variable_get('comment_block_count', 10);
617 foreach (comment_get_recent($number) as $comment) {
618 $items[] = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)) . '&nbsp;<span>' . t('@time ago', array('@time' => format_interval(REQUEST_TIME - $comment->changed))) . '</span>';
619 }
620
621 if ($items) {
622 return theme('item_list', array('items' => $items));
623 }
624 else {
625 return t('No comments available.');
626 }
627 }
628
629 /**
630 * Implements hook_node_view().
631 */
632 function comment_node_view($node, $view_mode) {
633 $links = array();
634
635 if ($node->comment != COMMENT_NODE_HIDDEN) {
636 if ($view_mode == 'rss') {
637 // Add a comments RSS element which is a URL to the comments of this node.
638 $node->rss_elements[] = array(
639 'key' => 'comments',
640 'value' => url('node/' . $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))
641 );
642 }
643 elseif ($view_mode == 'teaser') {
644 // Teaser view: display the number of comments that have been posted,
645 // or a link to add new comments if the user has permission, the node
646 // is open to new comments, and there currently are none.
647 if (user_access('access comments')) {
648 if (!empty($node->comment_count)) {
649 $links['comment-comments'] = array(
650 'title' => format_plural($node->comment_count, '1 comment', '@count comments'),
651 'href' => "node/$node->nid",
652 'attributes' => array('title' => t('Jump to the first comment of this posting.')),
653 'fragment' => 'comments',
654 'html' => TRUE,
655 );
656 // Show a link to the first new comment.
657 if ($new = comment_num_new($node->nid)) {
658 $links['comment-new-comments'] = array(
659 'title' => format_plural($new, '1 new comment', '@count new comments'),
660 'href' => "node/$node->nid",
661 'query' => comment_new_page_count($node->comment_count, $new, $node),
662 'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
663 'fragment' => 'new',
664 'html' => TRUE,
665 );
666 }
667 }
668 }
669 if ($node->comment == COMMENT_NODE_OPEN) {
670 if (user_access('post comments')) {
671 $links['comment-add'] = array(
672 'title' => t('Add new comment'),
673 'href' => "comment/reply/$node->nid",
674 'attributes' => array('title' => t('Add a new comment to this page.')),
675 'fragment' => 'comment-form',
676 );
677 }
678 else {
679 $links['comment-forbidden'] = array(
680 'title' => theme('comment_post_forbidden', array('node' => $node)),
681 'html' => TRUE,
682 );
683 }
684 }
685 }
686 elseif ($view_mode != 'search_index' && $view_mode != 'search_result') {
687 // Node in other view modes: add a "post comment" link if the user is
688 // allowed to post comments and if this node is allowing new comments.
689 // But we don't want this link if we're building the node for search
690 // indexing or constructing a search result excerpt.
691 if ($node->comment == COMMENT_NODE_OPEN) {
692 $comment_form_location = variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW);
693 if (user_access('post comments')) {
694 // Show the "post comment" link if the form is on another page, or
695 // if there are existing comments that the link will skip past.
696 if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE || (!empty($node->comment_count) && user_access('access comments'))) {
697 $links['comment-add'] = array(
698 'title' => t('Add new comment'),
699 'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
700 'href' => "node/$node->nid",
701 'fragment' => 'comment-form',
702 );
703 if ($comment_form_location == COMMENT_FORM_SEPARATE_PAGE) {
704 $links['comment-add']['href'] = "comment/reply/$node->nid";
705 }
706 }
707 }
708 else {
709 $links['comment-forbidden'] = array(
710 'title' => theme('comment_post_forbidden', array('node' => $node)),
711 'html' => TRUE,
712 );
713 }
714 }
715 }
716
717 $node->content['links']['comment'] = array(
718 '#theme' => 'links__node__comment',
719 '#links' => $links,
720 '#attributes' => array('class' => array('links', 'inline')),
721 );
722
723 // Only append comments when we are building a node on its own node detail
724 // page. We compare $node and $page_node to ensure that comments are not
725 // appended to other nodes shown on the page, for example a node_reference
726 // displayed in 'full' view mode within another node.
727 if ($node->comment && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) {
728 $node->content['comments'] = comment_node_page_additions($node);
729 }
730 }
731 }
732
733 /**
734 * Builds the comment-related elements for node detail pages.
735 *
736 * @param $node
737 * The node object for which to build the comment-related elements.
738 *
739 * @return
740 * A renderable array representing the comment-related page elements for the
741 * node.
742 */
743 function comment_node_page_additions($node) {
744 $additions = array();
745
746 // Only attempt to render comments if the node has visible comments.
747 // Unpublished comments are not included in $node->comment_count, so show
748 // comments unconditionally if the user is an administrator.
749 if (($node->comment_count && user_access('access comments')) || user_access('administer comments')) {
750 $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
751 $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
752 if ($cids = comment_get_thread($node, $mode, $comments_per_page)) {
753 $comments = comment_load_multiple($cids);
754 comment_prepare_thread($comments);
755 $build = comment_view_multiple($comments, $node);
756 $build['pager']['#theme'] = 'pager';
757 $additions['comments'] = $build;
758 }
759 }
760
761 // Append comment form if needed.
762 if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) {
763 $comment = entity_create('comment', array('nid' => $node->nid));
764 $additions['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
765 }
766
767 if ($additions) {
768 $additions += array(
769 '#theme' => 'comment_wrapper__node_' . $node->type,
770 '#node' => $node,
771 'comments' => array(),
772 'comment_form' => array(),
773 );
774 }
775
776 return $additions;
777 }
778
779 /**
780 * Retrieves comments for a thread.
781 *
782 * @param $node
783 * The node whose comment(s) needs rendering.
784 * @param $mode
785 * The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED.
786 * @param $comments_per_page
787 * The amount of comments to display per page.
788 *
789 * @return
790 * An array of the IDs of the comment to be displayed.
791 *
792 * To display threaded comments in the correct order we keep a 'thread' field
793 * and order by that value. This field keeps this data in
794 * a way which is easy to update and convenient to use.
795 *
796 * A "thread" value starts at "1". If we add a child (A) to this comment,
797 * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
798 * brother of (A) will get "1.2". Next brother of the parent of (A) will get
799 * "2" and so on.
800 *
801 * First of all note that the thread field stores the depth of the comment:
802 * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
803 *
804 * Now to get the ordering right, consider this example:
805 *
806 * 1
807 * 1.1
808 * 1.1.1
809 * 1.2
810 * 2
811 *
812 * If we "ORDER BY thread ASC" we get the above result, and this is the
813 * natural order sorted by time. However, if we "ORDER BY thread DESC"
814 * we get:
815 *
816 * 2
817 * 1.2
818 * 1.1.1
819 * 1.1
820 * 1
821 *
822 * Clearly, this is not a natural way to see a thread, and users will get
823 * confused. The natural order to show a thread by time desc would be:
824 *
825 * 2
826 * 1
827 * 1.2
828 * 1.1
829 * 1.1.1
830 *
831 * which is what we already did before the standard pager patch. To achieve
832 * this we simply add a "/" at the end of each "thread" value. This way, the
833 * thread fields will look like this:
834 *
835 * 1/
836 * 1.1/
837 * 1.1.1/
838 * 1.2/
839 * 2/
840 *
841 * we add "/" since this char is, in ASCII, higher than every number, so if
842 * now we "ORDER BY thread DESC" we get the correct order. However this would
843 * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
844 * to consider the trailing "/" so we use a substring only.
845 */
846 function comment_get_thread($node, $mode, $comments_per_page) {
847 $query = db_select('comment', 'c')->extend('PagerDefault');
848 $query->addField('c', 'cid');
849 $query
850 ->condition('c.nid', $node->nid)
851 ->addTag('node_access')
852 ->addTag('comment_filter')
853 ->addMetaData('node', $node)
854 ->limit($comments_per_page);
855
856 $count_query = db_select('comment', 'c');
857 $count_query->addExpression('COUNT(*)');
858 $count_query
859 ->condition('c.nid', $node->nid)
860 ->addTag('node_access')
861 ->addTag('comment_filter')
862 ->addMetaData('node', $node);
863
864 if (!user_access('administer comments')) {
865 $query->condition('c.status', COMMENT_PUBLISHED);
866 $count_query->condition('c.status', COMMENT_PUBLISHED);
867 }
868 if ($mode === COMMENT_MODE_FLAT) {
869 $query->orderBy('c.cid', 'ASC');
870 }
871 else {
872 // See comment above. Analysis reveals that this doesn't cost too
873 // much. It scales much much better than having the whole comment
874 // structure.
875 $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
876 $query->orderBy('torder', 'ASC');
877 }
878
879 $query->setCountQuery($count_query);
880 $cids = $query->execute()->fetchCol();
881
882 return $cids;
883 }
884
885 /**
886 * Calculates the indentation level of each comment in a comment thread.
887 *
888 * This function loops over an array representing a comment thread. For each
889 * comment, the function calculates the indentation level and saves it in the
890 * 'divs' property of the comment object.
891 *
892 * @param array $comments
893 * An array of comment objects, keyed by comment ID.
894 */
895 function comment_prepare_thread(&$comments) {
896 // A flag stating if we are still searching for first new comment on the thread.
897 $first_new = TRUE;
898
899 // A counter that helps track how indented we are.
900 $divs = 0;
901
902 foreach ($comments as $key => $comment) {
903 if ($first_new && $comment->new != MARK_READ) {
904 // Assign the anchor only for the first new comment. This avoids duplicate
905 // id attributes on a page.
906 $first_new = FALSE;
907 $comment->first_new = TRUE;
908 }
909
910 // The $divs element instructs #prefix whether to add an indent div or
911 // close existing divs (a negative value).
912 $comment->depth = count(explode('.', $comment->thread)) - 1;
913 if ($comment->depth > $divs) {
914 $comment->divs = 1;
915 $divs++;
916 }
917 else {
918 $comment->divs = $comment->depth - $divs;
919 while ($comment->depth < $divs) {
920 $divs--;
921 }
922 }
923 $comments[$key] = $comment;
924 }
925
926 // The final comment must close up some hanging divs
927 $comments[$key]->divs_final = $divs;
928 }
929
930 /**
931 * Generates an array for rendering a comment.
932 *
933 * @param $comment
934 * The comment object.
935 * @param $node
936 * The node the comment is attached to.
937 * @param $view_mode
938 * View mode, e.g. 'full', 'teaser'...
939 * @param $langcode
940 * (optional) A language code to use for rendering. Defaults to the global
941 * content language of the current request.
942 *
943 * @return
944 * An array as expected by drupal_render().
945 */
946 function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) {
947 if (!isset($langcode)) {
948 $langcode = $GLOBALS['language_content']->langcode;
949 }
950
951 // Populate $comment->content with a render() array.
952 comment_build_content($comment, $node, $view_mode, $langcode);
953
954 $build = $comment->content;
955 // We don't need duplicate rendering info in comment->content.
956 unset($comment->content);
957
958 $build += array(
959 '#theme' => 'comment__node_' . $node->type,
960 '#comment' => $comment,
961 '#node' => $node,
962 '#view_mode' => $view_mode,
963 '#language' => $langcode,
964 );
965
966 if (empty($comment->in_preview)) {
967 $prefix = '';
968 $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED;
969
970 // Add 'new' anchor if needed.
971 if (!empty($comment->first_new)) {
972 $prefix .= "<a id=\"new\"></a>\n";
973 }
974
975 // Add indentation div or close open divs as needed.
976 if ($is_threaded) {
977 $prefix .= $comment->divs <= 0 ? str_repeat('</div>', abs($comment->divs)) : "\n" . '<div class="indented">';
978 }
979
980 // Add anchor for each comment.
981 $prefix .= "<a id=\"comment-$comment->cid\"></a>\n";
982 $build['#prefix'] = $prefix;
983
984 // Close all open divs.
985 if ($is_threaded && !empty($comment->divs_final)) {
986 $build['#suffix'] = str_repeat('</div>', $comment->divs_final);
987 }
988 }
989
990 // Allow modules to modify the structured comment.
991 $type = 'comment';
992 drupal_alter(array('comment_view', 'entity_view'), $build, $type);
993
994 return $build;
995 }
996
997 /**
998 * Builds a structured array representing the comment's content.
999 *
1000 * The content built for the comment (field values, comments, file attachments
1001 * or other comment components) will vary depending on the $view_mode parameter.
1002 *
1003 * @param $comment
1004 * A comment object.
1005 * @param $node
1006 * The node the comment is attached to.
1007 * @param $view_mode
1008 * View mode, e.g. 'full', 'teaser'...
1009 * @param $langcode
1010 * (optional) A language code to use for rendering. Defaults to the global
1011 * content language of the current request.
1012 */
1013 function comment_build_content($comment, $node, $view_mode = 'full', $langcode = NULL) {
1014 if (!isset($langcode)) {
1015 $langcode = $GLOBALS['language_content']->langcode;
1016 }
1017
1018 // Remove previously built content, if exists.
1019 $comment->content = array();
1020
1021 // Build fields content.
1022 field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode, $langcode);
1023 entity_prepare_view('comment', array($comment->cid => $comment), $langcode);
1024 $comment->content += field_attach_view('comment', $comment, $view_mode, $langcode);
1025
1026 $comment->content['links'] = array(
1027 '#theme' => 'links__comment',
1028 '#pre_render' => array('drupal_pre_render_links'),
1029 '#attributes' => array('class' => array('links', 'inline')),
1030 );
1031 if (empty($comment->in_preview)) {
1032 $comment->content['links']['comment'] = array(
1033 '#theme' => 'links__comment__comment',
1034 '#links' => comment_links($comment, $node),
1035 '#attributes' => array('class' => array('links', 'inline')),
1036 );
1037 }
1038
1039 // Allow modules to make their own additions to the comment.
1040 module_invoke_all('comment_view', $comment, $view_mode, $langcode);
1041 module_invoke_all('entity_view', $comment, 'comment', $view_mode, $langcode);
1042 }
1043
1044 /**
1045 * Adds reply, edit, delete, etc. links, depending on user permissions.
1046 *
1047 * @param $comment
1048 * The comment object.
1049 * @param $node
1050 * The node the comment is attached to.
1051 *
1052 * @return
1053 * A structured array of links.
1054 */
1055 function comment_links($comment, $node) {
1056 $links = array();
1057 if ($node->comment == COMMENT_NODE_OPEN) {
1058 if (user_access('administer comments') && user_access('post comments')) {
1059 $links['comment-delete'] = array(
1060 'title' => t('delete'),
1061 'href' => "comment/$comment->cid/delete",
1062 'html' => TRUE,
1063 );
1064 $links['comment-edit'] = array(
1065 'title' => t('edit'),
1066 'href' => "comment/$comment->cid/edit",
1067 'html' => TRUE,
1068 );
1069 $links['comment-reply'] = array(
1070 'title' => t('reply'),
1071 'href' => "comment/reply/$comment->nid/$comment->cid",
1072 'html' => TRUE,
1073 );
1074 if ($comment->status == COMMENT_NOT_PUBLISHED) {
1075 $links['comment-approve'] = array(
1076 'title' => t('approve'),
1077 'href' => "comment/$comment->cid/approve",
1078 'html' => TRUE,
1079 'query' => array('token' => drupal_get_token("comment/$comment->cid/approve")),
1080 );
1081 }
1082 }
1083 elseif (user_access('post comments')) {
1084 if (comment_access('edit', $comment)) {
1085 $links['comment-edit'] = array(
1086 'title' => t('edit'),
1087 'href' => "comment/$comment->cid/edit",
1088 'html' => TRUE,
1089 );
1090 }
1091 $links['comment-reply'] = array(
1092 'title' => t('reply'),
1093 'href' => "comment/reply/$comment->nid/$comment->cid",
1094 'html' => TRUE,
1095 );
1096 }
1097 else {
1098 $links['comment-forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
1099 $links['comment-forbidden']['html'] = TRUE;
1100 }
1101 }
1102 return $links;
1103 }
1104
1105 /**
1106 * Constructs render array from an array of loaded comments.
1107 *
1108 * @param $comments
1109 * An array of comments as returned by comment_load_multiple().
1110 * @param $node
1111 * The node the comments are attached to.
1112 * @param $view_mode
1113 * View mode, e.g. 'full', 'teaser'...
1114 * @param $weight
1115 * An integer representing the weight of the first comment in the list.
1116 * @param $langcode
1117 * A string indicating the language field values are to be shown in. If no
1118 * language is provided the current content language is used.
1119 *
1120 * @return
1121 * An array in the format expected by drupal_render().
1122 *
1123 * @see drupal_render()
1124 */
1125 function comment_view_multiple($comments, $node, $view_mode = 'full', $weight = 0, $langcode = NULL) {
1126 field_attach_prepare_view('comment', $comments, $view_mode, $langcode);
1127 entity_prepare_view('comment', $comments, $langcode);
1128
1129 $build = array(
1130 '#sorted' => TRUE,
1131 );
1132 foreach ($comments as $comment) {
1133 $build[$comment->cid] = comment_view($comment, $node, $view_mode, $langcode);
1134 $build[$comment->cid]['#weight'] = $weight;
1135 $weight++;
1136 }
1137 return $build;
1138 }
1139
1140 /**
1141 * Implements hook_form_FORM_ID_alter().
1142 */
1143 function comment_form_node_type_form_alter(&$form, $form_state) {
1144 if (isset($form['type'])) {
1145 $form['comment'] = array(
1146 '#type' => 'fieldset',
1147 '#title' => t('Comment settings'),
1148 '#collapsible' => TRUE,
1149 '#collapsed' => TRUE,
1150 '#group' => 'additional_settings',
1151 '#attributes' => array(
1152 'class' => array('comment-node-type-settings-form'),
1153 ),
1154 '#attached' => array(
1155 'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
1156 ),
1157 );
1158 // Unlike coment_form_node_form_alter(), all of these settings are applied
1159 // as defaults to all new nodes. Therefore, it would be wrong to use #states
1160 // to hide the other settings based on the primary comment setting.
1161 $form['comment']['comment'] = array(
1162 '#type' => 'select',
1163 '#title' => t('Default comment setting for new content'),
1164 '#default_value' => variable_get('comment_' . $form['#node_type']->type, COMMENT_NODE_OPEN),
1165 '#options' => array(
1166 COMMENT_NODE_OPEN => t('Open'),
1167 COMMENT_NODE_CLOSED => t('Closed'),
1168 COMMENT_NODE_HIDDEN => t('Hidden'),
1169 ),
1170 );
1171 $form['comment']['comment_default_mode'] = array(
1172 '#type' => 'checkbox',
1173 '#title' => t('Threading'),
1174 '#default_value' => variable_get('comment_default_mode_' . $form['#node_type']->type, COMMENT_MODE_THREADED),
1175 '#description' => t('Show comment replies in a threaded list.'),
1176 );
1177 $form['comment']['comment_default_per_page'] = array(
1178 '#type' => 'select',
1179 '#title' => t('Comments per page'),
1180 '#default_value' => variable_get('comment_default_per_page_' . $form['#node_type']->type, 50),
1181 '#options' => _comment_per_page(),
1182 );
1183 $form['comment']['comment_anonymous'] = array(
1184 '#type' => 'select',
1185 '#title' => t('Anonymous commenting'),
1186 '#default_value' => variable_get('comment_anonymous_' . $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT),
1187 '#options' => array(
1188 COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
1189 COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
1190 COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'),
1191 ),
1192 '#access' => user_access('post comments', drupal_anonymous_user()),
1193 );
1194 $form['comment']['comment_subject_field'] = array(
1195 '#type' => 'checkbox',
1196 '#title' => t('Allow comment title'),
1197 '#default_value' => variable_get('comment_subject_field_' . $form['#node_type']->type, 1),
1198 );
1199 $form['comment']['comment_form_location'] = array(
1200 '#type' => 'checkbox',
1201 '#title' => t('Show reply form on the same page as comments'),
1202 '#default_value' => variable_get('comment_form_location_' . $form['#node_type']->type, COMMENT_FORM_BELOW),
1203 );
1204 $form['comment']['comment_preview'] = array(
1205 '#type' => 'radios',
1206 '#title' => t('Preview comment'),
1207 '#default_value' => variable_get('comment_preview_' . $form['#node_type']->type, DRUPAL_OPTIONAL),
1208 '#options' => array(
1209 DRUPAL_DISABLED => t('Disabled'),
1210 DRUPAL_OPTIONAL => t('Optional'),
1211 DRUPAL_REQUIRED => t('Required'),
1212 ),
1213 );
1214 }
1215 }
1216
1217 /**
1218 * Implements hook_form_BASE_FORM_ID_alter().
1219 */
1220 function comment_form_node_form_alter(&$form, $form_state) {
1221 $node = $form['#node'];
1222 $form['comment_settings'] = array(
1223 '#type' => 'fieldset',
1224 '#access' => user_access('administer comments'),
1225 '#title' => t('Comment settings'),
1226 '#collapsible' => TRUE,
1227 '#collapsed' => TRUE,
1228 '#group' => 'additional_settings',
1229 '#attributes' => array(
1230 'class' => array('comment-node-settings-form'),
1231 ),
1232 '#attached' => array(
1233 'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
1234 ),
1235 '#weight' => 30,
1236 );
1237 $comment_count = isset($node->nid) ? db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->nid))->fetchField() : 0;
1238 $comment_settings = ($node->comment == COMMENT_NODE_HIDDEN && empty($comment_count)) ? COMMENT_NODE_CLOSED : $node->comment;
1239 $form['comment_settings']['comment'] = array(
1240 '#type' => 'radios',
1241 '#title' => t('Comments'),
1242 '#title_display' => 'invisible',
1243 '#parents' => array('comment'),
1244 '#default_value' => $comment_settings,
1245 '#options' => array(
1246 COMMENT_NODE_OPEN => t('Open'),
1247 COMMENT_NODE_CLOSED => t('Closed'),
1248 COMMENT_NODE_HIDDEN => t('Hidden'),
1249 ),
1250 COMMENT_NODE_OPEN => array(
1251 '#description' => t('Users with the "Post comments" permission can post comments.'),
1252 ),
1253 COMMENT_NODE_CLOSED => array(
1254 '#description' => t('Users cannot post comments, but existing comments will be displayed.'),
1255 ),
1256 COMMENT_NODE_HIDDEN => array(
1257 '#description' => t('Comments are hidden from view.'),
1258 ),
1259 );
1260 // If the node doesn't have any comments, the "hidden" option makes no
1261 // sense, so don't even bother presenting it to the user.
1262 if (empty($comment_count)) {
1263 $form['comment_settings']['comment'][COMMENT_NODE_HIDDEN]['#access'] = FALSE;
1264 // Also adjust the description of the "closed" option.
1265 $form['comment_settings']['comment'][COMMENT_NODE_CLOSED]['#description'] = t('Users cannot post comments.');
1266 }
1267 }
1268
1269 /**
1270 * Implements hook_node_load().
1271 */
1272 function comment_node_load($nodes, $types) {
1273 $comments_enabled = array();
1274
1275 // Check if comments are enabled for each node. If comments are disabled,
1276 // assign values without hitting the database.
1277 foreach ($nodes as $node) {
1278 // Store whether comments are enabled for this node.
1279 if ($node->comment != COMMENT_NODE_HIDDEN) {
1280 $comments_enabled[] = $node->nid;
1281 }
1282 else {
1283 $node->cid = 0;
1284 $node->last_comment_timestamp = $node->created;
1285 $node->last_comment_name = '';
1286 $node->last_comment_uid = $node->uid;
1287 $node->comment_count = 0;
1288 }
1289 }
1290
1291 // For nodes with comments enabled, fetch information from the database.
1292 if (!empty($comments_enabled)) {
1293 $result = db_query('SELECT nid, cid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count FROM {node_comment_statistics} WHERE nid IN (:comments_enabled)', array(':comments_enabled' => $comments_enabled));
1294 foreach ($result as $record) {
1295 $nodes[$record->nid]->cid = $record->cid;
1296 $nodes[$record->nid]->last_comment_timestamp = $record->last_comment_timestamp;
1297 $nodes[$record->nid]->last_comment_name = $record->last_comment_name;
1298 $nodes[$record->nid]->last_comment_uid = $record->last_comment_uid;
1299 $nodes[$record->nid]->comment_count = $record->comment_count;
1300 }
1301 }
1302 }
1303
1304 /**
1305 * Implements hook_node_prepare().
1306 */
1307 function comment_node_prepare($node) {
1308 if (!isset($node->comment)) {
1309 $node->comment = variable_get("comment_$node->type", COMMENT_NODE_OPEN);
1310 }
1311 }
1312
1313 /**
1314 * Implements hook_node_insert().
1315 */
1316 function comment_node_insert($node) {
1317 // Allow bulk updates and inserts to temporarily disable the
1318 // maintenance of the {node_comment_statistics} table.
1319 if (variable_get('comment_maintain_node_statistics', TRUE)) {
1320 db_insert('node_comment_statistics')
1321 ->fields(array(
1322 'nid' => $node->nid,
1323 'cid' => 0,
1324 'last_comment_timestamp' => $node->changed,
1325 'last_comment_name' => NULL,
1326 'last_comment_uid' => $node->uid,
1327 'comment_count' => 0,
1328 ))
1329 ->execute();
1330 }
1331 }
1332
1333 /**
1334 * Implements hook_node_predelete().
1335 */
1336 function comment_node_predelete($node) {
1337 $cids = db_query('SELECT cid FROM {comment} WHERE nid = :nid', array(':nid' => $node->nid))->fetchCol();
1338 comment_delete_multiple($cids);
1339 db_delete('node_comment_statistics')
1340 ->condition('nid', $node->nid)
1341 ->execute();
1342 }
1343
1344 /**
1345 * Implements hook_node_update_index().
1346 */
1347 function comment_node_update_index($node) {
1348 $index_comments = &drupal_static(__FUNCTION__);
1349
1350 if ($index_comments === NULL) {
1351 // Find and save roles that can 'access comments' or 'search content'.
1352 $perms = array('access comments' => array(), 'search content' => array());
1353 $result = db_query("SELECT rid, permission FROM {role_permission} WHERE permission IN ('access comments', 'search content')");
1354 foreach ($result as $record) {
1355 $perms[$record->permission][$record->rid] = $record->rid;
1356 }
1357
1358 // Prevent indexing of comments if there are any roles that can search but
1359 // not view comments.
1360 $index_comments = TRUE;
1361 foreach ($perms['search content'] as $rid) {
1362 if (!isset($perms['access comments'][$rid]) && ($rid <= DRUPAL_AUTHENTICATED_RID || !isset($perms['access comments'][DRUPAL_AUTHENTICATED_RID]))) {
1363 $index_comments = FALSE;
1364 break;
1365 }
1366 }
1367 }
1368
1369 if ($index_comments) {
1370 $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
1371 $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
1372 if ($node->comment && $cids = comment_get_thread($node, $mode, $comments_per_page)) {
1373 $comments = comment_load_multiple($cids);
1374 comment_prepare_thread($comments);
1375 $build = comment_view_multiple($comments, $node);
1376 return drupal_render($build);
1377 }
1378 }
1379 return '';
1380 }
1381
1382 /**
1383 * Implements hook_update_index().
1384 */
1385 function comment_update_index() {
1386 // Store the maximum possible comments per thread (used for ranking by reply count)
1387 variable_set('node_cron_comments_scale', 1.0 / max(1, db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}')->fetchField()));
1388 }
1389
1390 /**
1391 * Implements hook_node_search_result().
1392 *
1393 * Formats a comment count string and returns it, for display with search
1394 * results.
1395 */
1396 function comment_node_search_result($node) {
1397 // Do not make a string if comments are hidden.
1398 if (user_access('access comments') && $node->comment != COMMENT_NODE_HIDDEN) {
1399 $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField();
1400 // Do not make a string if comments are closed and there are currently
1401 // zero comments.
1402 if ($node->comment != COMMENT_NODE_CLOSED || $comments > 0) {
1403 return array('comment' => format_plural($comments, '1 comment', '@count comments'));
1404 }
1405 }
1406 }
1407
1408 /**
1409 * Implements hook_user_cancel().
1410 */
1411 function comment_user_cancel($edit, $account, $method) {
1412 switch ($method) {
1413 case 'user_cancel_block_unpublish':
1414 $comments = comment_load_multiple(array(), array('uid' => $account->uid));
1415 foreach ($comments as $comment) {
1416 $comment->status = 0;
1417 comment_save($comment);
1418 }
1419 break;
1420
1421 case 'user_cancel_reassign':
1422 $comments = comment_load_multiple(array(), array('uid' => $account->uid));
1423 foreach ($comments as $comment) {
1424 $comment->uid = 0;
1425 comment_save($comment);
1426 }
1427 break;
1428 }
1429 }
1430
1431 /**
1432 * Implements hook_user_predelete().
1433 */
1434 function comment_user_predelete($account) {
1435 $cids = db_query('SELECT c.cid FROM {comment} c WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol();
1436 comment_delete_multiple($cids);
1437 }
1438
1439 /**
1440 * Determines whether the current user has access to a particular comment.
1441 *
1442 * Authenticated users can edit their comments as long they have not been
1443 * replied to. This prevents people from changing or revising their statements
1444 * based on the replies to their posts.
1445 *
1446 * @param $op
1447 * The operation that is to be performed on the comment. Only 'edit' is
1448 * recognized now.
1449 * @param $comment
1450 * The comment object.
1451 *
1452 * @return
1453 * TRUE if the current user has acces to the comment, FALSE otherwise.
1454 */
1455 function comment_access($op, $comment) {
1456 global $user;
1457
1458 if ($op == 'edit') {
1459 return ($user->uid && $user->uid == $comment->uid && $comment->status == COMMENT_PUBLISHED && user_access('edit own comments')) || user_access('administer comments');
1460 }
1461 }
1462
1463 /**
1464 * Accepts a submission of new or changed comment content.
1465 *
1466 * @param $comment
1467 * A comment object.
1468 */
1469 function comment_save($comment) {
1470 $comment->save();
1471 }
1472
1473 /**
1474 * Deletes a comment and all its replies.
1475 *
1476 * @param $cid
1477 * The ID of the comment to delete.
1478 */
1479 function comment_delete($cid) {
1480 comment_delete_multiple(array($cid));
1481 }
1482
1483 /**
1484 * Deletes comments and all their replies.
1485 *
1486 * @param $cids
1487 * The IDs of the comments to delete.
1488 *
1489 * @see hook_comment_predelete()
1490 * @see hook_comment_delete()
1491 */
1492 function comment_delete_multiple($cids) {
1493 entity_delete_multiple('comment', $cids);
1494 }
1495
1496 /**
1497 * Loads comments from the database.
1498 *
1499 * @param $cids
1500 * An array of comment IDs.
1501 * @param $conditions
1502 * (deprecated) An associative array of conditions on the {comments}
1503 * table, where the keys are the database fields and the values are the
1504 * values those fields must have. Instead, it is preferable to use
1505 * EntityFieldQuery to retrieve a list of entity IDs loadable by
1506 * this function.
1507 * @param $reset
1508 * Whether to reset the internal static entity cache. Note that the static
1509 * cache is disabled in comment_entity_info() by default.
1510 *
1511 * @return
1512 * An array of comment objects, indexed by comment ID.
1513 *
1514 * @todo Remove $conditions in Drupal 8.
1515 *
1516 * @see entity_load()
1517 * @see EntityFieldQuery
1518 */
1519 function comment_load_multiple($cids = array(), $conditions = array(), $reset = FALSE) {
1520 return entity_load('comment', $cids, $conditions, $reset);
1521 }
1522
1523 /**
1524 * Loads the entire comment by comment ID.
1525 *
1526 * @param $cid
1527 * The ID of the comment to be loaded.
1528 * @param $reset
1529 * Whether to reset the internal static entity cache. Note that the static
1530 * cache is disabled in comment_entity_info() by default.
1531 *
1532 * @return
1533 * The comment object.
1534 */
1535 function comment_load($cid, $reset = FALSE) {
1536 $comment = comment_load_multiple(array($cid), array(), $reset);
1537 return $comment ? $comment[$cid] : FALSE;
1538 }
1539
1540 /**
1541 * Gets the number of new comments for the current user and the specified node.
1542 *
1543 * @param $nid
1544 * Node ID to count comments for.
1545 * @param $timestamp
1546 * Time to count from (defaults to time of last user access
1547 * to node).
1548 *
1549 * @return
1550 * The number of new comments or FALSE if the user is not logged in.
1551 */
1552 function comment_num_new($nid, $timestamp = 0) {
1553 global $user;
1554
1555 if ($user->uid) {
1556 // Retrieve the timestamp at which the current user last viewed this node.
1557 if (!$timestamp) {
1558 $timestamp = node_last_viewed($nid);
1559 }
1560 $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT);
1561
1562 // Use the timestamp to retrieve the number of new comments.
1563 return db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND created > :timestamp AND status = :status', array(
1564 ':nid' => $nid,
1565 ':timestamp' => $timestamp,
1566 ':status' => COMMENT_PUBLISHED,
1567 ))->fetchField();
1568 }
1569 else {
1570 return FALSE;
1571 }
1572
1573 }
1574
1575 /**
1576 * Gets the display ordinal for a comment, starting from 0.
1577 *
1578 * Count the number of comments which appear before the comment we want to
1579 * display, taking into account display settings and threading.
1580 *
1581 * @param $cid
1582 * The comment ID.
1583 * @param $node_type
1584 * The node type of the comment's parent.
1585 *
1586 * @return
1587 * The display ordinal for the comment.
1588 *
1589 * @see comment_get_display_page()
1590 */
1591 function comment_get_display_ordinal($cid, $node_type) {
1592 // Count how many comments (c1) are before $cid (c2) in display order. This is
1593 // the 0-based display ordinal.
1594 $query = db_select('comment', 'c1');
1595 $query->innerJoin('comment', 'c2', 'c2.nid = c1.nid');
1596 $query->addExpression('COUNT(*)', 'count');
1597 $query->condition('c2.cid', $cid);
1598 if (!user_access('administer comments')) {
1599 $query->condition('c1.status', COMMENT_PUBLISHED);
1600 }
1601 $mode = variable_get('comment_default_mode_' . $node_type, COMMENT_MODE_THREADED);
1602
1603 if ($mode == COMMENT_MODE_FLAT) {
1604 // For flat comments, cid is used for ordering comments due to
1605 // unpredicatable behavior with timestamp, so we make the same assumption
1606 // here.
1607 $query->condition('c1.cid', $cid, '<');
1608 }
1609 else {
1610 // For threaded comments, the c.thread column is used for ordering. We can
1611 // use the sorting code for comparison, but must remove the trailing slash.
1612 // See comment_view_multiple().
1613 $query->where('SUBSTRING(c1.thread, 1, (LENGTH(c1.thread) -1)) < SUBSTRING(c2.thread, 1, (LENGTH(c2.thread) -1))');
1614 }
1615
1616 return $query->execute()->fetchField();
1617 }
1618
1619 /**
1620 * Returns the page number for a comment.
1621 *
1622 * Finds the correct page number for a comment taking into account display
1623 * and paging settings.
1624 *
1625 * @param $cid
1626 * The comment ID.
1627 * @param $node_type
1628 * The node type the comment is attached to.
1629 *
1630 * @return
1631 * The page number.
1632 */
1633 function comment_get_display_page($cid, $node_type) {
1634 $ordinal = comment_get_display_ordinal($cid, $node_type);
1635 $comments_per_page = variable_get('comment_default_per_page_' . $node_type, 50);
1636 return floor($ordinal / $comments_per_page);
1637 }
1638
1639 /**
1640 * Page callback: Displays the comment editing form.
1641 *
1642 * Path: comment/%comment/edit
1643 *
1644 * @param $comment
1645 * The comment object representing the comment to be edited.
1646 *
1647 * @see comment_menu()
1648 */
1649 function comment_edit_page($comment) {
1650 drupal_set_title(t('Edit comment %comment', array('%comment' => $comment->subject)), PASS_THROUGH);
1651 $node = node_load($comment->nid);
1652 return drupal_get_form("comment_node_{$node->type}_form", $comment);
1653 }
1654
1655 /**
1656 * Implements hook_forms().
1657 */
1658 function comment_forms() {
1659 $forms = array();
1660 foreach (node_type_get_types() as $type) {
1661 $forms["comment_node_{$type->type}_form"]['callback'] = 'comment_form';
1662 }
1663 return $forms;
1664 }
1665
1666 /**
1667 * Form constructor for the basic commenting form.
1668 *
1669 * @see comment_form_validate()
1670 * @see comment_form_submit()
1671 * @see comment_form_build_preview()
1672 * @ingroup forms
1673 */
1674 function comment_form($form, &$form_state, $comment) {
1675 global $user, $language_content;
1676
1677 // During initial form build, add the comment entity to the form state for
1678 // use during form building and processing. During a rebuild, use what is in
1679 // the form state.
1680 if (!isset($form_state['comment'])) {
1681 $form_state['comment'] = $comment;
1682 }
1683 else {
1684 $comment = $form_state['comment'];
1685 }
1686
1687 $node = node_load($comment->nid);
1688 $form['#node'] = $node;
1689
1690 // Use #comment-form as unique jump target, regardless of node type.
1691 $form['#id'] = drupal_html_id('comment_form');
1692 $form['#theme'] = array('comment_form__node_' . $node->type, 'comment_form');
1693
1694 $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT);
1695 $is_admin = (!empty($comment->cid) && user_access('administer comments'));
1696
1697 if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
1698 $form['#attached']['library'][] = array('system', 'jquery.cookie');
1699 $form['#attributes']['class'][] = 'user-info-from-cookie';
1700 }
1701
1702 // If not replying to a comment, use our dedicated page callback for new
1703 // comments on nodes.
1704 if (empty($comment->cid) && empty($comment->pid)) {
1705 $form['#action'] = url('comment/reply/' . $comment->nid);
1706 }
1707
1708 if (isset($form_state['comment_preview'])) {
1709 $form += $form_state['comment_preview'];
1710 }
1711
1712 // Display author information in a fieldset for comment moderators.
1713 if ($is_admin) {
1714 $form['author'] = array(
1715 '#type' => 'fieldset',
1716 '#title' => t('Administration'),
1717 '#collapsible' => TRUE,
1718 '#collapsed' => TRUE,
1719 '#weight' => -2,
1720 );
1721 }
1722 else {
1723 // Sets the author form elements above the subject.
1724 $form['author'] = array(
1725 '#weight' => -2,
1726 );
1727 }
1728
1729 // Prepare default values for form elements.
1730 if ($is_admin) {
1731 $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name);
1732 $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED);
1733 $date = (!empty($comment->date) ? $comment->date : format_date($comment->created, 'custom', 'Y-m-d H:i O'));
1734 }
1735 else {
1736 if ($user->uid) {
1737 $author = $user->name;
1738 }
1739 else {
1740 $author = ($comment->name ? $comment->name : '');
1741 }
1742 $status = (user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED);
1743 $date = '';
1744 }
1745
1746 // Add the author name field depending on the current user.
1747 if ($is_admin) {
1748 $form['author']['name'] = array(
1749 '#type' => 'textfield',
1750 '#title' => t('Authored by'),
1751 '#default_value' => $author,
1752 '#maxlength' => 60,
1753 '#size' => 30,
1754 '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
1755 '#autocomplete_path' => 'user/autocomplete',
1756 );
1757 }
1758 elseif ($user->uid) {
1759 $form['author']['_author'] = array(
1760 '#type' => 'item',
1761 '#title' => t('Your name'),
1762 '#markup' => theme('username', array('account' => $user)),
1763 );
1764 $form['author']['name'] = array(
1765 '#type' => 'value',
1766 '#value' => $author,
1767 );
1768 }
1769 else {
1770 $form['author']['name'] = array(
1771 '#type' => 'textfield',
1772 '#title' => t('Your name'),
1773 '#default_value' => $author,
1774 '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
1775 '#maxlength' => 60,
1776 '#size' => 30,
1777 );
1778 }
1779
1780 // Add author e-mail and homepage fields depending on the current user.
1781 $form['author']['mail'] = array(
1782 '#type' => 'textfield',
1783 '#title' => t('E-mail'),
1784 '#default_value' => $comment->mail,
1785 '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
1786 '#maxlength' => 64,
1787 '#size' => 30,
1788 '#description' => t('The content of this field is kept private and will not be shown publicly.'),
1789 '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
1790 );
1791 $form['author']['homepage'] = array(
1792 '#type' => 'textfield',
1793 '#title' => t('Homepage'),
1794 '#default_value' => $comment->homepage,
1795 '#maxlength' => 255,
1796 '#size' => 30,
1797 '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
1798 );
1799
1800 // Add administrative comment publishing options.
1801 $form['author']['date'] = array(
1802 '#type' => 'textfield',
1803 '#title' => t('Authored on'),
1804 '#default_value' => $date,
1805 '#maxlength' => 25,
1806 '#size' => 20,
1807 '#access' => $is_admin,
1808 );
1809 $form['author']['status'] = array(
1810 '#type' => 'radios',
1811 '#title' => t('Status'),
1812 '#default_value' => $status,
1813 '#options' => array(
1814 COMMENT_PUBLISHED => t('Published'),
1815 COMMENT_NOT_PUBLISHED => t('Not published'),
1816 ),
1817 '#access' => $is_admin,
1818 );
1819
1820 $form['subject'] = array(
1821 '#type' => 'textfield',
1822 '#title' => t('Subject'),
1823 '#maxlength' => 64,
1824 '#default_value' => $comment->subject,
1825 '#access' => variable_get('comment_subject_field_' . $node->type, 1) == 1,
1826 '#weight' => -1,
1827 );
1828
1829 // Used for conditional validation of author fields.
1830 $form['is_anonymous'] = array(
1831 '#type' => 'value',
1832 '#value' => ($comment->cid ? !$comment->uid : !$user->uid),
1833 );
1834
1835 // Add internal comment properties.
1836 foreach (array('cid', 'pid', 'nid', 'uid') as $key) {
1837 $form[$key] = array('#type' => 'value', '#value' => $comment->$key);
1838 }
1839 $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type);
1840
1841 // If a content type has multilingual support we set the comment to inherit the
1842 // content language. Otherwise mark the comment as language neutral.
1843 $comment_langcode = $comment->language;
1844 if (($comment_langcode == LANGUAGE_NONE) && variable_get('language_content_type_' . $node->type, 0)) {
1845 $comment_langcode = $language_content->langcode;
1846 }
1847 $form['language'] = array(
1848 '#type' => 'value',
1849 '#value' => $comment_langcode,
1850 );
1851
1852 // Only show the save button if comment previews are optional or if we are
1853 // already previewing the submission.
1854 $form['actions'] = array('#type' => 'actions');
1855 $form['actions']['submit'] = array(
1856 '#type' => 'submit',
1857 '#value' => t('Save'),
1858 '#access' => ($comment->cid && user_access('administer comments')) || variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || isset($form_state['comment_preview']),
1859 '#weight' => 19,
1860 );
1861 $form['actions']['preview'] = array(
1862 '#type' => 'submit',
1863 '#value' => t('Preview'),
1864 '#access' => (variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED),
1865 '#weight' => 20,
1866 '#submit' => array('comment_form_build_preview'),
1867 );
1868
1869 // Attach fields.
1870 $comment->node_type = 'comment_node_' . $node->type;
1871 field_attach_form('comment', $comment, $form, $form_state);
1872
1873 return $form;
1874 }
1875
1876 /**
1877 * Form submission handler for the 'preview' button in comment_form().
1878 */
1879 function comment_form_build_preview($form, &$form_state) {
1880 $comment = comment_form_submit_build_comment($form, $form_state);
1881 $form_state['comment_preview'] = comment_preview($comment);
1882 $form_state['rebuild'] = TRUE;
1883 }
1884
1885 /**
1886 * Generates a comment preview.
1887 *
1888 * @see comment_form_build_preview()
1889 */
1890 function comment_preview($comment) {
1891 global $user;
1892
1893 drupal_set_title(t('Preview comment'), PASS_THROUGH);
1894
1895 $node = node_load($comment->nid);
1896
1897 if (!form_get_errors()) {
1898 $comment->format = $comment->comment_body[LANGUAGE_NONE][0]['format'];
1899 // Attach the user and time information.
1900 if (!empty($comment->name)) {
1901 $account = user_load_by_name($comment->name);
1902 }
1903 elseif ($user->uid && empty($comment->is_anonymous)) {
1904 $account = $user;
1905 }
1906
1907 if (!empty($account->uid)) {
1908 $comment->uid = $account->uid;
1909 $comment->name = check_plain($account->name);
1910 }
1911 elseif (empty($comment->name)) {
1912 $comment->name = variable_get('anonymous', t('Anonymous'));
1913 }
1914
1915 $comment->created = !empty($comment->created) ? $comment->created : REQUEST_TIME;
1916 $comment->changed = REQUEST_TIME;
1917 $comment->in_preview = TRUE;
1918 $comment_build = comment_view($comment, $node);
1919 $comment_build['#weight'] = -100;
1920
1921 $form['comment_preview'] = $comment_build;
1922 }
1923
1924 if ($comment->pid) {
1925 $build = array();
1926 if ($comments = comment_load_multiple(array($comment->pid), array('status' => COMMENT_PUBLISHED))) {
1927 $parent_comment = $comments[$comment->pid];
1928 $build = comment_view($parent_comment, $node);
1929 }
1930 }
1931 else {
1932 $build = node_view($node);
1933 }
1934
1935 $form['comment_output_below'] = $build;
1936 $form['comment_output_below']['#weight'] = 100;
1937
1938 return $form;
1939 }
1940
1941 /**
1942 * Form validation handler for comment_form().
1943 *
1944 * @see comment_form_submit()
1945 */
1946 function comment_form_validate($form, &$form_state) {
1947 global $user;
1948
1949 entity_form_field_validate('comment', $form, $form_state);
1950
1951 if (!empty($form_state['values']['cid'])) {
1952 // Verify the name in case it is being changed from being anonymous.
1953 $account = user_load_by_name($form_state['values']['name']);
1954 $form_state['values']['uid'] = $account ? $account->uid : 0;
1955
1956 if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) {
1957 form_set_error('date', t('You have to specify a valid date.'));
1958 }
1959 if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) {
1960 form_set_error('name', t('You have to specify a valid author.'));
1961 }
1962 }
1963 elseif ($form_state['values']['is_anonymous']) {
1964 // Validate anonymous comment author fields (if given). If the (original)
1965 // author of this comment was an anonymous user, verify that no registered
1966 // user with this name exists.
1967 if ($form_state['values']['name']) {
1968 $query = db_select('users', 'u');
1969 $query->addField('u', 'uid', 'uid');
1970 $taken = $query
1971 ->condition('name', db_like($form_state['values']['name']), 'LIKE')
1972 ->countQuery()
1973 ->execute()
1974 ->fetchField();
1975 if ($taken) {
1976 form_set_error('name', t('The name you used belongs to a registered user.'));
1977 }
1978 }
1979 }
1980 if ($form_state['values']['mail'] && !valid_email_address($form_state['values']['mail'])) {
1981 form_set_error('mail', t('The e-mail address you specified is not valid.'));
1982 }
1983 if ($form_state['values']['homepage'] && !valid_url($form_state['values']['homepage'], TRUE)) {
1984 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>.'));
1985 }
1986 }
1987
1988 /**
1989 * Prepare a comment for submission.
1990 */
1991 function comment_submit($comment) {
1992 if (empty($comment->date)) {
1993 $comment->date = 'now';
1994 }
1995 $comment->created = strtotime($comment->date);
1996 $comment->changed = REQUEST_TIME;
1997
1998 // If the comment was posted by a registered user, assign the author's ID.
1999 // @todo Too fragile. Should be prepared and stored in comment_form() already.
2000 if (!$comment->is_anonymous && !empty($comment->name) && ($account = user_load_by_name($comment->name))) {
2001 $comment->uid = $account->uid;
2002 }
2003 // If the comment was posted by an anonymous user and no author name was
2004 // required, use "Anonymous" by default.
2005 if ($comment->is_anonymous && (!isset($comment->name) || $comment->name === '')) {
2006 $comment->name = variable_get('anonymous', t('Anonymous'));
2007 }
2008
2009 // Validate the comment's subject. If not specified, extract from comment body.
2010 if (trim($comment->subject) == '') {
2011 // The body may be in any format, so:
2012 // 1) Filter it into HTML
2013 // 2) Strip out all HTML tags
2014 // 3) Convert entities back to plain-text.
2015 $comment_body = $comment->comment_body[LANGUAGE_NONE][0];
2016 if (isset($comment_body['format'])) {
2017 $comment_text = check_markup($comment_body['value'], $comment_body['format']);
2018 }
2019 else {
2020 $comment_text = check_plain($comment_body['value']);
2021 }
2022 $comment->subject = truncate_utf8(trim(decode_entities(strip_tags($comment_text))), 29, TRUE);
2023 // Edge cases where the comment body is populated only by HTML tags will
2024 // require a default subject.
2025 if ($comment->subject == '') {
2026 $comment->subject = t('(No subject)');
2027 }
2028 }
2029 return $comment;
2030 }
2031
2032 /**
2033 * Updates the comment entity by processing the submission's values.
2034 *
2035 * This is the default builder function for the comment form. It is called
2036 * during the "Save" and "Preview" submit handlers to retrieve the entity to
2037 * save or preview. This function can also be called by a "Next" button of a
2038 * wizard to update the form state's entity with the current step's values
2039 * before proceeding to the next step.
2040 *
2041 * @see comment_form()
2042 * @see comment_form_preview()
2043 * @see comment_form_submit()
2044 */
2045 function comment_form_submit_build_comment($form, &$form_state) {
2046 $comment = $form_state['comment'];
2047 entity_form_submit_build_entity('comment', $comment, $form, $form_state);
2048 comment_submit($comment);
2049 return $comment;
2050 }
2051
2052 /**
2053 * Form submission handler for comment_form().
2054 *
2055 * @see comment_form_validate()
2056 * @see comment_form_submit_build_comment()
2057 */
2058 function comment_form_submit($form, &$form_state) {
2059 $node = node_load($form_state['values']['nid']);
2060 $comment = comment_form_submit_build_comment($form, $form_state);
2061 if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) {
2062 // Save the anonymous user information to a cookie for reuse.
2063 if (user_is_anonymous()) {
2064 user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage'))));
2065 }
2066
2067 comment_save($comment);
2068 $form_state['values']['cid'] = $comment->cid;
2069
2070 // Add an entry to the watchdog log.
2071 watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)));
2072
2073 // Explain the approval queue if necessary.
2074 if ($comment->status == COMMENT_NOT_PUBLISHED) {
2075 if (!user_access('administer comments')) {
2076 drupal_set_message(t('Your comment has been queued for review by site administrators and will be published after approval.'));
2077 }
2078 }
2079 else {
2080 drupal_set_message(t('Your comment has been posted.'));
2081 }
2082 $query = array();
2083 // Find the current display page for this comment.
2084 $page = comment_get_display_page($comment->cid, $node->type);
2085 if ($page > 0) {
2086 $query['page'] = $page;
2087 }
2088 // Redirect to the newly posted comment.
2089 $redirect = array('node/' . $node->nid, array('query' => $query, 'fragment' => 'comment-' . $comment->cid));
2090 }
2091 else {
2092 watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject), WATCHDOG_WARNING);
2093 drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject)), 'error');
2094 // Redirect the user to the node they are commenting on.
2095 $redirect = 'node/' . $node->nid;
2096 }
2097 $form_state['redirect'] = $redirect;
2098 // Clear the block and page caches so that anonymous users see the comment
2099 // they have posted.
2100 cache_clear_all();
2101 }
2102
2103 /**
2104 * Implements hook_preprocess_block().
2105 */
2106 function comment_preprocess_block(&$variables) {
2107 if ($variables['block']->module == 'comment') {
2108 $variables['attributes_array']['role'] = 'navigation';
2109 }
2110 }
2111
2112 /**
2113 * Preprocesses variables for comment.tpl.php.
2114 *
2115 * @see comment.tpl.php
2116 */
2117 function template_preprocess_comment(&$variables) {
2118 $comment = $variables['elements']['#comment'];
2119 $node = $variables['elements']['#node'];
2120 $variables['comment'] = $comment;
2121 $variables['node'] = $node;
2122 $variables['author'] = theme('username', array('account' => $comment));
2123 $variables['created'] = format_date($comment->created);
2124 $variables['changed'] = format_date($comment->changed);
2125
2126 $variables['new'] = !empty($comment->new) ? t('new') : '';
2127 $variables['user_picture'] = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', array('account' => $comment)) : '';
2128 $variables['signature'] = $comment->signature;
2129
2130 $uri = entity_uri('comment', $comment);
2131 $uri['options'] += array('attributes' => array('class' => 'permalink', 'rel' => 'bookmark'));
2132
2133 $variables['title'] = l($comment->subject, $uri['path'], $uri['options']);
2134 $variables['permalink'] = l(t('Permalink'), $uri['path'], $uri['options']);
2135 $variables['submitted'] = t('Submitted by !username on !datetime', array('!username' => $variables['author'], '!datetime' => $variables['created']));
2136
2137 // Preprocess fields.
2138 field_attach_preprocess('comment', $comment, $variables['elements'], $variables);
2139
2140 // Helpful $content variable for templates.
2141 foreach (element_children($variables['elements']) as $key) {
2142 $variables['content'][$key] = $variables['elements'][$key];
2143 }
2144
2145 // Set status to a string representation of comment->status.
2146 if (isset($comment->in_preview)) {
2147 $variables['status'] = 'comment-preview';
2148 }
2149 else {
2150 $variables['status'] = ($comment->status == COMMENT_NOT_PUBLISHED) ? 'comment-unpublished' : 'comment-published';
2151 }
2152
2153 // Gather comment classes.
2154 // 'comment-published' class is not needed, it is either 'comment-preview' or
2155 // 'comment-unpublished'.
2156 if ($variables['status'] != 'comment-published') {
2157 $variables['classes_array'][] = $variables['status'];
2158 }
2159 if ($variables['new']) {
2160 $variables['classes_array'][] = 'comment-new';
2161 }
2162 if (!$comment->uid) {
2163 $variables['classes_array'][] = 'comment-by-anonymous';
2164 }
2165 else {
2166 if ($comment->uid == $variables['node']->uid) {
2167 $variables['classes_array'][] = 'comment-by-node-author';
2168 }
2169 if ($comment->uid == $variables['user']->uid) {
2170 $variables['classes_array'][] = 'comment-by-viewer';
2171 }
2172 }
2173 }
2174
2175 /**
2176 * Returns HTML for a "you can't post comments" notice.
2177 *
2178 * @param $variables
2179 * An associative array containing:
2180 * - node: The comment node.
2181 *
2182 * @ingroup themeable
2183 */
2184 function theme_comment_post_forbidden($variables) {
2185 $node = $variables['node'];
2186 global $user;
2187
2188 // Since this is expensive to compute, we cache it so that a page with many
2189 // comments only has to query the database once for all the links.
2190 $authenticated_post_comments = &drupal_static(__FUNCTION__, NULL);
2191
2192 if (!$user->uid) {
2193 if (!isset($authenticated_post_comments)) {
2194 // We only output a link if we are certain that users will get permission
2195 // to post comments by logging in.
2196 $comment_roles = user_roles(TRUE, 'post comments');
2197 $authenticated_post_comments = isset($comment_roles[DRUPAL_AUTHENTICATED_RID]);
2198 }
2199
2200 if ($authenticated_post_comments) {
2201 // We cannot use drupal_get_destination() because these links
2202 // sometimes appear on /node and taxonomy listing pages.
2203 if (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_SEPARATE_PAGE) {
2204 $destination = array('destination' => "comment/reply/$node->nid#comment-form");
2205 }
2206 else {
2207 $destination = array('destination' => "node/$node->nid#comment-form");
2208 }
2209
2210 if (variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)) {
2211 // Users can register themselves.
2212 return t('<a href="@login">Log in</a> or <a href="@register">register</a> to post comments', array('@login' => url('user/login', array('query' => $destination)), '@register' => url('user/register', array('query' => $destination))));
2213 }
2214 else {
2215 // Only admins can add new users, no public registration.
2216 return t('<a href="@login">Log in</a> to post comments', array('@login' => url('user/login', array('query' => $destination))));
2217 }
2218 }
2219 }
2220 }
2221
2222 /**
2223 * Preprocesses variables for comment-wrapper.tpl.php.
2224 *
2225 * @see comment-wrapper.tpl.php
2226 * @see theme_comment_wrapper()
2227 */
2228 function template_preprocess_comment_wrapper(&$variables) {
2229 // Provide contextual information.
2230 $variables['node'] = $variables['content']['#node'];
2231 $variables['display_mode'] = variable_get('comment_default_mode_' . $variables['node']->type, COMMENT_MODE_THREADED);
2232 // The comment form is optional and may not exist.
2233 $variables['content'] += array('comment_form' => array());
2234 }
2235
2236 /**
2237 * Returns an array of viewing modes for comment listings.
2238 *
2239 * We can't use a global variable array because the locale system
2240 * is not initialized yet when the Comment module is loaded.
2241 */
2242 function _comment_get_modes() {
2243 return array(
2244 COMMENT_MODE_FLAT => t('Flat list'),
2245 COMMENT_MODE_THREADED => t('Threaded list')
2246 );
2247 }
2248
2249 /**
2250 * Returns an array of "comments per page" values that users can select from.
2251 */
2252 function _comment_per_page() {
2253 return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300));
2254 }
2255
2256 /**
2257 * Generates a sorting code.
2258 *
2259 * Consists of a leading character indicating length, followed by N digits
2260 * with a numerical value in base 36 (alphadecimal). These codes can be sorted
2261 * as strings without altering numerical order.
2262 *
2263 * It goes:
2264 * 00, 01, 02, ..., 0y, 0z,
2265 * 110, 111, ... , 1zy, 1zz,
2266 * 2100, 2101, ..., 2zzy, 2zzz,
2267 * 31000, 31001, ...
2268 */
2269 function comment_int_to_alphadecimal($i = 0) {
2270 $num = base_convert((int) $i, 10, 36);
2271 $length = strlen($num);
2272
2273 return chr($length + ord('0') - 1) . $num;
2274 }
2275
2276 /**
2277 * Decodes a sorting code back to an integer.
2278 *
2279 * @see comment_int_to_alphadecimal()
2280 */
2281 function comment_alphadecimal_to_int($c = '00') {
2282 return base_convert(substr($c, 1), 36, 10);
2283 }
2284
2285 /**
2286 * Increments a sorting code to the next value.
2287 *
2288 * @see comment_int_to_alphadecimal()
2289 */
2290 function comment_increment_alphadecimal($c = '00') {
2291 return comment_int_to_alphadecimal(comment_alphadecimal_to_int($c) + 1);
2292 }
2293
2294 /**
2295 * Implements hook_action_info().
2296 */
2297 function comment_action_info() {
2298 return array(
2299 'comment_publish_action' => array(
2300 'label' => t('Publish comment'),
2301 'type' => 'comment',
2302 'configurable' => FALSE,
2303 'behavior' => array('changes_property'),
2304 'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
2305 ),
2306 'comment_unpublish_action' => array(
2307 'label' => t('Unpublish comment'),
2308 'type' => 'comment',
2309 'configurable' => FALSE,
2310 'behavior' => array('changes_property'),
2311 'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
2312 ),
2313 'comment_unpublish_by_keyword_action' => array(
2314 'label' => t('Unpublish comment containing keyword(s)'),
2315 'type' => 'comment',
2316 'configurable' => TRUE,
2317 'behavior' => array('changes_property'),
2318 'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
2319 ),
2320 'comment_save_action' => array(
2321 'label' => t('Save comment'),
2322 'type' => 'comment',
2323 'configurable' => FALSE,
2324 'triggers' => array('comment_insert', 'comment_update'),
2325 ),
2326 );
2327 }
2328
2329 /**
2330 * Publishes a comment.
2331 *
2332 * @param $comment
2333 * An optional comment object.
2334 * @param array $context
2335 * Array with components:
2336 * - 'cid': Comment ID. Required if $comment is not given.
2337 *
2338 * @ingroup actions
2339 */
2340 function comment_publish_action($comment, $context = array()) {
2341 if (isset($comment->subject)) {
2342 $subject = $comment->subject;
2343 $comment->status = COMMENT_PUBLISHED;
2344 }
2345 else {
2346 $cid = $context['cid'];
2347 $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
2348 db_update('comment')
2349 ->fields(array('status' => COMMENT_PUBLISHED))
2350 ->condition('cid', $cid)
2351 ->execute();
2352 }
2353 watchdog('action', 'Published comment %subject.', array('%subject' => $subject));
2354 }
2355
2356 /**
2357 * Unpublishes a comment.
2358 *
2359 * @param $comment
2360 * An optional comment object.
2361 * @param array $context
2362 * Array with components:
2363 * - 'cid': Comment ID. Required if $comment is not given.
2364 *
2365 * @ingroup actions
2366 */
2367 function comment_unpublish_action($comment, $context = array()) {
2368 if (isset($comment->subject)) {
2369 $subject = $comment->subject;
2370 $comment->status = COMMENT_NOT_PUBLISHED;
2371 }
2372 else {
2373 $cid = $context['cid'];
2374 $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
2375 db_update('comment')
2376 ->fields(array('status' => COMMENT_NOT_PUBLISHED))
2377 ->condition('cid', $cid)
2378 ->execute();
2379 }
2380 watchdog('action', 'Unpublished comment %subject.', array('%subject' => $subject));
2381 }
2382
2383 /**
2384 * Unpublishes a comment if it contains certain keywords.
2385 *
2386 * @param $comment
2387 * Comment object to modify.
2388 * @param array $context
2389 * Array with components:
2390 * - 'keywords': Keywords to look for. If the comment contains at least one
2391 * of the keywords, it is unpublished.
2392 *
2393 * @ingroup actions
2394 * @see comment_unpublish_by_keyword_action_form()
2395 * @see comment_unpublish_by_keyword_action_submit()
2396 */
2397 function comment_unpublish_by_keyword_action($comment, $context) {
2398 foreach ($context['keywords'] as $keyword) {
2399 $text = drupal_render($comment);
2400 if (strpos($text, $keyword) !== FALSE) {
2401 $comment->status = COMMENT_NOT_PUBLISHED;
2402 watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject));
2403 break;
2404 }
2405 }
2406 }
2407
2408 /**
2409 * Form constructor for the blacklisted keywords form.
2410 *
2411 * @ingroup forms
2412 * @see comment_unpublish_by_keyword_action()
2413 * @see comment_unpublish_by_keyword_action_submit()
2414 */
2415 function comment_unpublish_by_keyword_action_form($context) {
2416 $form['keywords'] = array(
2417 '#title' => t('Keywords'),
2418 '#type' => 'textarea',
2419 '#description' => t('The comment will be unpublished if it contains any of the phrases above. Use a case-sensitive, comma-separated list of phrases. Example: funny, bungee jumping, "Company, Inc."'),
2420 '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '',
2421 );
2422
2423 return $form;
2424 }
2425
2426 /**
2427 * Form submission handler for comment_unpublish_by_keyword_action_form().
2428 *
2429 * @see comment_unpublish_by_keyword_action()
2430 */
2431 function comment_unpublish_by_keyword_action_submit($form, $form_state) {
2432 return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
2433 }
2434
2435 /**
2436 * Saves a comment.
2437 *
2438 * @ingroup actions
2439 */
2440 function comment_save_action($comment) {
2441 comment_save($comment);
2442 cache_clear_all();
2443 watchdog('action', 'Saved comment %title', array('%title' => $comment->subject));
2444 }
2445
2446 /**
2447 * Implements hook_ranking().
2448 */
2449 function comment_ranking() {
2450 return array(
2451 'comments' => array(
2452 'title' => t('Number of comments'),
2453 'join' => array(
2454 'type' => 'LEFT',
2455 'table' => 'node_comment_statistics',
2456 'alias' => 'node_comment_statistics',
2457 'on' => 'node_comment_statistics.nid = i.sid',
2458 ),
2459 // Inverse law that maps the highest reply count on the site to 1 and 0 to 0.
2460 'score' => '2.0 - 2.0 / (1.0 + node_comment_statistics.comment_count * CAST(:scale AS DECIMAL))',
2461 'arguments' => array(':scale' => variable_get('node_cron_comments_scale', 0)),
2462 ),
2463 );
2464 }
2465
2466 /**
2467 * Implements hook_rdf_mapping().
2468 */
2469 function comment_rdf_mapping() {
2470 return array(
2471 array(
2472 'type' => 'comment',
2473 'bundle' => RDF_DEFAULT_BUNDLE,
2474 'mapping' => array(
2475 'rdftype' => array('sioc:Post', 'sioct:Comment'),
2476 'title' => array(
2477 'predicates' => array('dc:title'),
2478 ),
2479 'created' => array(
2480 'predicates' => array('dc:date', 'dc:created'),
2481 'datatype' => 'xsd:dateTime',
2482 'callback' => 'date_iso8601',
2483 ),
2484 'changed' => array(
2485 'predicates' => array('dc:modified'),
2486 'datatype' => 'xsd:dateTime',
2487 'callback' => 'date_iso8601',
2488 ),
2489 'comment_body' => array(
2490 'predicates' => array('content:encoded'),
2491 ),
2492 'pid' => array(
2493 'predicates' => array('sioc:reply_of'),
2494 'type' => 'rel',
2495 ),
2496 'uid' => array(
2497 'predicates' => array('sioc:has_creator'),
2498 'type' => 'rel',
2499 ),
2500 'name' => array(
2501 'predicates' => array('foaf:name'),
2502 ),
2503 ),
2504 ),
2505 );
2506 }
2507
2508 /**
2509 * Implements hook_file_download_access().
2510 */
2511 function comment_file_download_access($field, $entity_type, $entity) {
2512 if ($entity_type == 'comment') {
2513 if (user_access('access comments') && $entity->status == COMMENT_PUBLISHED || user_access('administer comments')) {
2514 $node = node_load($entity->nid);
2515 return node_access('view', $node);
2516 }
2517 return FALSE;
2518 }
2519 }