Comment Moderation: Part Two, adding the Actions

In my last post, I walked through the high level plan for the Comment Moderation feature. We came up with a summary of tasks. In this post we will be creating the views and actions. Below is the list of the tasks, bolded tasks are being tackled today.
Create View with 2 page panes.

In my last post, I walked through the high level plan for the Comment Moderation feature. We came up with a summary of tasks. In this post we will be creating the views and actions. Below is the list of the tasks, bolded tasks are being tackled today.
Create View with 2 page panes.

  • Approval Queue
    • Fields: Subject, Author, email address, IP, Node Title, Post Time, edit and moderate links.
    • Actions: Delete, Delete and block email, Publish, Report to Mollom and Delete.
    • Exposed Filters: Email, IP
  • Published Comment
    • Fields: Subject, Author, email, Node title, Post Time, edit
    • Actions: Delete, Delete and block email, unpublish, report to Mollom and delete, report to Mollom and unpublish.
    • Exposed Filers: Email, IP
  • Configuration:
    • Mollom
    • Permissions
    • Require fields on comment form
  • Custom Code:
    • Expose IP address and Email to views.
    • Block comments by Email
    • Update comment email address for register users.

 
Creating the Feature
Since we are going to be creating a feature, we want to do that first. This will give us the structure for our module. It's important to create the feature before you add any custom code. Once the feature is created, you can add any code you would like to the .module file without any worries about losing the code in an update or recreate. Features does not touch the .module file after the initial creation. You can even create a .install file and use any of the normal hooks you do in any other module. After all, a feature is just a specialized type of module.
 
Views
Since features are created based upon meta data (views, content types, etc), we will be creating the views first, then creating the feature. We will be creating one view with multiple page displays. The paths will override the current comment moderation screens. Go ahead and create the view now with 2 page displays using the fields from a above and the path settings below. I'll wait.
 
Approval Queue

  • Path: admin/content/comment/approval
  • Menu: Menu tab
  • Style: Bulk Operations

Published Queue

  • Path: admin/content/comment/new
  • Menu: Default menu tab, Normal menu item
  • Style: Bulk Operations

So are you done? How far did you get? I bet you noticed that the IP and email fields were not available. We will have to expose those to views. What else was missing? That's right, several of those actions were not available. We will have to create them also. One more thing you may have had trouble with are the moderation links. Lets walk through each of these starting with the moderation links. Then we will create our feature and start our coding.
 
Moderation Links
We are going to imitate the default comment moderation page as much as possible, so we will create a "Global: Custom text" field called "operations". We will place this in the text field "[edit_comment]  <a href="../../../comment/moderate/[cid]">moderate</a>" Notice that we are using the [edit_comment] and [cid] tokens. For us to use those tokens in this field, we need to add them to the view before the operations field. Add the "edit comment" and "cid" fields and select the "Exclude from display" option.
Alright, we have done as much as we can without custom code. Let's create the feature. Navigate to the create feature screen (admin/build/features/create). Give it a name, description and version (6.x-1.0). For now, just select the view you created and click "Download Feature." Now we have the base for our feature. Extract it and move it to our sites/all/modules folder. I like to organize my modules folder into /contrib, /custom and /features directories (under sites/all/modules). This would be placed in the features folder. Open up the .module file in your favorite IDE. You will see one line of code.

include_once('comments_feature.features.inc');

Leave this line alone. The rest of the file is yours. This file will never be touched by features again. This applies for the drush update and recreate in the UI. Let's start coding :)
 
Expose IP and Email view fields
First add the hook_views_api(). This tells views that our module will be using the views api (version 2). It also tells views to look in comments_feature.views.inc for the views API hooks.

/**
 * Implementation of hook_views_api().
 */

function comments_feature_views_api() {
  return array('api' =&gt; 2);
}

Now create the file "comments_feature.views.inc" for our views code. We will only be using hook_views_data(). This is the main hook used to add fields, tables, filters, arguments and sorts to a view.

/**
 * Implementation of hook_views_data()
 */

