WordPress.org

Make WordPress Core

Ticket #17817: 17817.10.diff

File 17817.10.diff, 18.7 KB (added by dougwollison, 4 years ago)

Updated doc block for apply_filters; shouldn't claim $priority and $accepted_args are optional. Also removed random extra semicolon after an if block.

  • 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
     
    8790 * @since 0.71
    8891 *
    8992 * @global array $wp_filter      A multidimensional array of all hooks and the callbacks hooked to them.
    90  * @global array $merged_filters Tracks the tags that need to be merged for later. If the hook is added,
    91  *                               it doesn't need to run through that process.
    9293 *
    9394 * @param string   $tag             The name of the filter to hook the $function_to_add callback to.
    9495 * @param callable $function_to_add The callback to be run when the filter is applied.
     
    101102 * @return true
    102103 */
    103104function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
    104         global $wp_filter, $merged_filters;
    105 
    106         $idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
    107         $wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
    108         unset( $merged_filters[ $tag ] );
     105        global $wp_filter;
     106        if ( ! isset( $wp_filter[ $tag ] ) ) {
     107                $wp_filter[ $tag ] = new WP_Hook();
     108        }
     109        $wp_filter[ $tag ]->add_filter( $tag, $function_to_add, $priority, $accepted_args );
    109110        return true;
    110111}
    111112
     
    126127 *                   return value.
    127128 */
    128129function has_filter($tag, $function_to_check = false) {
    129         // Don't reset the internal array pointer
    130         $wp_filter = $GLOBALS['wp_filter'];
     130        global $wp_filter;
    131131
    132         $has = ! empty( $wp_filter[ $tag ] );
    133 
    134         // Make sure at least one priority has a filter callback
    135         if ( $has ) {
    136                 $exists = false;
    137                 foreach ( $wp_filter[ $tag ] as $callbacks ) {
    138                         if ( ! empty( $callbacks ) ) {
    139                                 $exists = true;
    140                                 break;
    141                         }
    142                 }
    143 
    144                 if ( ! $exists ) {
    145                         $has = false;
    146                 }
    147         }
    148 
    149         if ( false === $function_to_check || false === $has )
    150                 return $has;
    151 
    152         if ( !$idx = _wp_filter_build_unique_id($tag, $function_to_check, false) )
     132        if ( ! isset( $wp_filter[ $tag ] ) ) {
    153133                return false;
    154 
    155         foreach ( (array) array_keys($wp_filter[$tag]) as $priority ) {
    156                 if ( isset($wp_filter[$tag][$priority][$idx]) )
    157                         return $priority;
    158134        }
    159135
    160         return false;
     136        return $wp_filter[ $tag ]->has_filter( $tag, $function_to_check );
    161137}
    162138
    163139/**
     
    188164 * @since 0.71
    189165 *
    190166 * @global array $wp_filter         Stores all of the filters.
    191  * @global array $merged_filters    Merges the filter hooks using this function.
    192167 * @global array $wp_current_filter Stores the list of current filters with the current one last.
    193168 *
    194169 * @param string $tag     The name of the filter hook.
     
    197172 * @return mixed The filtered value after all hooked functions are applied to it.
    198173 */
    199174function apply_filters( $tag, $value ) {
    200         global $wp_filter, $merged_filters, $wp_current_filter;
     175        global $wp_filter, $wp_current_filter;
    201176
    202177        $args = array();
    203178
     
    217192        if ( !isset($wp_filter['all']) )
    218193                $wp_current_filter[] = $tag;
    219194
    220         // Sort.
    221         if ( !isset( $merged_filters[ $tag ] ) ) {
    222                 ksort($wp_filter[$tag]);
    223                 $merged_filters[ $tag ] = true;
    224         }
    225 
    226         reset( $wp_filter[ $tag ] );
    227 
    228195        if ( empty($args) )
    229196                $args = func_get_args();
    230197
    231         do {
    232                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    233                         if ( !is_null($the_['function']) ){
    234                                 $args[1] = $value;
    235                                 $value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
    236                         }
     198        // don't pass the tag name to WP_Hook
     199        array_shift( $args );
    237200
    238         } while ( next($wp_filter[$tag]) !== false );
     201        $filtered = $wp_filter[ $tag ]->apply_filters( $value, $args );
    239202
    240203        array_pop( $wp_current_filter );
    241204
    242         return $value;
     205        return $filtered;
    243206}
    244207
    245208/**
     
    251214 * functions hooked to `$tag` are supplied using an array.
    252215 *
    253216 * @global array $wp_filter         Stores all of the filters
    254  * @global array $merged_filters    Merges the filter hooks using this function.
    255217 * @global array $wp_current_filter Stores the list of current filters with the current one last
    256218 *
    257219 * @param string $tag  The name of the filter hook.
     
    259221 * @return mixed The filtered value after all hooked functions are applied to it.
    260222 */
    261223function apply_filters_ref_array($tag, $args) {
    262         global $wp_filter, $merged_filters, $wp_current_filter;
     224        global $wp_filter, $wp_current_filter;
    263225
    264226        // Do 'all' actions first
    265227        if ( isset($wp_filter['all']) ) {
     
    277239        if ( !isset($wp_filter['all']) )
    278240                $wp_current_filter[] = $tag;
    279241
    280         // Sort
    281         if ( !isset( $merged_filters[ $tag ] ) ) {
    282                 ksort($wp_filter[$tag]);
    283                 $merged_filters[ $tag ] = true;
    284         }
     242        $filtered = $wp_filter[ $tag ]->apply_filters( $args[0], $args );
    285243
    286         reset( $wp_filter[ $tag ] );
    287 
    288         do {
    289                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    290                         if ( !is_null($the_['function']) )
    291                                 $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    292 
    293         } while ( next($wp_filter[$tag]) !== false );
    294 
    295244        array_pop( $wp_current_filter );
    296245
    297         return $args[0];
     246        return $filtered;
    298247}
    299248
    300249/**
     
    319268 * @return bool    Whether the function existed before it was removed.
    320269 */
    321270function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
    322         $function_to_remove = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
     271        global $wp_filter;
    323272
    324         $r = isset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
    325 
    326         if ( true === $r ) {
    327                 unset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
    328                 if ( empty( $GLOBALS['wp_filter'][ $tag ][ $priority ] ) ) {
    329                         unset( $GLOBALS['wp_filter'][ $tag ][ $priority ] );
     273        $r = false;
     274        if ( isset( $wp_filter[ $tag ] ) ) {
     275                $r = $wp_filter[ $tag ]->remove_filter( $tag, $function_to_remove, $priority );
     276                if ( ! $wp_filter[ $tag ]->callbacks ) {
     277                        unset( $wp_filter[ $tag ] );
    330278                }
    331                 if ( empty( $GLOBALS['wp_filter'][ $tag ] ) ) {
    332                         $GLOBALS['wp_filter'][ $tag ] = array();
    333                 }
    334                 unset( $GLOBALS['merged_filters'][ $tag ] );
    335279        }
    336280
    337281        return $r;
     
    342286 *
    343287 * @since 2.7.0
    344288 *
    345  * @global array $wp_filter         Stores all of the filters
    346  * @global array $merged_filters    Merges the filter hooks using this function.
     289 * @global array $wp_filter  Stores all of the filters
    347290 *
    348291 * @param string   $tag      The filter to remove hooks from.
    349292 * @param int|bool $priority Optional. The priority number to remove. Default false.
    350293 * @return true True when finished.
    351294 */
    352295function remove_all_filters( $tag, $priority = false ) {
    353         global $wp_filter, $merged_filters;
     296        global $wp_filter;
    354297
    355298        if ( isset( $wp_filter[ $tag ]) ) {
    356                 if ( false === $priority ) {
    357                         $wp_filter[ $tag ] = array();
    358                 } elseif ( isset( $wp_filter[ $tag ][ $priority ] ) ) {
    359                         $wp_filter[ $tag ][ $priority ] = array();
     299                $wp_filter[ $tag ]->remove_all_filters( $priority );
     300                if ( ! $wp_filter[ $tag ]->has_filters() ) {
     301                        unset( $wp_filter[ $tag ] );
    360302                }
    361303        }
    362304
    363         unset( $merged_filters[ $tag ] );
    364 
    365305        return true;
    366306}
    367307
     
    480420 *                        functions hooked to the action. Default empty.
    481421 */
    482422function do_action($tag, $arg = '') {
    483         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     423        global $wp_filter, $wp_actions, $wp_current_filter;
    484424
    485425        if ( ! isset($wp_actions[$tag]) )
    486426                $wp_actions[$tag] = 1;
     
    511451        for ( $a = 2, $num = func_num_args(); $a < $num; $a++ )
    512452                $args[] = func_get_arg($a);
    513453
    514         // Sort
    515         if ( !isset( $merged_filters[ $tag ] ) ) {
    516                 ksort($wp_filter[$tag]);
    517                 $merged_filters[ $tag ] = true;
    518         }
     454        $wp_filter[ $tag ]->do_action( $args );
    519455
    520         reset( $wp_filter[ $tag ] );
    521 
    522         do {
    523                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    524                         if ( !is_null($the_['function']) )
    525                                 call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    526 
    527         } while ( next($wp_filter[$tag]) !== false );
    528 
    529456        array_pop($wp_current_filter);
    530457}
    531458
     
    564491 * @param array  $args The arguments supplied to the functions hooked to `$tag`.
    565492 */
    566493function do_action_ref_array($tag, $args) {
    567         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     494        global $wp_filter, $wp_actions, $wp_current_filter;
    568495
    569496        if ( ! isset($wp_actions[$tag]) )
    570497                $wp_actions[$tag] = 1;
     
    587514        if ( !isset($wp_filter['all']) )
    588515                $wp_current_filter[] = $tag;
    589516
    590         // Sort
    591         if ( !isset( $merged_filters[ $tag ] ) ) {
    592                 ksort($wp_filter[$tag]);
    593                 $merged_filters[ $tag ] = true;
    594         }
     517        $wp_filter[ $tag ]->do_action( $args );
    595518
    596         reset( $wp_filter[ $tag ] );
    597 
    598         do {
    599                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    600                         if ( !is_null($the_['function']) )
    601                                 call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    602 
    603         } while ( next($wp_filter[$tag]) !== false );
    604 
    605519        array_pop($wp_current_filter);
    606520}
    607521
     
    865779function _wp_call_all_hook($args) {
    866780        global $wp_filter;
    867781
    868         reset( $wp_filter['all'] );
    869         do {
    870                 foreach ( (array) current($wp_filter['all']) as $the_ )
    871                         if ( !is_null($the_['function']) )
    872                                 call_user_func_array($the_['function'], $args);
    873 
    874         } while ( next($wp_filter['all']) !== false );
     782        $wp_filter['all']->do_all_hook( $args );
    875783}
    876784
    877785/**
  • tests/phpunit/includes/functions.php

     
    1818
    1919// For adding hooks before loading WP
    2020function tests_add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
    21         global $wp_filter, $merged_filters;
     21        global $wp_filter;
    2222
    2323        $idx = _test_filter_build_unique_id($tag, $function_to_add, $priority);
    2424        $wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
    25         unset( $merged_filters[ $tag ] );
    2625        return true;
    2726}
    2827
    2928function _test_filter_build_unique_id($tag, $function, $priority) {
    30         global $wp_filter;
    31         static $filter_id_count = 0;
    32 
    3329        if ( is_string($function) )
    3430                return $function;
    3531
  • tests/phpunit/includes/testcase.php

     
    218218         * @return void
    219219         */
    220220        protected function _backup_hooks() {
    221                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     221                $globals = array( 'wp_actions', 'wp_current_filter' );
    222222                foreach ( $globals as $key ) {
    223223                        self::$hooks_saved[ $key ] = $GLOBALS[ $key ];
    224224                }
     225                self::$hooks_saved['wp_filter'] = array();
     226                foreach ( $GLOBALS['wp_filter'] as $hook_name => $hook_object ) {
     227                        self::$hooks_saved['wp_filter'][ $hook_name ] = clone $hook_object;
     228                }
    225229        }
    226230
    227231        /**
     
    235239         * @return void
    236240         */
    237241        protected function _restore_hooks() {
    238                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     242                $globals = array( 'wp_actions', 'wp_current_filter' );
    239243                foreach ( $globals as $key ) {
    240244                        if ( isset( self::$hooks_saved[ $key ] ) ) {
    241245                                $GLOBALS[ $key ] = self::$hooks_saved[ $key ];
    242246                        }
    243247                }
     248                if ( isset( self::$hooks_saved['wp_filter'] ) ) {
     249                        $GLOBALS['wp_filter'] = array();
     250                        foreach ( self::$hooks_saved['wp_filter'] as $hook_name => $hook_object ) {
     251                                $GLOBALS['wp_filter'][ $hook_name ] = clone $hook_object;
     252                        }
     253                }
    244254        }
    245255
    246256        function flush_cache() {
  • 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}