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