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
Comment #2
GBurg commentedComment #3
cilefen commentedNice 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.
Comment #4
cilefen commentedThis was being discussed in #1777166-6: hook_comment_publish() docs are completely wrong
Comment #5
cilefen commentedComment #6
rashid_786 commentedworking on this part.
Comment #7
rashid_786 commentedComment #8
rashid_786 commentedComment #9
joshi.rohit100Wrong patch.
Comment #10
rashid_786 commentedThanks @rohit for pointing!
Updated with new patch.
Comment #11
ajitsRemoving the tab from the patch.
Comment #12
joshi.rohit100Looks fine to me now.
Comment #13
ajitsWe always use
elseifinstead ofelse if. Reference Coding standards: Control structures.Comment #14
albertski commentedIt's a RTBC for me!
Comment #15
GBurg commentedI 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:
Comment #16
joshi.rohit100@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
Comment #17
ajitsComment #18
GBurg commented@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!
Comment #19
joshi.rohit100Gburg:
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 ?
Comment #20
GBurg commentedHi 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.
Comment #21
joshi.rohit100but here status is not changed.
$comment->original->status = COMMENT_PUBLISHEDThis is current comment status.
Now I edit comment and set status published, then
$comment->status == COMMENT_PUBLISHEDSo both status are same. So this
($comment->original == null || $comment->original->status != $comment->status)will turn out to be FALSE.
Comment #22
ajitsI agree with #18. I think the publish and unpublish hooks should only be called when the event actually happens.
Currently,
hook_comment_publishis called every time the comment is saved if the comment status isCOMMENT_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.
Comment #23
energee commented@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".
Comment #24
joelpittetRemoving Novice to triage.
Comment #25
owenbush commentedI 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.
Comment #27
poker10 commentedThanks 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.
Comment #28
poker10 commentedI 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.
Comment #30
poker10 commentedComment #31
mcdruid commentedI 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.
Comment #32
mcdruid commentedDraft CR: https://www.drupal.org/node/3324532
Comment #34
mcdruid commentedThanks 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.