WordPress.org

Make WordPress Core

Ticket #17817: 17817.7.patch

File 17817.7.patch, 17.2 KB (added by leewillis77, 5 years ago)
  • src/wp-includes/plugin.php

     
    2020 */
    2121
    2222// Initialize the filter globals.
    23 global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
    24 
    25 if ( ! isset( $wp_filter ) )
     23require( ABSPATH . '/wp-includes/class-wp-hook.php' );
     24/** @var WP_Hook[] $wp_filter */
     25global $wp_filter, $wp_actions, $wp_current_filter;
     26
     27if ( !empty( $wp_filter ) ) {
     28        $wp_filter = WP_Hook::build_preinitialized_hooks( $wp_filter );
     29} else {
    2630        $wp_filter = array();
     31}
    2732
    2833if ( ! isset( $wp_actions ) )
    2934        $wp_actions = array();
    3035
    31 if ( ! isset( $merged_filters ) )
    32         $merged_filters = array();
    33 
    3436if ( ! isset( $wp_current_filter ) )
    3537        $wp_current_filter = array();
    3638
     
    6668 * @since 0.71
    6769 *
    6870 * @global array $wp_filter      A multidimensional array of all hooks and the callbacks hooked to them.
    69  * @global array $merged_filters Tracks the tags that need to be merged for later. If the hook is added,
    70  *                               it doesn't need to run through that process.
    7171 *
    7272 * @param string   $tag             The name of the filter to hook the $function_to_add callback to.
    7373 * @param callback $function_to_add The callback to be run when the filter is applied.
     
    8080 * @return boolean true
    8181 */
    8282function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
    83         global $wp_filter, $merged_filters;
     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 );
    8488
    85         $idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
    86         $wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
    87         unset( $merged_filters[ $tag ] );
    8889        return true;
    8990}
    9091
     
    105106 *                  return value.
    106107 */
    107108function has_filter($tag, $function_to_check = false) {
    108         // Don't reset the internal array pointer
    109         $wp_filter = $GLOBALS['wp_filter'];
    110 
    111         $has = ! empty( $wp_filter[ $tag ] );
     109        global $wp_filter;
    112110
    113         // Make sure at least one priority has a filter callback
    114         if ( $has ) {
    115                 $exists = false;
    116                 foreach ( $wp_filter[ $tag ] as $callbacks ) {
    117                         if ( ! empty( $callbacks ) ) {
    118                                 $exists = true;
    119                                 break;
    120                         }
    121                 }
    122 
    123                 if ( ! $exists ) {
    124                         $has = false;
    125                 }
    126         }
     111        $has = isset($wp_filter[$tag]) && $wp_filter[$tag]->has_filters();
    127112
    128113        if ( false === $function_to_check || false == $has )
    129114                return $has;
     
    131116        if ( !$idx = _wp_filter_build_unique_id($tag, $function_to_check, false) )
    132117                return false;
    133118
    134         foreach ( (array) array_keys($wp_filter[$tag]) as $priority ) {
    135                 if ( isset($wp_filter[$tag][$priority][$idx]) )
    136                         return $priority;
    137         }
    138 
    139         return false;
     119        return $wp_filter[$tag]->has_filter($idx);
    140120}
    141121
    142122/**
     
    167147 * @since 0.71
    168148 *
    169149 * @global array $wp_filter         Stores all of the filters.
    170  * @global array $merged_filters    Merges the filter hooks using this function.
    171150 * @global array $wp_current_filter Stores the list of current filters with the current one last.
    172151 *
    173152 * @param string $tag   The name of the filter hook.
     
    176155 * @return mixed The filtered value after all hooked functions are applied to it.
    177156 */
    178157function apply_filters( $tag, $value ) {
    179         global $wp_filter, $merged_filters, $wp_current_filter;
     158        global $wp_filter, $wp_current_filter;
    180159
    181160        $args = array();
    182161
     
    185164                $wp_current_filter[] = $tag;
    186165                $args = func_get_args();
    187166                _wp_call_all_hook($args);
     167                array_shift($args);
    188168        }
    189169
    190170        if ( !isset($wp_filter[$tag]) ) {
     
    196176        if ( !isset($wp_filter['all']) )
    197177                $wp_current_filter[] = $tag;
    198178
    199         // Sort.
    200         if ( !isset( $merged_filters[ $tag ] ) ) {
    201                 ksort($wp_filter[$tag]);
    202                 $merged_filters[ $tag ] = true;
    203         }
    204 
    205         reset( $wp_filter[ $tag ] );
    206 
    207         if ( empty($args) )
     179        if ( empty($args) ) {
    208180                $args = func_get_args();
     181                array_shift($args);
     182        }
    209183
    210         do {
    211                 foreach( (array) current($wp_filter[$tag]) as $the_ )
    212                         if ( !is_null($the_['function']) ){
    213                                 $args[1] = $value;
    214                                 $value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
    215                         }
    216 
    217         } while ( next($wp_filter[$tag]) !== false );
     184        $value = $wp_filter[$tag]->apply_filters( $value, $args );
    218185
    219186        array_pop( $wp_current_filter );
    220187
     
    230197 * @since 3.0.0
    231198 *
    232199 * @global array $wp_filter         Stores all of the filters
    233  * @global array $merged_filters    Merges the filter hooks using this function.
    234200 * @global array $wp_current_filter Stores the list of current filters with the current one last
    235201 *
    236202 * @param string $tag  The name of the filter hook.
     
    238204 * @return mixed The filtered value after all hooked functions are applied to it.
    239205 */
    240206function apply_filters_ref_array($tag, $args) {
    241         global $wp_filter, $merged_filters, $wp_current_filter;
     207        global $wp_filter, $wp_current_filter;
    242208
    243209        // Do 'all' actions first
    244210        if ( isset($wp_filter['all']) ) {
     
    256222        if ( !isset($wp_filter['all']) )
    257223                $wp_current_filter[] = $tag;
    258224
    259         // Sort
    260         if ( !isset( $merged_filters[ $tag ] ) ) {
    261                 ksort($wp_filter[$tag]);
    262                 $merged_filters[ $tag ] = true;
    263         }
    264 
    265         reset( $wp_filter[ $tag ] );
    266 
    267         do {
    268                 foreach( (array) current($wp_filter[$tag]) as $the_ )
    269                         if ( !is_null($the_['function']) )
    270                                 $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    271 
    272         } while ( next($wp_filter[$tag]) !== false );
     225        $value = $wp_filter[$tag]->apply_filters( $args[0], $args );
    273226
    274227        array_pop( $wp_current_filter );
    275228
    276         return $args[0];
     229        return $value;
    277230}
    278231
    279232/**
     
    295248 * @return boolean Whether the function existed before it was removed.
    296249 */
    297250function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
    298         $function_to_remove = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
    299 
    300         $r = isset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
     251        global $wp_filter;
    301252
    302         if ( true === $r ) {
    303                 unset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
    304                 if ( empty( $GLOBALS['wp_filter'][ $tag ][ $priority ] ) ) {
    305                         unset( $GLOBALS['wp_filter'][ $tag ][ $priority ] );
     253        $r = false;
     254        if ( isset($wp_filter[$tag]) ) {
     255                $function_to_remove = _wp_filter_build_unique_id($tag, $function_to_remove, $priority);
     256                $r = $wp_filter[$tag]->remove_filter($function_to_remove, $priority);
     257                if ( empty($wp_filter[$tag]->callbacks) ) {
     258                        unset($wp_filter[$tag]);
    306259                }
    307                 if ( empty( $GLOBALS['wp_filter'][ $tag ] ) ) {
    308                         $GLOBALS['wp_filter'][ $tag ] = array();
    309                 }
    310                 unset( $GLOBALS['merged_filters'][ $tag ] );
    311260        }
    312261
    313262        return $r;
     
    323272 * @return bool True when finished.
    324273 */
    325274function remove_all_filters( $tag, $priority = false ) {
    326         global $wp_filter, $merged_filters;
    327 
    328         if ( isset( $wp_filter[ $tag ]) ) {
    329                 if ( false !== $priority && isset( $wp_filter[ $tag ][ $priority ] ) ) {
    330                         $wp_filter[ $tag ][ $priority ] = array();
    331                 } else {
    332                         $wp_filter[ $tag ] = array();
    333                 }
    334         }
     275        global $wp_filter;
    335276
    336         if ( isset( $merged_filters[ $tag ] ) ) {
    337                 unset( $merged_filters[ $tag ] );
     277        if( isset($wp_filter[$tag]) ) {
     278                $wp_filter[$tag]->remove_all_filters($priority);
     279                unset($wp_filter[$tag]);
    338280        }
    339281
    340282        return true;
     
    460402 * @return null Will return null if $tag does not exist in $wp_filter array.
    461403 */
    462404function do_action($tag, $arg = '') {
    463         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     405        global $wp_filter, $wp_actions, $wp_current_filter;
    464406
    465407        if ( ! isset($wp_actions[$tag]) )
    466408                $wp_actions[$tag] = 1;
     
    491433        for ( $a = 2; $a < func_num_args(); $a++ )
    492434                $args[] = func_get_arg($a);
    493435
    494         // Sort
    495         if ( !isset( $merged_filters[ $tag ] ) ) {
    496                 ksort($wp_filter[$tag]);
    497                 $merged_filters[ $tag ] = true;
    498         }
    499 
    500         reset( $wp_filter[ $tag ] );
    501 
    502         do {
    503                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    504                         if ( !is_null($the_['function']) )
    505                                 call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    506 
    507         } while ( next($wp_filter[$tag]) !== false );
     436        $wp_filter[$tag]->do_action( $args );
    508437
    509438        array_pop($wp_current_filter);
    510439}
     
    543472 * @return null Will return null if $tag does not exist in $wp_filter array
    544473 */
    545474function do_action_ref_array($tag, $args) {
    546         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     475        global $wp_filter, $wp_actions, $wp_current_filter;
    547476
    548477        if ( ! isset($wp_actions[$tag]) )
    549478                $wp_actions[$tag] = 1;
     
    566495        if ( !isset($wp_filter['all']) )
    567496                $wp_current_filter[] = $tag;
    568497
    569         // Sort
    570         if ( !isset( $merged_filters[ $tag ] ) ) {
    571                 ksort($wp_filter[$tag]);
    572                 $merged_filters[ $tag ] = true;
    573         }
    574 
    575         reset( $wp_filter[ $tag ] );
    576 
    577         do {
    578                 foreach( (array) current($wp_filter[$tag]) as $the_ )
    579                         if ( !is_null($the_['function']) )
    580                                 call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    581 
    582         } while ( next($wp_filter[$tag]) !== false );
     498        $wp_filter[$tag]->do_action( $args );
    583499
    584500        array_pop($wp_current_filter);
    585501}
     
    838754 */
    839755function _wp_call_all_hook($args) {
    840756        global $wp_filter;
    841 
    842         reset( $wp_filter['all'] );
    843         do {
    844                 foreach( (array) current($wp_filter['all']) as $the_ )
    845                         if ( !is_null($the_['function']) )
    846                                 call_user_func_array($the_['function'], $args);
    847 
    848         } while ( next($wp_filter['all']) !== false );
     757        $wp_filter['all']->do_all_hook($args);
    849758}
    850759
    851760/**
  • 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

     
    7979         * @return void
    8080         */
    8181        protected function _backup_hooks() {
    82                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     82                $globals = array( 'wp_actions', 'wp_current_filter', 'wp_filter' );
    8383                foreach ( $globals as $key ) {
    8484                        self::$hooks_saved[ $key ] = $GLOBALS[ $key ];
    8585                }
     
    9696         * @return void
    9797         */
    9898        protected function _restore_hooks() {
    99                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     99                $globals = array( 'wp_actions', 'wp_current_filter', 'wp_filter' );
    100100                foreach ( $globals as $key ) {
    101101                        if ( isset( self::$hooks_saved[ $key ] ) ) {
    102102                                $GLOBALS[ $key ] = self::$hooks_saved[ $key ];
  • 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        function test_action_args_3() {
     122                $a1 = new MockAction();
     123                $a2 = new MockAction();
     124                $a3 = new MockAction();
     125                $tag = rand_str();
     126                $val1 = rand_str();
     127                $val2 = rand_str();
     128
     129                // a1 accepts two arguments, a2 doesn't, a3 accepts two arguments
     130                add_action($tag, array(&$a1, 'action'), 10, 2);
     131                add_action($tag, array(&$a2, 'action'));
     132                add_action($tag, array(&$a3, 'action'), 10, 2);
     133                // call the action with two arguments
     134                do_action($tag, $val1, $val2);
     135
     136                $call_count = $a1->get_call_count();
     137                // a1 should be called with both args
     138                $this->assertEquals(1, $call_count);
     139                $argsvar1 = $a1->get_args();
     140                $this->assertEquals( array( $val1, $val2 ), array_pop( $argsvar1 ) );
     141
     142                // a2 should be called with one only
     143                $this->assertEquals(1, $a2->get_call_count());
     144                $argsvar2 = $a2->get_args();
     145                $this->assertEquals( array( $val1 ), array_pop( $argsvar2 ) );
     146
     147                // a3 should be called with both args
     148                $this->assertEquals(1, $a3->get_call_count());
     149                $argsvar3 = $a3->get_args();
     150                $this->assertEquals( array( $val1, $val2 ), array_pop( $argsvar3 ) );
     151        }
     152
    117153        function test_action_priority() {
    118154                $a = new MockAction();
    119155                $tag = rand_str();
     
    258294        }
    259295
    260296        /**
     297         * @ticket 17817
     298         */
     299        function test_action_recursion() {
     300                $tag = rand_str();
     301                $a = new MockAction();
     302                $b = new MockAction();
     303
     304                add_action( $tag, array($a, 'action'), 11, 1 );
     305                add_action( $tag, array($b, 'action'), 13, 1 );
     306                add_action( $tag, array($this, 'action_that_causes_recursion'), 12, 1 );
     307                do_action( $tag, $tag );
     308
     309                $this->assertEquals( 2, $a->get_call_count(), 'recursive actions should call all callbacks with earlier priority' );
     310                $this->assertEquals( 2, $b->get_call_count(), 'recursive actions should call callbacks with later priority' );
     311        }
     312
     313        function action_that_causes_recursion( $tag ) {
     314                static $recursing = FALSE;
     315                if ( !$recursing ) {
     316                        $recursing = TRUE;
     317                        do_action( $tag, $tag );
     318                }
     319                $recursing = FALSE;
     320        }
     321
     322        /**
     323         * @ticket 9968
     324         */
     325        function test_action_callback_manipulation_while_running() {
     326                $tag = rand_str();
     327                $a = new MockAction();
     328                $b = new MockAction();
     329                $c = new MockAction();
     330                $d = new MockAction();
     331                $e = new MockAction();
     332
     333                add_action( $tag, array($a, 'action'), 11, 2 );
     334                add_action( $tag, array($this, 'action_that_manipulates_a_running_hook'), 12, 2 );
     335                add_action( $tag, array($b, 'action'), 12, 2 );
     336
     337                do_action( $tag, $tag, array($a,$b,$c,$d,$e) );
     338                do_action( $tag, $tag, array($a,$b,$c,$d,$e) );
     339
     340                $this->assertEquals( 2, $a->get_call_count(), 'callbacks should run unless otherwise instructed' );
     341                $this->assertEquals( 1, $b->get_call_count(), 'callback removed by same priority callback should still get called' );
     342                $this->assertEquals( 1, $c->get_call_count(), 'callback added by same priority callback should not get called' );
     343                $this->assertEquals( 2, $d->get_call_count(), 'callback added by earlier priority callback should get called' );
     344                $this->assertEquals( 1, $e->get_call_count(), 'callback added by later priority callback should not get called' );
     345        }
     346
     347        function action_that_manipulates_a_running_hook( $tag, $mocks ) {
     348                remove_action( $tag, array($mocks[1], 'action'), 12, 2 );
     349                add_action( $tag, array($mocks[2], 'action' ), 12, 2 );
     350                add_action( $tag, array($mocks[3], 'action' ), 13, 2 );
     351                add_action( $tag, array($mocks[4], 'action' ), 10, 2 );
     352        }
     353
     354        /**
     355         * @ticket 17817
     356         *
     357         * This specificaly addresses the concern raised at
     358         * https://core.trac.wordpress.org/ticket/17817#comment:52
     359         */
     360        function test_remove_anonymous_callback() {
     361                $tag = rand_str();
     362                $a = new MockAction();
     363                add_action( $tag, array( $a, 'action' ), 12, 1 );
     364                $this->assertTrue( has_action( $tag ) );
     365
     366                $hook = $GLOBALS['wp_filter'][ $tag ];
     367
     368                // From http://wordpress.stackexchange.com/a/57088/6445
     369                foreach ( $hook as $priority => $filter ) {
     370                        foreach ( $filter as $identifier => $function ) {
     371                                if ( is_array( $function)
     372                                        && is_a( $function['function'][0], 'MockAction' )
     373                                        && 'action' === $function['function'][1]
     374                                ) {
     375                                        remove_filter(
     376                                                $tag,
     377                                                array ( $function['function'][0], 'action' ),
     378                                                $priority
     379                                        );
     380                                }
     381                        }
     382                }
     383
     384                $this->assertFalse( has_action( $tag ) );
     385        }
     386
     387        /**
    261388         * Make sure current_action() behaves as current_filter()
    262389         *
    263390         * @ticket 14994
  • tests/phpunit/tests/filters.php

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