home *** CD-ROM | disk | FTP | other *** search
/ Cricao de Sites - 650 Layouts Prontos / WebMasters.iso / CMS / drupal-6.0.exe / drupal-6.0 / modules / comment / comment.module < prev    next >
Encoding:
Text File  |  2008-01-25  |  72.6 KB  |  2,103 lines

  1. <?php
  2. // $Id: comment.module,v 1.617 2008/01/25 16:19:12 goba Exp $
  3.  
  4. /**
  5.  * @file
  6.  * Enables users to comment on published content.
  7.  *
  8.  * When enabled, the Drupal comment module creates a discussion
  9.  * board for each Drupal node. Users can post comments to discuss
  10.  * a forum topic, weblog post, story, collaborative book page, etc.
  11.  */
  12.  
  13. /**
  14.  * Comment is published.
  15.  */
  16. define('COMMENT_PUBLISHED', 0);
  17.  
  18. /**
  19.  * Comment is awaiting approval.
  20.  */
  21. define('COMMENT_NOT_PUBLISHED', 1);
  22.  
  23. /**
  24.  * Comments are displayed in a flat list - collapsed.
  25.  */
  26. define('COMMENT_MODE_FLAT_COLLAPSED', 1);
  27.  
  28. /**
  29.  * Comments are displayed in a flat list - expanded.
  30.  */
  31. define('COMMENT_MODE_FLAT_EXPANDED', 2);
  32.  
  33. /**
  34.  * Comments are displayed as a threaded list - collapsed.
  35.  */
  36. define('COMMENT_MODE_THREADED_COLLAPSED', 3);
  37.  
  38. /**
  39.  * Comments are displayed as a threaded list - expanded.
  40.  */
  41. define('COMMENT_MODE_THREADED_EXPANDED', 4);
  42.  
  43. /**
  44.  * Comments are ordered by date - newest first.
  45.  */
  46. define('COMMENT_ORDER_NEWEST_FIRST', 1);
  47.  
  48. /**
  49.  * Comments are ordered by date - oldest first.
  50.  */
  51. define('COMMENT_ORDER_OLDEST_FIRST', 2);
  52.  
  53. /**
  54.  * Comment controls should be shown above the comment list.
  55.  */
  56. define('COMMENT_CONTROLS_ABOVE', 0);
  57.  
  58. /**
  59.  * Comment controls should be shown below the comment list.
  60.  */
  61. define('COMMENT_CONTROLS_BELOW', 1);
  62.  
  63. /**
  64.  * Comment controls should be shown both above and below the comment list.
  65.  */
  66. define('COMMENT_CONTROLS_ABOVE_BELOW', 2);
  67.  
  68. /**
  69.  * Comment controls are hidden.
  70.  */
  71. define('COMMENT_CONTROLS_HIDDEN', 3);
  72.  
  73. /**
  74.  * Anonymous posters may not enter their contact information.
  75.  */
  76. define('COMMENT_ANONYMOUS_MAYNOT_CONTACT', 0);
  77.  
  78. /**
  79.  * Anonymous posters may leave their contact information.
  80.  */
  81. define('COMMENT_ANONYMOUS_MAY_CONTACT', 1);
  82.  
  83. /**
  84.  * Anonymous posters must leave their contact information.
  85.  */
  86. define('COMMENT_ANONYMOUS_MUST_CONTACT', 2);
  87.  
  88. /**
  89.  * Comment form should be displayed on a separate page.
  90.  */
  91. define('COMMENT_FORM_SEPARATE_PAGE', 0);
  92.  
  93. /**
  94.  * Comment form should be shown below post or list of comments.
  95.  */
  96. define('COMMENT_FORM_BELOW', 1);
  97.  
  98. /**
  99.  * Comments for this node are disabled.
  100.  */
  101. define('COMMENT_NODE_DISABLED', 0);
  102.  
  103. /**
  104.  * Comments for this node are locked.
  105.  */
  106. define('COMMENT_NODE_READ_ONLY', 1);
  107.  
  108. /**
  109.  * Comments are enabled on this node.
  110.  */
  111. define('COMMENT_NODE_READ_WRITE', 2);
  112.  
  113. /**
  114.  * Comment preview is optional.
  115.  */
  116. define('COMMENT_PREVIEW_OPTIONAL', 0);
  117.  
  118. /**
  119.  * Comment preview is required.
  120.  */
  121. define('COMMENT_PREVIEW_REQUIRED', 1);
  122.  
  123. /**
  124.  * Implementation of hook_help().
  125.  */
  126. function comment_help($path, $arg) {
  127.   switch ($path) {
  128.     case 'admin/help#comment':
  129.       $output = '<p>'. t('The comment module allows visitors to comment on your posts, creating ad hoc discussion boards. Any <a href="@content-type">content type</a> may have its <em>Default comment setting</em> set to <em>Read/Write</em> to allow comments, or <em>Disabled</em>, to prevent comments. Comment display settings and other controls may also be customized for each content type (some display settings are customizable by individual users).', array('@content-type' => url('admin/content/types'))) .'</p>';
  130.       $output .= '<p>'. t('Comment permissions are assigned to user roles, and are used to determine whether anonymous users (or other roles) are allowed to comment on posts. If anonymous users are allowed to comment, their individual contact information may be retained in cookies stored on their local computer for use in later comment submissions. When a comment has no replies, it may be (optionally) edited by its author. The comment module uses the same input formats and HTML tags available when creating other forms of content.') .'</p>';
  131.       $output .= '<p>'. t('For more information, see the online handbook entry for <a href="@comment">Comment module</a>.', array('@comment' => 'http://drupal.org/handbook/modules/comment/')) .'</p>';
  132.       return $output;
  133.     case 'admin/content/comment':
  134.       return '<p>'. t("Below is a list of the latest comments posted to your site. Click on a subject to see the comment, the author's name to edit the author's user information, 'edit' to modify the text, and 'delete' to remove their submission.") .'</p>';
  135.     case 'admin/content/comment/approval':
  136.       return '<p>'. t("Below is a list of the comments posted to your site that need approval. To approve a comment, click on 'edit' and then change its 'moderation status' to Approved. Click on a subject to see the comment, the author's name to edit the author's user information, 'edit' to modify the text, and 'delete' to remove their submission.") .'</p>';
  137.   }
  138. }
  139.  
  140. /**
  141.  * Implementation of hook_theme().
  142.  */
  143. function comment_theme() {
  144.   return array(
  145.     'comment_block' => array(
  146.       'arguments' => array(),
  147.     ),
  148.     'comment_admin_overview' => array(
  149.       'arguments' => array('form' => NULL),
  150.     ),
  151.     'comment_preview' => array(
  152.       'arguments' => array('comment' => NULL, 'node' => NULL, 'links' => array(), 'visible' => 1),
  153.     ),
  154.     'comment_view' => array(
  155.       'arguments' => array('comment' => NULL, 'node' => NULL, 'links' => array(), 'visible' => 1),
  156.     ),
  157.     'comment_controls' => array(
  158.       'arguments' => array('form' => NULL),
  159.     ),
  160.     'comment' => array(
  161.       'template' => 'comment',
  162.       'arguments' => array('comment' => NULL, 'node' => NULL, 'links' => array()),
  163.     ),
  164.     'comment_folded' => array(
  165.       'template' => 'comment-folded',
  166.       'arguments' => array('comment' => NULL),
  167.     ),
  168.     'comment_flat_collapsed' => array(
  169.       'arguments' => array('comment' => NULL, 'node' => NULL),
  170.     ),
  171.     'comment_flat_expanded' => array(
  172.       'arguments' => array('comment' => NULL, 'node' => NULL),
  173.     ),
  174.     'comment_thread_collapsed' => array(
  175.       'arguments' => array('comment' => NULL, 'node' => NULL),
  176.     ),
  177.     'comment_thread_expanded' => array(
  178.       'arguments' => array('comment' => NULL, 'node' => NULL),
  179.     ),
  180.     'comment_post_forbidden' => array(
  181.       'arguments' => array('nid' => NULL),
  182.     ),
  183.     'comment_wrapper' => array(
  184.       'template' => 'comment-wrapper',
  185.       'arguments' => array('content' => NULL, 'node' => NULL),
  186.     ),
  187.     'comment_submitted' => array(
  188.       'arguments' => array('comment' => NULL),
  189.     ),
  190.   );
  191. }
  192.  
  193. /**
  194.  * Implementation of hook_menu().
  195.  */
  196. function comment_menu() {
  197.   $items['admin/content/comment'] = array(
  198.     'title' => 'Comments',
  199.     'description' => 'List and edit site comments and the comment moderation queue.',
  200.     'page callback' => 'comment_admin',
  201.     'access arguments' => array('administer comments'),
  202.     'file' => 'comment.admin.inc',
  203.   );
  204.  
  205.   // Tabs:
  206.   $items['admin/content/comment/new'] = array(
  207.     'title' => 'Published comments',
  208.     'type' => MENU_DEFAULT_LOCAL_TASK,
  209.     'weight' => -10,
  210.   );
  211.   $items['admin/content/comment/approval'] = array(
  212.     'title' => 'Approval queue',
  213.     'page arguments' => array('approval'),
  214.     'type' => MENU_LOCAL_TASK,
  215.     'file' => 'comment.admin.inc',
  216.   );
  217.  
  218.   $items['comment/delete'] = array(
  219.     'title' => 'Delete comment',
  220.     'page callback' => 'comment_delete',
  221.     'access arguments' => array('administer comments'),
  222.     'type' => MENU_CALLBACK,
  223.     'file' => 'comment.admin.inc',
  224.   );
  225.  
  226.   $items['comment/edit'] = array(
  227.     'title' => 'Edit comment',
  228.     'page callback' => 'comment_edit',
  229.     'access arguments' => array('post comments'),
  230.     'type' => MENU_CALLBACK,
  231.     'file' => 'comment.pages.inc',
  232.   );
  233.   $items['comment/reply/%node'] = array(
  234.     'title' => 'Reply to comment',
  235.     'page callback' => 'comment_reply',
  236.     'page arguments' => array(2),
  237.     'access callback' => 'node_access',
  238.     'access arguments' => array('view', 2),
  239.     'type' => MENU_CALLBACK,
  240.     'file' => 'comment.pages.inc',
  241.   );
  242.  
  243.   return $items;
  244. }
  245.  
  246. /**
  247.  * Implementation of hook_node_type().
  248.  */
  249. function comment_node_type($op, $info) {
  250.   $settings = array(
  251.     'comment',
  252.     'comment_default_mode',
  253.     'comment_default_order',
  254.     'comment_default_per_page',
  255.     'comment_controls',
  256.     'comment_anonymous',
  257.     'comment_subject_field',
  258.     'comment_preview',
  259.     'comment_form_location',
  260.   );
  261.   switch ($op) {
  262.     case 'delete':
  263.       foreach ($settings as $setting) {
  264.         variable_del($setting .'_'. $info->type);
  265.       }
  266.       break;
  267.   }
  268. }
  269.  
  270. /**
  271.  * Implementation of hook_perm().
  272.  */
  273. function comment_perm() {
  274.   return array('access comments', 'post comments', 'administer comments', 'post comments without approval');
  275. }
  276.  
  277. /**
  278.  * Implementation of hook_block().
  279.  *
  280.  * Generates a block with the most recent comments.
  281.  */
  282. function comment_block($op = 'list', $delta = 0) {
  283.   if ($op == 'list') {
  284.     $blocks[0]['info'] = t('Recent comments');
  285.     return $blocks;
  286.   }
  287.   else if ($op == 'view' && user_access('access comments')) {
  288.     $block['subject'] = t('Recent comments');
  289.     $block['content'] = theme('comment_block');
  290.     return $block;
  291.   }
  292. }
  293.  
  294. /**
  295.  * Find a number of recent comments. This is done in two steps.
  296.  *   1. Find the n (specified by $number) nodes that have the most recent
  297.  *      comments.  This is done by querying node_comment_statistics which has
  298.  *      an index on last_comment_timestamp, and is thus a fast query.
  299.  *   2. Loading the information from the comments table based on the nids found
  300.  *      in step 1.
  301.  *
  302.  * @param $number
  303.  *   (optional) The maximum number of comments to find.
  304.  * @return
  305.  *   An array of comment objects each containing a nid,
  306.  *   subject, cid, and timestamp, or an empty array if there are no recent
  307.  *   comments visible to the current user.
  308.  */
  309. function comment_get_recent($number = 10) {
  310.   // Select the $number nodes (visible to the current user) with the most
  311.   // recent comments. This is efficient due to the index on
  312.   // last_comment_timestamp.
  313.   $result = db_query_range(db_rewrite_sql("SELECT nc.nid FROM {node_comment_statistics} nc WHERE nc.comment_count > 0 ORDER BY nc.last_comment_timestamp DESC", 'nc'), 0, $number);
  314.  
  315.   $nids = array();
  316.   while ($row = db_fetch_object($result)) {
  317.     $nids[] = $row->nid;
  318.   }
  319.  
  320.   $comments = array();
  321.   if (!empty($nids)) {
  322.     // From among the comments on the nodes selected in the first query,
  323.     // find the $number most recent comments.
  324.     $result = db_query_range('SELECT c.nid, c.subject, c.cid, c.timestamp FROM {comments} c INNER JOIN {node} n ON n.nid = c.nid WHERE c.nid IN ('. implode(',', $nids) .') AND n.status = 1 AND c.status = %d ORDER BY c.cid DESC', COMMENT_PUBLISHED, 0, $number);
  325.     while ($comment = db_fetch_object($result)) {
  326.       $comments[] = $comment;
  327.     }
  328.   }
  329.  
  330.   return $comments;
  331. }
  332.  
  333. /**
  334.  * Calculate page number for first new comment.
  335.  *
  336.  * @param $num_comments
  337.  *   Number of comments.
  338.  * @param $new_replies
  339.  *   Number of new replies.
  340.  * @param $node
  341.  *   The first new comment node.
  342.  * @return
  343.  *   "page=X" if the page number is greater than zero; empty string otherwise.
  344.  */
  345. function comment_new_page_count($num_comments, $new_replies, $node) {
  346.   $comments_per_page = _comment_get_display_setting('comments_per_page', $node);
  347.   $mode = _comment_get_display_setting('mode', $node);
  348.   $order = _comment_get_display_setting('sort', $node);
  349.   $pagenum = NULL;
  350.   $flat = in_array($mode, array(COMMENT_MODE_FLAT_COLLAPSED, COMMENT_MODE_FLAT_EXPANDED));
  351.   if ($num_comments <= $comments_per_page || ($flat && $order == COMMENT_ORDER_NEWEST_FIRST)) {
  352.     // Only one page of comments or flat forum and newest first.
  353.     // First new comment will always be on first page.
  354.     $pageno = 0;
  355.   }
  356.   else {
  357.     if ($flat) {
  358.       // Flat comments and oldest first.
  359.       $count = $num_comments - $new_replies;
  360.     }
  361.     else {
  362.       // Threaded comments. See the documentation for comment_render().
  363.       if ($order == COMMENT_ORDER_NEWEST_FIRST) {
  364.         // Newest first: find the last thread with new comment
  365.         $result = db_query('(SELECT thread FROM {comments} WHERE nid = %d  AND status = 0 ORDER BY timestamp DESC LIMIT %d) ORDER BY thread DESC LIMIT 1', $node->nid, $new_replies);
  366.         $thread = db_result($result);
  367.         $result_count = db_query("SELECT COUNT(*) FROM {comments} WHERE nid = %d AND status = 0 AND thread > '". $thread ."'", $node->nid);
  368.       }
  369.       else {
  370.         // Oldest first: find the first thread with new comment
  371.         $result = db_query('(SELECT thread FROM {comments} WHERE nid = %d  AND status = 0 ORDER BY timestamp DESC LIMIT %d) ORDER BY SUBSTRING(thread, 1, (LENGTH(thread) - 1)) LIMIT 1', $node->nid, $new_replies);
  372.         $thread = substr(db_result($result), 0, -1);
  373.         $result_count = db_query("SELECT COUNT(*) FROM {comments} WHERE nid = %d AND status = 0 AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < '". $thread ."'", $node->nid);
  374.       }
  375.       $count = db_result($result_count);
  376.     }
  377.     $pageno =  $count / $comments_per_page;
  378.   }
  379.   if ($pageno >= 1) {
  380.     $pagenum = "page=". intval($pageno);
  381.   }
  382.   return $pagenum;
  383. }
  384.  
  385. /**
  386.  * Returns a formatted list of recent comments to be displayed in the comment block.
  387.  *
  388.  * @return
  389.  *   The comment list HTML.
  390.  * @ingroup themeable
  391.  */
  392. function theme_comment_block() {
  393.   $items = array();
  394.   foreach (comment_get_recent() as $comment) {
  395.     $items[] = l($comment->subject, 'node/'. $comment->nid, array('fragment' => 'comment-'. $comment->cid)) .'<br />'. t('@time ago', array('@time' => format_interval(time() - $comment->timestamp)));
  396.   }
  397.   if ($items) {
  398.     return theme('item_list', $items);
  399.   }
  400. }
  401.  
  402. /**
  403.  * Implementation of hook_link().
  404.  */
  405. function comment_link($type, $node = NULL, $teaser = FALSE) {
  406.   $links = array();
  407.  
  408.   if ($type == 'node' && $node->comment) {
  409.  
  410.     if ($teaser) {
  411.       // Main page: display the number of comments that have been posted.
  412.  
  413.       if (user_access('access comments')) {
  414.         $all = comment_num_all($node->nid);
  415.  
  416.         if ($all) {
  417.           $links['comment_comments'] = array(
  418.             'title' => format_plural($all, '1 comment', '@count comments'),
  419.             'href' => "node/$node->nid",
  420.             'attributes' => array('title' => t('Jump to the first comment of this posting.')),
  421.             'fragment' => 'comments'
  422.           );
  423.  
  424.           $new = comment_num_new($node->nid);
  425.  
  426.           if ($new) {
  427.             $links['comment_new_comments'] = array(
  428.               'title' => format_plural($new, '1 new comment', '@count new comments'),
  429.               'href' => "node/$node->nid",
  430.               'query' => comment_new_page_count($all, $new, $node),
  431.               'attributes' => array('title' => t('Jump to the first new comment of this posting.')),
  432.               'fragment' => 'new'
  433.             );
  434.           }
  435.         }
  436.         else {
  437.           if ($node->comment == COMMENT_NODE_READ_WRITE) {
  438.             if (user_access('post comments')) {
  439.               $links['comment_add'] = array(
  440.                 'title' => t('Add new comment'),
  441.                 'href' => "comment/reply/$node->nid",
  442.                 'attributes' => array('title' => t('Add a new comment to this page.')),
  443.                 'fragment' => 'comment-form'
  444.               );
  445.             }
  446.             else {
  447.               $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
  448.             }
  449.           }
  450.         }
  451.       }
  452.     }
  453.     else {
  454.       // Node page: add a "post comment" link if the user is allowed to
  455.       // post comments, if this node is not read-only, and if the comment form isn't already shown
  456.  
  457.       if ($node->comment == COMMENT_NODE_READ_WRITE) {
  458.         if (user_access('post comments')) {
  459.           if (variable_get('comment_form_location_'. $node->type, COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
  460.             $links['comment_add'] = array(
  461.               'title' => t('Add new comment'),
  462.               'href' => "comment/reply/$node->nid",
  463.               'attributes' => array('title' => t('Share your thoughts and opinions related to this posting.')),
  464.               'fragment' => 'comment-form'
  465.             );
  466.           }
  467.         }
  468.         else {
  469.           $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
  470.         }
  471.       }
  472.     }
  473.   }
  474.  
  475.   if ($type == 'comment') {
  476.     $links = comment_links($node, $teaser);
  477.   }
  478.   if (isset($links['comment_forbidden'])) {
  479.     $links['comment_forbidden']['html'] = TRUE;
  480.   }
  481.  
  482.   return $links;
  483. }
  484.  
  485. /**
  486.  * Implementation of hook_form_alter().
  487.  */
  488. function comment_form_alter(&$form, $form_state, $form_id) {
  489.   if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
  490.     $form['comment'] = array(
  491.       '#type' => 'fieldset',
  492.       '#title' => t('Comment settings'),
  493.       '#collapsible' => TRUE,
  494.       '#collapsed' => TRUE,
  495.     );
  496.     $form['comment']['comment'] = array(
  497.       '#type' => 'radios',
  498.       '#title' => t('Default comment setting'),
  499.       '#default_value' => variable_get('comment_'. $form['#node_type']->type, COMMENT_NODE_READ_WRITE),
  500.       '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')),
  501.       '#description' => t('Users with the <em>administer comments</em> permission will be able to override this setting.'),
  502.     );
  503.     $form['comment']['comment_default_mode'] = array(
  504.       '#type' => 'radios',
  505.       '#title' => t('Default display mode'),
  506.       '#default_value' => variable_get('comment_default_mode_'. $form['#node_type']->type, COMMENT_MODE_THREADED_EXPANDED),
  507.       '#options' => _comment_get_modes(),
  508.       '#description' => t('The default view for comments. Expanded views display the body of the comment. Threaded views keep replies together.'),
  509.     );
  510.     $form['comment']['comment_default_order'] = array(
  511.       '#type' => 'radios',
  512.       '#title' => t('Default display order'),
  513.       '#default_value' => variable_get('comment_default_order_'. $form['#node_type']->type, COMMENT_ORDER_NEWEST_FIRST),
  514.       '#options' => _comment_get_orders(),
  515.       '#description' => t('The default sorting for new users and anonymous users while viewing comments. These users may change their view using the comment control panel. For registered users, this change is remembered as a persistent user preference.'),
  516.     );
  517.     $form['comment']['comment_default_per_page'] = array(
  518.       '#type' => 'select',
  519.       '#title' => t('Default comments per page'),
  520.       '#default_value' => variable_get('comment_default_per_page_'. $form['#node_type']->type, 50),
  521.       '#options' => _comment_per_page(),
  522.       '#description' => t('Default number of comments for each page: more comments are distributed in several pages.'),
  523.     );
  524.     $form['comment']['comment_controls'] = array(
  525.       '#type' => 'radios',
  526.       '#title' => t('Comment controls'),
  527.       '#default_value' => variable_get('comment_controls_'. $form['#node_type']->type, COMMENT_CONTROLS_HIDDEN),
  528.       '#options' => array(
  529.         t('Display above the comments'),
  530.         t('Display below the comments'),
  531.         t('Display above and below the comments'),
  532.         t('Do not display')),
  533.       '#description' => t('Position of the comment controls box. The comment controls let the user change the default display mode and display order of comments.'),
  534.     );
  535.     $form['comment']['comment_anonymous'] = array(
  536.       '#type' => 'radios',
  537.       '#title' => t('Anonymous commenting'),
  538.       '#default_value' => variable_get('comment_anonymous_'. $form['#node_type']->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT),
  539.       '#options' => array(
  540.         COMMENT_ANONYMOUS_MAYNOT_CONTACT => t('Anonymous posters may not enter their contact information'),
  541.         COMMENT_ANONYMOUS_MAY_CONTACT => t('Anonymous posters may leave their contact information'),
  542.         COMMENT_ANONYMOUS_MUST_CONTACT => t('Anonymous posters must leave their contact information')),
  543.       '#description' => t('This option is enabled when anonymous users have permission to post comments on the <a href="@url">permissions page</a>.', array('@url' => url('admin/user/permissions', array('fragment' => 'module-comment')))),
  544.     );
  545.     if (!user_access('post comments', drupal_anonymous_user())) {
  546.       $form['comment']['comment_anonymous']['#disabled'] = TRUE;
  547.     }
  548.     $form['comment']['comment_subject_field'] = array(
  549.       '#type' => 'radios',
  550.       '#title' => t('Comment subject field'),
  551.       '#default_value' => variable_get('comment_subject_field_'. $form['#node_type']->type, 1),
  552.       '#options' => array(t('Disabled'), t('Enabled')),
  553.       '#description' => t('Can users provide a unique subject for their comments?'),
  554.     );
  555.     $form['comment']['comment_preview'] = array(
  556.       '#type' => 'radios',
  557.       '#title' => t('Preview comment'),
  558.       '#default_value' => variable_get('comment_preview_'. $form['#node_type']->type, COMMENT_PREVIEW_REQUIRED),
  559.       '#options' => array(t('Optional'), t('Required')),
  560.       '#description' => t("Forces a user to look at their comment by clicking on a 'Preview' button before they can actually add the comment"),
  561.     );
  562.     $form['comment']['comment_form_location'] = array(
  563.       '#type' => 'radios',
  564.       '#title' => t('Location of comment submission form'),
  565.       '#default_value' => variable_get('comment_form_location_'. $form['#node_type']->type, COMMENT_FORM_SEPARATE_PAGE),
  566.       '#options' => array(t('Display on separate page'), t('Display below post or comments')),
  567.     );
  568.   }
  569.   elseif (isset($form['type']) && isset($form['#node'])) {
  570.     if ($form['type']['#value'] .'_node_form' == $form_id) {
  571.       $node = $form['#node'];
  572.       $form['comment_settings'] = array(
  573.         '#type' => 'fieldset',
  574.         '#access' => user_access('administer comments'),
  575.         '#title' => t('Comment settings'),
  576.         '#collapsible' => TRUE,
  577.         '#collapsed' => TRUE,
  578.         '#weight' => 30,
  579.       );
  580.       $form['comment_settings']['comment'] = array(
  581.         '#type' => 'radios',
  582.         '#parents' => array('comment'),
  583.         '#default_value' => $node->comment,
  584.         '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')),
  585.       );
  586.     }
  587.   }
  588. }
  589.  
  590. /**
  591.  * Implementation of hook_nodeapi().
  592.  */
  593. function comment_nodeapi(&$node, $op, $arg = 0) {
  594.   switch ($op) {
  595.     case 'load':
  596.       return db_fetch_array(db_query("SELECT last_comment_timestamp, last_comment_name, comment_count FROM {node_comment_statistics} WHERE nid = %d", $node->nid));
  597.       break;
  598.  
  599.     case 'prepare':
  600.       if (!isset($node->comment)) {
  601.         $node->comment = variable_get("comment_$node->type", COMMENT_NODE_READ_WRITE);
  602.       }
  603.       break;
  604.  
  605.     case 'insert':
  606.       db_query('INSERT INTO {node_comment_statistics} (nid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count) VALUES (%d, %d, NULL, %d, 0)', $node->nid, $node->changed, $node->uid);
  607.       break;
  608.  
  609.     case 'delete':
  610.       db_query('DELETE FROM {comments} WHERE nid = %d', $node->nid);
  611.       db_query('DELETE FROM {node_comment_statistics} WHERE nid = %d', $node->nid);
  612.       break;
  613.  
  614.     case 'update index':
  615.       $text = '';
  616.       $comments = db_query('SELECT subject, comment, format FROM {comments} WHERE nid = %d AND status = %d', $node->nid, COMMENT_PUBLISHED);
  617.       while ($comment = db_fetch_object($comments)) {
  618.         $text .= '<h2>'. check_plain($comment->subject) .'</h2>'. check_markup($comment->comment, $comment->format, FALSE);
  619.       }
  620.       return $text;
  621.  
  622.     case 'search result':
  623.       $comments = db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $node->nid));
  624.       return format_plural($comments, '1 comment', '@count comments');
  625.  
  626.     case 'rss item':
  627.       if ($node->comment != COMMENT_NODE_DISABLED) {
  628.         return array(array('key' => 'comments', 'value' => url('node/'. $node->nid, array('fragment' => 'comments', 'absolute' => TRUE))));
  629.       }
  630.       else {
  631.         return array();
  632.       }
  633.   }
  634. }
  635.  
  636. /**
  637.  * Implementation of hook_user().
  638.  */
  639. function comment_user($type, $edit, &$user, $category = NULL) {
  640.   if ($type == 'delete') {
  641.     db_query('UPDATE {comments} SET uid = 0 WHERE uid = %d', $user->uid);
  642.     db_query('UPDATE {node_comment_statistics} SET last_comment_uid = 0 WHERE last_comment_uid = %d', $user->uid);
  643.   }
  644. }
  645.  
  646. /**
  647.  * This is *not* a hook_access() implementation. This function is called
  648.  * to determine whether the current user has access to a particular comment.
  649.  *
  650.  * Authenticated users can edit their comments as long they have not been
  651.  * replied to. This prevents people from changing or revising their
  652.  * statements based on the replies to their posts.
  653.  *
  654.  * @param $op
  655.  *   The operation that is to be performed on the comment. Only 'edit' is recognized now.
  656.  * @param $comment
  657.  *   The comment object.
  658.  * @return
  659.  *   TRUE if the current user has acces to the comment, FALSE otherwise.
  660.  */
  661. function comment_access($op, $comment) {
  662.   global $user;
  663.  
  664.   if ($op == 'edit') {
  665.     return ($user->uid && $user->uid == $comment->uid && comment_num_replies($comment->cid) == 0) || user_access('administer comments');
  666.   }
  667. }
  668.  
  669. /**
  670.  * A simple helper function.
  671.  *
  672.  * @return
  673.  *   The 0th and the 1st path components joined by a slash.
  674.  */
  675. function comment_node_url() {
  676.   return arg(0) .'/'. arg(1);
  677. }
  678.  
  679. /**
  680.  * Accepts a submission of new or changed comment content.
  681.  *
  682.  * @param $edit
  683.  *   A comment array.
  684.  *
  685.  * @return
  686.  *   If the comment is successfully saved the comment ID is returned. If the comment
  687.  *   is not saved, FALSE is returned.
  688.  */
  689. function comment_save($edit) {
  690.   global $user;
  691.   if (user_access('post comments') && (user_access('administer comments') || node_comment_mode($edit['nid']) == COMMENT_NODE_READ_WRITE)) {
  692.     if (!form_get_errors()) {
  693.       $edit += array(
  694.         'mail' => '',
  695.         'homepage' => '',
  696.         'name' => '',
  697.         'status' => user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED,
  698.       );
  699.       if ($edit['cid']) {
  700.         // Update the comment in the database.
  701.         db_query("UPDATE {comments} SET status = %d, timestamp = %d, subject = '%s', comment = '%s', format = %d, uid = %d, name = '%s', mail = '%s', homepage = '%s' WHERE cid = %d", $edit['status'], $edit['timestamp'], $edit['subject'], $edit['comment'], $edit['format'], $edit['uid'], $edit['name'], $edit['mail'], $edit['homepage'], $edit['cid']);
  702.  
  703.         // Allow modules to respond to the updating of a comment.
  704.         comment_invoke_comment($edit, 'update');
  705.  
  706.         // Add an entry to the watchdog log.
  707.         watchdog('content', 'Comment: updated %subject.', array('%subject' => $edit['subject']), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], array('fragment' => 'comment-'. $edit['cid'])));
  708.       }
  709.       else {
  710.         // Add the comment to database.
  711.         // Here we are building the thread field. See the documentation for
  712.         // comment_render().
  713.         if ($edit['pid'] == 0) {
  714.           // This is a comment with no parent comment (depth 0): we start
  715.           // by retrieving the maximum thread level.
  716.           $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $edit['nid']));
  717.  
  718.           // Strip the "/" from the end of the thread.
  719.           $max = rtrim($max, '/');
  720.  
  721.           // Finally, build the thread field for this new comment.
  722.           $thread = int2vancode(vancode2int($max) + 1) .'/';
  723.         }
  724.         else {
  725.           // This is comment with a parent comment: we increase
  726.           // the part of the thread value at the proper depth.
  727.  
  728.           // Get the parent comment:
  729.           $parent = _comment_load($edit['pid']);
  730.  
  731.           // Strip the "/" from the end of the parent thread.
  732.           $parent->thread = (string) rtrim((string) $parent->thread, '/');
  733.  
  734.           // Get the max value in _this_ thread.
  735.           $max = db_result(db_query("SELECT MAX(thread) FROM {comments} WHERE thread LIKE '%s.%%' AND nid = %d", $parent->thread, $edit['nid']));
  736.  
  737.           if ($max == '') {
  738.             // First child of this parent.
  739.             $thread = $parent->thread .'.'. int2vancode(0) .'/';
  740.           }
  741.           else {
  742.             // Strip the "/" at the end of the thread.
  743.             $max = rtrim($max, '/');
  744.  
  745.             // We need to get the value at the correct depth.
  746.             $parts = explode('.', $max);
  747.             $parent_depth = count(explode('.', $parent->thread));
  748.             $last = $parts[$parent_depth];
  749.  
  750.             // Finally, build the thread field for this new comment.
  751.             $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
  752.           }
  753.         }
  754.  
  755.         $edit['timestamp'] = time();
  756.  
  757.         if ($edit['uid'] === $user->uid) { // '===' because we want to modify anonymous users too
  758.           $edit['name'] = $user->name;
  759.         }
  760.  
  761.         db_query("INSERT INTO {comments} (nid, pid, uid, subject, comment, format, hostname, timestamp, status, thread, name, mail, homepage) VALUES (%d, %d, %d, '%s', '%s', %d, '%s', %d, %d, '%s', '%s', '%s', '%s')", $edit['nid'], $edit['pid'], $edit['uid'], $edit['subject'], $edit['comment'], $edit['format'], ip_address(), $edit['timestamp'], $edit['status'], $thread, $edit['name'], $edit['mail'], $edit['homepage']);
  762.         $edit['cid'] = db_last_insert_id('comments', 'cid');
  763.  
  764.         // Tell the other modules a new comment has been submitted.
  765.         comment_invoke_comment($edit, 'insert');
  766.  
  767.         // Add an entry to the watchdog log.
  768.         watchdog('content', 'Comment: added %subject.', array('%subject' => $edit['subject']), WATCHDOG_NOTICE, l(t('view'), 'node/'. $edit['nid'], array('fragment' => 'comment-'. $edit['cid'])));
  769.       }
  770.       _comment_update_node_statistics($edit['nid']);
  771.  
  772.       // Clear the cache so an anonymous user can see his comment being added.
  773.       cache_clear_all();
  774.  
  775.       // Explain the approval queue if necessary, and then
  776.       // redirect the user to the node he's commenting on.
  777.       if ($edit['status'] == COMMENT_NOT_PUBLISHED) {
  778.         drupal_set_message(t('Your comment has been queued for moderation by site administrators and will be published after approval.'));
  779.       }
  780.       else {
  781.         comment_invoke_comment($edit, 'publish');
  782.       }
  783.       return $edit['cid'];
  784.     }
  785.     else {
  786.       return FALSE;
  787.     }
  788.   }
  789.   else {
  790.     watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $edit['subject']), WATCHDOG_WARNING);
  791.     drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $edit['subject'])), 'error');
  792.     return FALSE;
  793.   }
  794. }
  795.  
  796. /**
  797.  * Build command links for a comment (e.g.\ edit, reply, delete) with respect to the current user's access permissions.
  798.  *
  799.  * @param $comment
  800.  *   The comment to which the links will be related.
  801.  * @param $return
  802.  *   Not used.
  803.  * @return
  804.  *   An associative array containing the links.
  805.  */
  806. function comment_links($comment, $return = 1) {
  807.   global $user;
  808.  
  809.   $links = array();
  810.  
  811.   // If we are viewing just this comment, we link back to the node.
  812.   if ($return) {
  813.     $links['comment_parent'] = array(
  814.       'title' => t('parent'),
  815.       'href' => comment_node_url(),
  816.       'fragment' => "comment-$comment->cid"
  817.     );
  818.   }
  819.  
  820.   if (node_comment_mode($comment->nid) == COMMENT_NODE_READ_WRITE) {
  821.     if (user_access('administer comments') && user_access('post comments')) {
  822.       $links['comment_delete'] = array(
  823.         'title' => t('delete'),
  824.         'href' => "comment/delete/$comment->cid"
  825.       );
  826.       $links['comment_edit'] = array(
  827.         'title' => t('edit'),
  828.         'href' => "comment/edit/$comment->cid"
  829.       );
  830.       $links['comment_reply'] = array(
  831.         'title' => t('reply'),
  832.         'href' => "comment/reply/$comment->nid/$comment->cid"
  833.       );
  834.     }
  835.     else if (user_access('post comments')) {
  836.       if (comment_access('edit', $comment)) {
  837.         $links['comment_edit'] = array(
  838.           'title' => t('edit'),
  839.           'href' => "comment/edit/$comment->cid"
  840.         );
  841.       }
  842.       $links['comment_reply'] = array(
  843.         'title' => t('reply'),
  844.         'href' => "comment/reply/$comment->nid/$comment->cid"
  845.       );
  846.     }
  847.     else {
  848.       $node = node_load($comment->nid);
  849.       $links['comment_forbidden']['title'] = theme('comment_post_forbidden', $node);
  850.     }
  851.   }
  852.  
  853.   return $links;
  854. }
  855.  
  856. /**
  857.  * Renders comment(s).
  858.  *
  859.  * @param $node
  860.  *   The node which comment(s) needs rendering.
  861.  * @param $cid
  862.  *   Optional, if given, only one comment is rendered.
  863.  *
  864.  * To display threaded comments in the correct order we keep a 'thread' field
  865.  * and order by that value. This field keeps this data in
  866.  * a way which is easy to update and convenient to use.
  867.  *
  868.  * A "thread" value starts at "1". If we add a child (A) to this comment,
  869.  * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
  870.  * brother of (A) will get "1.2". Next brother of the parent of (A) will get
  871.  * "2" and so on.
  872.  *
  873.  * First of all note that the thread field stores the depth of the comment:
  874.  * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
  875.  *
  876.  * Now to get the ordering right, consider this example:
  877.  *
  878.  * 1
  879.  * 1.1
  880.  * 1.1.1
  881.  * 1.2
  882.  * 2
  883.  *
  884.  * If we "ORDER BY thread ASC" we get the above result, and this is the
  885.  * natural order sorted by time. However, if we "ORDER BY thread DESC"
  886.  * we get:
  887.  *
  888.  * 2
  889.  * 1.2
  890.  * 1.1.1
  891.  * 1.1
  892.  * 1
  893.  *
  894.  * Clearly, this is not a natural way to see a thread, and users will get
  895.  * confused. The natural order to show a thread by time desc would be:
  896.  *
  897.  * 2
  898.  * 1
  899.  * 1.2
  900.  * 1.1
  901.  * 1.1.1
  902.  *
  903.  * which is what we already did before the standard pager patch. To achieve
  904.  * this we simply add a "/" at the end of each "thread" value. This way out
  905.  * thread fields will look like depicted below:
  906.  *
  907.  * 1/
  908.  * 1.1/
  909.  * 1.1.1/
  910.  * 1.2/
  911.  * 2/
  912.  *
  913.  * we add "/" since this char is, in ASCII, higher than every number, so if
  914.  * now we "ORDER BY thread DESC" we get the correct order. However this would
  915.  * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
  916.  * to consider the trailing "/" so we use a substring only.
  917.  */
  918. function comment_render($node, $cid = 0) {
  919.   global $user;
  920.  
  921.   $output = '';
  922.  
  923.   if (user_access('access comments')) {
  924.     // Pre-process variables.
  925.     $nid = $node->nid;
  926.     if (empty($nid)) {
  927.       $nid = 0;
  928.     }
  929.  
  930.     $mode = _comment_get_display_setting('mode', $node);
  931.     $order = _comment_get_display_setting('sort', $node);
  932.     $comments_per_page = _comment_get_display_setting('comments_per_page', $node);
  933.  
  934.     if ($cid && is_numeric($cid)) {
  935.       // Single comment view.
  936.       $query = 'SELECT c.cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.picture, u.data, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d';
  937.       $query_args = array($cid);
  938.       if (!user_access('administer comments')) {
  939.         $query .= ' AND c.status = %d';
  940.         $query_args[] = COMMENT_PUBLISHED;
  941.       }
  942.  
  943.       $query = db_rewrite_sql($query, 'c', 'cid');
  944.       $result = db_query($query, $query_args);
  945.  
  946.       if ($comment = db_fetch_object($result)) {
  947.         $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
  948.         $links = module_invoke_all('link', 'comment', $comment, 1);
  949.         drupal_alter('link', $links, $node);
  950.  
  951.         $output .= theme('comment_view', $comment, $node, $links);
  952.       }
  953.     }
  954.     else {
  955.       // Multiple comment view
  956.       $query_count = 'SELECT COUNT(*) FROM {comments} WHERE nid = %d';
  957.       $query = 'SELECT c.cid as cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.picture, u.data, c.thread, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.nid = %d';
  958.  
  959.       $query_args = array($nid);
  960.       if (!user_access('administer comments')) {
  961.         $query .= ' AND c.status = %d';
  962.         $query_count .= ' AND status = %d';
  963.         $query_args[] = COMMENT_PUBLISHED;
  964.       }
  965.  
  966.       if ($order == COMMENT_ORDER_NEWEST_FIRST) {
  967.         if ($mode == COMMENT_MODE_FLAT_COLLAPSED || $mode == COMMENT_MODE_FLAT_EXPANDED) {
  968.           $query .= ' ORDER BY c.cid DESC';
  969.         }
  970.         else {
  971.           $query .= ' ORDER BY c.thread DESC';
  972.         }
  973.       }
  974.       else if ($order == COMMENT_ORDER_OLDEST_FIRST) {
  975.         if ($mode == COMMENT_MODE_FLAT_COLLAPSED || $mode == COMMENT_MODE_FLAT_EXPANDED) {
  976.           $query .= ' ORDER BY c.cid';
  977.         }
  978.         else {
  979.           // See comment above. Analysis reveals that this doesn't cost too
  980.           // much. It scales much much better than having the whole comment
  981.           // structure.
  982.           $query .= ' ORDER BY SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))';
  983.         }
  984.       }
  985.       $query = db_rewrite_sql($query, 'c', 'cid');
  986.       $query_count = db_rewrite_sql($query_count, 'c', 'cid');
  987.  
  988.       // Start a form, for use with comment control.
  989.       $result = pager_query($query, $comments_per_page, 0, $query_count, $query_args);
  990.  
  991.       $divs = 0;
  992.       $num_rows = FALSE;
  993.       $comments = '';
  994.       drupal_add_css(drupal_get_path('module', 'comment') .'/comment.css');
  995.       while ($comment = db_fetch_object($result)) {
  996.         $comment = drupal_unpack($comment);
  997.         $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
  998.         $comment->depth = count(explode('.', $comment->thread)) - 1;
  999.  
  1000.         if ($mode == COMMENT_MODE_THREADED_COLLAPSED || $mode == COMMENT_MODE_THREADED_EXPANDED) {
  1001.           if ($comment->depth > $divs) {
  1002.             $divs++;
  1003.             $comments .= '<div class="indented">';
  1004.           }
  1005.           else {
  1006.             while ($comment->depth < $divs) {
  1007.               $divs--;
  1008.               $comments .= '</div>';
  1009.             }
  1010.           }
  1011.         }
  1012.  
  1013.         if ($mode == COMMENT_MODE_FLAT_COLLAPSED) {
  1014.           $comments .= theme('comment_flat_collapsed', $comment, $node);
  1015.         }
  1016.         else if ($mode == COMMENT_MODE_FLAT_EXPANDED) {
  1017.           $comments .= theme('comment_flat_expanded', $comment, $node);
  1018.         }
  1019.         else if ($mode == COMMENT_MODE_THREADED_COLLAPSED) {
  1020.           $comments .= theme('comment_thread_collapsed', $comment, $node);
  1021.         }
  1022.         else if ($mode == COMMENT_MODE_THREADED_EXPANDED) {
  1023.           $comments .= theme('comment_thread_expanded', $comment, $node);
  1024.         }
  1025.  
  1026.         $num_rows = TRUE;
  1027.       }
  1028.       while ($divs-- > 0) {
  1029.         $comments .= '</div>';
  1030.       }
  1031.  
  1032.       $comment_controls = variable_get('comment_controls_'. $node->type, COMMENT_CONTROLS_HIDDEN);
  1033.       if ($num_rows && ($comment_controls == COMMENT_CONTROLS_ABOVE || $comment_controls == COMMENT_CONTROLS_ABOVE_BELOW)) {
  1034.         $output .= drupal_get_form('comment_controls', $mode, $order, $comments_per_page);
  1035.       }
  1036.  
  1037.       $output .= $comments;
  1038.       $output .= theme('pager', NULL, $comments_per_page, 0);
  1039.  
  1040.       if ($num_rows && ($comment_controls == COMMENT_CONTROLS_BELOW || $comment_controls == COMMENT_CONTROLS_ABOVE_BELOW)) {
  1041.         $output .= drupal_get_form('comment_controls', $mode, $order, $comments_per_page);
  1042.       }
  1043.     }
  1044.  
  1045.     // If enabled, show new comment form if it's not already being displayed.
  1046.     $reply = arg(0) == 'comment' && arg(1) == 'reply';
  1047.     if (user_access('post comments') && node_comment_mode($nid) == COMMENT_NODE_READ_WRITE && (variable_get('comment_form_location_'. $node->type, COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_BELOW) && !$reply) {
  1048.       $output .= comment_form_box(array('nid' => $nid), t('Post new comment'));
  1049.     }
  1050.  
  1051.     $output = theme('comment_wrapper', $output, $node);
  1052.   }
  1053.  
  1054.   return $output;
  1055. }
  1056.  
  1057. /**
  1058.  * Comment operations. We offer different update operations depending on
  1059.  * which comment administration page we're on.
  1060.  *
  1061.  * @param $action
  1062.  *   The comment administration page.
  1063.  * @return
  1064.  *   An associative array containing the offered operations.
  1065.  */
  1066. function comment_operations($action = NULL) {
  1067.   if ($action == 'publish') {
  1068.     $operations = array(
  1069.       'publish' => array(t('Publish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_PUBLISHED .' WHERE cid = %d'),
  1070.       'delete' => array(t('Delete the selected comments'), '')
  1071.     );
  1072.   }
  1073.   else if ($action == 'unpublish') {
  1074.     $operations = array(
  1075.       'unpublish' => array(t('Unpublish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_NOT_PUBLISHED .' WHERE cid = %d'),
  1076.       'delete' => array(t('Delete the selected comments'), '')
  1077.     );
  1078.   }
  1079.   else {
  1080.     $operations = array(
  1081.       'publish' => array(t('Publish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_PUBLISHED .' WHERE cid = %d'),
  1082.       'unpublish' => array(t('Unpublish the selected comments'), 'UPDATE {comments} SET status = '. COMMENT_NOT_PUBLISHED .' WHERE cid = %d'),
  1083.       'delete' => array(t('Delete the selected comments'), '')
  1084.     );
  1085.   }
  1086.   return $operations;
  1087. }
  1088.  
  1089. /**
  1090.  * Misc functions: helpers, privates, history
  1091.  */
  1092.  
  1093. /**
  1094.  * Load the entire comment by cid.
  1095.  *
  1096.  * @param $cid
  1097.  *   The identifying comment id.
  1098.  * @return
  1099.  *   The comment object.
  1100.  */
  1101. function _comment_load($cid) {
  1102.   return db_fetch_object(db_query('SELECT * FROM {comments} WHERE cid = %d', $cid));
  1103. }
  1104.  
  1105. /**
  1106.  * Get comment count for a node.
  1107.  *
  1108.  * @param $nid
  1109.  *   The node id.
  1110.  * @return
  1111.  *   The comment count.
  1112.  */
  1113. function comment_num_all($nid) {
  1114.   static $cache;
  1115.  
  1116.   if (!isset($cache[$nid])) {
  1117.     $cache[$nid] = db_result(db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = %d', $nid));
  1118.   }
  1119.   return $cache[$nid];
  1120. }
  1121.  
  1122. /**
  1123.  * Get replies count for a comment.
  1124.  *
  1125.  * @param $pid
  1126.  *   The comment id.
  1127.  * @return
  1128.  *   The replies count.
  1129.  */
  1130. function comment_num_replies($pid) {
  1131.   static $cache;
  1132.  
  1133.   if (!isset($cache[$pid])) {
  1134.     $cache[$pid] = db_result(db_query('SELECT COUNT(cid) FROM {comments} WHERE pid = %d AND status = %d', $pid, COMMENT_PUBLISHED));
  1135.   }
  1136.  
  1137.   return $cache[$pid];
  1138. }
  1139.  
  1140. /**
  1141.  * Get number of new comments for current user and specified node.
  1142.  *
  1143.  * @param $nid
  1144.  *   node-id to count comments for
  1145.  * @param $timestamp
  1146.  *   time to count from (defaults to time of last user access
  1147.  *   to node)
  1148.  */
  1149. function comment_num_new($nid, $timestamp = 0) {
  1150.   global $user;
  1151.  
  1152.   if ($user->uid) {
  1153.     // Retrieve the timestamp at which the current user last viewed the
  1154.     // specified node.
  1155.     if (!$timestamp) {
  1156.       $timestamp = node_last_viewed($nid);
  1157.     }
  1158.     $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT);
  1159.  
  1160.     // Use the timestamp to retrieve the number of new comments.
  1161.     $result = db_result(db_query('SELECT COUNT(c.cid) FROM {node} n INNER JOIN {comments} c ON n.nid = c.nid WHERE n.nid = %d AND timestamp > %d AND c.status = %d', $nid, $timestamp, COMMENT_PUBLISHED));
  1162.  
  1163.     return $result;
  1164.   }
  1165.   else {
  1166.     return 0;
  1167.   }
  1168.  
  1169. }
  1170.  
  1171. /**
  1172.  * Validate comment data.
  1173.  *
  1174.  * @param $edit
  1175.  *   An associative array containig the comment data.
  1176.  * @return
  1177.  *   The original $edit.
  1178.  */
  1179. function comment_validate($edit) {
  1180.   global $user;
  1181.  
  1182.   // Invoke other validation handlers
  1183.   comment_invoke_comment($edit, 'validate');
  1184.  
  1185.   if (isset($edit['date'])) {
  1186.     // As of PHP 5.1.0, strtotime returns FALSE upon failure instead of -1.
  1187.     if (strtotime($edit['date']) <= 0) {
  1188.       form_set_error('date', t('You have to specify a valid date.'));
  1189.     }
  1190.   }
  1191.   if (isset($edit['author']) && !$account = user_load(array('name' => $edit['author']))) {
  1192.     form_set_error('author', t('You have to specify a valid author.'));
  1193.   }
  1194.  
  1195.   // Check validity of name, mail and homepage (if given)
  1196.   if (!$user->uid || isset($edit['is_anonymous'])) {
  1197.     $node = node_load($edit['nid']);
  1198.     if (variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) > COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
  1199.       if ($edit['name']) {
  1200.         $taken = db_result(db_query("SELECT COUNT(uid) FROM {users} WHERE LOWER(name) = '%s'", $edit['name']));
  1201.  
  1202.         if ($taken != 0) {
  1203.           form_set_error('name', t('The name you used belongs to a registered user.'));
  1204.         }
  1205.  
  1206.       }
  1207.       else if (variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
  1208.         form_set_error('name', t('You have to leave your name.'));
  1209.       }
  1210.  
  1211.       if ($edit['mail']) {
  1212.         if (!valid_email_address($edit['mail'])) {
  1213.           form_set_error('mail', t('The e-mail address you specified is not valid.'));
  1214.         }
  1215.       }
  1216.       else if (variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
  1217.         form_set_error('mail', t('You have to leave an e-mail address.'));
  1218.       }
  1219.  
  1220.       if ($edit['homepage']) {
  1221.         if (!valid_url($edit['homepage'], TRUE)) {
  1222.           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>.'));
  1223.         }
  1224.       }
  1225.     }
  1226.   }
  1227.  
  1228.   return $edit;
  1229. }
  1230.  
  1231. /**
  1232.  * Generate the basic commenting form, for appending to a node or display on a separate page.
  1233.  *
  1234.  * @param $title
  1235.  *   Not used.
  1236.  * @ingroup forms
  1237.  * @see comment_form_validate()
  1238.  * @see comment_form_submit()
  1239.  */
  1240. function comment_form(&$form_state, $edit, $title = NULL) {
  1241.   global $user;
  1242.  
  1243.   $op = isset($_POST['op']) ? $_POST['op'] : '';
  1244.   $node = node_load($edit['nid']);
  1245.  
  1246.   if (!$user->uid && variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
  1247.     drupal_add_js(drupal_get_path('module', 'comment') .'/comment.js');
  1248.   }
  1249.   $edit += array('name' => '', 'mail' => '', 'homepage' => '');
  1250.   if ($user->uid) {
  1251.     if (!empty($edit['cid']) && user_access('administer comments')) {
  1252.       if (!empty($edit['author'])) {
  1253.         $author = $edit['author'];
  1254.       }
  1255.       elseif (!empty($edit['name'])) {
  1256.         $author = $edit['name'];
  1257.       }
  1258.       else {
  1259.         $author = $edit['registered_name'];
  1260.       }
  1261.  
  1262.       if (!empty($edit['status'])) {
  1263.         $status = $edit['status'];
  1264.       }
  1265.       else {
  1266.         $status = 0;
  1267.       }
  1268.  
  1269.       if (!empty($edit['date'])) {
  1270.         $date = $edit['date'];
  1271.       }
  1272.       else {
  1273.         $date = format_date($edit['timestamp'], 'custom', 'Y-m-d H:i O');
  1274.       }
  1275.  
  1276.       $form['admin'] = array(
  1277.         '#type' => 'fieldset',
  1278.         '#title' => t('Administration'),
  1279.         '#collapsible' => TRUE,
  1280.         '#collapsed' => TRUE,
  1281.         '#weight' => -2,
  1282.       );
  1283.  
  1284.       if ($edit['registered_name'] != '') {
  1285.         // The comment is by a registered user
  1286.         $form['admin']['author'] = array(
  1287.           '#type' => 'textfield',
  1288.           '#title' => t('Authored by'),
  1289.           '#size' => 30,
  1290.           '#maxlength' => 60,
  1291.           '#autocomplete_path' => 'user/autocomplete',
  1292.           '#default_value' => $author,
  1293.           '#weight' => -1,
  1294.         );
  1295.       }
  1296.       else {
  1297.         // The comment is by an anonymous user
  1298.         $form['is_anonymous'] = array(
  1299.           '#type' => 'value',
  1300.           '#value' => TRUE,
  1301.         );
  1302.         $form['admin']['name'] = array(
  1303.           '#type' => 'textfield',
  1304.           '#title' => t('Authored by'),
  1305.           '#size' => 30,
  1306.           '#maxlength' => 60,
  1307.           '#default_value' => $author,
  1308.           '#weight' => -1,
  1309.         );
  1310.         $form['admin']['mail'] = array(
  1311.           '#type' => 'textfield',
  1312.           '#title' => t('E-mail'),
  1313.           '#maxlength' => 64,
  1314.           '#size' => 30,
  1315.           '#default_value' => $edit['mail'],
  1316.           '#description' => t('The content of this field is kept private and will not be shown publicly.'),
  1317.         );
  1318.  
  1319.         $form['admin']['homepage'] = array(
  1320.           '#type' => 'textfield',
  1321.           '#title' => t('Homepage'),
  1322.           '#maxlength' => 255,
  1323.           '#size' => 30,
  1324.           '#default_value' => $edit['homepage'],
  1325.         );
  1326.       }
  1327.  
  1328.       $form['admin']['date'] = array('#type' => 'textfield', '#parents' => array('date'), '#title' => t('Authored on'), '#size' => 20, '#maxlength' => 25, '#default_value' => $date, '#weight' => -1);
  1329.  
  1330.       $form['admin']['status'] = array('#type' => 'radios', '#parents' => array('status'), '#title' => t('Status'), '#default_value' =>  $status, '#options' => array(t('Published'), t('Not published')), '#weight' => -1);
  1331.  
  1332.     }
  1333.     else {
  1334.       $form['_author'] = array('#type' => 'item', '#title' => t('Your name'), '#value' => theme('username', $user)
  1335.       );
  1336.       $form['author'] = array('#type' => 'value', '#value' => $user->name);
  1337.     }
  1338.   }
  1339.   else if (variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MAY_CONTACT) {
  1340.     $form['name'] = array('#type' => 'textfield', '#title' => t('Your name'), '#maxlength' => 60, '#size' => 30, '#default_value' => $edit['name'] ? $edit['name'] : variable_get('anonymous', t('Anonymous'))
  1341.     );
  1342.  
  1343.     $form['mail'] = array('#type' => 'textfield', '#title' => t('E-mail'), '#maxlength' => 64, '#size' => 30, '#default_value' => $edit['mail'], '#description' => t('The content of this field is kept private and will not be shown publicly.')
  1344.     );
  1345.  
  1346.     $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
  1347.   }
  1348.   else if (variable_get('comment_anonymous_'. $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
  1349.     $form['name'] = array('#type' => 'textfield', '#title' => t('Your name'), '#maxlength' => 60, '#size' => 30, '#default_value' => $edit['name'] ? $edit['name'] : variable_get('anonymous', t('Anonymous')), '#required' => TRUE);
  1350.  
  1351.     $form['mail'] = array('#type' => 'textfield', '#title' => t('E-mail'), '#maxlength' => 64, '#size' => 30, '#default_value' => $edit['mail'], '#description' => t('The content of this field is kept private and will not be shown publicly.'), '#required' => TRUE);
  1352.  
  1353.     $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
  1354.   }
  1355.  
  1356.   if (variable_get('comment_subject_field_'. $node->type, 1) == 1) {
  1357.     $form['subject'] = array('#type' => 'textfield', '#title' => t('Subject'), '#maxlength' => 64, '#default_value' => !empty($edit['subject']) ? $edit['subject'] : '');
  1358.   }
  1359.  
  1360.   if (!empty($edit['comment'])) {
  1361.     $default = $edit['comment'];
  1362.   }
  1363.   else {
  1364.     $default = '';
  1365.   }
  1366.  
  1367.   $form['comment_filter']['comment'] = array(
  1368.     '#type' => 'textarea',
  1369.     '#title' => t('Comment'),
  1370.     '#rows' => 15,
  1371.     '#default_value' => $default,
  1372.     '#required' => TRUE,
  1373.   );
  1374.   if (!isset($edit['format'])) {
  1375.     $edit['format'] = FILTER_FORMAT_DEFAULT;
  1376.   }
  1377.   $form['comment_filter']['format'] = filter_form($edit['format']);
  1378.  
  1379.   $form['cid'] = array('#type' => 'value', '#value' => !empty($edit['cid']) ? $edit['cid'] : NULL);
  1380.   $form['pid'] = array('#type' => 'value', '#value' => !empty($edit['pid']) ? $edit['pid'] : NULL);
  1381.   $form['nid'] = array('#type' => 'value', '#value' => $edit['nid']);
  1382.   $form['uid'] = array('#type' => 'value', '#value' => !empty($edit['uid']) ? $edit['uid'] : NULL);
  1383.  
  1384.   // Only show save button if preview is optional or if we are in preview mode.
  1385.   // We show the save button in preview mode even if there are form errors so that
  1386.   // optional form elements (e.g., captcha) can be updated in preview mode.
  1387.   if (!form_get_errors() && ((variable_get('comment_preview_'. $node->type, COMMENT_PREVIEW_REQUIRED) == COMMENT_PREVIEW_OPTIONAL) || ($op == t('Preview')) || ($op == t('Save')))) {
  1388.     $form['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#weight' => 19);
  1389.   }
  1390.  
  1391.   $form['preview'] = array('#type' => 'button', '#value' => t('Preview'), '#weight' => 20);
  1392.   $form['#token'] = 'comment'. $edit['nid'] . (isset($edit['pid']) ? $edit['pid'] : '');
  1393.  
  1394.   if ($op == t('Preview')) {
  1395.     $form['#after_build'] = array('comment_form_add_preview');
  1396.   }
  1397.  
  1398.   if (empty($edit['cid']) && empty($edit['pid'])) {
  1399.     $form['#action'] = url('comment/reply/'. $edit['nid']);
  1400.   }
  1401.  
  1402.   return $form;
  1403. }
  1404.  
  1405. /**
  1406.  * Theme the comment form box.
  1407.  *
  1408.  * @param $edit
  1409.  *   The form structure.
  1410.  * @param $title
  1411.  *   The form title.
  1412.  */
  1413. function comment_form_box($edit, $title = NULL) {
  1414.   return theme('box', $title, drupal_get_form('comment_form', $edit, $title));
  1415. }
  1416.  
  1417. /**
  1418.  * Form builder; Generate and validate a comment preview form.
  1419.  *
  1420.  * @ingroup forms
  1421.  */
  1422. function comment_form_add_preview($form, &$form_state) {
  1423.   global $user;
  1424.   $edit = $form_state['values'];
  1425.   drupal_set_title(t('Preview comment'));
  1426.  
  1427.   $output = '';
  1428.   $node = node_load($edit['nid']);
  1429.  
  1430.   // Invoke full validation for the form, to protect against cross site
  1431.   // request forgeries (CSRF) and setting arbitrary values for fields such as
  1432.   // the input format. Preview the comment only when form validation does not
  1433.   // set any errors.
  1434.   drupal_validate_form($form['form_id']['#value'], $form, $form_state);
  1435.   if (!form_get_errors()) {
  1436.     _comment_form_submit($edit);
  1437.     $comment = (object)$edit;
  1438.  
  1439.     // Attach the user and time information.
  1440.     if (!empty($edit['author'])) {
  1441.       $account = user_load(array('name' => $edit['author']));
  1442.     }
  1443.     elseif ($user->uid && !isset($edit['is_anonymous'])) {
  1444.       $account = $user;
  1445.     }
  1446.     if (!empty($account)) {
  1447.       $comment->uid = $account->uid;
  1448.       $comment->name = check_plain($account->name);
  1449.     }
  1450.     elseif (empty($comment->name)) {
  1451.       $comment->name = variable_get('anonymous', t('Anonymous'));
  1452.     }
  1453.     $comment->timestamp = !empty($edit['timestamp']) ? $edit['timestamp'] : time();
  1454.     $output .= theme('comment_view', $comment, $node);
  1455.   }
  1456.   $form['comment_preview'] = array(
  1457.     '#value' => $output,
  1458.     '#weight' => -100,
  1459.     '#prefix' => '<div class="preview">',
  1460.     '#suffix' => '</div>',
  1461.   );
  1462.  
  1463.   $output = '';
  1464.  
  1465.   if ($edit['pid']) {
  1466.     $comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.signature, u.picture, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d AND c.status = %d', $edit['pid'], COMMENT_PUBLISHED));
  1467.     $comment = drupal_unpack($comment);
  1468.     $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
  1469.     $output .= theme('comment_view', $comment, $node);
  1470.   }
  1471.   else {
  1472.     $suffix = empty($form['#suffix']) ? '' : $form['#suffix'];
  1473.     $form['#suffix'] = $suffix . node_view($node);
  1474.     $edit['pid'] = 0;
  1475.   }
  1476.  
  1477.   $form['comment_preview_below'] = array('#value' => $output, '#weight' => 100);
  1478.  
  1479.   return $form;
  1480. }
  1481.  
  1482. /**
  1483.  * Validate comment form submissions.
  1484.  */
  1485. function comment_form_validate($form, &$form_state) {
  1486.   global $user;
  1487.   if ($user->uid === 0) {
  1488.     foreach (array('name', 'homepage', 'mail') as $field) {
  1489.       // Set cookie for 365 days.
  1490.       if (isset($form_state['values'][$field])) {
  1491.         setcookie('comment_info_'. $field, $form_state['values'][$field], time() + 31536000, '/');
  1492.       }
  1493.     }
  1494.   }
  1495.   comment_validate($form_state['values']);
  1496. }
  1497.  
  1498. /**
  1499.  * Prepare a comment for submission.
  1500.  *
  1501.  * @param $comment_values
  1502.  *   An associative array containing the comment data.
  1503.  */
  1504. function _comment_form_submit(&$comment_values) {
  1505.   $comment_values += array('subject' => '');
  1506.   if (!isset($comment_values['date'])) {
  1507.     $comment_values['date'] = 'now';
  1508.   }
  1509.   $comment_values['timestamp'] = strtotime($comment_values['date']);
  1510.   if (isset($comment_values['author'])) {
  1511.     $account = user_load(array('name' => $comment_values['author']));
  1512.     $comment_values['uid'] = $account->uid;
  1513.     $comment_values['name'] = $comment_values['author'];
  1514.   }
  1515.   // Validate the comment's subject. If not specified, extract
  1516.   // one from the comment's body.
  1517.   if (trim($comment_values['subject']) == '') {
  1518.     // The body may be in any format, so we:
  1519.     // 1) Filter it into HTML
  1520.     // 2) Strip out all HTML tags
  1521.     // 3) Convert entities back to plain-text.
  1522.     // Note: format is checked by check_markup().
  1523.     $comment_values['subject'] = trim(truncate_utf8(decode_entities(strip_tags(check_markup($comment_values['comment'], $comment_values['format']))), 29, TRUE));
  1524.     // Edge cases where the comment body is populated only by HTML tags will
  1525.     // require a default subject.
  1526.     if ($comment_values['subject'] == '') {
  1527.       $comment_values['subject'] = t('(No subject)');
  1528.     }
  1529.   }
  1530. }
  1531.  
  1532. /**
  1533.  * Process comment form submissions; prepare the comment, store it, and set a redirection target.
  1534.  */
  1535. function comment_form_submit($form, &$form_state) {
  1536.   _comment_form_submit($form_state['values']);
  1537.   if ($cid = comment_save($form_state['values'])) {
  1538.     $node = node_load($form_state['values']['nid']);
  1539.     $page = comment_new_page_count($node->comment_count, 1, $node);
  1540.     $form_state['redirect'] = array('node/'. $node->nid, $page, "comment-$cid");
  1541.     return;
  1542.   }
  1543. }
  1544.  
  1545. /**
  1546.  * Theme a single comment block.
  1547.  *
  1548.  * @param $comment
  1549.  *   The comment object.
  1550.  * @param $node
  1551.  *   The comment node.
  1552.  * @param $links
  1553.  *   An associative array containing control links.
  1554.  * @param $visible
  1555.  *   Switches between folded/unfolded view.
  1556.  * @ingroup themeable
  1557.  */
  1558. function theme_comment_view($comment, $node, $links = array(), $visible = TRUE) {
  1559.   static $first_new = TRUE;
  1560.  
  1561.   $output = '';
  1562.   $comment->new = node_mark($comment->nid, $comment->timestamp);
  1563.   if ($first_new && $comment->new != MARK_READ) {
  1564.     // Assign the anchor only for the first new comment. This avoids duplicate
  1565.     // id attributes on a page.
  1566.     $first_new = FALSE;
  1567.     $output .= "<a id=\"new\"></a>\n";
  1568.   }
  1569.  
  1570.   $output .= "<a id=\"comment-$comment->cid\"></a>\n";
  1571.  
  1572.   // Switch to folded/unfolded view of the comment
  1573.   if ($visible) {
  1574.     $comment->comment = check_markup($comment->comment, $comment->format, FALSE);
  1575.  
  1576.     // Comment API hook
  1577.     comment_invoke_comment($comment, 'view');
  1578.  
  1579.     $output .= theme('comment', $comment, $node, $links);
  1580.   }
  1581.   else {
  1582.     $output .= theme('comment_folded', $comment);
  1583.   }
  1584.  
  1585.   return $output;
  1586. }
  1587.  
  1588.  
  1589. /**
  1590.  * Build a comment control form.
  1591.  *
  1592.  * @param $mode
  1593.  *   Comment display mode.
  1594.  * @param $order
  1595.  *   Comment order mode.
  1596.  * @param $comments_per_page
  1597.  *   Comments per page.
  1598.  * @ingroup forms
  1599.  */
  1600. function comment_controls($mode = COMMENT_MODE_THREADED_EXPANDED, $order = COMMENT_ORDER_NEWEST_FIRST, $comments_per_page = 50) {
  1601.   $form['mode'] = array('#type' => 'select',
  1602.     '#default_value' => $mode,
  1603.     '#options' => _comment_get_modes(),
  1604.     '#weight' => 1,
  1605.   );
  1606.   $form['order'] = array(
  1607.     '#type' => 'select',
  1608.     '#default_value' => $order,
  1609.     '#options' => _comment_get_orders(),
  1610.     '#weight' => 2,
  1611.   );
  1612.   foreach (_comment_per_page() as $i) {
  1613.     $options[$i] = t('!a comments per page', array('!a' => $i));
  1614.   }
  1615.   $form['comments_per_page'] = array('#type' => 'select',
  1616.     '#default_value' => $comments_per_page,
  1617.     '#options' => $options,
  1618.     '#weight' => 3,
  1619.   );
  1620.  
  1621.   $form['submit'] = array('#type' => 'submit',
  1622.     '#value' => t('Save settings'),
  1623.     '#weight' => 20,
  1624.   );
  1625.  
  1626.   return $form;
  1627. }
  1628.  
  1629. /**
  1630.  * Theme comment controls box where the user can change the default display mode and display order of comments.
  1631.  *
  1632.  * @param $form
  1633.  *   The form structure.
  1634.  * @ingroup themeable
  1635.  */
  1636. function theme_comment_controls($form) {
  1637.   $output = '<div class="container-inline">';
  1638.   $output .=  drupal_render($form);
  1639.   $output .= '</div>';
  1640.   $output .= '<div class="description">'. t('Select your preferred way to display the comments and click "Save settings" to activate your changes.') .'</div>';
  1641.   return theme('box', t('Comment viewing options'), $output);
  1642. }
  1643.  
  1644. /**
  1645.  * Process comment_controls form submissions.
  1646.  */
  1647. function comment_controls_submit($form, &$form_state) {
  1648.   global $user;
  1649.  
  1650.   $mode = $form_state['values']['mode'];
  1651.   $order = $form_state['values']['order'];
  1652.   $comments_per_page = $form_state['values']['comments_per_page'];
  1653.  
  1654.   if ($user->uid) {
  1655.     $account = user_save($user, array('mode' => $mode, 'sort' => $order, 'comments_per_page' => $comments_per_page));
  1656.     // Terminate if an error occured during user_save().
  1657.     if (!$account) {
  1658.       drupal_set_message(t("Error saving user account."), 'error');
  1659.       return;
  1660.     }
  1661.     $user = $account;
  1662.   }
  1663.   else {
  1664.     $_SESSION['comment_mode'] = $mode;
  1665.     $_SESSION['comment_sort'] = $order;
  1666.     $_SESSION['comment_comments_per_page'] = $comments_per_page;
  1667.   }
  1668. }
  1669.  
  1670. /**
  1671.  * Process variables for comment.tpl.php.
  1672.  *
  1673.  * @see comment.tpl.php
  1674.  * @see theme_comment()
  1675.  */
  1676. function template_preprocess_comment(&$variables) {
  1677.   $comment = $variables['comment'];
  1678.   $node = $variables['node'];
  1679.   $variables['author']    = theme('username', $comment);
  1680.   $variables['content']   = $comment->comment;
  1681.   $variables['date']      = format_date($comment->timestamp);
  1682.   $variables['links']     = isset($variables['links']) ? theme('links', $variables['links']) : '';
  1683.   $variables['new']       = $comment->new ? t('new') : '';
  1684.   $variables['picture']   = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', $comment) : '';
  1685.   $variables['signature'] = $comment->signature;
  1686.   $variables['submitted'] = theme('comment_submitted', $comment);
  1687.   $variables['title']     = l($comment->subject, $_GET['q'], array('fragment' => "comment-$comment->cid"));
  1688.   $variables['template_files'][] = 'comment-'. $node->type;
  1689.   // set status to a string representation of comment->status.
  1690.   if (isset($comment->preview)) {
  1691.     $variables['status']  = 'comment-preview';
  1692.   }
  1693.   else {
  1694.     $variables['status']  = ($comment->status == COMMENT_NOT_PUBLISHED) ? 'comment-unpublished' : 'comment-published';
  1695.   }
  1696. }
  1697.  
  1698. /**
  1699.  * Process variables for comment-folded.tpl.php.
  1700.  *
  1701.  * @see comment-folded.tpl.php
  1702.  * @see theme_comment_folded()
  1703.  */
  1704. function template_preprocess_comment_folded(&$variables) {
  1705.   $comment = $variables['comment'];
  1706.   $variables['author'] = theme('username', $comment);
  1707.   $variables['date']   = format_date($comment->timestamp);
  1708.   $variables['new']    = $comment->new ? t('new') : '';
  1709.   $variables['title']  = l($comment->subject, comment_node_url() .'/'. $comment->cid, array('fragment' => "comment-$comment->cid"));
  1710. }
  1711.  
  1712. /**
  1713.  * Theme comment flat collapsed view.
  1714.  *
  1715.  * @param $comment
  1716.  *   The comment to be themed.
  1717.  * @param $node
  1718.  *   The comment node.
  1719.  * @ingroup themeable
  1720.  */
  1721. function theme_comment_flat_collapsed($comment, $node) {
  1722.   return theme('comment_view', $comment, $node, '', 0);
  1723. }
  1724.  
  1725. /**
  1726.  * Theme comment flat expanded view.
  1727.  *
  1728.  * @param $comment
  1729.  *   The comment to be themed.
  1730.  * @param $node
  1731.  *   The comment node.
  1732.  * @ingroup themeable
  1733.  */
  1734. function theme_comment_flat_expanded($comment, $node) {
  1735.   return theme('comment_view', $comment, $node, module_invoke_all('link', 'comment', $comment, 0));
  1736. }
  1737.  
  1738. /**
  1739.  * Theme comment thread collapsed view.
  1740.  *
  1741.  * @param $comment
  1742.  *   The comment to be themed.
  1743.  * @param $node
  1744.  *   The comment node.
  1745.  * @ingroup themeable
  1746.  */
  1747. function theme_comment_thread_collapsed($comment, $node) {
  1748.   return theme('comment_view', $comment, $node, '', 0);
  1749. }
  1750.  
  1751. /**
  1752.  * Theme comment thread expanded view.
  1753.  *
  1754.  * @param $comment
  1755.  *   The comment to be themed.
  1756.  * @param $node
  1757.  *   The comment node.
  1758.  * @ingroup themeable
  1759.  */
  1760. function theme_comment_thread_expanded($comment, $node) {
  1761.   return theme('comment_view', $comment, $node, module_invoke_all('link', 'comment', $comment, 0));
  1762. }
  1763.  
  1764. /**
  1765.  * Theme a "you can't post comments" notice.
  1766.  *
  1767.  * @param $node
  1768.  *   The comment node.
  1769.  * @ingroup themeable
  1770.  */
  1771. function theme_comment_post_forbidden($node) {
  1772.   global $user;
  1773.   static $authenticated_post_comments;
  1774.   
  1775.   if (!$user->uid) {
  1776.     if (!isset($authenticated_post_comments)) {
  1777.       // We only output any link if we are certain, that users get permission
  1778.       // to post comments by logging in. We also locally cache this information.
  1779.       $authenticated_post_comments = array_key_exists(DRUPAL_AUTHENTICATED_RID, user_roles(TRUE, 'post comments') + user_roles(TRUE, 'post comments without approval'));
  1780.     }
  1781.     
  1782.     if ($authenticated_post_comments) {
  1783.       // We cannot use drupal_get_destination() because these links
  1784.       // sometimes appear on /node and taxonomy listing pages.
  1785.       if (variable_get('comment_form_location_'. $node->type, COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_SEPARATE_PAGE) {
  1786.         $destination = 'destination='. drupal_urlencode("comment/reply/$node->nid#comment-form");
  1787.       }
  1788.       else {
  1789.         $destination = 'destination='. drupal_urlencode("node/$node->nid#comment-form");
  1790.       }
  1791.  
  1792.       if (variable_get('user_register', 1)) {
  1793.         // Users can register themselves.
  1794.         return t('<a href="@login">Login</a> or <a href="@register">register</a> to post comments', array('@login' => url('user/login', array('query' => $destination)), '@register' => url('user/register', array('query' => $destination))));
  1795.       }
  1796.       else {
  1797.         // Only admins can add new users, no public registration.
  1798.         return t('<a href="@login">Login</a> to post comments', array('@login' => url('user/login', array('query' => $destination))));
  1799.       }
  1800.     }
  1801.   }
  1802. }
  1803.  
  1804. /**
  1805.  * Process variables for comment-wrapper.tpl.php.
  1806.  *
  1807.  * @see comment-wrapper.tpl.php
  1808.  * @see theme_comment_wrapper()
  1809.  */
  1810. function template_preprocess_comment_wrapper(&$variables) {
  1811.   // Provide contextual information.
  1812.   $variables['display_mode']  = _comment_get_display_setting('mode', $variables['node']);
  1813.   $variables['display_order'] = _comment_get_display_setting('sort', $variables['node']);
  1814.   $variables['comment_controls_state'] = variable_get('comment_controls_'. $variables['node']->type, COMMENT_CONTROLS_HIDDEN);
  1815.   $variables['template_files'][] = 'comment-wrapper-'. $variables['node']->type;
  1816. }
  1817.  
  1818. /**
  1819.  * Theme a "Submitted by ..." notice.
  1820.  *
  1821.  * @param $comment
  1822.  *   The comment.
  1823.  * @ingroup themeable
  1824.  */
  1825. function theme_comment_submitted($comment) {
  1826.   return t('Submitted by !username on @datetime.',
  1827.     array(
  1828.       '!username' => theme('username', $comment),
  1829.       '@datetime' => format_date($comment->timestamp)
  1830.     ));
  1831. }
  1832.  
  1833. /**
  1834.  * Return an array of viewing modes for comment listings.
  1835.  *
  1836.  * We can't use a global variable array because the locale system
  1837.  * is not initialized yet when the comment module is loaded.
  1838.  */
  1839. function _comment_get_modes() {
  1840.   return array(
  1841.     COMMENT_MODE_FLAT_COLLAPSED => t('Flat list - collapsed'),
  1842.     COMMENT_MODE_FLAT_EXPANDED => t('Flat list - expanded'),
  1843.     COMMENT_MODE_THREADED_COLLAPSED => t('Threaded list - collapsed'),
  1844.     COMMENT_MODE_THREADED_EXPANDED => t('Threaded list - expanded')
  1845.   );
  1846. }
  1847.  
  1848. /**
  1849.  * Return an array of viewing orders for comment listings.
  1850.  *
  1851.  * We can't use a global variable array because the locale system
  1852.  * is not initialized yet when the comment module is loaded.
  1853.  */
  1854. function _comment_get_orders() {
  1855.   return array(
  1856.     COMMENT_ORDER_NEWEST_FIRST => t('Date - newest first'),
  1857.     COMMENT_ORDER_OLDEST_FIRST => t('Date - oldest first')
  1858.   );
  1859. }
  1860.  
  1861. /**
  1862.  * Return an array of "comments per page" settings from which the user
  1863.  * can choose.
  1864.  */
  1865. function _comment_per_page() {
  1866.   return drupal_map_assoc(array(10, 30, 50, 70, 90, 150, 200, 250, 300));
  1867. }
  1868.  
  1869. /**
  1870.  * Return a current comment display setting
  1871.  *
  1872.  * @param $setting
  1873.  *   can be one of these: 'mode', 'sort', 'comments_per_page'
  1874.  * @param $node
  1875.  *   The comment node in question.
  1876.  */
  1877. function _comment_get_display_setting($setting, $node) {
  1878.   global $user;
  1879.  
  1880.   if (isset($_GET[$setting])) {
  1881.     $value = $_GET[$setting];
  1882.   }
  1883.   else {
  1884.     // get the setting's site default
  1885.     switch ($setting) {
  1886.       case 'mode':
  1887.         $default = variable_get('comment_default_mode_'. $node->type, COMMENT_MODE_THREADED_EXPANDED);
  1888.         break;
  1889.       case 'sort':
  1890.         $default = variable_get('comment_default_order_'. $node->type, COMMENT_ORDER_NEWEST_FIRST);
  1891.         break;
  1892.       case 'comments_per_page':
  1893.         $default = variable_get('comment_default_per_page_'. $node->type, 50);
  1894.     }
  1895.     if (variable_get('comment_controls_'. $node->type, COMMENT_CONTROLS_HIDDEN) == COMMENT_CONTROLS_HIDDEN) {
  1896.       // if comment controls are disabled use site default
  1897.       $value = $default;
  1898.     }
  1899.     else {
  1900.       // otherwise use the user's setting if set
  1901.       if (isset($user->$setting) && $user->$setting) {
  1902.         $value = $user->$setting;
  1903.       }
  1904.       else if (isset($_SESSION['comment_'. $setting]) && $_SESSION['comment_'. $setting]) {
  1905.         $value = $_SESSION['comment_'. $setting];
  1906.       }
  1907.       else {
  1908.         $value = $default;
  1909.       }
  1910.     }
  1911.   }
  1912.   return $value;
  1913. }
  1914.  
  1915. /**
  1916.  * Updates the comment statistics for a given node. This should be called any
  1917.  * time a comment is added, deleted, or updated.
  1918.  *
  1919.  * The following fields are contained in the node_comment_statistics table.
  1920.  * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node.
  1921.  * - last_comment_name: the name of the anonymous poster for the last comment
  1922.  * - 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.
  1923.  * - comment_count: the total number of approved/published comments on this node.
  1924.  */
  1925. function _comment_update_node_statistics($nid) {
  1926.   $count = db_result(db_query('SELECT COUNT(cid) FROM {comments} WHERE nid = %d AND status = %d', $nid, COMMENT_PUBLISHED));
  1927.  
  1928.   // comments exist
  1929.   if ($count > 0) {
  1930.     $last_reply = db_fetch_object(db_query_range('SELECT cid, name, timestamp, uid FROM {comments} WHERE nid = %d AND status = %d ORDER BY cid DESC', $nid, COMMENT_PUBLISHED, 0, 1));
  1931.     db_query("UPDATE {node_comment_statistics} SET comment_count = %d, last_comment_timestamp = %d, last_comment_name = '%s', last_comment_uid = %d WHERE nid = %d", $count, $last_reply->timestamp, $last_reply->uid ? '' : $last_reply->name, $last_reply->uid, $nid);
  1932.   }
  1933.  
  1934.   // no comments
  1935.   else {
  1936.     $node = db_fetch_object(db_query("SELECT uid, created FROM {node} WHERE nid = %d", $nid));
  1937.     db_query("UPDATE {node_comment_statistics} SET comment_count = 0, last_comment_timestamp = %d, last_comment_name = '', last_comment_uid = %d WHERE nid = %d", $node->created, $node->uid, $nid);
  1938.   }
  1939. }
  1940.  
  1941. /**
  1942.  * Invoke a hook_comment() operation in all modules.
  1943.  *
  1944.  * @param &$comment
  1945.  *   A comment object.
  1946.  * @param $op
  1947.  *   A string containing the name of the comment operation.
  1948.  * @return
  1949.  *   The returned value of the invoked hooks.
  1950.  */
  1951. function comment_invoke_comment(&$comment, $op) {
  1952.   $return = array();
  1953.   foreach (module_implements('comment') as $name) {
  1954.     $function = $name .'_comment';
  1955.     $result = $function($comment, $op);
  1956.     if (isset($result) && is_array($result)) {
  1957.       $return = array_merge($return, $result);
  1958.     }
  1959.     else if (isset($result)) {
  1960.       $return[] = $result;
  1961.     }
  1962.   }
  1963.   return $return;
  1964. }
  1965.  
  1966. /**
  1967.  * Generate vancode.
  1968.  *
  1969.  * Consists of a leading character indicating length, followed by N digits
  1970.  * with a numerical value in base 36. Vancodes can be sorted as strings
  1971.  * without messing up numerical order.
  1972.  *
  1973.  * It goes:
  1974.  * 00, 01, 02, ..., 0y, 0z,
  1975.  * 110, 111, ... , 1zy, 1zz,
  1976.  * 2100, 2101, ..., 2zzy, 2zzz,
  1977.  * 31000, 31001, ...
  1978.  */
  1979. function int2vancode($i = 0) {
  1980.   $num = base_convert((int)$i, 10, 36);
  1981.   $length = strlen($num);
  1982.   return chr($length + ord('0') - 1) . $num;
  1983. }
  1984.  
  1985. /**
  1986.  * Decode vancode back to an integer.
  1987.  */
  1988. function vancode2int($c = '00') {
  1989.   return base_convert(substr($c, 1), 36, 10);
  1990. }
  1991.  
  1992. /**
  1993.  * Implementation of hook_hook_info().
  1994.  */
  1995. function comment_hook_info() {
  1996.   return array(
  1997.     'comment' => array(
  1998.       'comment' => array(
  1999.         'insert' => array(
  2000.           'runs when' => t('After saving a new comment'),
  2001.         ),
  2002.         'update' => array(
  2003.           'runs when' => t('After saving an updated comment'),
  2004.         ),
  2005.         'delete' => array(
  2006.           'runs when' => t('After deleting a comment')
  2007.         ),
  2008.         'view' => array(
  2009.           'runs when' => t('When a comment is being viewed by an authenticated user')
  2010.         ),
  2011.       ),
  2012.     ),
  2013.   );
  2014. }
  2015.  
  2016. /**
  2017.  * Implementation of hook_action_info().
  2018.  */
  2019. function comment_action_info() {
  2020.   return array(
  2021.     'comment_unpublish_action' => array(
  2022.       'description' => t('Unpublish comment'),
  2023.       'type' => 'comment',
  2024.       'configurable' => FALSE,
  2025.       'hooks' => array(
  2026.         'comment' => array('insert', 'update'),
  2027.       )
  2028.     ),
  2029.     'comment_unpublish_by_keyword_action' => array(
  2030.       'description' => t('Unpublish comment containing keyword(s)'),
  2031.       'type' => 'comment',
  2032.       'configurable' => TRUE,
  2033.       'hooks' => array(
  2034.         'comment' => array('insert', 'update'),
  2035.       )
  2036.     )
  2037.   );
  2038. }
  2039.  
  2040. /**
  2041.  * Drupal action to unpublish a comment.
  2042.  *
  2043.  * @param $context
  2044.  *   Keyed array. Must contain the id of the comment if $comment is not passed.
  2045.  * @param $comment
  2046.  *   An optional comment object.
  2047.  */
  2048. function comment_unpublish_action($comment, $context = array()) {
  2049.   if (isset($comment->cid)) {
  2050.     $cid = $comment->cid;
  2051.     $subject = $comment->subject;
  2052.   }
  2053.   else {
  2054.     $cid = $context['cid'];
  2055.     $subject = db_result(db_query("SELECT subject FROM {comments} WHERE cid = %d", $cid));
  2056.   }
  2057.   db_query('UPDATE {comments} SET status = %d WHERE cid = %d', COMMENT_NOT_PUBLISHED, $cid);
  2058.   watchdog('action', 'Unpublished comment %subject.', array('%subject' => $subject));
  2059. }
  2060.  
  2061. /**
  2062.  * Form builder; Prepare a form for blacklisted keywords.
  2063.  *
  2064.  * @ingroup forms
  2065.  */
  2066. function comment_unpublish_by_keyword_action_form($context) {
  2067.   $form['keywords'] = array(
  2068.     '#title' => t('Keywords'),
  2069.     '#type' => 'textarea',
  2070.     '#description' => t('The comment will be unpublished if it contains any of the character sequences above. Use a comma-separated list of character sequences. Example: funny, bungee jumping, "Company, Inc.". Character sequences are case-sensitive.'),
  2071.     '#default_value' => isset($context['keywords']) ? drupal_implode_tags($context['keywords']) : '',
  2072.   );
  2073.   return $form;
  2074. }
  2075.  
  2076. /**
  2077.  * Process comment_unpublish_by_keyword_action_form form submissions.
  2078.  */
  2079. function comment_unpublish_by_keyword_action_submit($form, $form_state) {
  2080.   return array('keywords' => drupal_explode_tags($form_state['values']['keywords']));
  2081. }
  2082.  
  2083. /**
  2084.  * Implementation of a configurable Drupal action.
  2085.  * Unpublish a comment if it contains a certain string.
  2086.  *
  2087.  * @param $context
  2088.  *   An array providing more information about the context of the call to this action.
  2089.  *   Unused here since this action currently only supports the insert and update ops of
  2090.  *   the comment hook, both of which provide a complete $comment object.
  2091.  * @param $comment
  2092.  *   A comment object.
  2093.  */
  2094. function comment_unpublish_by_keyword_action($comment, $context) {
  2095.   foreach ($context['keywords'] as $keyword) {
  2096.     if (strstr($comment->comment, $keyword) || strstr($comment->subject, $keyword)) {
  2097.       db_query('UPDATE {comments} SET status = %d WHERE cid = %d', COMMENT_NOT_PUBLISHED, $comment->cid);
  2098.       watchdog('action', 'Unpublished comment %subject.', array('%subject' => $comment->subject));
  2099.       break;
  2100.     }
  2101.   }
  2102. }
  2103.