Make WordPress Core

Ticket #17817: 17817.4.diff

File 17817.4.diff, 50.1 KB (added by jorbin, 9 years ago)
  • tests/phpunit/tests/hooks/getIterator.php

     
     1<?php
     2
     3/**
     4 * Test the IteratorAggregate implementation of WP_Hook
     5 *
     6 * @group hooks
     7 */
     8class Tests_WP_Hook_Get_Iterator extends WP_UnitTestCase {
     9       
     10        public function test_getIterator() {
     11                $callback_one = '__return_null';
     12                $callback_two = '__return_false';
     13                $hook = new WP_Hook();
     14                $tag = rand_str();
     15                $priority = rand( 1, 100 );
     16                $accepted_args = rand( 1, 100 );
     17
     18                $hook->add_filter( $callback_one, $priority, $accepted_args, $tag );
     19                $hook->add_filter( $callback_two, $priority + 1, $accepted_args, $tag );
     20
     21                $callbacks = $hook->getIterator();
     22
     23                $this->assertCount( 2, $callbacks );
     24                $this->assertTrue( isset( $callbacks[ $priority ] ) );
     25                $this->assertTrue( isset( $callbacks[ $priority + 1 ] ) );
     26
     27
     28                $callback_one_index = _wp_filter_build_unique_id( $tag, $callback_one, $priority );
     29                $callback_two_index = _wp_filter_build_unique_id( $tag, $callback_two, $priority + 1 );
     30
     31                $this->assertEquals( $callback_one, $callbacks[ $priority ][ $callback_one_index ][ 'function' ] );
     32                $this->assertEquals( $callback_two, $callbacks[ $priority + 1 ][ $callback_two_index ][ 'function' ] );
     33        }
     34
     35
     36        public function test_foreach() {
     37                $callback_one = '__return_null';
     38                $callback_two = '__return_false';
     39                $hook = new WP_Hook();
     40                $tag = rand_str();
     41                $priority = rand( 1, 100 );
     42                $accepted_args = rand( 1, 100 );
     43
     44                $hook->add_filter( $callback_one, $priority, $accepted_args, $tag );
     45                $hook->add_filter( $callback_two, $priority + 1, $accepted_args, $tag );
     46
     47                $functions = array();
     48                $priorities = array();
     49                foreach ( $hook as $key => $callbacks ) {
     50                        $priorities[] = $key;
     51                        foreach ( $callbacks as $function_index => $the_ ) {
     52                                $functions[] = $the_['function'];
     53                        }
     54                }
     55                $this->assertEqualSets( array( $priority, $priority + 1 ), $priorities );
     56                $this->assertEqualSets( array( $callback_one, $callback_two ), $functions );
     57        }
     58}
     59 No newline at end of file
  • tests/phpunit/tests/hooks/remove_all_filters.php

     
     1<?php
     2
     3/**
     4 * Test the remove_all_filters method of WP_Hook
     5 *
     6 * @group hooks
     7 */
     8class Tests_WP_Hook_Remove_All_Filters extends WP_UnitTestCase {
     9       
     10        public function test_remove_all_filters() {
     11                $callback = '__return_null';
     12                $hook = new WP_Hook();
     13                $tag = rand_str();
     14                $priority = rand( 1, 100 );
     15                $accepted_args = rand( 1, 100 );
     16
     17                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     18
     19                $hook->remove_all_filters();
     20
     21                $this->assertFalse( $hook->has_filters() );
     22        }
     23
     24        public function test_remove_all_filters_with_priority() {
     25                $callback_one = '__return_null';
     26                $callback_two = '__return_false';
     27                $hook = new WP_Hook();
     28                $tag = rand_str();
     29                $priority = rand( 1, 100 );
     30                $accepted_args = rand( 1, 100 );
     31
     32                $hook->add_filter( $callback_one, $priority, $accepted_args, $tag );
     33                $hook->add_filter( $callback_two, $priority + 1, $accepted_args, $tag );
     34
     35                $hook->remove_all_filters( $priority );
     36
     37                $this->assertFalse( $hook->has_filter( $callback_one, $tag ) );
     38                $this->assertTrue( $hook->has_filters() );
     39                $this->assertEquals( $priority + 1, $hook->has_filter( $callback_two, $tag ) );
     40        }
     41}
     42 No newline at end of file
  • tests/phpunit/tests/hooks/apply_filters.php

     
     1<?php
     2
     3/**
     4 * Test the apply_filters method of WP_Hook
     5 *
     6 * @group hooks
     7 */
     8class Tests_WP_Hook_Apply_Filters extends WP_UnitTestCase {
     9
     10        public function test_apply_filters_with_callback() {
     11                $a = new MockAction();
     12                $callback = array( $a, 'filter' );
     13                $hook = new WP_Hook();
     14                $tag = rand_str();
     15                $priority = rand( 1, 100 );
     16                $accepted_args = rand( 1, 100 );
     17                $arg = rand_str();
     18
     19                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     20
     21                $returned = $hook->apply_filters( $arg, array( $arg ) );
     22
     23                $this->assertEquals( $returned, $arg );
     24                $this->assertEquals( 1, $a->get_call_count() );
     25        }
     26
     27        public function test_apply_filters_with_multiple_calls() {
     28                $a = new MockAction();
     29                $callback = array( $a, 'filter' );
     30                $hook = new WP_Hook();
     31                $tag = rand_str();
     32                $priority = rand( 1, 100 );
     33                $accepted_args = rand( 1, 100 );
     34                $arg = rand_str();
     35
     36                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     37
     38                $returned_one = $hook->apply_filters( $arg, array( $arg ) );
     39                $returned_two = $hook->apply_filters( $returned_one, array( $returned_one ) );
     40
     41                $this->assertEquals( $returned_two, $arg );
     42                $this->assertEquals( 2, $a->get_call_count() );
     43        }
     44
     45}
     46 No newline at end of file
  • tests/phpunit/tests/hooks/add_filter.php

     
     1<?php
     2
     3/**
     4 * Test the add_filter method of WP_Hook
     5 *
     6 * @group hooks
     7 */
     8class Tests_WP_Hook_Add_Filter extends WP_UnitTestCase {
     9
     10        public function test_add_filter_with_function() {
     11                $callback = '__return_null';
     12                $hook = new WP_Hook();
     13                $tag = rand_str();
     14                $priority = rand( 1, 100 );
     15                $accepted_args = rand( 1, 100 );
     16
     17                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     18
     19                $function_index = _wp_filter_build_unique_id( $tag, $callback, $priority );
     20                $this->assertEquals( $callback, $hook->callbacks[ $priority ][ $function_index ]['function'] );
     21                $this->assertEquals( $accepted_args, $hook->callbacks[ $priority ][ $function_index ]['accepted_args'] );
     22        }
     23
     24        public function test_add_filter_with_object() {
     25                $a = new MockAction();
     26                $callback = array( $a, 'action' );
     27                $hook = new WP_Hook();
     28                $tag = rand_str();
     29                $priority = rand( 1, 100 );
     30                $accepted_args = rand( 1, 100 );
     31
     32                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     33
     34                $function_index = _wp_filter_build_unique_id( $tag, $callback, $priority );
     35                $this->assertEquals( $callback, $hook->callbacks[ $priority ][ $function_index ]['function'] );
     36                $this->assertEquals( $accepted_args, $hook->callbacks[ $priority ][ $function_index ]['accepted_args'] );
     37        }
     38
     39        public function test_add_filter_with_static_method() {
     40                $callback = array( 'MockAction', 'action' );
     41                $hook = new WP_Hook();
     42                $tag = rand_str();
     43                $priority = rand( 1, 100 );
     44                $accepted_args = rand( 1, 100 );
     45
     46                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     47
     48                $function_index = _wp_filter_build_unique_id( $tag, $callback, $priority );
     49                $this->assertEquals( $callback, $hook->callbacks[ $priority ][ $function_index ]['function'] );
     50                $this->assertEquals( $accepted_args, $hook->callbacks[ $priority ][ $function_index ]['accepted_args'] );
     51        }
     52
     53        public function test_add_two_filters_with_same_priority() {
     54                $callback_one = '__return_null';
     55                $callback_two = '__return_false';
     56                $hook = new WP_Hook();
     57                $tag = rand_str();
     58                $priority = rand( 1, 100 );
     59                $accepted_args = rand( 1, 100 );
     60
     61                $hook->add_filter( $callback_one, $priority, $accepted_args, $tag );
     62                $this->assertCount( 1, $hook->callbacks[ $priority ] );
     63
     64                $hook->add_filter( $callback_two, $priority, $accepted_args, $tag );
     65                $this->assertCount( 2, $hook->callbacks[ $priority ] );
     66        }
     67
     68        public function test_add_two_filters_with_different_priority() {
     69                $callback_one = '__return_null';
     70                $callback_two = '__return_false';
     71                $hook = new WP_Hook();
     72                $tag = rand_str();
     73                $priority = rand( 1, 100 );
     74                $accepted_args = rand( 1, 100 );
     75
     76                $hook->add_filter( $callback_one, $priority, $accepted_args, $tag );
     77                $this->assertCount( 1, $hook->callbacks[ $priority ] );
     78
     79                $hook->add_filter( $callback_two, $priority + 1, $accepted_args, $tag );
     80                $this->assertCount( 1, $hook->callbacks[ $priority ] );
     81                $this->assertCount( 1, $hook->callbacks[ $priority + 1 ] );
     82        }
     83
     84        public function test_readd_filter() {
     85                $callback = '__return_null';
     86                $hook = new WP_Hook();
     87                $tag = rand_str();
     88                $priority = rand( 1, 100 );
     89                $accepted_args = rand( 1, 100 );
     90
     91                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     92                $this->assertCount( 1, $hook->callbacks[ $priority ] );
     93
     94                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     95                $this->assertCount( 1, $hook->callbacks[ $priority ] );
     96        }
     97
     98        public function test_readd_filter_with_different_priority() {
     99                $callback = '__return_null';
     100                $hook = new WP_Hook();
     101                $tag = rand_str();
     102                $priority = rand( 1, 100 );
     103                $accepted_args = rand( 1, 100 );
     104
     105                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     106                $this->assertCount( 1, $hook->callbacks[ $priority ] );
     107
     108                $hook->add_filter( $callback, $priority + 1, $accepted_args, $tag );
     109                $this->assertCount( 1, $hook->callbacks[ $priority ] );
     110                $this->assertCount( 1, $hook->callbacks[ $priority + 1 ] );
     111        }
     112
     113        public function test_sort_after_add_filter() {
     114                $a = new MockAction();
     115                $b = new MockAction();
     116                $c = new MockAction();
     117                $hook = new WP_Hook();
     118                $tag = rand_str();
     119
     120                $hook->add_filter( array( $a, 'action' ), 10, 1, $tag );
     121                $hook->add_filter( array( $b, 'action' ), 5, 1, $tag );
     122                $hook->add_filter( array( $c, 'action' ), 8, 1, $tag );
     123
     124                $this->assertEquals( array( 5, 8, 10 ), array_keys( $hook->callbacks ) );
     125        }
     126}
     127 No newline at end of file
  • tests/phpunit/tests/hooks/do_action.php

     
     1<?php
     2
     3/**
     4 * Test the do_action method of WP_Hook
     5 *
     6 * @group hooks
     7 */
     8class Tests_WP_Hook_Do_Action extends WP_UnitTestCase {
     9        private $events = array();
     10
     11        public function setUp() {
     12                parent::setUp();
     13                $this->events = array();
     14        }
     15
     16        public function test_do_action_with_callback() {
     17                $a = new MockAction();
     18                $callback = array( $a, 'action' );
     19                $hook = new WP_Hook();
     20                $tag = rand_str();
     21                $priority = rand( 1, 100 );
     22                $accepted_args = rand( 1, 100 );
     23                $arg = rand_str();
     24
     25                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     26                $hook->do_action( array( $arg ) );
     27
     28                $this->assertEquals( 1, $a->get_call_count() );
     29        }
     30
     31        public function test_do_action_with_multiple_calls() {
     32                $a = new MockAction();
     33                $callback = array( $a, 'filter' );
     34                $hook = new WP_Hook();
     35                $tag = rand_str();
     36                $priority = rand( 1, 100 );
     37                $accepted_args = rand( 1, 100 );
     38                $arg = rand_str();
     39
     40                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     41                $hook->do_action( array( $arg ) );
     42                $hook->do_action( array( $arg ) );
     43
     44                $this->assertEquals( 2, $a->get_call_count() );
     45        }
     46
     47        public function test_do_action_with_multiple_callbacks_on_same_priority() {
     48                $a = new MockAction();
     49                $b = new MockAction();
     50                $callback_one = array( $a, 'filter' );
     51                $callback_two = array( $b, 'filter' );
     52                $hook = new WP_Hook();
     53                $tag = rand_str();
     54                $priority = rand( 1, 100 );
     55                $accepted_args = rand( 1, 100 );
     56                $arg = rand_str();
     57
     58                $hook->add_filter( $callback_one, $priority, $accepted_args, $tag );
     59                $hook->add_filter( $callback_two, $priority, $accepted_args, $tag );
     60                $hook->do_action( array( $arg ) );
     61
     62                $this->assertEquals( 1, $a->get_call_count() );
     63                $this->assertEquals( 1, $a->get_call_count() );
     64        }
     65
     66        public function test_do_action_with_multiple_callbacks_on_different_priorities() {
     67                $a = new MockAction();
     68                $b = new MockAction();
     69                $callback_one = array( $a, 'filter' );
     70                $callback_two = array( $b, 'filter' );
     71                $hook = new WP_Hook();
     72                $tag = rand_str();
     73                $priority = rand( 1, 100 );
     74                $accepted_args = rand( 1, 100 );
     75                $arg = rand_str();
     76
     77                $hook->add_filter( $callback_one, $priority, $accepted_args, $tag );
     78                $hook->add_filter( $callback_two, $priority, $accepted_args, $tag );
     79                $hook->do_action( array( $arg ) );
     80
     81                $this->assertEquals( 1, $a->get_call_count() );
     82                $this->assertEquals( 1, $a->get_call_count() );
     83        }
     84
     85        public function test_do_action_with_no_accepted_args() {
     86                $callback = array( $this, '_action_callback' );
     87                $hook = new WP_Hook();
     88                $tag = rand_str();
     89                $priority = rand( 1, 100 );
     90                $accepted_args = 0;
     91                $arg = rand_str();
     92
     93                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     94                $hook->do_action( array( $arg ) );
     95
     96                $this->assertEmpty( $this->events[0]['args'] );
     97        }
     98
     99        public function test_do_action_with_one_accepted_arg() {
     100                $callback = array( $this, '_action_callback' );
     101                $hook = new WP_Hook();
     102                $tag = rand_str();
     103                $priority = rand( 1, 100 );
     104                $accepted_args = 1;
     105                $arg = rand_str();
     106
     107                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     108                $hook->do_action( array( $arg ) );
     109
     110                $this->assertCount( 1, $this->events[0]['args'] );
     111        }
     112
     113        public function test_do_action_with_more_accepted_args() {
     114                $callback = array( $this, '_action_callback' );
     115                $hook = new WP_Hook();
     116                $tag = rand_str();
     117                $priority = rand( 1, 100 );
     118                $accepted_args = 1000;
     119                $arg = rand_str();
     120
     121                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     122                $hook->do_action( array( $arg ) );
     123
     124                $this->assertCount( 1, $this->events[0]['args'] );
     125        }
     126
     127        /**
     128         * Use this rather than MockAction so we can test callbacks with no args
     129         */
     130        public function _action_callback() {
     131                $args = func_get_args();
     132                $this->events[] = array('action' => __FUNCTION__, 'args'=>$args);
     133        }
     134}
     135 No newline at end of file
  • tests/phpunit/tests/hooks/has_filter.php

     
     1<?php
     2
     3/**
     4 * Test the has_filter method of WP_Hook
     5 *
     6 * @group hooks
     7 */
     8class Tests_WP_Hook_Has_Filter extends WP_UnitTestCase {
     9       
     10        public function test_has_filter_with_function() {
     11                $callback = '__return_null';
     12                $hook = new WP_Hook();
     13                $tag = rand_str();
     14                $priority = rand( 1, 100 );
     15                $accepted_args = rand( 1, 100 );
     16
     17                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     18
     19                $this->assertEquals( $priority, $hook->has_filter( $callback, $tag ) );
     20        }
     21
     22        public function test_has_filter_with_object() {
     23                $a = new MockAction();
     24                $callback = array( $a, 'action' );
     25                $hook = new WP_Hook();
     26                $tag = rand_str();
     27                $priority = rand( 1, 100 );
     28                $accepted_args = rand( 1, 100 );
     29
     30                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     31
     32                $this->assertEquals( $priority, $hook->has_filter( $callback, $tag ) );
     33        }
     34
     35        public function test_has_filter_with_static_method() {
     36                $callback = array( 'MockAction', 'action' );
     37                $hook = new WP_Hook();
     38                $tag = rand_str();
     39                $priority = rand( 1, 100 );
     40                $accepted_args = rand( 1, 100 );
     41
     42                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     43
     44                $this->assertEquals( $priority, $hook->has_filter( $callback, $tag ) );
     45        }
     46
     47        public function test_has_filter_without_callback() {
     48                $callback = '__return_null';
     49                $hook = new WP_Hook();
     50                $tag = rand_str();
     51                $priority = rand( 1, 100 );
     52                $accepted_args = rand( 1, 100 );
     53
     54                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     55
     56                $this->assertTrue( $hook->has_filter() );
     57        }
     58
     59        public function test_not_has_filter_without_callback() {
     60                $hook = new WP_Hook();
     61                $this->assertFalse( $hook->has_filter() );
     62        }
     63
     64        public function test_not_has_filter_with_callback() {
     65                $callback = '__return_null';
     66                $hook = new WP_Hook();
     67                $tag = rand_str();
     68
     69                $this->assertFalse( $hook->has_filter( $callback, $tag ) );
     70        }
     71
     72        public function test_has_filter_with_wrong_callback() {
     73                $callback = '__return_null';
     74                $hook = new WP_Hook();
     75                $tag = rand_str();
     76                $priority = rand( 1, 100 );
     77                $accepted_args = rand( 1, 100 );
     78
     79                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     80
     81                $this->assertFalse( $hook->has_filter( '__return_false', $tag ) );
     82        }
     83}
     84 No newline at end of file
  • tests/phpunit/tests/hooks/remove_filter.php

     
     1<?php
     2
     3/**
     4 * Test the remove_filter method of WP_Hook
     5 *
     6 * @group hooks
     7 */
     8class Tests_WP_Hook_Remove_Filter extends WP_UnitTestCase {
     9       
     10        public function test_remove_filter_with_function() {
     11                $callback = '__return_null';
     12                $hook = new WP_Hook();
     13                $tag = rand_str();
     14                $priority = rand( 1, 100 );
     15                $accepted_args = rand( 1, 100 );
     16
     17                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     18                $hook->remove_filter( $callback, $priority, $tag );
     19
     20                $this->assertFalse( isset( $hook->callbacks[ $priority ] ) );
     21        }
     22
     23        public function test_remove_filter_with_object() {
     24                $a = new MockAction();
     25                $callback = array( $a, 'action' );
     26                $hook = new WP_Hook();
     27                $tag = rand_str();
     28                $priority = rand( 1, 100 );
     29                $accepted_args = rand( 1, 100 );
     30
     31                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     32                $hook->remove_filter( $callback, $priority, $tag );
     33
     34                $this->assertFalse( isset( $hook->callbacks[ $priority ] ) );
     35        }
     36
     37        public function test_remove_filter_with_static_method() {
     38                $callback = array( 'MockAction', 'action' );
     39                $hook = new WP_Hook();
     40                $tag = rand_str();
     41                $priority = rand( 1, 100 );
     42                $accepted_args = rand( 1, 100 );
     43
     44                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     45                $hook->remove_filter( $callback, $priority, $tag );
     46
     47                $this->assertFalse( isset( $hook->callbacks[ $priority ] ) );
     48        }
     49
     50        public function test_remove_filters_with_another_at_same_priority() {
     51                $callback_one = '__return_null';
     52                $callback_two = '__return_false';
     53                $hook = new WP_Hook();
     54                $tag = rand_str();
     55                $priority = rand( 1, 100 );
     56                $accepted_args = rand( 1, 100 );
     57
     58                $hook->add_filter( $callback_one, $priority, $accepted_args, $tag );
     59                $hook->add_filter( $callback_two, $priority, $accepted_args, $tag );
     60
     61                $hook->remove_filter( $callback_one, $priority, $tag );
     62
     63                $this->assertCount( 1, $hook->callbacks[ $priority ] );
     64        }
     65
     66        public function test_remove_filter_with_another_at_different_priority() {
     67                $callback_one = '__return_null';
     68                $callback_two = '__return_false';
     69                $hook = new WP_Hook();
     70                $tag = rand_str();
     71                $priority = rand( 1, 100 );
     72                $accepted_args = rand( 1, 100 );
     73
     74                $hook->add_filter( $callback_one, $priority, $accepted_args, $tag );
     75                $hook->add_filter( $callback_two, $priority + 1, $accepted_args, $tag );
     76
     77                $hook->remove_filter( $callback_one, $priority, $tag );
     78                $this->assertFalse( isset( $hook->callbacks[ $priority ] ) );
     79                $this->assertCount( 1, $hook->callbacks[ $priority + 1 ] );
     80        }
     81}
     82 No newline at end of file
  • tests/phpunit/tests/hooks/has_filters.php

     
     1<?php
     2
     3/**
     4 * Test the has_filters method of WP_Hook
     5 *
     6 * @group hooks
     7 */
     8class Tests_WP_Hook_Has_Filters extends WP_UnitTestCase {
     9       
     10        public function test_has_filters_with_callback() {
     11                $callback = '__return_null';
     12                $hook = new WP_Hook();
     13                $tag = rand_str();
     14                $priority = rand( 1, 100 );
     15                $accepted_args = rand( 1, 100 );
     16
     17                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     18
     19                $this->assertTrue( $hook->has_filters() );
     20        }
     21
     22        public function test_has_filters_without_callback() {
     23                $hook = new WP_Hook();
     24                $this->assertFalse( $hook->has_filters() );
     25        }
     26
     27        public function test_not_has_filters_with_removed_callback() {
     28                $callback = '__return_null';
     29                $hook = new WP_Hook();
     30                $tag = rand_str();
     31                $priority = rand( 1, 100 );
     32                $accepted_args = rand( 1, 100 );
     33
     34                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     35                $hook->remove_filter( $callback, $priority, $tag );
     36                $this->assertFalse( $hook->has_filters() );
     37        }
     38
     39        public function test_not_has_filter_with_directly_removed_callback() {
     40                $callback = '__return_null';
     41                $hook = new WP_Hook();
     42                $tag = rand_str();
     43                $priority = rand( 1, 100 );
     44                $accepted_args = rand( 1, 100 );
     45
     46                $hook->add_filter( $callback, $priority, $accepted_args, $tag );
     47                $function_key = _wp_filter_build_unique_id( $tag, $callback, $priority );
     48                unset( $hook->callbacks[ $priority ][ $function_key ] );
     49
     50                $this->assertFalse( $hook->has_filters() );
     51        }
     52}
     53 No newline at end of file
  • tests/phpunit/tests/actions.php

     
    114114                $this->assertEquals( array( $val1 ), array_pop( $argsvar2 ) );
    115115        }
    116116
     117        /**
     118         * Test that multiple callbacks receive the correct number of args even when the number
     119         * is less than, or greater than previous hooks.
     120         *
     121         * @see https://core.trac.wordpress.org/ticket/17817#comment:72
     122         * @ticket 17817
     123         */
     124        function test_action_args_3() {
     125                $a1 = new MockAction();
     126                $a2 = new MockAction();
     127                $a3 = new MockAction();
     128                $tag = rand_str();
     129                $val1 = rand_str();
     130                $val2 = rand_str();
     131
     132                // a1 accepts two arguments, a2 doesn't, a3 accepts two arguments
     133                add_action( $tag, array( &$a1, 'action' ), 10, 2 );
     134                add_action( $tag, array( &$a2, 'action' ) );
     135                add_action( $tag, array( &$a3, 'action' ), 10, 2 );
     136                // call the action with two arguments
     137                do_action( $tag, $val1, $val2 );
     138
     139                $call_count = $a1->get_call_count();
     140                // a1 should be called with both args
     141                $this->assertEquals( 1, $call_count );
     142                $argsvar1 = $a1->get_args();
     143                $this->assertEquals( array( $val1, $val2 ), array_pop( $argsvar1 ) );
     144
     145                // a2 should be called with one only
     146                $this->assertEquals( 1, $a2->get_call_count() );
     147                $argsvar2 = $a2->get_args();
     148                $this->assertEquals( array( $val1 ), array_pop( $argsvar2 ) );
     149
     150                // a3 should be called with both args
     151                $this->assertEquals( 1, $a3->get_call_count() );
     152                $argsvar3 = $a3->get_args();
     153                $this->assertEquals( array( $val1, $val2 ), array_pop( $argsvar3 ) );
     154        }
     155
    117156        function test_action_priority() {
    118157                $a = new MockAction();
    119158                $tag = rand_str();
     
    258297        }
    259298
    260299        /**
     300         * @ticket 17817
     301         */
     302        function test_action_recursion() {
     303                $tag = rand_str();
     304                $a = new MockAction();
     305                $b = new MockAction();
     306
     307                add_action( $tag, array( $a, 'action' ), 11, 1 );
     308                add_action( $tag, array( $b, 'action' ), 13, 1 );
     309                add_action( $tag, array( $this, 'action_that_causes_recursion' ), 12, 1 );
     310                do_action( $tag, $tag );
     311
     312                $this->assertEquals( 2, $a->get_call_count(), 'recursive actions should call all callbacks with earlier priority' );
     313                $this->assertEquals( 2, $b->get_call_count(), 'recursive actions should call callbacks with later priority' );
     314        }
     315
     316        function action_that_causes_recursion( $tag ) {
     317                static $recursing = false;
     318                if ( ! $recursing ) {
     319                        $recursing = true;
     320                        do_action( $tag, $tag );
     321                }
     322                $recursing = false;
     323        }
     324
     325        /**
     326         * @ticket 9968
     327         * @ticket 17817
     328         */
     329        function test_action_callback_manipulation_while_running() {
     330                $tag = rand_str();
     331                $a = new MockAction();
     332                $b = new MockAction();
     333                $c = new MockAction();
     334                $d = new MockAction();
     335                $e = new MockAction();
     336
     337                add_action( $tag, array( $a, 'action' ), 11, 2 );
     338                add_action( $tag, array( $this, 'action_that_manipulates_a_running_hook' ), 12, 2 );
     339                add_action( $tag, array( $b, 'action' ), 12, 2 );
     340
     341                do_action( $tag, $tag, array( $a, $b, $c, $d, $e ) );
     342                do_action( $tag, $tag, array( $a, $b, $c, $d, $e ) );
     343
     344                $this->assertEquals( 2, $a->get_call_count(), 'callbacks should run unless otherwise instructed' );
     345                $this->assertEquals( 1, $b->get_call_count(), 'callback removed by same priority callback should still get called' );
     346                $this->assertEquals( 1, $c->get_call_count(), 'callback added by same priority callback should not get called' );
     347                $this->assertEquals( 2, $d->get_call_count(), 'callback added by earlier priority callback should get called' );
     348                $this->assertEquals( 1, $e->get_call_count(), 'callback added by later priority callback should not get called' );
     349        }
     350
     351        function action_that_manipulates_a_running_hook( $tag, $mocks ) {
     352                remove_action( $tag, array( $mocks[ 1 ], 'action' ), 12, 2 );
     353                add_action( $tag, array( $mocks[ 2 ], 'action' ), 12, 2 );
     354                add_action( $tag, array( $mocks[ 3 ], 'action' ), 13, 2 );
     355                add_action( $tag, array( $mocks[ 4 ], 'action' ), 10, 2 );
     356        }
     357
     358        /**
     359         * @ticket 17817
     360         *
     361         * This specificaly addresses the concern raised at
     362         * https://core.trac.wordpress.org/ticket/17817#comment:52
     363         */
     364        function test_remove_anonymous_callback() {
     365                $tag = rand_str();
     366                $a = new MockAction();
     367                add_action( $tag, array( $a, 'action' ), 12, 1 );
     368                $this->assertTrue( has_action( $tag ) );
     369
     370                $hook = $GLOBALS['wp_filter'][ $tag ];
     371
     372                // From http://wordpress.stackexchange.com/a/57088/6445
     373                foreach ( $hook as $priority => $filter ) {
     374                        foreach ( $filter as $identifier => $function ) {
     375                                if ( is_array( $function )
     376                                        && is_a( $function['function'][ 0 ], 'MockAction' )
     377                                        && 'action' === $function['function'][ 1 ]
     378                                ) {
     379                                        remove_filter(
     380                                                $tag,
     381                                                array( $function['function'][ 0 ], 'action' ),
     382                                                $priority
     383                                        );
     384                                }
     385                        }
     386                }
     387
     388                $this->assertFalse( has_action( $tag ) );
     389        }
     390
     391
     392        /**
     393         * Test the ArrayAccess methods of WP_Hook
     394         *
     395         * @ticket 17817
     396         */
     397        function test_array_access_of_wp_filter_global() {
     398                global $wp_filter;
     399                $tag = rand_str();
     400
     401                add_action( $tag, '__return_null', 11, 1 );
     402
     403                $this->assertTrue( isset( $wp_filter[ $tag ][ 11 ] ) );
     404                $this->assertArrayHasKey( '__return_null', $wp_filter[ $tag ][ 11 ] );
     405
     406                unset( $wp_filter[ $tag ][ 11 ] );
     407                $this->assertFalse( has_action( $tag, '__return_null' ) );
     408
     409                $wp_filter[ $tag ][ 11 ] = array( '__return_null' => array( 'function' => '__return_null', 'accepted_args' => 1 ) );
     410                $this->assertEquals( 11, has_action( $tag, '__return_null' ) );
     411        }
     412
     413        /**
    261414         * Make sure current_action() behaves as current_filter()
    262415         *
    263416         * @ticket 14994
  • tests/phpunit/tests/filters.php

     
    294294                remove_all_filters( $tag, 12 );
    295295                $this->assertFalse( has_filter( $tag ) );
    296296        }
    297 
    298         /**
    299          * @ticket 29070
    300          */
    301          function test_has_filter_doesnt_reset_wp_filter() {
    302                 add_action( 'action_test_has_filter_doesnt_reset_wp_filter', '__return_null', 1 );
    303                 add_action( 'action_test_has_filter_doesnt_reset_wp_filter', '__return_null', 2 );
    304                 add_action( 'action_test_has_filter_doesnt_reset_wp_filter', '__return_null', 3 );
    305                 add_action( 'action_test_has_filter_doesnt_reset_wp_filter', array( $this, '_action_test_has_filter_doesnt_reset_wp_filter' ), 4 );
    306 
    307                 do_action( 'action_test_has_filter_doesnt_reset_wp_filter' );
    308          }
    309          function _action_test_has_filter_doesnt_reset_wp_filter() {
    310                 global $wp_filter;
    311 
    312                 has_action( 'action_test_has_filter_doesnt_reset_wp_filter', '_function_that_doesnt_exist' );
    313 
    314                 $filters = current( $wp_filter['action_test_has_filter_doesnt_reset_wp_filter'] );
    315                 $the_ = current( $filters );
    316                 $this->assertEquals( $the_['function'], array( $this, '_action_test_has_filter_doesnt_reset_wp_filter' ) );
    317          }
    318297}
  • tests/phpunit/includes/functions.php

     
    22
    33// For adding hooks before loading WP
    44function tests_add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
    5         global $wp_filter, $merged_filters;
     5        global $wp_filter;
    66
    77        $idx = _test_filter_build_unique_id($tag, $function_to_add, $priority);
    88        $wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
    9         unset( $merged_filters[ $tag ] );
    109        return true;
    1110}
    1211
    1312function _test_filter_build_unique_id($tag, $function, $priority) {
    14         global $wp_filter;
    15         static $filter_id_count = 0;
    16 
    1713        if ( is_string($function) )
    1814                return $function;
    1915
  • tests/phpunit/includes/testcase.php

     
    142142         * @return void
    143143         */
    144144        protected function _backup_hooks() {
    145                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     145                $globals = array( 'wp_actions', 'wp_current_filter' );
    146146                foreach ( $globals as $key ) {
    147147                        self::$hooks_saved[ $key ] = $GLOBALS[ $key ];
    148148                }
     149                self::$hooks_saved['wp_filter'] = array();
     150                foreach ( $GLOBALS['wp_filter'] as $hook_name => $hook_object ) {
     151                        self::$hooks_saved['wp_filter'][ $hook_name ] = clone $hook_object;
     152                }
    149153        }
    150154
    151155        /**
     
    159163         * @return void
    160164         */
    161165        protected function _restore_hooks() {
    162                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     166                $globals = array( 'wp_actions', 'wp_current_filter' );
    163167                foreach ( $globals as $key ) {
    164168                        if ( isset( self::$hooks_saved[ $key ] ) ) {
    165169                                $GLOBALS[ $key ] = self::$hooks_saved[ $key ];
    166170                        }
    167171                }
     172                if ( isset( self::$hooks_saved['wp_filter'] ) ) {
     173                        $GLOBALS['wp_filter'] = array();
     174                        foreach ( self::$hooks_saved['wp_filter'] as $hook_name => $hook_object ) {
     175                                $GLOBALS['wp_filter'][ $hook_name ] = clone $hook_object;
     176                        }
     177                }
    168178        }
    169179
    170180        function flush_cache() {
  • src/wp-includes/plugin.php

     
    2020 */
    2121
    2222// Initialize the filter globals.
    23 global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     23require( ABSPATH . WPINC . '/class-wp-hook.php' );
    2424
    25 if ( ! isset( $wp_filter ) )
     25/** @var WP_Hook[] $wp_filter */
     26global $wp_filter, $wp_actions, $wp_current_filter;
     27
     28if ( $wp_filter ) {
     29        $wp_filter = WP_Hook::build_preinitialized_hooks( $wp_filter );
     30} else {
    2631        $wp_filter = array();
     32}
    2733
    2834if ( ! isset( $wp_actions ) )
    2935        $wp_actions = array();
    3036
    31 if ( ! isset( $merged_filters ) )
    32         $merged_filters = array();
    33 
    3437if ( ! isset( $wp_current_filter ) )
    3538        $wp_current_filter = array();
    3639
     
    6568 * @since 0.71
    6669 *
    6770 * @global array $wp_filter      A multidimensional array of all hooks and the callbacks hooked to them.
    68  * @global array $merged_filters Tracks the tags that need to be merged for later. If the hook is added,
    69  *                               it doesn't need to run through that process.
    7071 *
    7172 * @param string   $tag             The name of the filter to hook the $function_to_add callback to.
    7273 * @param callback $function_to_add The callback to be run when the filter is applied.
     
    7980 * @return true
    8081 */
    8182function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
    82         global $wp_filter, $merged_filters;
    83 
    84         $idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
    85         $wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
    86         unset( $merged_filters[ $tag ] );
     83        global $wp_filter;
     84        if ( ! isset( $wp_filter[ $tag ] ) ) {
     85                $wp_filter[ $tag ] = new WP_Hook();
     86        }
     87        $wp_filter[ $tag ]->add_filter( $function_to_add, $priority, $accepted_args, $tag );
    8788        return true;
    8889}
    8990
     
    104105 *                   return value.
    105106 */
    106107function has_filter($tag, $function_to_check = false) {
    107         // Don't reset the internal array pointer
    108         $wp_filter = $GLOBALS['wp_filter'];
     108        global $wp_filter;
    109109
    110         $has = ! empty( $wp_filter[ $tag ] );
    111 
    112         // Make sure at least one priority has a filter callback
    113         if ( $has ) {
    114                 $exists = false;
    115                 foreach ( $wp_filter[ $tag ] as $callbacks ) {
    116                         if ( ! empty( $callbacks ) ) {
    117                                 $exists = true;
    118                                 break;
    119                         }
    120                 }
    121 
    122                 if ( ! $exists ) {
    123                         $has = false;
    124                 }
    125         }
    126 
    127         if ( false === $function_to_check || false === $has )
    128                 return $has;
    129 
    130         if ( !$idx = _wp_filter_build_unique_id($tag, $function_to_check, false) )
     110        if ( ! isset( $wp_filter[ $tag ] ) ) {
    131111                return false;
    132 
    133         foreach ( (array) array_keys($wp_filter[$tag]) as $priority ) {
    134                 if ( isset($wp_filter[$tag][$priority][$idx]) )
    135                         return $priority;
    136112        }
    137113
    138         return false;
     114        return $wp_filter[ $tag ]->has_filter( $function_to_check, $tag );
    139115}
    140116
    141117/**
     
    166142 * @since 0.71
    167143 *
    168144 * @global array $wp_filter         Stores all of the filters.
    169  * @global array $merged_filters    Merges the filter hooks using this function.
    170145 * @global array $wp_current_filter Stores the list of current filters with the current one last.
    171146 *
    172147 * @param string $tag   The name of the filter hook.
     
    175150 * @return mixed The filtered value after all hooked functions are applied to it.
    176151 */
    177152function apply_filters( $tag, $value ) {
    178         global $wp_filter, $merged_filters, $wp_current_filter;
     153        global $wp_filter, $wp_current_filter;
    179154
    180155        $args = array();
    181156
     
    195170        if ( !isset($wp_filter['all']) )
    196171                $wp_current_filter[] = $tag;
    197172
    198         // Sort.
    199         if ( !isset( $merged_filters[ $tag ] ) ) {
    200                 ksort($wp_filter[$tag]);
    201                 $merged_filters[ $tag ] = true;
    202         }
    203 
    204         reset( $wp_filter[ $tag ] );
    205 
    206173        if ( empty($args) )
    207174                $args = func_get_args();
    208175
    209         do {
    210                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    211                         if ( !is_null($the_['function']) ){
    212                                 $args[1] = $value;
    213                                 $value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
    214                         }
     176        // don't pass the tag name to WP_Hook
     177        array_shift( $args );
    215178
    216         } while ( next($wp_filter[$tag]) !== false );
     179        $filtered = $wp_filter[ $tag ]->apply_filters( $value, $args );
    217180
    218181        array_pop( $wp_current_filter );
    219182
    220         return $value;
     183        return $filtered;
    221184}
    222185
    223186/**
     
    229192 * functions hooked to `$tag` are supplied using an array.
    230193 *
    231194 * @global array $wp_filter         Stores all of the filters
    232  * @global array $merged_filters    Merges the filter hooks using this function.
    233195 * @global array $wp_current_filter Stores the list of current filters with the current one last
    234196 *
    235197 * @param string $tag  The name of the filter hook.
     
    237199 * @return mixed The filtered value after all hooked functions are applied to it.
    238200 */
    239201function apply_filters_ref_array($tag, $args) {
    240         global $wp_filter, $merged_filters, $wp_current_filter;
     202        global $wp_filter, $wp_current_filter;
    241203
    242204        // Do 'all' actions first
    243205        if ( isset($wp_filter['all']) ) {
     
    255217        if ( !isset($wp_filter['all']) )
    256218                $wp_current_filter[] = $tag;
    257219
    258         // Sort
    259         if ( !isset( $merged_filters[ $tag ] ) ) {
    260                 ksort($wp_filter[$tag]);
    261                 $merged_filters[ $tag ] = true;
    262         }
     220        $filtered = $wp_filter[ $tag ]->apply_filters( $args[0], $args );
    263221
    264         reset( $wp_filter[ $tag ] );
    265 
    266         do {
    267                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    268                         if ( !is_null($the_['function']) )
    269                                 $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    270 
    271         } while ( next($wp_filter[$tag]) !== false );
    272 
    273222        array_pop( $wp_current_filter );
    274223
    275         return $args[0];
     224        return $filtered;
    276225}
    277226
    278227/**
     
    297246 * @return bool    Whether the function existed before it was removed.
    298247 */
    299248function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
    300         $function_to_remove = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
     249        global $wp_filter;
    301250
    302         $r = isset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
    303 
    304         if ( true === $r ) {
    305                 unset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
    306                 if ( empty( $GLOBALS['wp_filter'][ $tag ][ $priority ] ) ) {
    307                         unset( $GLOBALS['wp_filter'][ $tag ][ $priority ] );
     251        $r = false;
     252        if ( isset( $wp_filter[ $tag ] ) ) {
     253                $r = $wp_filter[ $tag ]->remove_filter( $function_to_remove, $priority, $tag );
     254                if ( ! $wp_filter[ $tag ]->callbacks ) {
     255                        unset( $wp_filter[ $tag ] );
    308256                }
    309                 if ( empty( $GLOBALS['wp_filter'][ $tag ] ) ) {
    310                         $GLOBALS['wp_filter'][ $tag ] = array();
    311                 }
    312                 unset( $GLOBALS['merged_filters'][ $tag ] );
    313257        }
    314258
    315259        return $r;
     
    320264 *
    321265 * @since 2.7.0
    322266 *
    323  * @global array $wp_filter         Stores all of the filters
    324  * @global array $merged_filters    Merges the filter hooks using this function.
     267 * @global array $wp_filter  Stores all of the filters
    325268 *
    326269 * @param string   $tag      The filter to remove hooks from.
    327270 * @param int|bool $priority Optional. The priority number to remove. Default false.
    328271 * @return true True when finished.
    329272 */
    330273function remove_all_filters( $tag, $priority = false ) {
    331         global $wp_filter, $merged_filters;
     274        global $wp_filter;
    332275
    333276        if ( isset( $wp_filter[ $tag ]) ) {
    334                 if ( false === $priority ) {
    335                         $wp_filter[ $tag ] = array();
    336                 } elseif ( isset( $wp_filter[ $tag ][ $priority ] ) ) {
    337                         $wp_filter[ $tag ][ $priority ] = array();
     277                $wp_filter[ $tag ]->remove_all_filters( $priority );
     278                if ( ! $wp_filter[ $tag ]->has_filters() ) {
     279                        unset( $wp_filter[ $tag ] );
    338280                }
    339281        }
    340282
    341         unset( $merged_filters[ $tag ] );
    342 
    343283        return true;
    344284}
    345285
     
    458398 *                    functions hooked to the action. Default empty.
    459399 */
    460400function do_action($tag, $arg = '') {
    461         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     401        global $wp_filter, $wp_actions, $wp_current_filter;
    462402
    463403        if ( ! isset($wp_actions[$tag]) )
    464404                $wp_actions[$tag] = 1;
     
    489429        for ( $a = 2, $num = func_num_args(); $a < $num; $a++ )
    490430                $args[] = func_get_arg($a);
    491431
    492         // Sort
    493         if ( !isset( $merged_filters[ $tag ] ) ) {
    494                 ksort($wp_filter[$tag]);
    495                 $merged_filters[ $tag ] = true;
    496         }
     432        $wp_filter[ $tag ]->do_action( $args );
    497433
    498         reset( $wp_filter[ $tag ] );
    499 
    500         do {
    501                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    502                         if ( !is_null($the_['function']) )
    503                                 call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    504 
    505         } while ( next($wp_filter[$tag]) !== false );
    506 
    507434        array_pop($wp_current_filter);
    508435}
    509436
     
    542469 * @param array  $args The arguments supplied to the functions hooked to `$tag`.
    543470 */
    544471function do_action_ref_array($tag, $args) {
    545         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     472        global $wp_filter, $wp_actions, $wp_current_filter;
    546473
    547474        if ( ! isset($wp_actions[$tag]) )
    548475                $wp_actions[$tag] = 1;
     
    565492        if ( !isset($wp_filter['all']) )
    566493                $wp_current_filter[] = $tag;
    567494
    568         // Sort
    569         if ( !isset( $merged_filters[ $tag ] ) ) {
    570                 ksort($wp_filter[$tag]);
    571                 $merged_filters[ $tag ] = true;
    572         }
     495        $wp_filter[ $tag ]->do_action( $args );
    573496
    574         reset( $wp_filter[ $tag ] );
    575 
    576         do {
    577                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    578                         if ( !is_null($the_['function']) )
    579                                 call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    580 
    581         } while ( next($wp_filter[$tag]) !== false );
    582 
    583497        array_pop($wp_current_filter);
    584498}
    585499
     
    843757function _wp_call_all_hook($args) {
    844758        global $wp_filter;
    845759
    846         reset( $wp_filter['all'] );
    847         do {
    848                 foreach ( (array) current($wp_filter['all']) as $the_ )
    849                         if ( !is_null($the_['function']) )
    850                                 call_user_func_array($the_['function'], $args);
    851 
    852         } while ( next($wp_filter['all']) !== false );
     760        $wp_filter['all']->do_all_hook( $args );
    853761}
    854762
    855763/**
  • src/wp-includes/class-wp-hook.php

     
     1<?php
     2
     3/**
     4 * @package WordPress
     5 * @subpackage Plugin
     6 * @since 4.2.0
     7 */
     8final class WP_Hook implements IteratorAggregate, ArrayAccess {
     9        public $callbacks = array();
     10
     11        /**
     12         * The priority keys of actively running iterations of a hook
     13         *
     14         * @var array
     15         */
     16        private $iterations = array();
     17
     18        /**
     19         * How recursively has this hook been called?
     20         *
     21         * @var int
     22         */
     23        private $nesting_level = 0;
     24
     25        /**
     26         * Hook a function or method to a specific filter action.
     27         *
     28         * @param callable $function_to_add The callback to be run when the filter is applied.
     29         * @param int $priority Optional.   The order in which the functions associated with a
     30         *                                    particular action are executed. Lower numbers correspond with
     31         *                                    earlier execution, and functions with the same priority are executed
     32         *                                    in the order in which they were added to the action.
     33         *                                    Default: 10.
     34         * @param int $accepted_args        Optional. The number of arguments the function accepts. Default: 1.
     35         * @param string $tag               The name of the filter to hook the $function_to_add callback to.
     36         */
     37        public function add_filter( $function_to_add, $priority, $accepted_args, $tag ) {
     38                $idx = _wp_filter_build_unique_id( $tag, $function_to_add, $priority );
     39                $priority_existed = isset( $this->callbacks[ $priority ] );
     40
     41                $this->callbacks[ $priority ][ $idx ] = array(
     42                        'function' => $function_to_add,
     43                        'accepted_args' => $accepted_args
     44                );
     45
     46                // if we're adding a new priority to the list, put them back in sorted order
     47                if ( ! $priority_existed && count( $this->callbacks ) > 1 ) {
     48                        ksort( $this->callbacks, SORT_NUMERIC );
     49                }
     50
     51                if ( $this->nesting_level > 0 ) {
     52                        $this->resort_active_iterations();
     53                }
     54        }
     55
     56        /**
     57         * When a hook's callbacks are changed mid-iteration, the priority
     58         * keys need to be reset, with the array pointer at the correct location
     59         */
     60        private function resort_active_iterations() {
     61                $new_priorities = array_keys( $this->callbacks );
     62
     63                // if there are no remaining hooks, clear out all running iterations
     64                if ( ! $new_priorities ) {
     65                        foreach ( $this->iterations as $index => $iteration ) {
     66                                $this->iterations[ $index ] = $new_priorities;
     67                        }
     68                        return;
     69                }
     70
     71                $min = min( $new_priorities );
     72                foreach ( $this->iterations as $index => $iteration ) {
     73                        $current = current( $iteration );
     74                        $this->iterations[ $index ] = $new_priorities;
     75
     76                        if ( $current < $min ) {
     77                                array_unshift( $this->iterations[ $index ], $current );
     78                                continue;
     79                        }
     80                        while ( current( $this->iterations[ $index ] ) < $current ) {
     81                                if ( false === next( $this->iterations[ $index ] ) ) {
     82                                        break;
     83                                };
     84                        }
     85                }
     86        }
     87
     88        /**
     89         * @param callable $function_to_remove
     90         * @param int $priority
     91         * @param string $tag The filter hook to which the function to be removed is hooked.
     92         *                    Used for building the callback ID when SPL is not available.
     93         *
     94         * @return bool Whether the callback existed before it was removed
     95         */
     96        public function remove_filter( $function_to_remove, $priority, $tag ) {
     97                $function_key = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
     98
     99                $exists = isset( $this->callbacks[ $priority ][ $function_key ] );
     100                if ( $exists ) {
     101                        unset( $this->callbacks[ $priority ][ $function_key ] );
     102                        if ( ! $this->callbacks[ $priority ] ) {
     103                                unset( $this->callbacks[ $priority ] );
     104                                if ( $this->nesting_level > 0 ) {
     105                                        $this->resort_active_iterations();
     106                                }
     107                        }
     108                }
     109                return $exists;
     110        }
     111
     112        /**
     113         * Check if a specific action has been registered for this hook.
     114         *
     115         * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
     116         * @param string $tag  The name of the filter hook.
     117         *                     Used for building the callback ID when SPL is not available.
     118         * @return bool|int The priority of that hook is returned, or false if the function is not attached.
     119         */
     120        public function has_filter( $function_to_check = false, $tag = '' ) {
     121                if ( false === $function_to_check ) {
     122                        return $this->has_filters();
     123                }
     124
     125                $function_key =  _wp_filter_build_unique_id( $tag, $function_to_check, false );
     126                if ( !$function_key ) {
     127                        return false;
     128                }
     129
     130                foreach ( $this->callbacks as $priority => $callbacks ) {
     131                        if ( isset( $callbacks[ $function_key ] ) ) {
     132                                return $priority;
     133                        }
     134                }
     135
     136                return false;
     137        }
     138
     139        /**
     140         * Check if any callbacks have been registered for this hook
     141         *
     142         * @return bool
     143         */
     144        public function has_filters() {
     145                foreach ( $this->callbacks as $callbacks ) {
     146                        if ( $callbacks ) {
     147                                return true;
     148                        }
     149                }
     150                return false;
     151        }
     152
     153        /**
     154         * Remove all of the callbacks from the filter.
     155         *
     156         * @param int|bool $priority The priority number to remove.
     157         */
     158        public function remove_all_filters( $priority = false ) {
     159                if ( ! $this->callbacks ) {
     160                        return;
     161                }
     162
     163                if ( false === $priority ) {
     164                        $this->callbacks = array();
     165                } else if ( isset( $this->callbacks[ $priority ] ) ) {
     166                        unset( $this->callbacks[ $priority ] );
     167                }
     168
     169                if ( $this->nesting_level > 0 ) {
     170                        $this->resort_active_iterations();
     171                }
     172        }
     173
     174        /**
     175         * Call the functions added to a filter hook.
     176         *
     177         * @param mixed $value The value to filter.
     178         * @param array $args Arguments to pass to callbacks
     179         *
     180         * @return mixed The filtered value after all hooked functions are applied to it
     181         */
     182        public function apply_filters( $value, $args ) {
     183                if ( ! $this->callbacks ) {
     184                        return $value;
     185                }
     186                $nesting_level = $this->nesting_level++;
     187                $this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
     188                $num_args = count( $args );
     189
     190                do {
     191                        $priority = current( $this->iterations[ $nesting_level ] );
     192                        foreach ( $this->callbacks[ $priority ] as $the_ ) {
     193                                $args[ 0 ] = $value;
     194                                // avoid the array_slice if possible
     195                                if ( $the_['accepted_args'] == 0 ) {
     196                                        $value = call_user_func_array( $the_['function'], array() );
     197                                } elseif ( $the_['accepted_args'] >= $num_args ) {
     198                                        $value = call_user_func_array( $the_['function'], $args );
     199                                } else {
     200                                        $value = call_user_func_array( $the_['function'], array_slice( $args, 0, (int)$the_['accepted_args'] ) );
     201                                }
     202                        }
     203                } while ( false !== next( $this->iterations[ $nesting_level ] ) );
     204
     205                unset( $this->iterations[ $nesting_level ] );
     206                $this->nesting_level--;
     207                return $value;
     208        }
     209
     210        /**
     211         * Execute functions hooked on a specific action hook.
     212         *
     213         * @param mixed $args Arguments to pass to callbacks
     214         */
     215        public function do_action( $args ) {
     216                if ( ! $this->callbacks ) {
     217                        return;
     218                }
     219                $nesting_level = $this->nesting_level++;
     220                $this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
     221                $num_args = count( $args );
     222
     223                do {
     224                        $priority = current( $this->iterations[ $nesting_level ] );
     225                        foreach ( $this->callbacks[ $priority ] as $the_ ) {
     226                                // avoid the array_slice if possible
     227                                if ( $the_['accepted_args'] == 0 ) {
     228                                        call_user_func_array( $the_['function'], array() );
     229                                } elseif ( $the_['accepted_args'] >= $num_args ) {
     230                                        call_user_func_array( $the_['function'], $args );
     231                                } else {
     232                                        call_user_func_array( $the_['function'], array_slice( $args, 0, (int)$the_['accepted_args'] ) );
     233                                }
     234                        }
     235                } while ( next( $this->iterations[ $nesting_level ] ) !== false );
     236
     237                unset( $this->iterations[ $nesting_level ] );
     238                $this->nesting_level--;
     239        }
     240
     241        /**
     242         * Process the functions hooked into the 'all' hook.
     243         *
     244         * @param array $args Arguments to pass to callbacks
     245         */
     246        public function do_all_hook( &$args ) {
     247                $nesting_level = $this->nesting_level++;
     248                $this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
     249
     250                do {
     251                        $priority = current( $this->iterations[ $nesting_level ] );
     252                        foreach ( $this->callbacks[ $priority ] as $the_ ) {
     253                                call_user_func_array( $the_['function'], $args );
     254                        }
     255                } while ( false !== next( $this->iterations[ $nesting_level ] ) );
     256
     257                unset( $this->iterations[ $nesting_level ] );
     258                $this->nesting_level--;
     259        }
     260
     261        /**
     262         * Retrieve an external iterator
     263         *
     264         * Provided for backwards compatibility with plugins that iterate over the
     265         * $wp_filter global
     266         *
     267         * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
     268         * @return Traversable An instance of an object implementing Iterator or
     269         *                     Traversable
     270         */
     271        public function getIterator() {
     272                return new ArrayIterator( $this->callbacks );
     273        }
     274
     275        /**
     276         * Some plugins may set up filters before WordPress has initialized.
     277         * Normalize them to WP_Hook objects.
     278         *
     279         * @param array $filters
     280         * @return WP_Hook[]
     281         */
     282        public static function build_preinitialized_hooks( $filters ) {
     283                /** @var WP_Hook[] $normalized */
     284                $normalized = array();
     285                foreach ( $filters as $tag => $callback_groups ) {
     286                        if ( is_object( $callback_groups ) && $callback_groups instanceof WP_Hook ) {
     287                                $normalized[ $tag ] = $callback_groups;
     288                                continue;
     289                        }
     290                        $hook = new WP_Hook();
     291                        foreach ( $callback_groups as $priority => $callbacks ) {
     292                                foreach ( $callbacks as $cb ) {
     293                                        $hook->add_filter( $cb['function'], $priority, $cb['accepted_args'], $tag );
     294                                }
     295                        }
     296                        $normalized[ $tag ] = $hook;
     297                }
     298                return $normalized;
     299        }
     300
     301        /**
     302         * Whether an offset exists
     303         *
     304         * @link http://php.net/manual/en/arrayaccess.offsetexists.php
     305         *
     306         * @param mixed $offset An offset to check for.
     307         * @return boolean true on success or false on failure.
     308         */
     309        public function offsetExists( $offset ) {
     310                return isset( $this->callbacks[ $offset ] );
     311        }
     312
     313        /**
     314         * Offset to retrieve
     315         *
     316         * @link http://php.net/manual/en/arrayaccess.offsetget.php
     317         *
     318         * @param mixed $offset The offset to retrieve.
     319         * @return mixed
     320         */
     321        public function offsetGet( $offset ) {
     322                return isset( $this->callbacks[ $offset ] ) ? $this->callbacks[ $offset ] : null;
     323        }
     324
     325        /**
     326         * Offset to set
     327         *
     328         * @link http://php.net/manual/en/arrayaccess.offsetset.php
     329         *
     330         * @param mixed $offset The offset to assign the value to.
     331         * @param mixed $value The value to set.
     332         */
     333        public function offsetSet( $offset, $value ) {
     334                if ( is_null( $offset ) ) {
     335                        $this->callbacks[] = $value;
     336                } else {
     337                        $this->callbacks[ $offset ] = $value;
     338                }
     339        }
     340
     341        /**
     342         * Offset to unset
     343         *
     344         * @link http://php.net/manual/en/arrayaccess.offsetunset.php
     345         *
     346         * @param mixed $offset The offset to unset.
     347         */
     348        public function offsetUnset( $offset ) {
     349                unset( $this->callbacks[ $offset ] );
     350        }
     351}