thread)) { $node->thread = nodecomment_get_thread($node); } // MySQL-only upsert. Sorry Postgres guys. // When updating, do not change the original submitted IP Address. db_query( "INSERT INTO {node_comments} (cid, nid, pid, hostname, thread, name, uid, mail, homepage) VALUES (%d, %d, %d, '%s', '%s', '%s', %d, '%s', '%s') ON DUPLICATE KEY UPDATE nid=VALUES(nid), pid=VALUES(pid), thread=VALUES(thread), name=VALUES(name), uid=VALUES(uid), mail=VALUES(mail), homepage=VALUES(homepage)", $node->nid, $node->comment_target_nid, $node->comment_target_cid, ip_address(), $node->thread, $node->name, $node->uid, $node->mail, $node->homepage ); _nodecomment_update_node_statistics($node->comment_target_nid); // Explain the approval queue if necessary, and then // redirect the user to the node he's commenting on. if ($node->moderate == 1) { drupal_set_message(t('Your comment has been queued for moderation by site administrators and will be published after approval.')); } return $node->nid; } /** * Set the breadcrumb trail to include another node. * * This is used when viewing or adding a comment so that the parent node's * breadcrumb trail is used instead of the normal breadcrumb paths. * * @param $node * The node to use. */ function nodecomment_set_breadcrumb($node) { // If the node had any breadcrumb changes, they will be made via nodeapi('view') // as a general rule, so this will make them happen. node_invoke_nodeapi($node, 'view', FALSE, TRUE); // Then add the parent node to the trail. $breadcrumb = drupal_get_breadcrumb(); $breadcrumb[] = l($node->title, "node/$node->nid"); drupal_set_breadcrumb($breadcrumb); } /** * Get number of new comments for current user and specified node * * @param $nid node-id to count comments for * @param $timestamp time to count from (defaults to time of last user access * to node) */ function nodecomment_num_new($nid, $timestamp = 0) { global $user; if ($user->uid) { // Retrieve the timestamp at which the current user last viewed the // specified node. if (!$timestamp) { $timestamp = node_last_viewed($nid); } $timestamp = ($timestamp > NODE_NEW_LIMIT ? $timestamp : NODE_NEW_LIMIT); // Use the timestamp to retrieve the number of new comments. $result = db_result(db_query( "SELECT COUNT(cn.nid) FROM {node} n INNER JOIN {node_comments} c ON n.nid = c.nid INNER JOIN {node} cn ON c.cid = cn.nid WHERE n.nid = %d AND (cn.created > %d OR cn.changed > %d) AND cn.status = %d", $nid, $timestamp, $timestamp, 1 )); return $result; } else { return 0; } } /** * Calculate page number for any given comment. * * @param $comment * The comment. * @return * The page number. */ function nodecomment_page_count($comment, $node = NULL) { if (!$node) { if (empty($comment->comment_target_nid)) { return ''; } $node = node_load($comment->comment_target_nid); if (!nodecomment_get_comment_type($node->type)) { return ''; } } $comments_per_page = _comment_get_display_setting('comments_per_page', $node); $mode = _comment_get_display_setting('mode', $node); $order = _comment_get_display_setting('sort', $node); $flat = in_array($mode, array(COMMENT_MODE_FLAT_COLLAPSED, COMMENT_MODE_FLAT_EXPANDED)); if ($flat) { $field = 'n.nid'; $value = '%d'; $arg = $comment->nid; } else { $field = 'nc.thread'; $value = "'%s'"; $arg = $comment->thread; } if ($order == COMMENT_ORDER_NEWEST_FIRST) { $op = ' >= '; } else { $op = ' <= '; } $query = "SELECT COUNT(*) FROM {node_comments} nc INNER JOIN {node} n ON n.nid = nc.cid WHERE $field $op $value AND n.status <> 0 AND n.nid != %d AND nc.nid = %d"; $count = db_result(db_query($query, $arg, $comment->nid, $node->nid)); $pageno = intval($count / $comments_per_page); return $pageno; } /** * Calculate page number for first new comment. * * This works for both comments and nodecomments. * * @param $num_comments * Number of comments. * @param $new_replies * Number of new replies. * @param $node * The first new comment node. * @return * "page=X" if the page number is greater than zero; empty string otherwise. */ function nodecomment_new_page_count($num_comments, $new_replies, $node) { // Default to normal comments so this function works either way. if (!nodecomment_get_comment_type($node->type)) { return comment_new_page_count($num_comments, $new_replies, $node); } $comments_per_page = _comment_get_display_setting('comments_per_page', $node); $mode = _comment_get_display_setting('mode', $node); $order = _comment_get_display_setting('sort', $node); $pagenum = NULL; $flat = in_array($mode, array(COMMENT_MODE_FLAT_COLLAPSED, COMMENT_MODE_FLAT_EXPANDED)); if ($num_comments <= $comments_per_page || ($flat && $order == COMMENT_ORDER_NEWEST_FIRST)) { // Only one page of comments or flat forum and newest first. // First new comment will always be on first page. $pageno = 0; } else { if ($flat) { // Flat comments and oldest first. $count = $num_comments - $new_replies; } else { // Threaded comments. See the documentation for comment_render(). if ($order == COMMENT_ORDER_NEWEST_FIRST) { // Newest first: find the last thread with new comment $thread = db_result(db_query( "(SELECT thread FROM {node_comments} nc INNER JOIN {node} n ON n.nid = nc.cid WHERE nc.nid = %d AND n.status <> 0 ORDER BY n.created DESC LIMIT %d) ORDER BY thread DESC LIMIT 1", $node->nid, $new_replies )); $result_count = db_query( "SELECT COUNT(*) FROM {node_comments} nc INNER JOIN {node} n ON n.nid = nc.cid WHERE nc.nid = %d AND n.status <> 0 AND nc.thread > '%s'", $node->nid, $thread ); } else { // Oldest first: find the first thread with new comment $result = db_query( '(SELECT thread FROM {node_comments} nc INNER JOIN {node} n ON n.nid = nc.cid WHERE nc.nid = %d AND n.status <> 0 ORDER BY n.created DESC LIMIT %d) ORDER BY SUBSTRING(thread, 1, (LENGTH(thread) - 1)) LIMIT 1', $node->nid, $new_replies ); $thread = substr(db_result($result), 0, -1); $result_count = db_query( "SELECT COUNT(*) FROM {comments} nc INNER JOIN {node} n ON n.nid = nc.cid WHERE nc.nid = %d AND n.status <> 0 AND SUBSTRING(nc.thread, 1, (LENGTH(nc.thread) - 1)) < '%s'", $node->nid, $thread ); } $count = db_result($result_count); } $pageno = $count / $comments_per_page; } if ($pageno >= 1) { $pagenum = "page=". intval($pageno); } return $pagenum; } function nodecomment_get_thread($node) { // Here we are building the thread field. See the documentation for // comment_render(). if (empty($node->comment_target_cid)) { // This is a comment with no parent comment (depth 0): we start // by retrieving the maximum thread level. $max = db_result(db_query( "SELECT MAX(thread) FROM {node_comments} WHERE nid = %d", $node->comment_target_nid )); // Strip the "/" from the end of the thread. $max = rtrim($max, '/'); // Finally, build the thread field for this new comment. $thread = int2vancode(vancode2int($max) + 1) .'/'; } else { // This is comment with a parent comment: we increase // the part of the thread value at the proper depth. // Get the parent comment: $parent = node_load($node->comment_target_cid); // Strip the "/" from the end of the parent thread. $parent->thread = (string) rtrim((string) $parent->thread, '/'); // Get the max value in _this_ thread. $max = db_result(db_query( "SELECT MAX(thread) FROM {node_comments} WHERE thread LIKE '%s.%%' AND nid = %d", $parent->thread, $node->comment_target_nid )); if ($max == '') { // First child of this parent. $thread = $parent->thread .'.'. int2vancode(0) .'/'; } else { // Strip the "/" at the end of the thread. $max = rtrim($max, '/'); // We need to get the value at the correct depth. $parts = explode('.', $max); $parent_depth = count(explode('.', $parent->thread)); $last = $parts[$parent_depth]; // Finally, build the thread field for this new comment. $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/'; } } return $thread; } /** * Delete children comments from the same thread as comment. */ function _nodecomment_thread_delete_children($cid, $nid) { // Delete the comment's replies. // We have to be careful here to not delete comments from a separate thread // started by this node, if it has own comments. // To be sure about this, also check node id. $result = db_query("SELECT cid FROM {node_comments} WHERE pid = %d AND nid = %d", $cid, $nid); $delete = array(); while ($row = db_fetch_object($result)) { $delete[] = $row->cid; } foreach ($delete as $cid) { _nodecomment_delete_comment($cid); } } /** * Delete node comments of a node. */ function _nodecomment_delete_comments($nid) { $result = db_query('SELECT cid FROM {node_comments} WHERE nid = %d', $nid); $delete = array(); while ($row = db_fetch_object($result)) { $delete[] = $row->cid; } foreach ($delete as $cid) { _nodecomment_delete_comment($cid); } } /** * Delete a nodecomment. * * Based on code from node_delete() function but has different logics. * * Previously, the module used node_delete() which checks permissions. * If the user didn't have the permission to delete the comment, the comment * was left in the database as orphan. * * Having own function is better: * 1) We don't check permissions, so we don't create orphans. Usually only * moderators have the permission to delete other user's comments, so this * is essential for any community site where users delete own comments. * This also follows core comment behavior. * 2) We warn other modules about the delete event, if nodeapi delete * event is too late for them to react. They can also veto the deletion, * provided they allow alternate action. * 3) We don't print messages on screen. */ function _nodecomment_delete_comment($cid) { // Clear the cache before the load, so if multiple nodes are deleted, the // memory will not fill up with nodes (possibly) already removed. // This also allows to find out easily if the node is already deleted. $node = node_load($cid, NULL, TRUE); // The node might not exist already for a number of reasons. This is // generally ok. if (!$node) { return; } // Warn other modules about deletion and let them veto it. $delete = TRUE; $votes = nodecomment_invoke($node, 'delete_vote'); foreach ($votes as $vote) { if ($vote === FALSE) { $delete = FALSE; break; } } if ($delete) { // Let the modules prepare for deleting. nodecomment_invoke($node, 'delete'); db_query('DELETE FROM {node} WHERE nid = %d', $node->nid); db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid); db_query('DELETE FROM {node_access} WHERE nid = %d', $node->nid); // Call the node-specific callback (if any): node_invoke($node, 'delete'); node_invoke_nodeapi($node, 'delete'); // Clear the page and block caches. cache_clear_all(); // Remove this node from the search index if needed. if (function_exists('search_wipe')) { search_wipe($node->nid, 'node'); } // Log the event, but don't print messages on screen. watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->title)); } } /** * Updates the comment statistics for a given node. This should be called any * time a comment is added, deleted, or updated. * * The following fields are contained in the node_comment_statistics table: * * - last_comment_timestamp: the timestamp of the last comment for this node * or the node create stamp if no comments exist for the node. * * - last_comment_name: the name of the anonymous poster for the last comment. * * - 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. * * - comment_count: the total number of approved/published comments on this node. */ function _nodecomment_update_node_statistics($nid) { $count = db_result(db_query( 'SELECT COUNT(*) FROM {node_comments} nc INNER JOIN {node} n ON n.nid = nc.cid WHERE nc.nid = %d AND n.status = 1', $nid )); // Comments exist. if ($count > 0) { $last_reply = db_fetch_object(db_query_range( 'SELECT nc.cid, nc.name, n.created, n.changed, n.uid FROM {node} n LEFT JOIN {node_comments} nc ON n.nid = nc.cid WHERE nc.nid = %d AND n.status = 1 ORDER BY cid DESC', $nid, 0, 1 )); $timestamp = max($last_reply->created, $last_reply->changed); $name = $last_reply->uid ? '' : $last_reply->name; 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, $timestamp, $name, $last_reply->uid, $nid ); } // No comments. else { // The node might not exist if called from hook_nodeapi($op = 'delete'). if ($node = db_fetch_object(db_query("SELECT uid, created FROM {node} WHERE nid = %d", $nid))) { 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 ); } else { db_query("DELETE FROM {node_comment_statistics} WHERE nid = %d", $nid); } } } function nodecomment_form($node) { $comment_type = nodecomment_get_comment_type($node->type); if ($comment_type) { global $user; $new_node = array( 'uid' => $user->uid, 'name' => $user->name, 'type' => $comment_type, 'comment_target_nid' => $node->nid, 'comment_target_cid' => 0, ); module_load_include('inc', 'node', 'node.pages'); return drupal_get_form($comment_type .'_node_form', $new_node); } } /** * Menu callback; view a single node. */ function nodecomment_node_view($node, $cid = NULL) { drupal_set_title(check_plain($node->title)); $output = node_view($node, FALSE, TRUE); if (nodecomment_get_commentable($node)) { if ($node->comment_type) { $output .= nodecomment_render($node, $cid); } else { $output .= comment_render($node, $cid); } } // Update the history table, stating that this user viewed this node. node_tag_new($node->nid); return $output; } /** * Node comment's version of comment_render, to render all comments on a node. */ function nodecomment_render($node, $cid = 0) { global $user; $output = ''; if (user_access('access comments')) { // Pre-process variables. $nid = $node->nid; if (empty($nid)) { $nid = 0; } // Render nothing if there are no comments to render. if (!empty($node->comment_count)) { if ($cid && is_numeric($cid)) { // Single comment view. if ($comment = node_load($cid)) { $output = theme('node', $comment, TRUE, TRUE); } } else { $view_name = variable_get('node_comment_view_'. $node->type, 'nodecomments'); if ($view_name) { $output = views_embed_view($view_name, 'nodecomment_comments_1', $nid); } } } // If enabled, show new comment form. $comment_type = nodecomment_get_comment_type($node->type); if (user_access("create $comment_type content") && nodecomment_is_readwrite($node) && (variable_get('comment_form_location_'. $node->type, COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_BELOW)) { // There is likely a cleaner way to do this, but for now it will have to do. -- JE $friendly_name = node_get_types('name', $comment_type); $output .= nodecomment_form_box($node, t('Post new !type', array('!type' => $friendly_name))); } if ($output) { $output = theme('comment_wrapper', $output, $node); } } return $output; } function nodecomment_form_box($node, $title = NULL) { return theme('box', $title, nodecomment_form($node)); } /** * Public API function to retrieve the comment type for a node type. * * @param $node_type * The name of the node type for which the comment type will be retreived. * @return * Returns a string containing the node type which will be used for comments. * If node comments are not used for the passed in type, returns empty string. */ function nodecomment_get_comment_type($node_type) { return variable_get('node_comment_type_'. $node_type, ''); } /** * Return array of content types which serve as nodecomments. */ function nodecomment_get_comment_types() { $comment_types = array(); foreach (node_get_types('names') as $type => $blank) { $comment_type = nodecomment_get_comment_type($type); if ($comment_type) { $comment_types[$comment_type] = $comment_type; } } return $comment_types; } /** * Redirect to target node page containing a comment. Supports comment threads * spanning multiple pages. * * @param $node * node comment object */ function _nodecomment_target_node_redirect($node) { $pagenum = nodecomment_page_count($node); $query = NULL; if ($pagenum) { $query = array('page' => $pagenum); } $fragment = 'comment-' . $node->nid; drupal_goto("node/" . $node->comment_target_nid, $query, $fragment); } /** * Check if the content type should be treated as full featured content type, * even if it works as comment too. */ function nodecomment_is_content($type) { // Types which are not comments are always treated as content. if (!in_array($type, nodecomment_get_comment_types())) { return TRUE; } return variable_get('node_comment_is_content_' . $type, FALSE); } /** * Check if the node is commentable (read/write). */ function nodecomment_is_readwrite(&$node) { $commentable = nodecomment_get_commentable($node); return ($commentable == COMMENT_NODE_READ_WRITE); } /** * Get "commentable" setting. */ function nodecomment_get_commentable(&$node) { static $settings = array(); if (!isset($settings[$node->nid])) { $commentable = isset($node->node_comment) ? $node->node_comment : $node->comment; // Let modules override commentability dynamically. drupal_alter('nodecomment_commentable', $commentable, $node); $settings[$node->nid] = $commentable; } return $settings[$node->nid]; } /** * Return list of Node Comments module variables. * * @param $type * content type */ function _nodecomment_vars() { $vars = _nodecomment_type_vars(); $vars[] = 'node_comment_node_redirect'; return $vars; } /** * Return list of Node Comments module variables, associated with a content type. * * @param $type * Content type. If not provided, return variables for all content types. */ function _nodecomment_type_vars($type) { if ($type) { $types = array($type); } else { $types = array_keys(node_get_types('names')); } $vars = array(); foreach ($types as $type) { $vars[] = 'node_comment_type_' . $type; $vars[] = 'node_comment_view_' . $type; $vars[] = 'node_comment_plural_' . $type; $vars[] = 'node_comment_is_content_' . $type; } return $vars; } function nodecomment_invoke($node, $op) { $results = array(); $hook = 'nodecomment_' . $op; foreach (module_implements($hook) as $module) { $function = $module . '_' . $hook; $results[] = $function($node); } return $results; } function nodecomment_include($inc) { module_load_include('inc', 'nodecomment', "includes/nodecomment.$inc"); }