As I was writing a module that wanted to hook into the hook_comment_unpublish (as documented in the comment api) I found out that this hook is never invoked. I fixed this by altering the comment_save function (scroll down to see what I changed, as it is almost at the end):

function comment_save($comment) {
  global $user;

  $transaction = db_transaction();
  try {
    $defaults = array(
      'mail' => '',
      'homepage' => '',
      'name' => '',
      'status' => user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED,
    );
    foreach ($defaults as $key => $default) {
      if (!isset($comment->$key)) {
        $comment->$key = $default;
      }
    }
    // Make sure we have a bundle name.
    if (!isset($comment->node_type)) {
      $node = node_load($comment->nid);
      $comment->node_type = 'comment_node_' . $node->type;
    }

    // Load the stored entity, if any.
    if (!empty($comment->cid) && !isset($comment->original)) {
      $comment->original = entity_load_unchanged('comment', $comment->cid);
    }

    field_attach_presave('comment', $comment);

    // Allow modules to alter the comment before saving.
    module_invoke_all('comment_presave', $comment);
    module_invoke_all('entity_presave', $comment, 'comment');

    if ($comment->cid) {

      drupal_write_record('comment', $comment, 'cid');

      // Ignore slave server temporarily to give time for the
      // saved comment to be propagated to the slave.
      db_ignore_slave();

      // Update the {node_comment_statistics} table prior to executing hooks.
      _comment_update_node_statistics($comment->nid);

      field_attach_update('comment', $comment);
      // Allow modules to respond to the updating of a comment.
      module_invoke_all('comment_update', $comment);
      module_invoke_all('entity_update', $comment, 'comment');
    }
    else {
      // Add the comment to database. This next section builds the thread field.
      // Also see the documentation for comment_view().
      if (!empty($comment->thread)) {
        // Allow calling code to set thread itself.
        $thread = $comment->thread;
      }
      elseif ($comment->pid == 0) {
        // This is a comment with no parent comment (depth 0): we start
        // by retrieving the maximum thread level.
        $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField();
        // Strip the "/" from the end of the thread.
        $max = rtrim($max, '/');
        // We need to get the value at the correct depth.
        $parts = explode('.', $max);
        $firstsegment = $parts[0];
        // Finally, build the thread field for this new comment.
        $thread = int2vancode(vancode2int($firstsegment) + 1) . '/';
      }
      else {
        // This is a comment with a parent comment, so increase the part of the
        // thread value at the proper depth.

        // Get the parent comment:
        $parent = comment_load($comment->pid);
        // 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_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
          ':thread' => $parent->thread . '.%',
          ':nid' => $comment->nid,
        ))->fetchField();

        if ($max == '') {
          // First child of this parent.
          $thread = $parent->thread . '.' . int2vancode(0) . '/';
        }
        else {
          // Strip the "/" at the end of the thread.
          $max = rtrim($max, '/');
          // 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) . '/';
        }
      }

      if (empty($comment->created)) {
        $comment->created = REQUEST_TIME;
      }

      if (empty($comment->changed)) {
        $comment->changed = $comment->created;
      }

      if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well.
        $comment->name = $user->name;
      }

      // Ensure the parent id (pid) has a value set.
      if (empty($comment->pid)) {
        $comment->pid = 0;
      }

      // Add the values which aren't passed into the function.
      $comment->thread = $thread;
      $comment->hostname = ip_address();

      drupal_write_record('comment', $comment);

      // Ignore slave server temporarily to give time for the
      // created comment to be propagated to the slave.
      db_ignore_slave();

      // Update the {node_comment_statistics} table prior to executing hooks.
      _comment_update_node_statistics($comment->nid);

      field_attach_insert('comment', $comment);

      // Tell the other modules a new comment has been submitted.
      module_invoke_all('comment_insert', $comment);
      module_invoke_all('entity_insert', $comment, 'comment');
    }
    if ($comment->status == COMMENT_PUBLISHED) {
      module_invoke_all('comment_publish', $comment);
    } 
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// This is the part I added 
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    else if ($comment->original->status != $comment->status && $comment->status == COMMENT_NOT_PUBLISHED) {
      module_invoke_all('comment_unpublish', $comment);
    }
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// End of added part
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    unset($comment->original);
  }
  catch (Exception $e) {
    $transaction->rollback('comment');
    watchdog_exception('comment', $e);
    throw $e;
  }

}