function comments_feature_views_data() {

  // Comment IP Address
  $data['comments']['hostname'] = array(
    'title' =&gt; t('IP Address'),
    'help' =&gt; t('IP Address where the comment was logged from'),
    'field' =&gt; array(
      'handler' =&gt; 'views_handler_field',
      'click sortable' =&gt; TRUE,
    ),
    'filter' =&gt; array(
      'handler' =&gt; 'views_handler_filter_string',
    ),
    'sort' =&gt; array(
      'handler' =&gt; 'views_handler_sort',
    ),
    'argument' =&gt; array(
      'handler' =&gt; 'views_handler_argument_string',
    ),
  );
 
  $data['comments']['mail'] = array(
    'title' =&gt; t(&quot;Author's Email Address&quot;),
    '
help' =&gt; t(&quot;Author's Email Address&quot;),
    'field' =&gt; array(
      'handler' =&gt; 'views_handler_field',
      'click sortable' =&gt; TRUE,
    ),
    'filter' =&gt; array(
      'handler' =&gt; 'views_handler_filter_string',
    ),
    'sort' =&gt; array(
      'handler' =&gt; 'views_handler_sort',
    ),
    'argument' =&gt; array(
      'handler' =&gt; 'views_handler_argument_string',
    ),
  );
 
  // Comment Email Address

  return $data;
}

Now clear your cache, go back to the view and you should see 2 new fields available: Comment: IP Address and Comment: Author's Email Address. Now go back to the view and add the email field. Also add the IP and email exposed filters. We have now satisfied the ability to filter by IP and email. Good job!
 
Actions
Now let's tackle the create of the missing actions. If you remember from before, there were 4 missing actions that we need.

  • Publish Comment.
  • Delete comment and block email address (This will integrate with our block by email feature).
  • Report to Mollom as spam and unpublish.
  • Report to Mollom as spam and delete.

First we tell Drupal about our actions using hook_action_info().

/**
 * Implementation of hook_action_info().
 *
 * Add Custom Actions
 */

function comments_feature_action_info() {
  return array(
    'comments_feature_publish_comment_action' =&gt; array(
      'description' =&gt; t('Publish Comment'),
      'type' =&gt; 'comment',
      'configuration' =&gt; FALSE,
      'hooks' =&gt; array(
        'comment' =&gt; array('insert', 'update'),
      ),
    ),
    'comments_feature_block_email_action' =&gt; array(
      'description' =&gt; t('Delete comment and block email address'),
      'type' =&gt; 'comment',
      'configuration' =&gt; FALSE,
      'hooks' =&gt; array(
        'comment' =&gt; array('insert', 'update'),
      ),
    ),
    // Add Mollom Actions
    'comments_feature_mollom_unpublish' =&gt; array(
      'description' =&gt; t('Report to Mollom as spam and unpublish'),
      'type' =&gt; 'comment',
      'configuration' =&gt; FALSE,
      'hooks' =&gt; array(
        'comment' =&gt; array('insert', 'update'),
      ),
    ),
    'comments_feature_mollom_delete' =&gt; array(
      'description' =&gt; t('Report to Mollom as spam and delete'),
      'type' =&gt; 'comment',
      'configuration' =&gt; FALSE,
      'hooks' =&gt; array(
        'comment' =&gt; array('insert', 'update'),
      ),
    ),
  );
}

