Divi WordPress Them — the smartest and most flexible WordPress theme

Back to Top

remove_filter() and remove_action()’s bug, bad for developers

Previous Post:

remove_filter() and remove_action()’s bug, bad for developers

Developers who have used remove_filter() or remove_action() probably won’t notice there’s a bug that mysteriously makes those two functions break other filters or actions defined after they are used. The reason is unclear at the moment and bug reports have been fired but no milestone set for future releases, so I will just post the bug here and provide a (probably) neat workaround that allows you to bypass this bug.

To reproduce the bug, you need something as simple as this:

  1. function run_once() {
  2.     remove_filter('foobar', __FUNCTION__);
  3.     return 'test';
  4. }
  5. add_filter('foobar', 'run_once'); // by default, the priority is 10
  6.  
  7. function another_filter() {
  8.     return 'test again';
  9. }
  10. add_filter('foobar', 'another_filter', 11);
  11.  
  12. echo apply_filters('foobar', '');
function run_once() {
	remove_filter('foobar', __FUNCTION__);
	return 'test';
}
add_filter('foobar', 'run_once'); // by default, the priority is 10

function another_filter() {
	return 'test again';
}
add_filter('foobar', 'another_filter', 11);

echo apply_filters('foobar', '');

The output is ‘test’, which is basically not what we wanted (should be ‘test again’).

The run_once() filter is called at priority 10 and then gets removed so it won’t be accidentally called again. another_filter() is called at priority 11 and should change the contents filtered by run_once(), but for some reasons it doesn’t.

As you might have guessed, another_filter() never gets called. It has somehow been removed by the function remove_filter()1 (line 2), which should have removed run_once() only. Why on earth does this happen and how badly can it affect us developers?

Actually I found out about this bug while trying to integrate one of my plugins, BWP reCAPTCHA, with Akismet. Basically when a comment is considered spam by Akismet, it will be marked as spam in akismet.php:

  1. // filter handler used to return a spam result to pre_comment_approved
  2. function akismet_result_spam( $approved ) {
  3.     // bump the counter here instead of when the filter is added to reduce the possibility of overcounting
  4.     if ( $incr = apply_filters('akismet_spam_count_incr', 1) )
  5.         update_option( 'akismet_spam_count', get_option('akismet_spam_count') + $incr );
  6.     // this is a one-shot deal
  7.     remove_filter( 'pre_comment_approved', 'akismet_result_spam' );
  8.     return 'spam';
  9. }
// filter handler used to return a spam result to pre_comment_approved
function akismet_result_spam( $approved ) {
	// bump the counter here instead of when the filter is added to reduce the possibility of overcounting
	if ( $incr = apply_filters('akismet_spam_count_incr', 1) )
		update_option( 'akismet_spam_count', get_option('akismet_spam_count') + $incr );
	// this is a one-shot deal
	remove_filter( 'pre_comment_approved', 'akismet_result_spam' );
	return 'spam';
}

akismet_result_spam is actually added on line 348:

  1. add_filter('pre_comment_approved', 'akismet_result_spam');
add_filter('pre_comment_approved', 'akismet_result_spam');

BWP reCAPTCHA tries to ‘remark’ the comment as either ‘approved’ or ‘hold’ upon successful completion of reCAPTCHA by adding a filter with a priority of 11 to override akismet_result_spam:

  1. add_filter('pre_comment_approved', 'akismet_comment_status', 11);
add_filter('pre_comment_approved', 'akismet_comment_status', 11);

As a result of remove_filter() on line 181 in akismet.php and the currently discussed bug, the filter I use to remark the comment status above will surely fail.

This simply means that if a plugin uses remove_filter(), there’s a possibility that you will not be able to add a filter with higher priority to change the final output.

This bug also affects remove_action()2 and you can reproduce it using a similar snippet:

  1. function run_once() {
  2.     remove_action('foobar', __FUNCTION__);
  3.     echo 'test';
  4. }
  5. add_action('foobar', 'run_once'); // by default, the priority is 10
  6.  
  7. function another_action() {
  8.     echo 'test again';
  9. }
  10. add_action('foobar', 'another_action', 11);
  11.  
  12. do_action('foobar', '');
function run_once() {
	remove_action('foobar', __FUNCTION__);
	echo 'test';
}
add_action('foobar', 'run_once'); // by default, the priority is 10