I am also wondering, that maybe the hook_comment_publish should not be called every time a comment is updated.. So I might suggest to also look into this...

Comments

GBurg created an issue. See original summary.

GBurg’s picture

Issue summary: View changes
cilefen’s picture

Title: hook_comment_unpublish is never fired » hook_comment_unpublish is never invoked
Category: Task » Bug report

Nice catch!

The way Drupal queue issues work is that you must create a patch. Please see instructions on creating a patch. Once you've posted a patch, please remove the code inclusion in the issue summary.

cilefen’s picture

cilefen’s picture

Issue tags: +Novice
rashid_786’s picture

Assigned: Unassigned » rashid_786

working on this part.

rashid_786’s picture

rashid_786’s picture

Assigned: rashid_786 » Unassigned
Status: Active » Needs review
joshi.rohit100’s picture

Status: Needs review » Needs work

Wrong patch.

rashid_786’s picture

Status: Needs work » Needs review
StatusFileSize
new532 bytes

Thanks @rohit for pointing!
Updated with new patch.

ajits’s picture

StatusFileSize
new535 bytes

Removing the tab from the patch.

joshi.rohit100’s picture

Looks fine to me now.

ajits’s picture

StatusFileSize
new534 bytes

We always use elseif instead of else if. Reference Coding standards: Control structures.

albertski’s picture

Status: Needs review » Reviewed & tested by the community

It's a RTBC for me!

GBurg’s picture

I am sorry guys, but I don't agree with the patch. Now it will fire, every time a comment is saved (see https://www.drupal.org/comment/7490166#comment-7490166 ) There should be:

if (($comment->original == null || $comment->original->status != $comment->status) && $comment->status == COMMENT_PUBLISHED) {
      module_invoke_all('comment_publish', $comment);
    } 
    elseif (($comment->original == null || $comment->original->status != $comment->status) && $comment->status == COMMENT_NOT_PUBLISHED) {
      module_invoke_all('comment_unpublish', $comment);
    }
joshi.rohit100’s picture

Issue summary: View changes

@GBurg:

I totally agree with you but I think, the basic idea is whenever a comment is created or updated, invoke the hook, instead of checking what is its current state and what is last state and there is any change then invoke

ajits’s picture

Issue summary: View changes
GBurg’s picture

@joshi The documentation says:

https://api.drupal.org/api/drupal/modules%21comment%21comment.api.php/fu...
The comment is being published by the moderator.

and

https://api.drupal.org/api/drupal/modules%21comment%21comment.api.php/fu...
The comment is being unpublished by the moderator.

The current behavior is that the hook is fired when the comment is updated. Not when it is being published or being unpublished. What for use do these hooks have, when you can just hook into hook_comment_update ? The hooks should only fire when the status changes!

joshi.rohit100’s picture

Gburg:

I have a scenerio, don't know valid or not. What happen if an administrative user (or user having comment moderation permission) try to update the published comment and publish that comment with changes in one go?
In that case I think this will not work. But as we can see, we are publishing the comment with changes.

Don't know if valid scenerio or not. Thoughts ?

GBurg’s picture

Hi Joshi,

If you make changes and update the status in one go, it would still fire the hook, as the code only checks if the status has changed.

if (($comment->original == null || $comment->original->status != $comment->status) && $comment->status == COMMENT_PUBLISHED) {
 //...
}
joshi.rohit100’s picture

but here status is not changed.

$comment->original->status = COMMENT_PUBLISHED

This is current comment status.

