Make WordPress Core

Ticket #17817: 17817.13.patch

File 17817.13.patch, 30.0 KB (added by jbrinley, 10 years ago)
  • new file src/wp-includes/class-wp-hook.php

    diff --git src/wp-includes/class-wp-hook.php src/wp-includes/class-wp-hook.php
    new file mode 100644
    index 0000000..2ca730e
    - +  
     1<?php
     2
     3/**
     4 * @package WordPress
     5 * @subpackage Plugin
     6 * @since 4.2.0
     7 */
     8class 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}
  • src/wp-includes/plugin.php

    diff --git src/wp-includes/plugin.php src/wp-includes/plugin.php
    index e5e5288..50d110a 100644
     
    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' );
     24/** @var WP_Hook[] $wp_filter */
     25global $wp_filter, $wp_actions, $wp_current_filter;
    2426
    25 if ( ! isset( $wp_filter ) )
     27if ( $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
    if ( ! isset( $wp_current_filter ) ) 
    6567 * @since 0.71
    6668 *
    6769 * @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.
    7070 *
    7171 * @param string   $tag             The name of the filter to hook the $function_to_add callback to.
    7272 * @param callback $function_to_add The callback to be run when the filter is applied.
    if ( ! isset( $wp_current_filter ) ) 
    7979 * @return boolean true
    8080 */
    8181function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
    82         global $wp_filter, $merged_filters;
     82        global $wp_filter;
     83        if ( ! isset( $wp_filter[ $tag ] ) ) {
     84                $wp_filter[ $tag ] = new WP_Hook();
     85        }
     86        $wp_filter[ $tag ]->add_filter( $function_to_add, $priority, $accepted_args, $tag );
    8387
    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 ] );
    8788        return true;
    8889}
    8990
    function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 
    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'];
    109 
    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;
     108        global $wp_filter;
    129109
    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/**
    function has_filter($tag, $function_to_check = false) { 
    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.
    function has_filter($tag, $function_to_check = false) { 
    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
    function apply_filters( $tag, $value ) { 
    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 
    206         if ( empty($args) )
     173        if ( empty( $args ) ) {
    207174                $args = func_get_args();
     175        }
    208176
    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                         }
     177        array_shift( $args ); // don't pass the tag name to WP_Hook
    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/**
    function apply_filters( $tag, $value ) { 
    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.
    function apply_filters( $tag, $value ) { 
    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']) ) {
    function apply_filters_ref_array($tag, $args) { 
    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         }
    263 
    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 );
     220        $filtered = $wp_filter[ $tag ]->apply_filters( $args[0], $args );
    272221
    273222        array_pop( $wp_current_filter );
    274223
    275         return $args[0];
     224        return $filtered;
    276225}
    277226
    278227/**
    function apply_filters_ref_array($tag, $args) { 
    294243 * @return boolean Whether the function existed before it was removed.
    295244 */
    296245function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
    297         $function_to_remove = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
    298 
    299         $r = isset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
     246        global $wp_filter;
    300247
    301         if ( true === $r ) {
    302                 unset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
    303                 if ( empty( $GLOBALS['wp_filter'][ $tag ][ $priority ] ) ) {
    304                         unset( $GLOBALS['wp_filter'][ $tag ][ $priority ] );
    305                 }
    306                 if ( empty( $GLOBALS['wp_filter'][ $tag ] ) ) {
    307                         $GLOBALS['wp_filter'][ $tag ] = array();
     248        $r = false;
     249        if ( isset( $wp_filter[ $tag ] ) ) {
     250                $r = $wp_filter[ $tag ]->remove_filter( $function_to_remove, $priority, $tag );
     251                if ( ! $wp_filter[ $tag ]->callbacks ) {
     252                        unset( $wp_filter[ $tag ] );
    308253                }
    309                 unset( $GLOBALS['merged_filters'][ $tag ] );
    310254        }
    311255
    312256        return $r;
    function remove_filter( $tag, $function_to_remove, $priority = 10 ) { 
    322266 * @return bool True when finished.
    323267 */
    324268function remove_all_filters( $tag, $priority = false ) {
    325         global $wp_filter, $merged_filters;
     269        global $wp_filter;
    326270
    327         if ( isset( $wp_filter[ $tag ]) ) {
    328                 if ( false === $priority ) {
    329                         $wp_filter[ $tag ] = array();
    330                 } elseif ( isset( $wp_filter[ $tag ][ $priority ] ) ) {
    331                         $wp_filter[ $tag ][ $priority ] = array();
     271        if( isset( $wp_filter[ $tag ] ) ) {
     272                $wp_filter[ $tag ]->remove_all_filters( $priority );
     273                if ( ! $wp_filter[ $tag ]->has_filters() ) {
     274                        unset( $wp_filter[ $tag ] );
    332275                }
    333276        }
    334277
    335         if ( isset( $merged_filters[ $tag ] ) ) {
    336                 unset( $merged_filters[ $tag ] );
    337         }
    338 
    339278        return true;
    340279}
    341280
    function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1) 
    451390 * @return null Will return null if $tag does not exist in $wp_filter array.
    452391 */
    453392function do_action($tag, $arg = '') {
    454         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     393        global $wp_filter, $wp_actions, $wp_current_filter;
    455394
    456395        if ( ! isset($wp_actions[$tag]) )
    457396                $wp_actions[$tag] = 1;
    function do_action($tag, $arg = '') { 
    482421        for ( $a = 2, $num = func_num_args(); $a < $num; $a++ )
    483422                $args[] = func_get_arg($a);
    484423
    485         // Sort
    486         if ( !isset( $merged_filters[ $tag ] ) ) {
    487                 ksort($wp_filter[$tag]);
    488                 $merged_filters[ $tag ] = true;
    489         }
    490 
    491         reset( $wp_filter[ $tag ] );
    492 
    493         do {
    494                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    495                         if ( !is_null($the_['function']) )
    496                                 call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    497 
    498         } while ( next($wp_filter[$tag]) !== false );
     424        $wp_filter[ $tag ]->do_action( $args );
    499425
    500426        array_pop($wp_current_filter);
    501427}
    function did_action($tag) { 
    534460 * @return null Will return null if `$tag` does not exist in `$wp_filter` array.
    535461 */
    536462function do_action_ref_array($tag, $args) {
    537         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     463        global $wp_filter, $wp_actions, $wp_current_filter;
    538464
    539465        if ( ! isset($wp_actions[$tag]) )
    540466                $wp_actions[$tag] = 1;
    function do_action_ref_array($tag, $args) { 
    557483        if ( !isset($wp_filter['all']) )
    558484                $wp_current_filter[] = $tag;
    559485
    560         // Sort
    561         if ( !isset( $merged_filters[ $tag ] ) ) {
    562                 ksort($wp_filter[$tag]);
    563                 $merged_filters[ $tag ] = true;
    564         }
    565 
    566         reset( $wp_filter[ $tag ] );
    567 
    568         do {
    569                 foreach( (array) current($wp_filter[$tag]) as $the_ )
    570                         if ( !is_null($the_['function']) )
    571                                 call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    572 
    573         } while ( next($wp_filter[$tag]) !== false );
     486        $wp_filter[ $tag ]->do_action( $args );
    574487
    575488        array_pop($wp_current_filter);
    576489}
    function register_uninstall_hook( $file, $callback ) { 
    825738 */
    826739function _wp_call_all_hook($args) {
    827740        global $wp_filter;
    828 
    829         reset( $wp_filter['all'] );
    830         do {
    831                 foreach( (array) current($wp_filter['all']) as $the_ )
    832                         if ( !is_null($the_['function']) )
    833                                 call_user_func_array($the_['function'], $args);
    834 
    835         } while ( next($wp_filter['all']) !== false );
     741        $wp_filter['all']->do_all_hook( $args );
    836742}
    837743
    838744/**
  • tests/phpunit/includes/functions.php

    diff --git tests/phpunit/includes/functions.php tests/phpunit/includes/functions.php
    index e8d30cb..64f28f7 100644
     
    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

    diff --git tests/phpunit/includes/testcase.php tests/phpunit/includes/testcase.php
    index 81f9686..ee3053b 100644
    class WP_UnitTestCase extends PHPUnit_Framework_TestCase { 
    140140         * @return void
    141141         */
    142142        protected function _backup_hooks() {
    143                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     143                $globals = array( 'wp_actions', 'wp_current_filter' );
    144144                foreach ( $globals as $key ) {
    145145                        self::$hooks_saved[ $key ] = $GLOBALS[ $key ];
    146146                }
     147                self::$hooks_saved['wp_filter'] = array();
     148                foreach ( $GLOBALS['wp_filter'] as $hook_name => $hook_object ) {
     149                        self::$hooks_saved['wp_filter'][ $hook_name ] = clone( $hook_object );
     150                }
    147151        }
    148152
    149153        /**
    class WP_UnitTestCase extends PHPUnit_Framework_TestCase { 
    157161         * @return void
    158162         */
    159163        protected function _restore_hooks() {
    160                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     164                $globals = array( 'wp_actions', 'wp_current_filter' );
    161165                foreach ( $globals as $key ) {
    162166                        if ( isset( self::$hooks_saved[ $key ] ) ) {
    163167                                $GLOBALS[ $key ] = self::$hooks_saved[ $key ];
    164168                        }
    165169                }
     170                if ( isset( self::$hooks_saved['wp_filter'] ) ) {
     171                        $GLOBALS['wp_filter'] = array();
     172                        foreach ( self::$hooks_saved['wp_filter'] as $hook_name => $hook_object ) {
     173                                $GLOBALS['wp_filter'][ $hook_name ] = clone( $hook_object );
     174                        }
     175                }
    166176        }
    167177
    168178        function flush_cache() {
  • tests/phpunit/tests/actions.php

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

    diff --git tests/phpunit/tests/filters.php tests/phpunit/tests/filters.php
    index d9eda55..d768a68 100644
    class Tests_Filters extends WP_UnitTestCase { 
    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}