[wp-trac] [WordPress Trac] #40906: apply_filters skip all handlers in the next priority, when remove_filter is called

WordPress Trac noreply at wordpress.org
Fri Jun 2 02:26:52 UTC 2017


#40906: apply_filters skip all handlers in the next priority, when remove_filter is
called
--------------------------+-----------------------------
 Reporter:  wvega         |      Owner:
     Type:  defect (bug)  |     Status:  new
 Priority:  normal        |  Milestone:  Awaiting Review
Component:  Plugins       |    Version:  trunk
 Severity:  normal        |   Keywords:
  Focuses:                |
--------------------------+-----------------------------
 If a hook handler removes itself and it is the last handler for that hook
 at that priority, `apply_filters` will skip all handlers in the next
 priority.

 The conditions I identified as necessary to reproduce this issue are:

 1. The priority of the handler that removes itself must not be the first
 priority for that hook.
 2. The handler that removes itself must be the only handler registered for
 that priority at the time `remove_filter` is called.
 3. There must but handlers registered for the same hook using a priority
 greater than the priority of the handler that removes itself.

 To illustrate the problem better, please consider the following code:

 {{{#!php
 <?php

 public function test_do_action_with_handler_that_removes_itself() {
     $a = new MockAction();
     $b = new MockAction();

     $this->hook = new WP_Hook();

     $this->hook->add_filter( 'handler_that_removes_itself',
 '__return_null', 10, 1 );
     $this->hook->add_filter( 'handler_that_removes_itself', array( $this,
 '_handler_that_removes_itself' ), 20, 1 );
     $this->hook->add_filter( 'handler_that_removes_itself', array( $a,
 'action' ), 20 + 1, 1 );
     $this->hook->add_filter( 'handler_that_removes_itself', array( $b,
 'action' ), 20 + 2, 1 );

     $this->hook->do_action( array( null ) );

     $this->assertEquals( 1, $a->get_call_count(), 'One of the callbacks
 was never executed.' );
     $this->assertEquals( 1, $b->get_call_count() );
 }

 public function _handler_that_removes_itself() {
     $success = $this->hook->remove_filter( 'handler_that_removes_itself',
 array( $this, '_handler_that_removes_itself' ), 20 );
     $this->assertTrue( $success );
 }
 }}}

 The test above fails against [40871], because `$a->action` is never
 called.

 I took a look at #17817, but I don't think is a duplicate. I think it is
 an edge case for the modifications from [38571].

 The problem is that when a handler is removed, and there are no more
 handlers for the priority it was assigned to, `WP_Hook` calls
 `resort_active_iterations` leaving the array pointer on the position of
 the next priority. WordPress should continue calling the handlers in that
 position, but that's not what happens in this scenario. When
 `WP_Hook::apply_filters` executes `false !== next( $this->iterations[
 $nesting_level ] )` to figure out if there are more handlers to call, the
 pointer moves forward another position, completely skipping all handlers
 in the now current priority.

 I tried to solve this problem by setting the pointer of
 `$this->iterations[ $nesting_level ]` to the priority that was right
 before the priority used for `_handler_that_removes_itself`.

 That worked, but it also caused
 `Tests_WP_Hook_Add_Filter::test_remove_and_add_last_filter` to go into an
 infinite loop.

 Then, I found out that keeping the `$this->callbacks[ $priority ]` array,
 even after all handlers for that priority are removed, fixes the issue and
 causes no tests in the **hooks** group to break.

 I'm including a patch with the modifications that worked for me and the
 test from the beginning.

 Do you think that's a suitable solution?

--
Ticket URL: <https://core.trac.wordpress.org/ticket/40906>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list