cms/drupal/modules/comment/comment.module
changeset 541 e756a8c72c3d
equal deleted inserted replaced
540:07239de796bb 541:e756a8c72c3d
       
     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  * Implements callback_entity_info_uri().
       
   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   return MENU_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)) . '&nbsp;<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   $view_mode = key(entity_view_mode_prepare('comment', array($comment->cid => $comment), $view_mode, $langcode));
       
   997 
       
   998   // Build fields content.
       
   999   field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode, $langcode);
       
  1000   entity_prepare_view('comment', array($comment->cid => $comment), $langcode);
       
  1001   $comment->content += field_attach_view('comment', $comment, $view_mode, $langcode);
       
  1002 
       
  1003   $comment->content['links'] = array(
       
  1004     '#theme' => 'links__comment',
       
  1005     '#pre_render' => array('drupal_pre_render_links'),
       
  1006     '#attributes' => array('class' => array('links', 'inline')),
       
  1007   );
       
  1008   if (empty($comment->in_preview)) {
       
  1009     $comment->content['links']['comment'] = array(
       
  1010       '#theme' => 'links__comment__comment',
       
  1011       '#links' => comment_links($comment, $node),
       
  1012       '#attributes' => array('class' => array('links', 'inline')),
       
  1013     );
       
  1014   }
       
  1015 
       
  1016   // Allow modules to make their own additions to the comment.
       
  1017   module_invoke_all('comment_view', $comment, $view_mode, $langcode);
       
  1018   module_invoke_all('entity_view', $comment, 'comment', $view_mode, $langcode);
       
  1019 
       
  1020   // Make sure the current view mode is stored if no module has already
       
  1021   // populated the related key.
       
  1022   $comment->content += array('#view_mode' => $view_mode);
       
  1023 }
       
  1024 
       
  1025 /**
       
  1026  * Helper function, build links for an individual comment.
       
  1027  *
       
  1028  * Adds reply, edit, delete etc. depending on the current user permissions.
       
  1029  *
       
  1030  * @param $comment
       
  1031  *   The comment object.
       
  1032  * @param $node
       
  1033  *   The node the comment is attached to.
       
  1034  * @return
       
  1035  *   A structured array of links.
       
  1036  */
       
  1037 function comment_links($comment, $node) {
       
  1038   $links = array();
       
  1039   if ($node->comment == COMMENT_NODE_OPEN) {
       
  1040     if (user_access('administer comments') && user_access('post comments')) {
       
  1041       $links['comment-delete'] = array(
       
  1042         'title' => t('delete'),
       
  1043         'href' => "comment/$comment->cid/delete",
       
  1044         'html' => TRUE,
       
  1045       );
       
  1046       $links['comment-edit'] = array(
       
  1047         'title' => t('edit'),
       
  1048         'href' => "comment/$comment->cid/edit",
       
  1049         'html' => TRUE,
       
  1050       );
       
  1051       $links['comment-reply'] = array(
       
  1052         'title' => t('reply'),
       
  1053         'href' => "comment/reply/$comment->nid/$comment->cid",
       
  1054         'html' => TRUE,
       
  1055       );
       
  1056       if ($comment->status == COMMENT_NOT_PUBLISHED) {
       
  1057         $links['comment-approve'] = array(
       
  1058           'title' => t('approve'),
       
  1059           'href' => "comment/$comment->cid/approve",
       
  1060           'html' => TRUE,
       
  1061           'query' => array('token' => drupal_get_token("comment/$comment->cid/approve")),
       
  1062         );
       
  1063       }
       
  1064     }
       
  1065     elseif (user_access('post comments')) {
       
  1066       if (comment_access('edit', $comment)) {
       
  1067         $links['comment-edit'] = array(
       
  1068           'title' => t('edit'),
       
  1069           'href' => "comment/$comment->cid/edit",
       
  1070           'html' => TRUE,
       
  1071         );
       
  1072       }
       
  1073       $links['comment-reply'] = array(
       
  1074         'title' => t('reply'),
       
  1075         'href' => "comment/reply/$comment->nid/$comment->cid",
       
  1076         'html' => TRUE,
       
  1077       );
       
  1078     }
       
  1079     else {
       
  1080       $links['comment_forbidden']['title'] = theme('comment_post_forbidden', array('node' => $node));
       
  1081       $links['comment_forbidden']['html'] = TRUE;
       
  1082     }
       
  1083   }
       
  1084   return $links;
       
  1085 }
       
  1086 
       
  1087 /**
       
  1088  * Construct a drupal_render() style array from an array of loaded comments.
       
  1089  *
       
  1090  * @param $comments
       
  1091  *   An array of comments as returned by comment_load_multiple().
       
  1092  * @param $node
       
  1093  *   The node the comments are attached to.
       
  1094  * @param $view_mode
       
  1095  *   View mode, e.g. 'full', 'teaser'...
       
  1096  * @param $weight
       
  1097  *   An integer representing the weight of the first comment in the list.
       
  1098  * @param $langcode
       
  1099  *   A string indicating the language field values are to be shown in. If no
       
  1100  *   language is provided the current content language is used.
       
  1101  *
       
  1102  * @return
       
  1103  *   An array in the format expected by drupal_render().
       
  1104  */
       
  1105 function comment_view_multiple($comments, $node, $view_mode = 'full', $weight = 0, $langcode = NULL) {
       
  1106   $build = array();
       
  1107   $entities_by_view_mode = entity_view_mode_prepare('comment', $comments, $view_mode, $langcode);
       
  1108   foreach ($entities_by_view_mode as $entity_view_mode => $entities) {
       
  1109     field_attach_prepare_view('comment', $entities, $entity_view_mode, $langcode);
       
  1110     entity_prepare_view('comment', $entities, $langcode);
       
  1111 
       
  1112     foreach ($entities as $entity) {
       
  1113       $build[$entity->cid] = comment_view($entity, $node, $entity_view_mode, $langcode);
       
  1114     }
       
  1115   }
       
  1116 
       
  1117   foreach ($comments as $comment) {
       
  1118     $build[$comment->cid]['#weight'] = $weight;
       
  1119     $weight++;
       
  1120   }
       
  1121   // Sort here, to preserve the input order of the entities that were passed to
       
  1122   // this function.
       
  1123   uasort($build, 'element_sort');
       
  1124   $build['#sorted'] = TRUE;
       
  1125 
       
  1126   return $build;
       
  1127 }
       
  1128 
       
  1129 /**
       
  1130  * Implements hook_form_FORM_ID_alter().
       
  1131  */
       
  1132 function comment_form_node_type_form_alter(&$form, $form_state) {
       
  1133   if (isset($form['type'])) {
       
  1134     $form['comment'] = array(
       
  1135       '#type' => 'fieldset',
       
  1136       '#title' => t('Comment settings'),
       
  1137       '#collapsible' => TRUE,
       
  1138       '#collapsed' => TRUE,
       
  1139       '#group' => 'additional_settings',
       
  1140       '#attributes' => array(
       
  1141         'class' => array('comment-node-type-settings-form'),
       
  1142       ),
       
  1143       '#attached' => array(
       
  1144         'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
       
  1145       ),
       
  1146     );
       
  1147     // Unlike coment_form_node_form_alter(), all of these settings are applied
       
  1148     // as defaults to all new nodes. Therefore, it would be wrong to use #states
       
  1149     // to hide the other settings based on the primary comment setting.
       
  1150     $form['comment']['comment'] = array(
       
  1151       '#type' => 'select',
       
  1152       '#title' => t('Default comment setting for new content'),
       
  1153       '#default_value' => variable_get('comment_' . $form['#node_type']->type, COMMENT_NODE_OPEN),
       
  1154       '#options' => array(
       
  1155         COMMENT_NODE_OPEN => t('Open'),
       
  1156         COMMENT_NODE_CLOSED => t('Closed'),
       
  1157         COMMENT_NODE_HIDDEN => t('Hidden'),
       
  1158       ),
       
  1159     );
       
  1160     $form['comment']['comment_default_mode'] = array(
       
  1161       '#type' => 'checkbox',
       
  1162       '#title' => t('Threading'),
       
  1163       '#default_value' => variable_get('comment_default_mode_' . $form['#node_type']->type, COMMENT_MODE_THREADED),
       
  1164       '#description' => t('Show comment replies in a threaded list.'),
       
  1165     );
       
  1166     $form['comment']['comment_default_per_page'] = array(
       
  1167       '#type' => 'select',
       
  1168       '#title' => t('Comments per page'),
       
  1169       '#default_value' => variable_get('comment_default_per_page_' . $form['#node_type']->type, 50),
       
  1170       '#options' => _comment_per_page(),
       
  1171     );
       
  1172     $form['comment']['comment_anonymous'] = array(
       
  1173       '#type' => 'select',
       
  1174       '#title' => t('Anonymous commenting'),
       
  1175       '#default_value' => variable_get('comment_anonymous_' . $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT),
       
  1176       '#options' => array(
       
  1177         COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
       
  1178         COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
       
  1179         COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information'),
       
  1180       ),
       
  1181       '#access' => user_access('post comments', drupal_anonymous_user()),
       
  1182     );
       
  1183     $form['comment']['comment_subject_field'] = array(
       
  1184       '#type' => 'checkbox',
       
  1185       '#title' => t('Allow comment title'),
       
  1186       '#default_value' => variable_get('comment_subject_field_' . $form['#node_type']->type, 1),
       
  1187     );
       
  1188     $form['comment']['comment_form_location'] = array(
       
  1189       '#type' => 'checkbox',
       
  1190       '#title' => t('Show reply form on the same page as comments'),
       
  1191       '#default_value' => variable_get('comment_form_location_' . $form['#node_type']->type, COMMENT_FORM_BELOW),
       
  1192     );
       
  1193     $form['comment']['comment_preview'] = array(
       
  1194       '#type' => 'radios',
       
  1195       '#title' => t('Preview comment'),
       
  1196       '#default_value' => variable_get('comment_preview_' . $form['#node_type']->type, DRUPAL_OPTIONAL),
       
  1197       '#options' => array(
       
  1198         DRUPAL_DISABLED => t('Disabled'),
       
  1199         DRUPAL_OPTIONAL => t('Optional'),
       
  1200         DRUPAL_REQUIRED => t('Required'),
       
  1201       ),
       
  1202     );
       
  1203   }
       
  1204 }
       
  1205 
       
  1206 /**
       
  1207  * Implements hook_form_BASE_FORM_ID_alter().
       
  1208  */
       
  1209 function comment_form_node_form_alter(&$form, $form_state) {
       
  1210   $node = $form['#node'];
       
  1211   $form['comment_settings'] = array(
       
  1212     '#type' => 'fieldset',
       
  1213     '#access' => user_access('administer comments'),
       
  1214     '#title' => t('Comment settings'),
       
  1215     '#collapsible' => TRUE,
       
  1216     '#collapsed' => TRUE,
       
  1217     '#group' => 'additional_settings',
       
  1218     '#attributes' => array(
       
  1219       'class' => array('comment-node-settings-form'),
       
  1220     ),
       
  1221     '#attached' => array(
       
  1222       'js' => array(drupal_get_path('module', 'comment') . '/comment-node-form.js'),
       
  1223      ),
       
  1224     '#weight' => 30,
       
  1225   );
       
  1226   $comment_count = isset($node->nid) ? db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array(':nid' => $node->nid))->fetchField() : 0;
       
  1227   $comment_settings = ($node->comment == COMMENT_NODE_HIDDEN && empty($comment_count)) ? COMMENT_NODE_CLOSED : $node->comment;
       
  1228   $form['comment_settings']['comment'] = array(
       
  1229     '#type' => 'radios',
       
  1230     '#title' => t('Comments'),
       
  1231     '#title_display' => 'invisible',
       
  1232     '#parents' => array('comment'),
       
  1233     '#default_value' => $comment_settings,
       
  1234     '#options' => array(
       
  1235       COMMENT_NODE_OPEN => t('Open'),
       
  1236       COMMENT_NODE_CLOSED => t('Closed'),
       
  1237       COMMENT_NODE_HIDDEN => t('Hidden'),
       
  1238     ),
       
  1239     COMMENT_NODE_OPEN => array(
       
  1240       '#description' => t('Users with the "Post comments" permission can post comments.'),
       
  1241     ),
       
  1242     COMMENT_NODE_CLOSED => array(
       
  1243       '#description' => t('Users cannot post comments, but existing comments will be displayed.'),
       
  1244     ),
       
  1245     COMMENT_NODE_HIDDEN => array(
       
  1246       '#description' => t('Comments are hidden from view.'),
       
  1247     ),
       
  1248   );
       
  1249   // If the node doesn't have any comments, the "hidden" option makes no
       
  1250   // sense, so don't even bother presenting it to the user.
       
  1251   if (empty($comment_count)) {
       
  1252     $form['comment_settings']['comment'][COMMENT_NODE_HIDDEN]['#access'] = FALSE;
       
  1253     // Also adjust the description of the "closed" option.
       
  1254     $form['comment_settings']['comment'][COMMENT_NODE_CLOSED]['#description'] = t('Users cannot post comments.');
       
  1255   }
       
  1256 }
       
  1257 
       
  1258 /**
       
  1259  * Implements hook_node_load().
       
  1260  */
       
  1261 function comment_node_load($nodes, $types) {
       
  1262   $comments_enabled = array();
       
  1263 
       
  1264   // Check if comments are enabled for each node. If comments are disabled,
       
  1265   // assign values without hitting the database.
       
  1266   foreach ($nodes as $node) {
       
  1267     // Store whether comments are enabled for this node.
       
  1268     if ($node->comment != COMMENT_NODE_HIDDEN) {
       
  1269       $comments_enabled[] = $node->nid;
       
  1270     }
       
  1271     else {
       
  1272       $node->cid = 0;
       
  1273       $node->last_comment_timestamp = $node->created;
       
  1274       $node->last_comment_name = '';
       
  1275       $node->last_comment_uid = $node->uid;
       
  1276       $node->comment_count = 0;
       
  1277     }
       
  1278   }
       
  1279 
       
  1280   // For nodes with comments enabled, fetch information from the database.
       
  1281   if (!empty($comments_enabled)) {
       
  1282     $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));
       
  1283     foreach ($result as $record) {
       
  1284       $nodes[$record->nid]->cid = $record->cid;
       
  1285       $nodes[$record->nid]->last_comment_timestamp = $record->last_comment_timestamp;
       
  1286       $nodes[$record->nid]->last_comment_name = $record->last_comment_name;
       
  1287       $nodes[$record->nid]->last_comment_uid = $record->last_comment_uid;
       
  1288       $nodes[$record->nid]->comment_count = $record->comment_count;
       
  1289     }
       
  1290   }
       
  1291 }
       
  1292 
       
  1293 /**
       
  1294  * Implements hook_node_prepare().
       
  1295  */
       
  1296 function comment_node_prepare($node) {
       
  1297   if (!isset($node->comment)) {
       
  1298     $node->comment = variable_get("comment_$node->type", COMMENT_NODE_OPEN);
       
  1299   }
       
  1300 }
       
  1301 
       
  1302 /**
       
  1303  * Implements hook_node_insert().
       
  1304  */
       
  1305 function comment_node_insert($node) {
       
  1306   // Allow bulk updates and inserts to temporarily disable the
       
  1307   // maintenance of the {node_comment_statistics} table.
       
  1308   if (variable_get('comment_maintain_node_statistics', TRUE)) {
       
  1309     db_insert('node_comment_statistics')
       
  1310       ->fields(array(
       
  1311         'nid' => $node->nid,
       
  1312         'cid' => 0,
       
  1313         'last_comment_timestamp' => $node->changed,
       
  1314         'last_comment_name' => NULL,
       
  1315         'last_comment_uid' => $node->uid,
       
  1316         'comment_count' => 0,
       
  1317       ))
       
  1318       ->execute();
       
  1319   }
       
  1320 }
       
  1321 
       
  1322 /**
       
  1323  * Implements hook_node_delete().
       
  1324  */
       
  1325 function comment_node_delete($node) {
       
  1326   $cids = db_query('SELECT cid FROM {comment} WHERE nid = :nid', array(':nid' => $node->nid))->fetchCol();
       
  1327   comment_delete_multiple($cids);
       
  1328   db_delete('node_comment_statistics')
       
  1329     ->condition('nid', $node->nid)
       
  1330     ->execute();
       
  1331 }
       
  1332 
       
  1333 /**
       
  1334  * Implements hook_node_update_index().
       
  1335  */
       
  1336 function comment_node_update_index($node) {
       
  1337   $index_comments = &drupal_static(__FUNCTION__);
       
  1338 
       
  1339   if ($index_comments === NULL) {
       
  1340     // Find and save roles that can 'access comments' or 'search content'.
       
  1341     $perms = array('access comments' => array(), 'search content' => array());
       
  1342     $result = db_query("SELECT rid, permission FROM {role_permission} WHERE permission IN ('access comments', 'search content')");
       
  1343     foreach ($result as $record) {
       
  1344       $perms[$record->permission][$record->rid] = $record->rid;
       
  1345     }
       
  1346 
       
  1347     // Prevent indexing of comments if there are any roles that can search but
       
  1348     // not view comments.
       
  1349     $index_comments = TRUE;
       
  1350     foreach ($perms['search content'] as $rid) {
       
  1351       if (!isset($perms['access comments'][$rid]) && ($rid <= DRUPAL_AUTHENTICATED_RID || !isset($perms['access comments'][DRUPAL_AUTHENTICATED_RID]))) {
       
  1352         $index_comments = FALSE;
       
  1353         break;
       
  1354       }
       
  1355     }
       
  1356   }
       
  1357 
       
  1358   if ($index_comments) {
       
  1359     $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
       
  1360     $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
       
  1361     if ($node->comment && $cids = comment_get_thread($node, $mode, $comments_per_page)) {
       
  1362       $comments = comment_load_multiple($cids);
       
  1363       comment_prepare_thread($comments);
       
  1364       $build = comment_view_multiple($comments, $node);
       
  1365       return drupal_render($build);
       
  1366     }
       
  1367   }
       
  1368   return '';
       
  1369 }
       
  1370 
       
  1371 /**
       
  1372  * Implements hook_update_index().
       
  1373  */
       
  1374 function comment_update_index() {
       
  1375   // Store the maximum possible comments per thread (used for ranking by reply count)
       
  1376   variable_set('node_cron_comments_scale', 1.0 / max(1, db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}')->fetchField()));
       
  1377 }
       
  1378 
       
  1379 /**
       
  1380  * Implements hook_node_search_result().
       
  1381  *
       
  1382  * Formats a comment count string and returns it, for display with search
       
  1383  * results.
       
  1384  */
       
  1385 function comment_node_search_result($node) {
       
  1386   // Do not make a string if comments are hidden.
       
  1387   if (user_access('access comments') && $node->comment != COMMENT_NODE_HIDDEN) {
       
  1388     $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField();
       
  1389     // Do not make a string if comments are closed and there are currently
       
  1390     // zero comments.
       
  1391     if ($node->comment != COMMENT_NODE_CLOSED || $comments > 0) {
       
  1392       return array('comment' => format_plural($comments, '1 comment', '@count comments'));
       
  1393     }
       
  1394   }
       
  1395 }
       
  1396 
       
  1397 /**
       
  1398  * Implements hook_user_cancel().
       
  1399  */
       
  1400 function comment_user_cancel($edit, $account, $method) {
       
  1401   switch ($method) {
       
  1402     case 'user_cancel_block_unpublish':
       
  1403       $comments = comment_load_multiple(array(), array('uid' => $account->uid));
       
  1404       foreach ($comments as $comment) {
       
  1405         $comment->status = 0;
       
  1406         comment_save($comment);
       
  1407       }
       
  1408       break;
       
  1409 
       
  1410     case 'user_cancel_reassign':
       
  1411       $comments = comment_load_multiple(array(), array('uid' => $account->uid));
       
  1412       foreach ($comments as $comment) {
       
  1413         $comment->uid = 0;
       
  1414         comment_save($comment);
       
  1415       }
       
  1416       break;
       
  1417   }
       
  1418 }
       
  1419 
       
  1420 /**
       
  1421  * Implements hook_user_delete().
       
  1422  */
       
  1423 function comment_user_delete($account) {
       
  1424   $cids = db_query('SELECT c.cid FROM {comment} c WHERE uid = :uid', array(':uid' => $account->uid))->fetchCol();
       
  1425   comment_delete_multiple($cids);
       
  1426 }
       
  1427 
       
  1428 /**
       
  1429  * Determines whether the current user has access to a particular comment.
       
  1430  *
       
  1431  * Authenticated users can edit their comments as long they have not been
       
  1432  * replied to. This prevents people from changing or revising their statements
       
  1433  * based on the replies to their posts.
       
  1434  *
       
  1435  * @param $op
       
  1436  *   The operation that is to be performed on the comment. Only 'edit' is
       
  1437  *   recognized now.
       
  1438  * @param $comment
       
  1439  *   The comment object.
       
  1440  * @return
       
  1441  *   TRUE if the current user has acces to the comment, FALSE otherwise.
       
  1442  */
       
  1443 function comment_access($op, $comment) {
       
  1444   global $user;
       
  1445 
       
  1446   if ($op == 'edit') {
       
  1447     return ($user->uid && $user->uid == $comment->uid && $comment->status == COMMENT_PUBLISHED && user_access('edit own comments')) || user_access('administer comments');
       
  1448   }
       
  1449 }
       
  1450 
       
  1451 /**
       
  1452  * Accepts a submission of new or changed comment content.
       
  1453  *
       
  1454  * @param $comment
       
  1455  *   A comment object.
       
  1456  */
       
  1457 function comment_save($comment) {
       
  1458   global $user;
       
  1459 
       
  1460   $transaction = db_transaction();
       
  1461   try {
       
  1462     $defaults = array(
       
  1463       'mail' => '',
       
  1464       'homepage' => '',
       
  1465       'name' => '',
       
  1466       'status' => user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED,
       
  1467     );
       
  1468     foreach ($defaults as $key => $default) {
       
  1469       if (!isset($comment->$key)) {
       
  1470         $comment->$key = $default;
       
  1471       }
       
  1472     }
       
  1473     // Make sure we have a bundle name.
       
  1474     if (!isset($comment->node_type)) {
       
  1475       $node = node_load($comment->nid);
       
  1476       $comment->node_type = 'comment_node_' . $node->type;
       
  1477     }
       
  1478 
       
  1479     // Load the stored entity, if any.
       
  1480     if (!empty($comment->cid) && !isset($comment->original)) {
       
  1481       $comment->original = entity_load_unchanged('comment', $comment->cid);
       
  1482     }
       
  1483 
       
  1484     field_attach_presave('comment', $comment);
       
  1485 
       
  1486     // Allow modules to alter the comment before saving.
       
  1487     module_invoke_all('comment_presave', $comment);
       
  1488     module_invoke_all('entity_presave', $comment, 'comment');
       
  1489 
       
  1490     if ($comment->cid) {
       
  1491 
       
  1492       drupal_write_record('comment', $comment, 'cid');
       
  1493 
       
  1494       // Ignore slave server temporarily to give time for the
       
  1495       // saved comment to be propagated to the slave.
       
  1496       db_ignore_slave();
       
  1497 
       
  1498       // Update the {node_comment_statistics} table prior to executing hooks.
       
  1499       _comment_update_node_statistics($comment->nid);
       
  1500 
       
  1501       field_attach_update('comment', $comment);
       
  1502       // Allow modules to respond to the updating of a comment.
       
  1503       module_invoke_all('comment_update', $comment);
       
  1504       module_invoke_all('entity_update', $comment, 'comment');
       
  1505     }
       
  1506     else {
       
  1507       // Add the comment to database. This next section builds the thread field.
       
  1508       // Also see the documentation for comment_view().
       
  1509       if (!empty($comment->thread)) {
       
  1510         // Allow calling code to set thread itself.
       
  1511         $thread = $comment->thread;
       
  1512       }
       
  1513       elseif ($comment->pid == 0) {
       
  1514         // This is a comment with no parent comment (depth 0): we start
       
  1515         // by retrieving the maximum thread level.
       
  1516         $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField();
       
  1517         // Strip the "/" from the end of the thread.
       
  1518         $max = rtrim($max, '/');
       
  1519         // We need to get the value at the correct depth.
       
  1520         $parts = explode('.', $max);
       
  1521         $firstsegment = $parts[0];
       
  1522         // Finally, build the thread field for this new comment.
       
  1523         $thread = int2vancode(vancode2int($firstsegment) + 1) . '/';
       
  1524       }
       
  1525       else {
       
  1526         // This is a comment with a parent comment, so increase the part of the
       
  1527         // thread value at the proper depth.
       
  1528 
       
  1529         // Get the parent comment:
       
  1530         $parent = comment_load($comment->pid);
       
  1531         // Strip the "/" from the end of the parent thread.
       
  1532         $parent->thread = (string) rtrim((string) $parent->thread, '/');
       
  1533         // Get the max value in *this* thread.
       
  1534         $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
       
  1535           ':thread' => $parent->thread . '.%',
       
  1536           ':nid' => $comment->nid,
       
  1537         ))->fetchField();
       
  1538 
       
  1539         if ($max == '') {
       
  1540           // First child of this parent.
       
  1541           $thread = $parent->thread . '.' . int2vancode(0) . '/';
       
  1542         }
       
  1543         else {
       
  1544           // Strip the "/" at the end of the thread.
       
  1545           $max = rtrim($max, '/');
       
  1546           // Get the value at the correct depth.
       
  1547           $parts = explode('.', $max);
       
  1548           $parent_depth = count(explode('.', $parent->thread));
       
  1549           $last = $parts[$parent_depth];
       
  1550           // Finally, build the thread field for this new comment.
       
  1551           $thread = $parent->thread . '.' . int2vancode(vancode2int($last) + 1) . '/';
       
  1552         }
       
  1553       }
       
  1554 
       
  1555       if (empty($comment->created)) {
       
  1556         $comment->created = REQUEST_TIME;
       
  1557       }
       
  1558 
       
  1559       if (empty($comment->changed)) {
       
  1560         $comment->changed = $comment->created;
       
  1561       }
       
  1562 
       
  1563       if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well.
       
  1564         $comment->name = $user->name;
       
  1565       }
       
  1566 
       
  1567       // Ensure the parent id (pid) has a value set.
       
  1568       if (empty($comment->pid)) {
       
  1569         $comment->pid = 0;
       
  1570       }
       
  1571 
       
  1572       // Add the values which aren't passed into the function.
       
  1573       $comment->thread = $thread;
       
  1574       $comment->hostname = ip_address();
       
  1575 
       
  1576       drupal_write_record('comment', $comment);
       
  1577 
       
  1578       // Ignore slave server temporarily to give time for the
       
  1579       // created comment to be propagated to the slave.
       
  1580       db_ignore_slave();
       
  1581 
       
  1582       // Update the {node_comment_statistics} table prior to executing hooks.
       
  1583       _comment_update_node_statistics($comment->nid);
       
  1584 
       
  1585       field_attach_insert('comment', $comment);
       
  1586 
       
  1587       // Tell the other modules a new comment has been submitted.
       
  1588       module_invoke_all('comment_insert', $comment);
       
  1589       module_invoke_all('entity_insert', $comment, 'comment');
       
  1590     }
       
  1591     if ($comment->status == COMMENT_PUBLISHED) {
       
  1592       module_invoke_all('comment_publish', $comment);
       
  1593     }
       
  1594     unset($comment->original);
       
  1595   }
       
  1596   catch (Exception $e) {
       
  1597     $transaction->rollback('comment');
       
  1598     watchdog_exception('comment', $e);
       
  1599     throw $e;
       
  1600   }
       
  1601 
       
  1602 }
       
  1603 
       
  1604 /**
       
  1605  * Delete a comment and all its replies.
       
  1606  *
       
  1607  * @param $cid
       
  1608  *   The comment to delete.
       
  1609  */
       
  1610 function comment_delete($cid) {
       
  1611   comment_delete_multiple(array($cid));
       
  1612 }
       
  1613 
       
  1614 /**
       
  1615  * Delete comments and all their replies.
       
  1616  *
       
  1617  * @param $cids
       
  1618  *   The comment to delete.
       
  1619  */
       
  1620 function comment_delete_multiple($cids) {
       
  1621   $comments = comment_load_multiple($cids);
       
  1622   if ($comments) {
       
  1623     $transaction = db_transaction();
       
  1624     try {
       
  1625       // Delete the comments.
       
  1626       db_delete('comment')
       
  1627         ->condition('cid', array_keys($comments), 'IN')
       
  1628         ->execute();
       
  1629       foreach ($comments as $comment) {
       
  1630         field_attach_delete('comment', $comment);
       
  1631         module_invoke_all('comment_delete', $comment);
       
  1632         module_invoke_all('entity_delete', $comment, 'comment');
       
  1633 
       
  1634         // Delete the comment's replies.
       
  1635         $child_cids = db_query('SELECT cid FROM {comment} WHERE pid = :cid', array(':cid' => $comment->cid))->fetchCol();
       
  1636         comment_delete_multiple($child_cids);
       
  1637         _comment_update_node_statistics($comment->nid);
       
  1638       }
       
  1639     }
       
  1640     catch (Exception $e) {
       
  1641       $transaction->rollback();
       
  1642       watchdog_exception('comment', $e);
       
  1643       throw $e;
       
  1644     }
       
  1645   }
       
  1646 }
       
  1647 
       
  1648 /**
       
  1649  * Load comments from the database.
       
  1650  *
       
  1651  * @param $cids
       
  1652  *   An array of comment IDs.
       
  1653  * @param $conditions
       
  1654  *   (deprecated) An associative array of conditions on the {comments}
       
  1655  *   table, where the keys are the database fields and the values are the
       
  1656  *   values those fields must have. Instead, it is preferable to use
       
  1657  *   EntityFieldQuery to retrieve a list of entity IDs loadable by
       
  1658  *   this function.
       
  1659  * @param $reset
       
  1660  *   Whether to reset the internal static entity cache. Note that the static
       
  1661  *   cache is disabled in comment_entity_info() by default.
       
  1662  *
       
  1663  * @return
       
  1664  *   An array of comment objects, indexed by comment ID.
       
  1665  *
       
  1666  * @see entity_load()
       
  1667  * @see EntityFieldQuery
       
  1668  *
       
  1669  * @todo Remove $conditions in Drupal 8.
       
  1670  */
       
  1671 function comment_load_multiple($cids = array(), $conditions = array(), $reset = FALSE) {
       
  1672   return entity_load('comment', $cids, $conditions, $reset);
       
  1673 }
       
  1674 
       
  1675 /**
       
  1676  * Load the entire comment by cid.
       
  1677  *
       
  1678  * @param $cid
       
  1679  *   The identifying comment id.
       
  1680  * @param $reset
       
  1681  *   Whether to reset the internal static entity cache. Note that the static
       
  1682  *   cache is disabled in comment_entity_info() by default.
       
  1683  *
       
  1684  * @return
       
  1685  *   The comment object.
       
  1686  */
       
  1687 function comment_load($cid, $reset = FALSE) {
       
  1688   $comment = comment_load_multiple(array($cid), array(), $reset);
       
  1689   return $comment ? $comment[$cid] : FALSE;
       
  1690 }
       
  1691 
       
  1692 /**
       
  1693  * Controller class for comments.
       
  1694  *
       
  1695  * This extends the DrupalDefaultEntityController class, adding required
       
  1696  * special handling for comment objects.
       
  1697  */
       
  1698 class CommentController extends DrupalDefaultEntityController {
       
  1699 
       
  1700   protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
       
  1701     $query = parent::buildQuery($ids, $conditions, $revision_id);
       
  1702     // Specify additional fields from the user and node tables.
       
  1703     $query->innerJoin('node', 'n', 'base.nid = n.nid');
       
  1704     $query->addField('n', 'type', 'node_type');
       
  1705     $query->innerJoin('users', 'u', 'base.uid = u.uid');
       
  1706     $query->addField('u', 'name', 'registered_name');
       
  1707     $query->fields('u', array('uid', 'signature', 'signature_format', 'picture'));
       
  1708     return $query;
       
  1709   }
       
  1710 
       
  1711   protected function attachLoad(&$comments, $revision_id = FALSE) {
       
  1712     // Setup standard comment properties.
       
  1713     foreach ($comments as $key => $comment) {
       
  1714       $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
       
  1715       $comment->new = node_mark($comment->nid, $comment->changed);
       
  1716       $comment->node_type = 'comment_node_' . $comment->node_type;
       
  1717       $comments[$key] = $comment;
       
  1718     }
       
  1719     parent::attachLoad($comments, $revision_id);
       
  1720   }
       
  1721 }
       
  1722 
       
  1723 /**
       
  1724  * Get number of new comments for current user and specified node.
       
  1725  *
       
  1726  * @param $nid
       
  1727  *   Node-id to count comments for.
       
  1728  * @param $timestamp
       
  1729  *   Time to count from (defaults to time of last user access
       
  1730  *   to node).
       
  1731  * @return The result or FALSE on error.
       
  1732  */
       
  1733 function comment_num_new($nid, $timestamp = 0) {
       
  1734   global $user;
       
  1735 
       
  1736   if ($user->uid) {
       
  1737     // Retrieve the timestamp at which the current user last viewed this node.
       
  1738     if (!$timestamp) {
       
  1739       $timestamp = node_last_viewed($nid);
       
  1740     }
       
  1741     $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT);
       
  1742 
       
  1743     // Use the timestamp to retrieve the number of new comments.
       
  1744     return db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND created > :timestamp AND status = :status', array(
       
  1745       ':nid' => $nid,
       
  1746       ':timestamp' => $timestamp,
       
  1747       ':status' => COMMENT_PUBLISHED,
       
  1748       ))->fetchField();
       
  1749   }
       
  1750   else {
       
  1751     return FALSE;
       
  1752   }
       
  1753 
       
  1754 }
       
  1755 
       
  1756 /**
       
  1757  * Get the display ordinal for a comment, starting from 0.
       
  1758  *
       
  1759  * Count the number of comments which appear before the comment we want to
       
  1760  * display, taking into account display settings and threading.
       
  1761  *
       
  1762  * @param $cid
       
  1763  *   The comment ID.
       
  1764  * @param $node_type
       
  1765  *   The node type of the comment's parent.
       
  1766  * @return
       
  1767  *   The display ordinal for the comment.
       
  1768  * @see comment_get_display_page()
       
  1769  */
       
  1770 function comment_get_display_ordinal($cid, $node_type) {
       
  1771   // Count how many comments (c1) are before $cid (c2) in display order. This is
       
  1772   // the 0-based display ordinal.
       
  1773   $query = db_select('comment', 'c1');
       
  1774   $query->innerJoin('comment', 'c2', 'c2.nid = c1.nid');
       
  1775   $query->addExpression('COUNT(*)', 'count');
       
  1776   $query->condition('c2.cid', $cid);
       
  1777   if (!user_access('administer comments')) {
       
  1778     $query->condition('c1.status', COMMENT_PUBLISHED);
       
  1779   }
       
  1780   $mode = variable_get('comment_default_mode_' . $node_type, COMMENT_MODE_THREADED);
       
  1781 
       
  1782   if ($mode == COMMENT_MODE_FLAT) {
       
  1783     // For flat comments, cid is used for ordering comments due to
       
  1784     // unpredicatable behavior with timestamp, so we make the same assumption
       
  1785     // here.
       
  1786     $query->condition('c1.cid', $cid, '<');
       
  1787   }
       
  1788   else {
       
  1789     // For threaded comments, the c.thread column is used for ordering. We can
       
  1790     // use the vancode for comparison, but must remove the trailing slash.
       
  1791     // See comment_view_multiple().
       
  1792     $query->where('SUBSTRING(c1.thread, 1, (LENGTH(c1.thread) -1)) < SUBSTRING(c2.thread, 1, (LENGTH(c2.thread) -1))');
       
  1793   }
       
  1794 
       
  1795   return $query->execute()->fetchField();
       
  1796 }
       
  1797 
       
  1798 /**
       
  1799  * Return the page number for a comment.
       
  1800  *
       
  1801  * Finds the correct page number for a comment taking into account display
       
  1802  * and paging settings.
       
  1803  *
       
  1804  * @param $cid
       
  1805  *   The comment ID.
       
  1806  * @param $node_type
       
  1807  *   The node type the comment is attached to.
       
  1808  * @return
       
  1809  *   The page number.
       
  1810  */
       
  1811 function comment_get_display_page($cid, $node_type) {
       
  1812   $ordinal = comment_get_display_ordinal($cid, $node_type);
       
  1813   $comments_per_page = variable_get('comment_default_per_page_' . $node_type, 50);
       
  1814   return floor($ordinal / $comments_per_page);
       
  1815 }
       
  1816 
       
  1817 /**
       
  1818  * Page callback for comment editing.
       
  1819  */
       
  1820 function comment_edit_page($comment) {
       
  1821   drupal_set_title(t('Edit comment %comment', array('%comment' => $comment->subject)), PASS_THROUGH);
       
  1822   $node = node_load($comment->nid);
       
  1823   return drupal_get_form("comment_node_{$node->type}_form", $comment);
       
  1824 }
       
  1825 
       
  1826 /**
       
  1827  * Implements hook_forms().
       
  1828  */
       
  1829 function comment_forms() {
       
  1830   $forms = array();
       
  1831   foreach (node_type_get_types() as $type) {
       
  1832     $forms["comment_node_{$type->type}_form"]['callback'] = 'comment_form';
       
  1833   }
       
  1834   return $forms;
       
  1835 }
       
  1836 
       
  1837 /**
       
  1838  * Generate the basic commenting form, for appending to a node or display on a separate page.
       
  1839  *
       
  1840  * @see comment_form_validate()
       
  1841  * @see comment_form_submit()
       
  1842  *
       
  1843  * @ingroup forms
       
  1844  */
       
  1845 function comment_form($form, &$form_state, $comment) {
       
  1846   global $user;
       
  1847 
       
  1848   // During initial form build, add the comment entity to the form state for
       
  1849   // use during form building and processing. During a rebuild, use what is in
       
  1850   // the form state.
       
  1851   if (!isset($form_state['comment'])) {
       
  1852     $defaults = array(
       
  1853       'name' => '',
       
  1854       'mail' => '',
       
  1855       'homepage' => '',
       
  1856       'subject' => '',
       
  1857       'comment' => '',
       
  1858       'cid' => NULL,
       
  1859       'pid' => NULL,
       
  1860       'language' => LANGUAGE_NONE,
       
  1861       'uid' => 0,
       
  1862     );
       
  1863     foreach ($defaults as $key => $value) {
       
  1864       if (!isset($comment->$key)) {
       
  1865         $comment->$key = $value;
       
  1866       }
       
  1867     }
       
  1868     $form_state['comment'] = $comment;
       
  1869   }
       
  1870   else {
       
  1871     $comment = $form_state['comment'];
       
  1872   }
       
  1873 
       
  1874   $node = node_load($comment->nid);
       
  1875   $form['#node'] = $node;
       
  1876 
       
  1877   // Use #comment-form as unique jump target, regardless of node type.
       
  1878   $form['#id'] = drupal_html_id('comment_form');
       
  1879   $form['#attributes']['class'][] = 'comment-form';
       
  1880   $form['#theme'] = array('comment_form__node_' . $node->type, 'comment_form');
       
  1881 
       
  1882   $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT);
       
  1883   $is_admin = (!empty($comment->cid) && user_access('administer comments'));
       
  1884 
       
  1885   if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
       
  1886     $form['#attached']['library'][] = array('system', 'jquery.cookie');
       
  1887     $form['#attributes']['class'][] = 'user-info-from-cookie';
       
  1888   }
       
  1889 
       
  1890   // If not replying to a comment, use our dedicated page callback for new
       
  1891   // comments on nodes.
       
  1892   if (empty($comment->cid) && empty($comment->pid)) {
       
  1893     $form['#action'] = url('comment/reply/' . $comment->nid);
       
  1894   }
       
  1895 
       
  1896   if (isset($form_state['comment_preview'])) {
       
  1897     $form += $form_state['comment_preview'];
       
  1898   }
       
  1899 
       
  1900   // Display author information in a fieldset for comment moderators.
       
  1901   if ($is_admin) {
       
  1902     $form['author'] = array(
       
  1903       '#type' => 'fieldset',
       
  1904       '#title' => t('Administration'),
       
  1905       '#collapsible' => TRUE,
       
  1906       '#collapsed' => TRUE,
       
  1907       '#weight' => -2,
       
  1908     );
       
  1909   }
       
  1910   else {
       
  1911     // Sets the author form elements above the subject.
       
  1912     $form['author'] = array(
       
  1913       '#weight' => -2,
       
  1914     );
       
  1915   }
       
  1916 
       
  1917   // Prepare default values for form elements.
       
  1918   if ($is_admin) {
       
  1919     $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name);
       
  1920     $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED);
       
  1921     $date = (!empty($comment->date) ? $comment->date : format_date($comment->created, 'custom', 'Y-m-d H:i O'));
       
  1922   }
       
  1923   else {
       
  1924     if ($user->uid) {
       
  1925       $author = $user->name;
       
  1926     }
       
  1927     else {
       
  1928       $author = ($comment->name ? $comment->name : '');
       
  1929     }
       
  1930     $status = (user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED);
       
  1931     $date = '';
       
  1932   }
       
  1933 
       
  1934   // Add the author name field depending on the current user.
       
  1935   if ($is_admin) {
       
  1936     $form['author']['name'] = array(
       
  1937       '#type' => 'textfield',
       
  1938       '#title' => t('Authored by'),
       
  1939       '#default_value' => $author,
       
  1940       '#maxlength' => 60,
       
  1941       '#size' => 30,
       
  1942       '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
       
  1943       '#autocomplete_path' => 'user/autocomplete',
       
  1944     );
       
  1945   }
       
  1946   elseif ($user->uid) {
       
  1947     $form['author']['_author'] = array(
       
  1948       '#type' => 'item',
       
  1949       '#title' => t('Your name'),
       
  1950       '#markup' => theme('username', array('account' => $user)),
       
  1951     );
       
  1952     $form['author']['name'] = array(
       
  1953       '#type' => 'value',
       
  1954       '#value' => $author,
       
  1955     );
       
  1956   }
       
  1957   else {
       
  1958     $form['author']['name'] = array(
       
  1959       '#type' => 'textfield',
       
  1960       '#title' => t('Your name'),
       
  1961       '#default_value' => $author,
       
  1962       '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
       
  1963       '#maxlength' => 60,
       
  1964       '#size' => 30,
       
  1965     );
       
  1966   }
       
  1967 
       
  1968   // Add author e-mail and homepage fields depending on the current user.
       
  1969   $form['author']['mail'] = array(
       
  1970     '#type' => 'textfield',
       
  1971     '#title' => t('E-mail'),
       
  1972     '#default_value' => $comment->mail,
       
  1973     '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
       
  1974     '#maxlength' => 64,
       
  1975     '#size' => 30,
       
  1976     '#description' => t('The content of this field is kept private and will not be shown publicly.'),
       
  1977     '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
       
  1978   );
       
  1979   $form['author']['homepage'] = array(
       
  1980     '#type' => 'textfield',
       
  1981     '#title' => t('Homepage'),
       
  1982     '#default_value' => $comment->homepage,
       
  1983     '#maxlength' => 255,
       
  1984     '#size' => 30,
       
  1985     '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
       
  1986   );
       
  1987 
       
  1988   // Add administrative comment publishing options.
       
  1989   $form['author']['date'] = array(
       
  1990     '#type' => 'textfield',
       
  1991     '#title' => t('Authored on'),
       
  1992     '#default_value' => $date,
       
  1993     '#maxlength' => 25,
       
  1994     '#size' => 20,
       
  1995     '#access' => $is_admin,
       
  1996   );
       
  1997   $form['author']['status'] = array(
       
  1998     '#type' => 'radios',
       
  1999     '#title' => t('Status'),
       
  2000     '#default_value' => $status,
       
  2001     '#options' => array(
       
  2002       COMMENT_PUBLISHED => t('Published'),
       
  2003       COMMENT_NOT_PUBLISHED => t('Not published'),
       
  2004     ),
       
  2005     '#access' => $is_admin,
       
  2006   );
       
  2007 
       
  2008   $form['subject'] = array(
       
  2009     '#type' => 'textfield',
       
  2010     '#title' => t('Subject'),
       
  2011     '#maxlength' => 64,
       
  2012     '#default_value' => $comment->subject,
       
  2013     '#access' => variable_get('comment_subject_field_' . $node->type, 1) == 1,
       
  2014     '#weight' => -1,
       
  2015   );
       
  2016 
       
  2017   // Used for conditional validation of author fields.
       
  2018   $form['is_anonymous'] = array(
       
  2019     '#type' => 'value',
       
  2020     '#value' => ($comment->cid ? !$comment->uid : !$user->uid),
       
  2021   );
       
  2022 
       
  2023   // Add internal comment properties.
       
  2024   foreach (array('cid', 'pid', 'nid', 'language', 'uid') as $key) {
       
  2025     $form[$key] = array('#type' => 'value', '#value' => $comment->$key);
       
  2026   }
       
  2027   $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type);
       
  2028 
       
  2029   // Only show the save button if comment previews are optional or if we are
       
  2030   // already previewing the submission.
       
  2031   $form['actions'] = array('#type' => 'actions');
       
  2032   $form['actions']['submit'] = array(
       
  2033     '#type' => 'submit',
       
  2034     '#value' => t('Save'),
       
  2035     '#access' => ($comment->cid && user_access('administer comments')) || variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || isset($form_state['comment_preview']),
       
  2036     '#weight' => 19,
       
  2037   );
       
  2038   $form['actions']['preview'] = array(
       
  2039     '#type' => 'submit',
       
  2040     '#value' => t('Preview'),
       
  2041     '#access' => (variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED),
       
  2042     '#weight' => 20,
       
  2043     '#submit' => array('comment_form_build_preview'),
       
  2044   );
       
  2045 
       
  2046   // Attach fields.
       
  2047   $comment->node_type = 'comment_node_' . $node->type;
       
  2048   $langcode = entity_language('comment', $comment);
       
  2049   field_attach_form('comment', $comment, $form, $form_state, $langcode);
       
  2050 
       
  2051   return $form;
       
  2052 }
       
  2053 
       
  2054 /**
       
  2055  * Build a preview from submitted form values.
       
  2056  */
       
  2057 function comment_form_build_preview($form, &$form_state) {
       
  2058   $comment = comment_form_submit_build_comment($form, $form_state);
       
  2059   $form_state['comment_preview'] = comment_preview($comment);
       
  2060   $form_state['rebuild'] = TRUE;
       
  2061 }
       
  2062 
       
  2063 /**
       
  2064  * Generate a comment preview.
       
  2065  */
       
  2066 function comment_preview($comment) {
       
  2067   global $user;
       
  2068 
       
  2069   drupal_set_title(t('Preview comment'), PASS_THROUGH);
       
  2070 
       
  2071   $node = node_load($comment->nid);
       
  2072 
       
  2073   if (!form_get_errors()) {
       
  2074     $comment_body = field_get_items('comment', $comment, 'comment_body');
       
  2075     $comment->format = $comment_body[0]['format'];
       
  2076     // Attach the user and time information.
       
  2077     if (!empty($comment->name)) {
       
  2078       $account = user_load_by_name($comment->name);
       
  2079     }
       
  2080     elseif ($user->uid && empty($comment->is_anonymous)) {
       
  2081       $account = $user;
       
  2082     }
       
  2083 
       
  2084     if (!empty($account->uid)) {
       
  2085       $comment->uid = $account->uid;
       
  2086       $comment->name = check_plain($account->name);
       
  2087       $comment->signature = $account->signature;
       
  2088       $comment->signature_format = $account->signature_format;
       
  2089       $comment->picture = $account->picture;
       
  2090     }
       
  2091     elseif (empty($comment->name)) {
       
  2092       $comment->name = variable_get('anonymous', t('Anonymous'));
       
  2093     }
       
  2094 
       
  2095     $comment->created = !empty($comment->created) ? $comment->created : REQUEST_TIME;
       
  2096     $comment->changed = REQUEST_TIME;
       
  2097     $comment->in_preview = TRUE;
       
  2098     $comment_build = comment_view($comment, $node);
       
  2099     $comment_build['#weight'] = -100;
       
  2100 
       
  2101     $form['comment_preview'] = $comment_build;
       
  2102   }
       
  2103 
       
  2104   if ($comment->pid) {
       
  2105     $build = array();
       
  2106     if ($comments = comment_load_multiple(array($comment->pid), array('status' => COMMENT_PUBLISHED))) {
       
  2107       $parent_comment = $comments[$comment->pid];
       
  2108       $build = comment_view($parent_comment, $node);
       
  2109     }
       
  2110   }
       
  2111   else {
       
  2112     $build = node_view($node);
       
  2113   }
       
  2114 
       
  2115   $form['comment_output_below'] = $build;
       
  2116   $form['comment_output_below']['#weight'] = 100;
       
  2117 
       
  2118   return $form;
       
  2119 }
       
  2120 
       
  2121 /**
       
  2122  * Validate comment form submissions.
       
  2123  */
       
  2124 function comment_form_validate($form, &$form_state) {
       
  2125   global $user;
       
  2126 
       
  2127   entity_form_field_validate('comment', $form, $form_state);
       
  2128 
       
  2129   if (!empty($form_state['values']['cid'])) {
       
  2130     // Verify the name in case it is being changed from being anonymous.
       
  2131     $account = user_load_by_name($form_state['values']['name']);
       
  2132     $form_state['values']['uid'] = $account ? $account->uid : 0;
       
  2133 
       
  2134     if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) {
       
  2135       form_set_error('date', t('You have to specify a valid date.'));
       
  2136     }
       
  2137     if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) {
       
  2138       form_set_error('name', t('You have to specify a valid author.'));
       
  2139     }
       
  2140   }
       
  2141   elseif ($form_state['values']['is_anonymous']) {
       
  2142     // Validate anonymous comment author fields (if given). If the (original)
       
  2143     // author of this comment was an anonymous user, verify that no registered
       
  2144     // user with this name exists.
       
  2145     if ($form_state['values']['name']) {
       
  2146       $query = db_select('users', 'u');
       
  2147       $query->addField('u', 'uid', 'uid');
       
  2148       $taken = $query
       
  2149         ->condition('name', db_like($form_state['values']['name']), 'LIKE')
       
  2150         ->countQuery()
       
  2151         ->execute()
       
  2152         ->fetchField();
       
  2153       if ($taken) {
       
  2154         form_set_error('name', t('The name you used belongs to a registered user.'));
       
  2155       }
       
  2156     }
       
  2157   }
       
  2158   if ($form_state['values']['mail'] && !valid_email_address($form_state['values']['mail'])) {
       
  2159     form_set_error('mail', t('The e-mail address you specified is not valid.'));
       
  2160   }
       
  2161   if ($form_state['values']['homepage'] && !valid_url($form_state['values']['homepage'], TRUE)) {
       
  2162     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>.'));
       
  2163   }
       
  2164 }
       
  2165 
       
  2166 /**
       
  2167  * Prepare a comment for submission.
       
  2168  */
       
  2169 function comment_submit($comment) {
       
  2170   // @todo Legacy support. Remove in Drupal 8.
       
  2171   if (is_array($comment)) {
       
  2172     $comment += array('subject' => '');
       
  2173     $comment = (object) $comment;
       
  2174   }
       
  2175 
       
  2176   if (empty($comment->date)) {
       
  2177     $comment->date = 'now';
       
  2178   }
       
  2179   $comment->created = strtotime($comment->date);
       
  2180   $comment->changed = REQUEST_TIME;
       
  2181 
       
  2182   // If the comment was posted by a registered user, assign the author's ID.
       
  2183   // @todo Too fragile. Should be prepared and stored in comment_form() already.
       
  2184   if (!$comment->is_anonymous && !empty($comment->name) && ($account = user_load_by_name($comment->name))) {
       
  2185     $comment->uid = $account->uid;
       
  2186   }
       
  2187   // If the comment was posted by an anonymous user and no author name was
       
  2188   // required, use "Anonymous" by default.
       
  2189   if ($comment->is_anonymous && (!isset($comment->name) || $comment->name === '')) {
       
  2190     $comment->name = variable_get('anonymous', t('Anonymous'));
       
  2191   }
       
  2192 
       
  2193   // Validate the comment's subject. If not specified, extract from comment body.
       
  2194   if (trim($comment->subject) == '') {
       
  2195     // The body may be in any format, so:
       
  2196     // 1) Filter it into HTML
       
  2197     // 2) Strip out all HTML tags
       
  2198     // 3) Convert entities back to plain-text.
       
  2199     $field = field_info_field('comment_body');
       
  2200     $langcode = field_is_translatable('comment', $field) ? entity_language('comment', $comment) : LANGUAGE_NONE;
       
  2201     $comment_body = $comment->comment_body[$langcode][0];
       
  2202     if (isset($comment_body['format'])) {
       
  2203       $comment_text = check_markup($comment_body['value'], $comment_body['format']);
       
  2204     }
       
  2205     else {
       
  2206       $comment_text = check_plain($comment_body['value']);
       
  2207     }
       
  2208     $comment->subject = truncate_utf8(trim(decode_entities(strip_tags($comment_text))), 29, TRUE);
       
  2209     // Edge cases where the comment body is populated only by HTML tags will
       
  2210     // require a default subject.
       
  2211     if ($comment->subject == '') {
       
  2212       $comment->subject = t('(No subject)');
       
  2213     }
       
  2214   }
       
  2215   return $comment;
       
  2216 }
       
  2217 
       
  2218 /**
       
  2219  * Updates the form state's comment entity by processing this submission's values.
       
  2220  *
       
  2221  * This is the default builder function for the comment form. It is called
       
  2222  * during the "Save" and "Preview" submit handlers to retrieve the entity to
       
  2223  * save or preview. This function can also be called by a "Next" button of a
       
  2224  * wizard to update the form state's entity with the current step's values
       
  2225  * before proceeding to the next step.
       
  2226  *
       
  2227  * @see comment_form()
       
  2228  */
       
  2229 function comment_form_submit_build_comment($form, &$form_state) {
       
  2230   $comment = $form_state['comment'];
       
  2231   entity_form_submit_build_entity('comment', $comment, $form, $form_state);
       
  2232   comment_submit($comment);
       
  2233   return $comment;
       
  2234 }
       
  2235 
       
  2236 /**
       
  2237  * Process comment form submissions; prepare the comment, store it, and set a redirection target.
       
  2238  */
       
  2239 function comment_form_submit($form, &$form_state) {
       
  2240   $node = node_load($form_state['values']['nid']);
       
  2241   $comment = comment_form_submit_build_comment($form, $form_state);
       
  2242   if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) {
       
  2243     // Save the anonymous user information to a cookie for reuse.
       
  2244     if (user_is_anonymous()) {
       
  2245       user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage'))));
       
  2246     }
       
  2247 
       
  2248     comment_save($comment);
       
  2249     $form_state['values']['cid'] = $comment->cid;
       
  2250 
       
  2251     // Add an entry to the watchdog log.
       
  2252     watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)));
       
  2253 
       
  2254     // Explain the approval queue if necessary.
       
  2255     if ($comment->status == COMMENT_NOT_PUBLISHED) {
       
  2256       if (!user_access('administer comments')) {
       
  2257         drupal_set_message(t('Your comment has been queued for review by site administrators and will be published after approval.'));
       
  2258       }
       
  2259     }
       
  2260     else {
       
  2261       drupal_set_message(t('Your comment has been posted.'));
       
  2262     }
       
  2263     $query = array();
       
  2264     // Find the current display page for this comment.
       
  2265     $page = comment_get_display_page($comment->cid, $node->type);
       
  2266     if ($page > 0) {
       
  2267       $query['page'] = $page;
       
  2268     }
       
  2269     // Redirect to the newly posted comment.
       
  2270     $redirect = array('node/' . $node->nid, array('query' => $query, 'fragment' => 'comment-' . $comment->cid));
       
  2271   }
       
  2272   else {
       
  2273     watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject), WATCHDOG_WARNING);
       
  2274     drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject)), 'error');
       
  2275     // Redirect the user to the node they are commenting on.
       
  2276     $redirect = 'node/' . $node->nid;
       
  2277   }
       
  2278   $form_state['redirect'] = $redirect;
       
  2279   // Clear the block and page caches so that anonymous users see the comment
       
  2280   // they have posted.
       
  2281   cache_clear_all();
       
  2282 }
       
  2283 
       
  2284 /**
       
  2285  * Process variables for comment.tpl.php.
       
  2286  *
       
  2287  * @see comment.tpl.php
       
  2288  */
       
  2289 function template_preprocess_comment(&$variables) {
       
  2290   $comment = $variables['elements']['#comment'];
       
  2291   $node = $variables['elements']['#node'];
       
  2292   $variables['comment']   = $comment;
       
  2293   $variables['node']      = $node;
       
  2294   $variables['author']    = theme('username', array('account' => $comment));
       
  2295 
       
  2296   $variables['created']   = format_date($comment->created);
       
  2297 
       
  2298   // Avoid calling format_date() twice on the same timestamp.
       
  2299   if ($comment->changed == $comment->created) {
       
  2300     $variables['changed'] = $variables['created'];
       
  2301   }
       
  2302   else {
       
  2303     $variables['changed'] = format_date($comment->changed);
       
  2304   }
       
  2305 
       
  2306   $variables['new']       = !empty($comment->new) ? t('new') : '';
       
  2307   $variables['picture']   = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', array('account' => $comment)) : '';
       
  2308   $variables['signature'] = $comment->signature;
       
  2309 
       
  2310   $uri = entity_uri('comment', $comment);
       
  2311   $uri['options'] += array('attributes' => array('class' => array('permalink'), 'rel' => 'bookmark'));
       
  2312 
       
  2313   $variables['title']     = l($comment->subject, $uri['path'], $uri['options']);
       
  2314   $variables['permalink'] = l(t('Permalink'), $uri['path'], $uri['options']);
       
  2315   $variables['submitted'] = t('Submitted by !username on !datetime', array('!username' => $variables['author'], '!datetime' => $variables['created']));
       
  2316 
       
  2317   // Preprocess fields.
       
  2318   field_attach_preprocess('comment', $comment, $variables['elements'], $variables);
       
  2319 
       
  2320   // Helpful $content variable for templates.
       
  2321   foreach (element_children($variables['elements']) as $key) {
       
  2322     $variables['content'][$key] = $variables['elements'][$key];
       
  2323   }
       
  2324 
       
  2325   // Set status to a string representation of comment->status.
       
  2326   if (isset($comment->in_preview)) {
       
  2327     $variables['status'] = 'comment-preview';
       
  2328   }
       
  2329   else {
       
  2330     $variables['status'] = ($comment->status == COMMENT_NOT_PUBLISHED) ? 'comment-unpublished' : 'comment-published';
       
  2331   }
       
  2332 
       
  2333   // Gather comment classes.
       
  2334   // 'comment-published' class is not needed, it is either 'comment-preview' or
       
  2335   // 'comment-unpublished'.
       
  2336   if ($variables['status'] != 'comment-published') {
       
  2337     $variables['classes_array'][] = $variables['status'];
       
  2338   }
       
  2339   if ($variables['new']) {
       
  2340     $variables['classes_array'][] = 'comment-new';
       
  2341   }
       
  2342   if (!$comment->uid) {
       
  2343     $variables['classes_array'][] = 'comment-by-anonymous';
       
  2344   }
       
  2345   else {
       
  2346     if ($comment->uid == $variables['node']->uid) {
       
  2347       $variables['classes_array'][] = 'comment-by-node-author';
       
  2348     }
       
  2349     if ($comment->uid == $variables['user']->uid) {
       
  2350       $variables['classes_array'][] = 'comment-by-viewer';
       
  2351     }
       
  2352   }
       
  2353 }
       
  2354 
       
  2355 /**
       
  2356  * Returns HTML for a "you can't post comments" notice.
       
  2357  *
       
  2358  * @param $variables
       
  2359  *   An associative array containing:
       
  2360  *   - node: The comment node.
       
  2361  *
       
  2362  * @ingroup themeable
       
  2363  */
       
  2364 function theme_comment_post_forbidden($variables) {
       
  2365   $node = $variables['node'];
       
  2366   global $user;
       
  2367 
       
  2368   // Since this is expensive to compute, we cache it so that a page with many
       
  2369   // comments only has to query the database once for all the links.
       
  2370   $authenticated_post_comments = &drupal_static(__FUNCTION__, NULL);
       
  2371 
       
  2372   if (!$user->uid) {
       
  2373     if (!isset($authenticated_post_comments)) {
       
  2374       // We only output a link if we are certain that users will get permission
       
  2375       // to post comments by logging in.
       
  2376       $comment_roles = user_roles(TRUE, 'post comments');
       
  2377       $authenticated_post_comments = isset($comment_roles[DRUPAL_AUTHENTICATED_RID]);
       
  2378     }
       
  2379 
       
  2380     if ($authenticated_post_comments) {
       
  2381       // We cannot use drupal_get_destination() because these links
       
  2382       // sometimes appear on /node and taxonomy listing pages.
       
  2383       if (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_SEPARATE_PAGE) {
       
  2384         $destination = array('destination' => "comment/reply/$node->nid#comment-form");
       
  2385       }
       
  2386       else {
       
  2387         $destination = array('destination' => "node/$node->nid#comment-form");
       
  2388       }
       
  2389 
       
  2390       if (variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)) {
       
  2391         // Users can register themselves.
       
  2392         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))));
       
  2393       }
       
  2394       else {
       
  2395         // Only admins can add new users, no public registration.
       
  2396         return t('<a href="@login">Log in</a> to post comments', array('@login' => url('user/login', array('query' => $destination))));
       
  2397       }
       
  2398     }
       
  2399   }
       
  2400 }
       
  2401 
       
  2402 /**
       
  2403  * Process variables for comment-wrapper.tpl.php.
       
  2404  *
       
  2405  * @see comment-wrapper.tpl.php
       
  2406  */
       
  2407 function template_preprocess_comment_wrapper(&$variables) {
       
  2408   // Provide contextual information.
       
  2409   $variables['node'] = $variables['content']['#node'];
       
  2410   $variables['display_mode'] = variable_get('comment_default_mode_' . $variables['node']->type, COMMENT_MODE_THREADED);
       
  2411   // The comment form is optional and may not exist.
       
  2412   $variables['content'] += array('comment_form' => array());
       
  2413 }
       
  2414 
       
  2415 /**
       
  2416  * Return an array of viewing modes for comment listings.
       
  2417  *
       
  2418  * We can't use a global variable array because the locale system
       
  2419  * is not initialized yet when the comment module is loaded.
       
  2420  */
       
  2421 function _comment_get_modes() {
       
  2422   return array(
       
  2423     COMMENT_MODE_FLAT => t('Flat list'),
       
  2424     COMMENT_MODE_THREADED => t('Threaded list')
       
  2425   );
       
  2426 }
       
  2427 
       
  2428 /**
       
  2429  * Return an array of "comments per page" settings from which the user
       
  2430  * can choose.
       
  2431  */
       
  2432 function _comment_per_page() {
       
  2433   return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300));
       
  2434 }
       
  2435 
       
  2436 /**
       
  2437  * Updates the comment statistics for a given node. This should be called any
       
  2438  * time a comment is added, deleted, or updated.
       
  2439  *
       
  2440  * The following fields are contained in the node_comment_statistics table.
       
  2441  * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node.
       
  2442  * - last_comment_name: the name of the anonymous poster for the last comment
       
  2443  * - 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.
       
  2444  * - comment_count: the total number of approved/published comments on this node.
       
  2445  */
       
  2446 function _comment_update_node_statistics($nid) {
       
  2447   // Allow bulk updates and inserts to temporarily disable the
       
  2448   // maintenance of the {node_comment_statistics} table.
       
  2449   if (!variable_get('comment_maintain_node_statistics', TRUE)) {
       
  2450     return;
       
  2451   }
       
  2452 
       
  2453   $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array(
       
  2454     ':nid' => $nid,
       
  2455     ':status' => COMMENT_PUBLISHED,
       
  2456   ))->fetchField();
       
  2457 
       
  2458   if ($count > 0) {
       
  2459     // Comments exist.
       
  2460     $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(
       
  2461       ':nid' => $nid,
       
  2462       ':status' => COMMENT_PUBLISHED,
       
  2463     ))->fetchObject();
       
  2464     db_update('node_comment_statistics')
       
  2465       ->fields(array(
       
  2466         'cid' => $last_reply->cid,
       
  2467         'comment_count' => $count,
       
  2468         'last_comment_timestamp' => $last_reply->changed,
       
  2469         'last_comment_name' => $last_reply->uid ? '' : $last_reply->name,
       
  2470         'last_comment_uid' => $last_reply->uid,
       
  2471       ))
       
  2472       ->condition('nid', $nid)
       
  2473       ->execute();
       
  2474   }
       
  2475   else {
       
  2476     // Comments do not exist.
       
  2477     $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject();
       
  2478     db_update('node_comment_statistics')
       
  2479       ->fields(array(
       
  2480         'cid' => 0,
       
  2481         'comment_count' => 0,
       
  2482         'last_comment_timestamp' => $node->created,
       
  2483         'last_comment_name' => '',
       
  2484         'last_comment_uid' => $node->uid,
       
  2485       ))
       
  2486       ->condition('nid', $nid)
       
  2487       ->execute();
       
  2488   }
       
  2489 }
       
  2490 
       
  2491 /**
       
  2492  * Generate vancode.
       
  2493  *
       
  2494  * Consists of a leading character indicating length, followed by N digits
       
  2495  * with a numerical value in base 36. Vancodes can be sorted as strings
       
  2496  * without messing up numerical order.
       
  2497  *
       
  2498  * It goes:
       
  2499  * 00, 01, 02, ..., 0y, 0z,
       
  2500  * 110, 111, ... , 1zy, 1zz,
       
  2501  * 2100, 2101, ..., 2zzy, 2zzz,
       
  2502  * 31000, 31001, ...
       
  2503  */
       
  2504 function int2vancode($i = 0) {
       
  2505   $num = base_convert((int) $i, 10, 36);
       
  2506   $length = strlen($num);
       
  2507 
       
  2508   return chr($length + ord('0') - 1) . $num;
       
  2509 }
       
  2510 
       
  2511 /**
       
  2512  * Decode vancode back to an integer.
       
  2513  */
       
  2514 function vancode2int($c = '00') {
       
  2515   return base_convert(substr($c, 1), 36, 10);
       
  2516 }
       
  2517 
       
  2518 /**
       
  2519  * Implements hook_action_info().
       
  2520  */
       
  2521 function comment_action_info() {
       
  2522   return array(
       
  2523     'comment_publish_action' => array(
       
  2524       'label' => t('Publish comment'),
       
  2525       'type' => 'comment',
       
  2526       'configurable' => FALSE,
       
  2527       'behavior' => array('changes_property'),
       
  2528       'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
       
  2529     ),
       
  2530     'comment_unpublish_action' => array(
       
  2531       'label' => t('Unpublish comment'),
       
  2532       'type' => 'comment',
       
  2533       'configurable' => FALSE,
       
  2534       'behavior' => array('changes_property'),
       
  2535       'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
       
  2536     ),
       
  2537     'comment_unpublish_by_keyword_action' => array(
       
  2538       'label' => t('Unpublish comment containing keyword(s)'),
       
  2539       'type' => 'comment',
       
  2540       'configurable' => TRUE,
       
  2541       'behavior' => array('changes_property'),
       
  2542       'triggers' => array('comment_presave', 'comment_insert', 'comment_update'),
       
  2543     ),
       
  2544     'comment_save_action' => array(
       
  2545       'label' => t('Save comment'),
       
  2546       'type' => 'comment',
       
  2547       'configurable' => FALSE,
       
  2548       'triggers' => array('comment_insert', 'comment_update'),
       
  2549     ),
       
  2550   );
       
  2551 }
       
  2552 
       
  2553 /**
       
  2554  * Publishes a comment.
       
  2555  *
       
  2556  * @param $comment
       
  2557  *   An optional comment object.
       
  2558  * @param array $context
       
  2559  *   Array with components:
       
  2560  *   - 'cid': Comment ID. Required if $comment is not given.
       
  2561  *
       
  2562  * @ingroup actions
       
  2563  */
       
  2564 function comment_publish_action($comment, $context = array()) {
       
  2565   if (isset($comment->subject)) {
       
  2566     $subject = $comment->subject;
       
  2567     $comment->status = COMMENT_PUBLISHED;
       
  2568   }
       
  2569   else {
       
  2570     $cid = $context['cid'];
       
  2571     $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
       
  2572     db_update('comment')
       
  2573       ->fields(array('status' => COMMENT_PUBLISHED))
       
  2574       ->condition('cid', $cid)
       
  2575       ->execute();
       
  2576   }
       
  2577   watchdog('action', 'Published comment %subject.', array('%subject' => $subject));
       
  2578 }
       
  2579 
       
  2580 /**
       
  2581  * Unpublishes a comment.
       
  2582  *
       
  2583  * @param $comment
       
  2584  *   An optional comment object.
       
  2585  * @param array $context
       
  2586  *   Array with components:
       
  2587  *   - 'cid': Comment ID. Required if $comment is not given.
       
  2588  *
       
  2589  * @ingroup actions
       
  2590  */
       
  2591 function comment_unpublish_action($comment, $context = array()) {
       
  2592   if (isset($comment->subject)) {
       
  2593     $subject = $comment->subject;
       
  2594     $comment->status = COMMENT_NOT_PUBLISHED;
       
  2595   }
       
  2596   else {
       
  2597     $cid = $context['cid'];
       
  2598     $subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
       
  2599     db_update('comment')
       
  2600       ->fields(array('status' => COMMENT_NOT_PUBLISHED))
       
  2601       ->condition('cid', $cid)
       
  2602       ->execute();
       
  2603   }
       
  2604   watchdog('action', 'Unpublished comment %subject.', array('%subject' => $subject));
       
  2605 }
       
  2606 
       
  2607 /**
       
  2608  * Unpublishes a comment if it contains certain keywords.
       
  2609  *
       
  2610  * @param object $comment
       
  2611  *   Comment object to modify.
       
  2612  * @param array $context
       
  2613  *   Array with components:
       
  2614  *   - 'keywords': Keywords to look for. If the comment contains at least one
       
  2615  *     of the keywords, it is unpublished.
       
  2616  *
       
  2617  * @ingroup actions
       
  2618  * @see comment_unpublish_by_keyword_action_form()
       
  2619  * @see comment_unpublish_by_keyword_action_submit()
       
  2620  */
       
  2621 function comment_unpublish_by_keyword_action($comment, $context) {
       
  2622   $node = node_load($comment->nid);
       
  2623   $build = comment_view($comment, $node);
       
  2624   $text = drupal_render($build);
       
  2625   foreach ($context['keywords'] as $keyword) {
       
  2626     if (strpos($text, $keyword) !== FALSE) {
       
  2627       $comment->status = COMMENT_NOT_PUBLISHED;
       
  2628       comment_save($comment);
       
  2629       watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject));
       
  2630       break;
       
  2631     }
       
  2632   }
       
  2633 }
       
  2634 
       
  2635 /**
       
  2636  * Form builder; Prepare a form for blacklisted keywords.
       
  2637  *
       
  2638  * @ingroup forms
       
  2639  * @see comment_unpublish_by_keyword_action()
       
  2640  * @see comment_unpublish_by_keyword_action_submit()
       
  2641  */
       
  2642 function comment_unpublish_by_keyword_action_form($context) {
       
  2643   $form['keywords'] = array(
       
  2644     '#title' => t('Keywords'),
       
  2645     '#type' => 'textarea',
       
  2646     '#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."'),
       
  2647     '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '',
       
  2648   );
       
  2649 
       
  2650   return $form;
       
  2651 }
       
  2652 
       
  2653 /**
       
  2654  * Process comment_unpublish_by_keyword_action_form form submissions.
       
  2655  *
       
  2656  * @see comment_unpublish_by_keyword_action()
       
  2657  */
       
  2658 function comment_unpublish_by_keyword_action_submit($form, $form_state) {
       
  2659   return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
       
  2660 }
       
  2661 
       
  2662 /**
       
  2663  * Saves a comment.
       
  2664  *
       
  2665  * @ingroup actions
       
  2666  */
       
  2667 function comment_save_action($comment) {
       
  2668   comment_save($comment);
       
  2669   cache_clear_all();
       
  2670   watchdog('action', 'Saved comment %title', array('%title' => $comment->subject));
       
  2671 }
       
  2672 
       
  2673 /**
       
  2674  * Implements hook_ranking().
       
  2675  */
       
  2676 function comment_ranking() {
       
  2677   return array(
       
  2678     'comments' => array(
       
  2679       'title' => t('Number of comments'),
       
  2680       'join' => array(
       
  2681         'type' => 'LEFT',
       
  2682         'table' => 'node_comment_statistics',
       
  2683         'alias' => 'node_comment_statistics',
       
  2684         'on' => 'node_comment_statistics.nid = i.sid',
       
  2685       ),
       
  2686       // Inverse law that maps the highest reply count on the site to 1 and 0 to 0.
       
  2687       'score' => '2.0 - 2.0 / (1.0 + node_comment_statistics.comment_count * CAST(:scale AS DECIMAL))',
       
  2688       'arguments' => array(':scale' => variable_get('node_cron_comments_scale', 0)),
       
  2689     ),
       
  2690   );
       
  2691 }
       
  2692 
       
  2693 /**
       
  2694  * Implements hook_rdf_mapping().
       
  2695  */
       
  2696 function comment_rdf_mapping() {
       
  2697   return array(
       
  2698     array(
       
  2699       'type' => 'comment',
       
  2700       'bundle' => RDF_DEFAULT_BUNDLE,
       
  2701       'mapping' => array(
       
  2702         'rdftype' => array('sioc:Post', 'sioct:Comment'),
       
  2703         'title' => array(
       
  2704           'predicates' => array('dc:title'),
       
  2705         ),
       
  2706         'created' => array(
       
  2707           'predicates' => array('dc:date', 'dc:created'),
       
  2708           'datatype' => 'xsd:dateTime',
       
  2709           'callback' => 'date_iso8601',
       
  2710         ),
       
  2711         'changed' => array(
       
  2712           'predicates' => array('dc:modified'),
       
  2713           'datatype' => 'xsd:dateTime',
       
  2714           'callback' => 'date_iso8601',
       
  2715         ),
       
  2716         'comment_body' => array(
       
  2717           'predicates' => array('content:encoded'),
       
  2718         ),
       
  2719         'pid' => array(
       
  2720           'predicates' => array('sioc:reply_of'),
       
  2721           'type' => 'rel',
       
  2722         ),
       
  2723         'uid' => array(
       
  2724           'predicates' => array('sioc:has_creator'),
       
  2725           'type' => 'rel',
       
  2726         ),
       
  2727         'name' => array(
       
  2728           'predicates' => array('foaf:name'),
       
  2729         ),
       
  2730       ),
       
  2731     ),
       
  2732   );
       
  2733 }
       
  2734 
       
  2735 /**
       
  2736  * Implements hook_file_download_access().
       
  2737  */
       
  2738 function comment_file_download_access($field, $entity_type, $entity) {
       
  2739   if ($entity_type == 'comment') {
       
  2740     if (user_access('access comments') && $entity->status == COMMENT_PUBLISHED || user_access('administer comments')) {
       
  2741       $node = node_load($entity->nid);
       
  2742       return node_access('view', $node);
       
  2743     }
       
  2744     return FALSE;
       
  2745   }
       
  2746 }