WordPress.org

Make WordPress Core

Ticket #17817: 17817.12.patch

File 17817.12.patch, 29.8 KB (added by jbrinley, 4 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..f375c8c
    - +  
     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 ( empty( $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 string $function_key
     90         * @param int $priority
     91         *
     92         * @return bool Whether the callback existed before it was removed
     93         */
     94        public function remove_filter( $function_key, $priority ) {
     95                $exists = isset( $this->callbacks[ $priority ][ $function_key ] );
     96                if ( $exists ) {
     97                        unset( $this->callbacks[ $priority ][ $function_key ] );
     98                        if ( empty( $this->callbacks[ $priority ] ) ) {
     99                                unset( $this->callbacks[ $priority ] );
     100                                if ( $this->nesting_level > 0 ) {
     101                                        $this->resort_active_iterations();
     102                                }
     103                        }
     104                }
     105                return $exists;
     106        }
     107
     108        /**
     109         * Check if a specific action has been registered for this hook.
     110         *
     111         * @param string $function_key The hashed index of the filter
     112         * @return mixed The priority of that hook is returned, or false if the function is not attached.
     113         */
     114        public function has_filter( $function_key ) {
     115                foreach ( $this->callbacks as $priority => &$callbacks ) {
     116                        if ( isset( $callbacks[ $function_key ] ) ) {
     117                                return $priority;
     118                        }
     119                }
     120                return FALSE;
     121        }
     122
     123        /**
     124         * Check if any callbacks have been registered for this hook
     125         *
     126         * @return bool
     127         */
     128        public function has_filters() {
     129                foreach ( $this->callbacks as &$callbacks ) {
     130                        if ( !empty( $callbacks ) ) {
     131                                return TRUE;
     132                        }
     133                }
     134                return FALSE;
     135        }
     136
     137        /**
     138         * Remove all of the callbacks from the filter.
     139         *
     140         * @param int|bool $priority The priority number to remove.
     141         */
     142        public function remove_all_filters( $priority = FALSE ) {
     143                if ( empty( $this->callbacks ) ) {
     144                        return;
     145                }
     146
     147                if ( false === $priority ) {
     148                        $this->callbacks = array();
     149                } else if ( isset( $this->callbacks[ $priority ] ) ) {
     150                        unset( $this->callbacks[ $priority ] );
     151                }
     152
     153                if ( $this->nesting_level > 0 ) {
     154                        $this->resort_active_iterations();
     155                }
     156        }
     157
     158        /**
     159         * Call the functions added to a filter hook.
     160         *
     161         * @param mixed $value The value to filter.
     162         * @param array $args Arguments to pass to callbacks
     163         *
     164         * @return mixed The filtered value after all hooked functions are applied to it
     165         */
     166        public function apply_filters( $value, &$args ) {
     167                if ( empty( $this->callbacks ) ) {
     168                        return $value;
     169                }
     170                $nesting_level = $this->nesting_level++;
     171                $this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
     172
     173                do {
     174                        $priority = current( $this->iterations[ $nesting_level ] );
     175                        foreach ( $this->callbacks[ $priority ] as $the_ ) {
     176                                $args[ 0 ] = $value;
     177                                $value = call_user_func_array( $the_[ 'function' ], array_slice( $args, 0, (int)$the_[ 'accepted_args' ] ) );
     178                        }
     179                } while ( false !== next( $this->iterations[ $nesting_level ] ) );
     180
     181                unset( $this->iterations[ $nesting_level ] );
     182                $this->nesting_level--;
     183                return $value;
     184        }
     185
     186        /**
     187         * Execute functions hooked on a specific action hook.
     188         *
     189         * @param mixed $args Arguments to pass to callbacks
     190         */
     191        public function do_action( &$args ) {
     192                if ( empty( $this->callbacks ) ) {
     193                        return;
     194                }
     195                $nesting_level = $this->nesting_level++;
     196                $this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
     197                $num_args = count( $args );
     198
     199                do {
     200                        $priority = current( $this->iterations[ $nesting_level ] );
     201                        foreach ( $this->callbacks[ $priority ] as $the_ ) {
     202                                $func =& $the_[ 'function' ];
     203                                // avoid the array_slice if possible
     204                                if ( $the_[ 'accepted_args' ] == 0 ) {
     205                                        call_user_func_array( $func, array() );
     206                                } elseif ( $the_[ 'accepted_args' ] >= $num_args ) {
     207                                        call_user_func_array( $func, $args );
     208                                } else {
     209                                        call_user_func_array( $func, array_slice( $args, 0, (int)$the_[ 'accepted_args' ] ) );
     210                                }
     211                        }
     212                } while ( next( $this->iterations[ $nesting_level ] ) !== FALSE );
     213
     214                unset( $this->iterations[ $nesting_level ] );
     215                $this->nesting_level--;
     216        }
     217
     218        /**
     219         * Process the functions hooked into the 'all' hook.
     220         *
     221         * @param array $args Arguments to pass to callbacks
     222         */
     223        public function do_all_hook( &$args ) {
     224                $nesting_level = $this->nesting_level++;
     225                $this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
     226
     227                do {
     228                        $priority = current( $this->iterations[ $nesting_level ] );
     229                        foreach ( $this->callbacks[ $priority ] as $the_ ) {
     230                                call_user_func_array( $the_[ 'function' ], $args );
     231                        }
     232                } while ( false !== next( $this->iterations[ $nesting_level ] ) );
     233
     234                unset( $this->iterations[ $nesting_level ] );
     235                $this->nesting_level--;
     236        }
     237
     238        /**
     239         * Retrieve an external iterator
     240         *
     241         * Provided for backwards compatibility with plugins that iterate over the
     242         * $wp_filter global
     243         *
     244         * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
     245         * @return Traversable An instance of an object implementing Iterator or
     246         *                     Traversable
     247         */
     248        public function getIterator() {
     249                return new ArrayIterator( $this->callbacks );
     250        }
     251
     252        /**
     253         * Some plugins may set up filters before WordPress has initialized.
     254         * Normalize them to WP_Hook objects.
     255         *
     256         * @param array $filters
     257         * @return WP_Hook[]
     258         */
     259        public static function build_preinitialized_hooks( $filters ) {
     260                /** @var WP_Hook[] $normalized */
     261                $normalized = array();
     262                foreach ( $filters as $tag => $callback_groups ) {
     263                        if ( is_object( $callback_groups ) && $callback_groups instanceof WP_Hook ) {
     264                                $normalized[ $tag ] = $callback_groups;
     265                                continue;
     266                        }
     267                        $hook = new WP_Hook();
     268                        foreach ( $callback_groups as $priority => $callbacks ) {
     269                                foreach ( $callbacks as $cb ) {
     270                                        $hook->add_filter( $cb[ 'function' ], $priority, $cb[ 'accepted_args' ], $tag );
     271                                }
     272                        }
     273                        $normalized[ $tag ] = $hook;
     274                }
     275                return $normalized;
     276        }
     277
     278        /**
     279         * Whether an offset exists
     280         *
     281         * @link http://php.net/manual/en/arrayaccess.offsetexists.php
     282         *
     283         * @param mixed $offset An offset to check for.
     284         * @return boolean true on success or false on failure.
     285         */
     286        public function offsetExists( $offset ) {
     287                return isset( $this->callbacks[ $offset ] );
     288        }
     289
     290        /**
     291         * Offset to retrieve
     292         *
     293         * @link http://php.net/manual/en/arrayaccess.offsetget.php
     294         *
     295         * @param mixed $offset The offset to retrieve.
     296         * @return mixed
     297         */
     298        public function offsetGet( $offset ) {
     299                return isset( $this->callbacks[ $offset ] ) ? $this->callbacks[ $offset ] : null;
     300        }
     301
     302        /**
     303         * Offset to set
     304         *
     305         * @link http://php.net/manual/en/arrayaccess.offsetset.php
     306         *
     307         * @param mixed $offset The offset to assign the value to.
     308         * @param mixed $value The value to set.
     309         */
     310        public function offsetSet( $offset, $value ) {
     311                if ( is_null( $offset ) ) {
     312                        $this->callbacks[] = $value;
     313                } else {
     314                        $this->callbacks[ $offset ] = $value;
     315                }
     316        }
     317
     318        /**
     319         * Offset to unset
     320         *
     321         * @link http://php.net/manual/en/arrayaccess.offsetunset.php
     322         *
     323         * @param mixed $offset The offset to unset.
     324         */
     325        public function offsetUnset( $offset ) {
     326                unset( $this->callbacks[ $offset ] );
     327        }
     328}
  • src/wp-includes/plugin.php

    diff --git src/wp-includes/plugin.php src/wp-includes/plugin.php
    index 3ec3a08..607b4d7 100644
     
    2020 */
    2121
    2222// Initialize the filter globals.
    23 global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     23/** @var WP_Hook[] $wp_filter */
     24global $wp_filter, $wp_actions, $wp_current_filter;
    2425
    25 if ( ! isset( $wp_filter ) )
     26if ( !empty( $wp_filter ) ) {
     27        $wp_filter = WP_Hook::build_preinitialized_hooks( $wp_filter );
     28} else {
    2629        $wp_filter = array();
     30}
    2731
    2832if ( ! isset( $wp_actions ) )
    2933        $wp_actions = array();
    3034
    31 if ( ! isset( $merged_filters ) )
    32         $merged_filters = array();
    33 
    3435if ( ! isset( $wp_current_filter ) )
    3536        $wp_current_filter = array();
    3637
    if ( ! isset( $wp_current_filter ) ) 
    6566 * @since 0.71
    6667 *
    6768 * @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.
    7069 *
    7170 * @param string   $tag             The name of the filter to hook the $function_to_add callback to.
    7271 * @param callback $function_to_add The callback to be run when the filter is applied.
    if ( ! isset( $wp_current_filter ) ) 
    7978 * @return boolean true
    8079 */
    8180function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
    82         global $wp_filter, $merged_filters;
     81        global $wp_filter;
     82        if ( ! isset( $wp_filter[ $tag ] ) ) {
     83                $wp_filter[ $tag ] = new WP_Hook();
     84        }
     85        $wp_filter[ $tag ]->add_filter( $function_to_add, $priority, $accepted_args, $tag );
    8386
    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 ] );
    8787        return true;
    8888}
    8989
    function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 
    104104 *                  return value.
    105105 */
    106106function 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                 }
     107        global $wp_filter;
    121108
    122                 if ( ! $exists ) {
    123                         $has = false;
    124                 }
    125         }
     109        $has = isset( $wp_filter[ $tag ] ) && $wp_filter[ $tag ]->has_filters();
    126110
    127111        if ( false === $function_to_check || false == $has )
    128112                return $has;
    function has_filter($tag, $function_to_check = false) { 
    130114        if ( !$idx = _wp_filter_build_unique_id($tag, $function_to_check, false) )
    131115                return false;
    132116
    133         foreach ( (array) array_keys($wp_filter[$tag]) as $priority ) {
    134                 if ( isset($wp_filter[$tag][$priority][$idx]) )
    135                         return $priority;
    136         }
    137 
    138         return false;
     117        return $wp_filter[ $tag ]->has_filter( $idx );
    139118}
    140119
    141120/**
    function has_filter($tag, $function_to_check = false) { 
    166145 * @since 0.71
    167146 *
    168147 * @global array $wp_filter         Stores all of the filters.
    169  * @global array $merged_filters    Merges the filter hooks using this function.
    170148 * @global array $wp_current_filter Stores the list of current filters with the current one last.
    171149 *
    172150 * @param string $tag   The name of the filter hook.
    function has_filter($tag, $function_to_check = false) { 
    175153 * @return mixed The filtered value after all hooked functions are applied to it.
    176154 */
    177155function apply_filters( $tag, $value ) {
    178         global $wp_filter, $merged_filters, $wp_current_filter;
     156        global $wp_filter, $wp_current_filter;
    179157
    180158        $args = array();
    181159
    function apply_filters( $tag, $value ) { 
    184162                $wp_current_filter[] = $tag;
    185163                $args = func_get_args();
    186164                _wp_call_all_hook($args);
     165                array_shift( $args );
    187166        }
    188167
    189168        if ( !isset($wp_filter[$tag]) ) {
    function apply_filters( $tag, $value ) { 
    195174        if ( !isset($wp_filter['all']) )
    196175                $wp_current_filter[] = $tag;
    197176
    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) )
     177        if ( empty( $args ) ) {
    207178                $args = func_get_args();
     179                array_shift( $args );
     180        }
    208181
    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                         }
    215 
    216         } while ( next($wp_filter[$tag]) !== false );
     182        $filtered = $wp_filter[ $tag ]->apply_filters( $value, $args );
    217183
    218184        array_pop( $wp_current_filter );
    219185
    220         return $value;
     186        return $filtered;
    221187}
    222188
    223189/**
    function apply_filters( $tag, $value ) { 
    229195 * functions hooked to `$tag` are supplied using an array.
    230196 *
    231197 * @global array $wp_filter         Stores all of the filters
    232  * @global array $merged_filters    Merges the filter hooks using this function.
    233198 * @global array $wp_current_filter Stores the list of current filters with the current one last
    234199 *
    235200 * @param string $tag  The name of the filter hook.
    function apply_filters( $tag, $value ) { 
    237202 * @return mixed The filtered value after all hooked functions are applied to it.
    238203 */
    239204function apply_filters_ref_array($tag, $args) {
    240         global $wp_filter, $merged_filters, $wp_current_filter;
     205        global $wp_filter, $wp_current_filter;
    241206
    242207        // Do 'all' actions first
    243208        if ( isset($wp_filter['all']) ) {
    function apply_filters_ref_array($tag, $args) { 
    255220        if ( !isset($wp_filter['all']) )
    256221                $wp_current_filter[] = $tag;
    257222
    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 );
     223        $filtered = $wp_filter[ $tag ]->apply_filters( $args[0], $args );
    272224
    273225        array_pop( $wp_current_filter );
    274226
    275         return $args[0];
     227        return $filtered;
    276228}
    277229
    278230/**
    function apply_filters_ref_array($tag, $args) { 
    294246 * @return boolean Whether the function existed before it was removed.
    295247 */
    296248function 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 ] );
     249        global $wp_filter;
    300250
    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();
     251        $r = false;
     252        if ( isset( $wp_filter[ $tag ] ) ) {
     253                $function_to_remove = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
     254                $r = $wp_filter[ $tag ]->remove_filter( $function_to_remove, $priority );
     255                if ( empty( $wp_filter[ $tag ]->callbacks ) ) {
     256                        unset( $wp_filter[ $tag ] );
    308257                }
    309                 unset( $GLOBALS['merged_filters'][ $tag ] );
    310258        }
    311259
    312260        return $r;
    function remove_filter( $tag, $function_to_remove, $priority = 10 ) { 
    322270 * @return bool True when finished.
    323271 */
    324272function remove_all_filters( $tag, $priority = false ) {
    325         global $wp_filter, $merged_filters;
     273        global $wp_filter;
    326274
    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();
     275        if( isset( $wp_filter[ $tag ] ) ) {
     276                $wp_filter[ $tag ]->remove_all_filters( $priority );
     277                if ( ! $wp_filter[ $tag ]->has_filters() ) {
     278                        unset( $wp_filter[ $tag ] );
    332279                }
    333280        }
    334281
    335         if ( isset( $merged_filters[ $tag ] ) ) {
    336                 unset( $merged_filters[ $tag ] );
    337         }
    338 
    339282        return true;
    340283}
    341284
    function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1) 
    451394 * @return null Will return null if $tag does not exist in $wp_filter array.
    452395 */
    453396function do_action($tag, $arg = '') {
    454         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     397        global $wp_filter, $wp_actions, $wp_current_filter;
    455398
    456399        if ( ! isset($wp_actions[$tag]) )
    457400                $wp_actions[$tag] = 1;
    function do_action($tag, $arg = '') { 
    482425        for ( $a = 2; $a < func_num_args(); $a++ )
    483426                $args[] = func_get_arg($a);
    484427
    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 );
     428        $wp_filter[ $tag ]->do_action( $args );
    499429
    500430        array_pop($wp_current_filter);
    501431}
    function did_action($tag) { 
    534464 * @return null Will return null if `$tag` does not exist in `$wp_filter` array.
    535465 */
    536466function do_action_ref_array($tag, $args) {
    537         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     467        global $wp_filter, $wp_actions, $wp_current_filter;
    538468
    539469        if ( ! isset($wp_actions[$tag]) )
    540470                $wp_actions[$tag] = 1;
    function do_action_ref_array($tag, $args) { 
    557487        if ( !isset($wp_filter['all']) )
    558488                $wp_current_filter[] = $tag;
    559489
    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 );
     490        $wp_filter[ $tag ]->do_action( $args );
    574491
    575492        array_pop($wp_current_filter);
    576493}
    function register_uninstall_hook( $file, $callback ) { 
    825742 */
    826743function _wp_call_all_hook($args) {
    827744        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 );
     745        $wp_filter[ 'all' ]->do_all_hook( $args );
    836746}
    837747
    838748/**
  • src/wp-settings.php

    diff --git src/wp-settings.php src/wp-settings.php
    index 715a2c2..98418d1 100644
    require( ABSPATH . WPINC . '/compat.php' ); 
    7272require( ABSPATH . WPINC . '/functions.php' );
    7373require( ABSPATH . WPINC . '/class-wp.php' );
    7474require( ABSPATH . WPINC . '/class-wp-error.php' );
     75require( ABSPATH . WPINC . '/class-wp-hook.php' );
    7576require( ABSPATH . WPINC . '/plugin.php' );
    7677require( ABSPATH . WPINC . '/pomo/mo.php' );
    7778
  • 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 1150bc0..11d317e 100644
    class WP_UnitTestCase extends PHPUnit_Framework_TestCase { 
    130130         * @return void
    131131         */
    132132        protected function _backup_hooks() {
    133                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     133                $globals = array( 'wp_actions', 'wp_current_filter' );
    134134                foreach ( $globals as $key ) {
    135135                        self::$hooks_saved[ $key ] = $GLOBALS[ $key ];
    136136                }
     137                self::$hooks_saved[ 'wp_filter' ] = array();
     138                foreach ( $GLOBALS[ 'wp_filter' ] as $hook_name => $hook_object ) {
     139                        self::$hooks_saved[ 'wp_filter' ][ $hook_name ] = clone( $hook_object );
     140                }
    137141        }
    138142
    139143        /**
    class WP_UnitTestCase extends PHPUnit_Framework_TestCase { 
    147151         * @return void
    148152         */
    149153        protected function _restore_hooks() {
    150                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     154                $globals = array( 'wp_actions', 'wp_current_filter' );
    151155                foreach ( $globals as $key ) {
    152156                        if ( isset( self::$hooks_saved[ $key ] ) ) {
    153157                                $GLOBALS[ $key ] = self::$hooks_saved[ $key ];
    154158                        }
    155159                }
     160                if ( isset( self::$hooks_saved[ 'wp_filter' ] ) ) {
     161                        $GLOBALS[ 'wp_filter' ] = array();
     162                        foreach ( self::$hooks_saved[ 'wp_filter' ] as $hook_name => $hook_object ) {
     163                                $GLOBALS[ 'wp_filter' ][ $hook_name ] = clone( $hook_object );
     164                        }
     165                }
    156166        }
    157167
    158168        function flush_cache() {
  • tests/phpunit/tests/actions.php

    diff --git tests/phpunit/tests/actions.php tests/phpunit/tests/actions.php
    index 583c8ce..d61cbda 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}