Note: At the time of writing this blog post, I submitted a patch (http://drupal.org/node/655846) to Mollom to add this actions so the Mollom actions may not be needed.
Let's look at the code for the action function. For some unknown and crazy reason, the publish action was left out of D6 core, but never fear, it has been added to D7. So to create the publish action, I took the unpublish action (http://api.drupal.org/api/function/comment_unpublish_action/6) and changed the update statement from updating the status to COMMENT_NOT_PUBLISHED to COMMENT_PUBLISHED.

/**
 * Action Callback to publish a comment.
 */

function comments_feature_publish_comment_action($comment, $context = array()) {
  if (isset($comment-&gt;cid)) {
    $cid = $comment-&gt;cid;
    $subject = $comment-&gt;subject;
  }
  else {
    $cid = $context['cid'];
    $subject = db_result(db_query(&quot;SELECT subject FROM {comments}
      WHERE cid = %d&quot;, $cid));
  }
  db_query('UPDATE {comments} SET status = %d WHERE cid = %d',
    COMMENT_PUBLISHED, $cid);
  watchdog(
    'action',
    'Published comment %subject.',
    array('%subject' =&gt; $subject)
  );
}

To create the Mollom actions we inspect the Mollom module. The function that interests me the most is mollom_report_comment_submit. This is the submit handler for reporting a comment as spam to Mollom. Here is the function form Mollom:

/**
 * This function deletes a comment and optionally sends feedback to Mollom.
 */

function mollom_report_comment_submit($form, &amp;$form_state) {
  if ($form_state['values']['confirm']) {
    if ($comment = _comment_load($form_state['values']['cid'])) {
      // Load the Mollom session data:
      $data = mollom_get_data('comment-'. $comment-&gt;cid);

      // Provide feedback to Mollom if available:
      if (isset($data) &amp;&amp; isset($data-&gt;session) &amp;&amp;
        isset($form_state['values']['feedback']) &amp;&amp;
        $form_state['values']['feedback'] != 'none') {
          mollom(
            'mollom.sendFeedback',
            array(
              'session_id' =&gt; $data-&gt;session,
              'feedback' =&gt; $form_state['values']['feedback']
            )
          );
      }

      // Delete a comment and its replies:
      module_load_include('inc', 'comment', 'comment.admin');
      _comment_delete_thread($comment);
      _comment_update_node_statistics($comment-&gt;nid);
      cache_clear_all();

      drupal_set_message(t('The comment has been deleted.'));
    }
  }
  $form_state['redirect'] = &quot;node/$comment-&gt;nid&quot;;
}

With the action function, we are passed the comment so we don't need to load it. The same function can be used for both the delete and unpublish actions with different parameters passed.

/**
 * Action callback to report to mollom and unpublish.
 */

function comments_feature_mollom_unpublish($comment, $context = array()) {
  comments_feature_mollom_action($comment, 'unpublish');
}

/**
 * Action callback to report to mollom and delete.
 */

function comments_feature_mollom_delete($comment, $context = array()) {
  comments_feature_mollom_action($comment, 'delete');
}

/**
 * Pulled from Mollom module.  Preform action and send to Mollom as Spam
 */

function comments_feature_mollom_action($comment, $op) {

  // First, send the proper information to the XML-RPC server:
  if ($data = mollom_get_data('comment-'. $comment-&gt;cid)) {
    mollom('mollom.sendFeedback', array('session_id' =&gt; $data-&gt;session, 'feedback' =&gt; 'spam'));
  }

  // Second, perform the proper operation on the comments:
  if ($op == 'unpublish') {
    db_query(&quot;UPDATE {comments} SET status = %d WHERE cid = %d&quot;,
      COMMENT_NOT_PUBLISHED, $cid);
    _comment_update_node_statistics($comment-&gt;nid);
  }
  elseif ($op == 'delete') {
    _comment_delete_thread($comment);
    _comment_update_node_statistics($comment-&gt;nid);
  }

  if ($operation == 'delete') {
    drupal_set_message(t('The selected comments have been reported as
      inappropriate and are deleted.'
));
  }
  else {
    drupal_set_message(t('The selected comments have been reported as
      inappropriate and are unpublished.'
));
  }
}

The final action is to add the email address from the comment to a block list. The code for the blocking will be covered in the next blog post. The list of emails will be kept in a variable. I selected a variable so you can use caching (if using memcache/APC) with minimal effort.

/**
 * Action Callback to block an email
 */

function comments_feature_block_email_action($comment, $context = array()) {
  $emails = variable_get('comments_blocked_emails', array());

  // Delete the comment
  // Delete a comment and its replies:
  module_load_include('inc', 'comment', 'comment.admin');
  _comment_delete_thread($comment);
  _comment_update_node_statistics($comment-&gt;nid);

  // Add the email to the block list
  $emails[$comment-&gt;mail] = $comment-&gt;mail;
  variable_set('comments_blocked_emails', $emails);
}

Now that we have coded all of our actions, let's go into our view and add the correct actions to the correct displays.

  • Add the Delete and block email, Publish, Report to Mollom and Delete to the "Approval Queue" display.
  • Delete and block email, report to Mollom and delete, report to Mollom and unpublish.

Save the view and test out what we have. Our views and actions are now complete. Feel free to add/edit the view fields to whatever meets your needs. Once you are happy with your view, update your feature to reflect the new fields. This can be done in one of 2 ways:

  • Update view drush: "drush features update "
  • Re-export view the GUI: At admin/build/features, click the "recreate" link next to the feature name.

I prefer using drush, especially if you are not adding any new views or content types to your feature. Either way, the code in your .module file will be saved.
 
Conclusion
We have come a long way in our comment moderation feature. We have all of our views and actions complete. After testing the views, you probably noticed that the IP address is not displaying for registered users. This is because Drupal does not store this information. In the final blog post of this series, we will be completing the final 2 lines in the list above: Block comments by Email and Update comment email address for register users.

Neil Hastings