Simpletest Coverage - modules/comment/comment.module

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

Legend

Missed
lines code that were not excersized during program execution.
Covered
lines code were excersized during program execution.
Comment/non executable
Comment or non-executable line of code.
Dead
lines of code that according to xdebug could not be executed. This is counted as coverage code because in almost all cases it is code that runnable.