Now I edit comment and set status published, then

$comment->status == COMMENT_PUBLISHED

So both status are same. So this

($comment->original == null || $comment->original->status != $comment->status)

will turn out to be FALSE.

ajits’s picture

Status: Reviewed & tested by the community » Postponed
Related issues: +#2558629: hook_comment publish to be called only on the status change and not every time

I agree with #18. I think the publish and unpublish hooks should only be called when the event actually happens.
Currently, hook_comment_publish is called every time the comment is saved if the comment status is COMMENT_PUBLISHED.
This issue should be resolved first before progressing on the current issue. Created #2558629: hook_comment publish to be called only on the status change and not every time for the same. We will get back to this issue once the other is committed.

energee’s picture

@AjitS Unless comment_publish is changed, these should work the same way.Given that the hook is called "hook_comment_unpublish" it definitely insinuates that would be called when the comment "is unpublished".

joelpittet’s picture

Issue tags: -Novice

Removing Novice to triage.

owenbush’s picture

I have recreated the patch to take into account moving a comment to unpublished (triggering the unpublish event) vs just updating/saving a comment when it is already unpublished (which will not trigger the event).

I also made an effort to consolidate the same logic into the publish event, as that would trigger on every save of a published comment.

Version: 7.39 » 7.x-dev

Core issues are now filed against the dev versions where changes will be made. Document the specific release you are using in your issue comment. More information about choosing a version.

poker10’s picture

Status: Postponed » Needs review
Issue tags: +Needs tests

Thanks for the work here!

Personally I disagree with the #22 to focus on #2558629: hook_comment publish to be called only on the status change and not every time first and then this. I would switch it. This issue should only fix this hook to invoke as it was supposed to - see patch #13. This patch should be commited to D7, as it does not changes the current behavior.

Then we can decide to either update documentation (#1777166: hook_comment_publish() docs are completely wrong) or fix invoking of both hooks (#2558629: hook_comment publish to be called only on the status change and not every time), but the later one with change the behavior and can cause issues in existing sites, which can be problematic in D7 right now.

Therefore I am changing the status to Needs review and the second issue postponing until this one is done.

Patch #13 still applies and works, but we should add tests to cover invocation of these hooks.

poker10’s picture

Issue tags: -Needs tests
StatusFileSize
new3.34 KB
new3.87 KB
new3.01 KB

I have added the tests to the patch #13. Patch itself is unchanged. Uploading also the test-only version to show that the hook is not invoked now.

The last submitted patch, 28: 2554965-28_test-only.patch, failed testing. View results

poker10’s picture

Issue tags: +Needs change record
mcdruid’s picture

Status: Needs review » Reviewed & tested by the community

I agree with the approach in #27 / #28; I think it's correct to add the actual invocation of the unpublish hook to match the publish counterpart, and then consider whether the way the publish/unpublish hooks works should be tweaked after that.

Can we think of any negative consequences of this change? I suppose some sites / modules may have implemented the hook but it's never done anything. If it does suddenly start firing, could bad things happen? Perhaps, especially as - per the discussion in this issue and #1777166: hook_comment_publish() docs are completely wrong - the hook is (too) broad and doesn't only fire when the published state changes.

I'm inclined to think that adding the missing hook to work consistently is an appropriate fix to make though.

Changing the way the hooks work so that they only fire on a change of state would be a more risky alteration to D7's functionality at this stage.

Tests look good (and are consistent with the way that entity_crud_hook_test.test and its helper module work), thanks.

Definitely needs a Change Record.

mcdruid’s picture

  • mcdruid committed 2f9e3a1 on 7.x
    Issue #2554965 by poker10, AjitS, rashid_786, owenbush, GBurg, joshi....
mcdruid’s picture

Status: Reviewed & tested by the community » Fixed

Thanks everybody!

Debate about how these hooks actually work can continue in #1777166: hook_comment_publish() docs are completely wrong now that they are both implemented in a consistent - albeit questionable - way.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.