function another_action() {
	echo 'test again';
}
add_action('foobar', 'another_action', 11);

do_action('foobar', '');

The output is ‘test’ which is simply wrong.

The Workaround

If you haven’t noticed it already, there is one similarity in the way our snippets and Akismet use add_filter(), as described below:

  1. // our snippet
  2. add_filter('foobar', 'run_once');
  3. // akismet.php: 348
  4. add_filter('pre_comment_approved', 'akismet_result_spam');
// our snippet
add_filter('foobar', 'run_once');
// akismet.php: 348
add_filter('pre_comment_approved', 'akismet_result_spam');

that is: no priority is specified, which basically means they both execute at the default priority of 10. However, a quick test like this:

  1. function run_once() {
  2.     remove_filter('foobar', __FUNCTION__, 11); // we need to remove the filter at correct priority, as per the documentation1
  3.     return 'test';
  4. }
  5. add_filter('foobar', 'run_once', 11); // priority is set to something different than 10
  6.  
  7. function another_filter() {
  8.     return 'test again';
  9. }
  10. add_filter('foobar', 'another_filter', 12);
  11.  
  12. echo apply_filters('foobar', '');
function run_once() {
	remove_filter('foobar', __FUNCTION__, 11); // we need to remove the filter at correct priority, as per the documentation1
	return 'test';
}
add_filter('foobar', 'run_once', 11); // priority is set to something different than 10

function another_filter() {
	return 'test again';
}
add_filter('foobar', 'another_filter', 12);

echo apply_filters('foobar', '');

outputs ‘test’ which is again not what we want. This tells us that even with priority specified and different than 10, remove_filter() will still remove our filter at priority 12.

Is there no way to work around this then?

Fortunately, after scratching my head over this issue for hours I have found another workaround which again relates to priority:

  1. function run_once() {
  2.     remove_filter('foobar', __FUNCTION__);
  3.     return 'test';
  4. }
  5. add_filter('foobar', 'run_once'); // again we use the default priority of 10
  6.  
  7. function another_filter() {
  8.     return 'test again';
  9. }
  10. add_filter('foobar', 'another_filter', 10);
  11. add_filter('foobar', 'another_filter', 11);
  12.  
  13. echo apply_filters('foobar', '');
function run_once() {
	remove_filter('foobar', __FUNCTION__);
	return 'test';
}
add_filter('foobar', 'run_once'); // again we use the default priority of 10

function another_filter() {
	return 'test again';
}
add_filter('foobar', 'another_filter', 10);
add_filter('foobar', 'another_filter', 11);

echo apply_filters('foobar', '');

I think you get the idea: we simply add the same filters two times with the first time’s filter having a priority of 10 while the second time’s having a priority greater than 10. The output is ‘test again’ as desired and we do not need to modify any third party codes like the quick test above, which is simply amazing.

If you are a developers and are interested in this bug, you can follow a bug report here: http://core.trac.wordpress.org/ticket/9968. This bug is pretty hilarious in my opinion and I hope the workaround might help you with your work. ‘Til next time!

References

  1. http://codex.wordpress.org/Function_Reference/remove_fi ... ove_filter [] [] []
  2. http://codex.wordpress.org/Function_Reference/remove_ac ... ove_action []
Print Article Trackback Trackback to this Article   Subscribe to Comments RSS Subscribe to Comments RSS

2 Opinions for remove_filter() and remove_action()’s bug, bad for developers (1 Trackback)

  1. User's Gravatar
    2
    Stephen Harris March 2, 2013 at 3:30 am – Permalink

    Thanks for posting this. Two years on and its still an issue. I came it across it too while debugging a plug-in conflict. I posted a gist of it here: https://gist.github.com/stephenharris/5063965 – though having not looked at the source until I read this article, I slightly misunderstood the bug. It seemed like it was removing callbacks with priority 10.

    It’s a bug of the worst kind – completely obscure and utterly ridiculous.

  1. Show latest Posts from each Post Type - Better WordPress

    [...] Has a specific priority to remove the filter later - @see3 add_filter('posts_groupby', 'bwp_posts_groupby', 11); function bwp_posts_groupby() { return [...]

Speak Up Your Mind!

An asterisk (*) indicates a required field and must be filled.




  • Web page and e-mail addresses turn into links automatically.
  • Wrap codes in: <code lang=""></code> or <pre lang="" extra="">
  • Lines and paragraphs break automatically.

Next Post: