WordPress.org

Make WordPress Core

Ticket #17817: 17817.diff

File 17817.diff, 31.6 KB (added by wonderboymusic, 5 years ago)
  • src/wp-includes/class-wp-hook.php

     
     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 ( ! $priority_existed && count( $this->callbacks ) > 1 ) {
     47                        ksort( $this->callbacks, SORT_NUMERIC );
     48                }
     49
     50                if ( $this->nesting_level > 0 ) {
     51                        $this->resort_active_iterations();
     52                }
     53        }
     54
     55        /**
     56         * When a hook's callbacks are changed mid-iteration, the priority
     57         * keys need to be reset, with the array pointer at the correct location
     58         */
     59        private function resort_active_iterations() {
     60                $new_priorities = array_keys( $this->callbacks );
     61
     62                // if there are no remaining hooks, clear out all running iterations
     63                if ( empty( $new_priorities ) ) {
     64                        foreach ( $this->iterations as $index => $iteration ) {
     65                                $this->iterations[ $index ] = $new_priorities;
     66                        }
     67                        return;
     68                }
     69
     70                $min = min( $new_priorities );
     71                foreach ( $this->iterations as $index => $iteration ) {
     72                        $current = current( $iteration );
     73                        $this->iterations[ $index ] = $new_priorities;
     74
     75                        if ( $current < $min ) {
     76                                array_unshift( $this->iterations[ $index ], $current );
     77                                continue;
     78                        }
     79                        while ( current( $this->iterations[ $index ] ) < $current ) {
     80                                if ( false === next( $this->iterations[ $index ] ) ) {
     81                                        break;
     82                                }
     83                        }
     84                }
     85        }
     86
     87        /**
     88         * @param string $function_key
     89         * @param int    $priority
     90         *
     91         * @return bool Whether the callback existed before it was removed
     92         */
     93        public function remove_filter( $function_key, $priority ) {
     94                $exists = isset( $this->callbacks[ $priority ][ $function_key ] );
     95                if ( $exists ) {
     96                        unset( $this->callbacks[ $priority ][ $function_key ] );
     97                        if ( empty( $this->callbacks[ $priority ] ) ) {
     98                                unset( $this->callbacks[ $priority ] );
     99                                if ( $this->nesting_level > 0 ) {
     100                                        $this->resort_active_iterations();
     101                                }
     102                        }
     103                }
     104                return $exists;
     105        }
     106
     107        /**
     108         * Check if an specific action has been registered for this hook.
     109         *
     110         * @param string $function_key The hashed index of the filter
     111         * @return mixed The priority of that hook is returned, or false if the function is not attached.
     112         */
     113        public function has_filter( $function_key ) {
     114                foreach ( $this->callbacks as $priority => &$callbacks ) {
     115                        if ( isset( $callbacks[ $function_key ] ) ) {
     116                                return $priority;
     117                        }
     118                }
     119                return false;
     120        }
     121
     122        /**
     123         * Check if any callbacks have been registered for this hook
     124         *
     125         * @return bool
     126         */
     127        public function has_filters() {
     128                foreach ( $this->callbacks as &$callbacks ) {
     129                        if ( ! empty( $callbacks ) ) {
     130                                return true;
     131                        }
     132                }
     133                return false;
     134        }
     135
     136        /**
     137         * Remove all of the callbacks from the filter.
     138         *
     139         * @param int|bool $priority The priority number to remove.
     140         */
     141        public function remove_all_filters( $priority = false ) {
     142                if ( empty( $this->callbacks ) ) {
     143                        return;
     144                }
     145                if ( false !== $priority && isset( $this->callbacks[ $priority ] ) ) {
     146                        unset( $this->callbacks[ $priority ] );
     147                } else {
     148                        $this->callbacks = array();
     149                }
     150                if ( $this->nesting_level > 0 ) {
     151                        $this->resort_active_iterations();
     152                }
     153        }
     154
     155        /**
     156         * Call the functions added to a filter hook.
     157         *
     158         * @param mixed $value The value to filter.
     159         * @param array $args Arguments to pass to callbacks
     160         *
     161         * @return mixed The filtered value after all hooked functions are applied to it
     162         */
     163        public function apply_filters( $value, &$args ) {
     164                if ( empty( $this->callbacks ) ) {
     165                        return $value;
     166                }
     167                $nesting_level = $this->nesting_level++;
     168                $this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
     169
     170                do {
     171                        $priority = current( $this->iterations[ $nesting_level ] );
     172                        foreach ( $this->callbacks[ $priority ] as $the_ ) {
     173                                $args[0] = $value;
     174                                $value = call_user_func_array( $the_['function'], array_slice( $args, 0, (int) $the_['accepted_args'] ) );
     175                        }
     176                } while ( false !== next( $this->iterations[ $nesting_level ] ) );
     177
     178                unset( $this->iterations[ $nesting_level ] );
     179                $this->nesting_level--;
     180                return $value;
     181        }
     182
     183        /**
     184         * Execute functions hooked on a specific action hook.
     185         *
     186         * @param mixed $args Arguments to pass to callbacks
     187         * @return void
     188         */
     189        public function do_action( &$args ) {
     190                if ( empty( $this->callbacks ) ) {
     191                        return;
     192                }
     193                $nesting_level = $this->nesting_level++;
     194                $this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
     195                $num_args = count( $args );
     196
     197                do {
     198                        $priority = current( $this->iterations[ $nesting_level ] );
     199                        foreach ( $this->callbacks[ $priority ] as $the_ ) {
     200                                $func =& $the_['function'];
     201                                // avoid the array_slice if possible
     202                                if ( $the_['accepted_args'] == 0 ) {
     203                                        call_user_func_array( $func, array() );
     204                                } elseif ( $the_['accepted_args'] >= $num_args ) {
     205                                        call_user_func_array( $func, $args );
     206                                } else {
     207                                        call_user_func_array( $func, array_slice( $args, 0, (int) $the_['accepted_args'] ) );
     208                                }
     209                        }
     210                } while ( false !== next( $this->iterations[ $nesting_level ] ) );
     211
     212                unset( $this->iterations[ $nesting_level ] );
     213                $this->nesting_level--;
     214        }
     215
     216        /**
     217         * Process the functions hooked into the 'all' hook.
     218         *
     219         * @param array $args Arguments to pass to callbacks
     220         */
     221        public function do_all_hook( &$args ) {
     222                $nesting_level = $this->nesting_level++;
     223                $this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
     224
     225                do {
     226                        $priority = current( $this->iterations[ $nesting_level ] );
     227                        foreach ( $this->callbacks[ $priority ] as $the_ ) {
     228                                call_user_func_array( $the_['function'], $args );
     229                        }
     230                } while ( false !== next( $this->iterations[ $nesting_level ] ) );
     231
     232                unset( $this->iterations[ $nesting_level ] );
     233                $this->nesting_level--;
     234        }
     235
     236        /**
     237         * Retrieve an external iterator
     238         *
     239         * Provided for backwards compatibility with plugins that iterate over the
     240         * $wp_filter global
     241         *
     242         * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
     243         * @return Traversable An instance of an object implementing Iterator or
     244         *                     Traversable
     245         */
     246        public function getIterator() {
     247                return new ArrayIterator( $this->callbacks );
     248        }
     249
     250        /**
     251         * Some plugins may set up filters before WordPress has initialized.
     252         * Normalize them to WP_Hook objects.
     253         *
     254         * @param array $filters
     255         * @return WP_Hook[]
     256         */
     257        public static function build_preinitialized_hooks( $filters ) {
     258                /** @var WP_Hook[] $normalized */
     259                $normalized = array();
     260                foreach ( $filters as $tag => $callback_groups ) {
     261                        if ( is_object( $callback_groups ) && $callback_groups instanceof WP_Hook ) {
     262                                $normalized[ $tag ] = $callback_groups;
     263                                continue;
     264                        }
     265                        $hook = new WP_Hook();
     266                        foreach ( $callback_groups as $priority => $callbacks ) {
     267                                foreach ( $callbacks as $cb ) {
     268                                        $hook->add_filter( $cb['function'], $priority, $cb['accepted_args'], $tag );
     269                                }
     270                        }
     271                        $normalized[ $tag ] = $hook;
     272                }
     273                return $normalized;
     274        }
     275
     276        /**
     277         * Whether an offset exists
     278         *
     279         * @link http://php.net/manual/en/arrayaccess.offsetexists.php
     280         *
     281         * @param mixed $offset  An offset to check for.
     282         *
     283         * @return bool
     284         */
     285        public function offsetExists( $offset ) {
     286                return isset( $this->callbacks[ $offset ] );
     287        }
     288
     289        /**
     290         * Offset to retrieve
     291         *
     292         * @link http://php.net/manual/en/arrayaccess.offsetget.php
     293         *
     294         * @param mixed $offset The offset to retrieve.
     295         *
     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}
     329
  • src/wp-includes/plugin.php

     
    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
     
    6465 *
    6566 * @since 0.71
    6667 *
    67  * @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.
     68 * @global array $wp_filter A multidimensional array of all hooks and the callbacks hooked to them.
    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.
     
    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;
    8382
    84         $idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
    85         $wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
    86         unset( $merged_filters[ $tag ] );
     83        if ( ! isset( $wp_filter[ $tag ] ) ) {
     84                $wp_filter[ $tag ] = new WP_Hook();
     85        }
     86
     87        $wp_filter[ $tag ]->add_filter( $function_to_add, $priority, $accepted_args, $tag );
     88
    8789        return true;
    8890}
    8991
     
    103105 *                  that evaluates to false (e.g.) 0, so use the === operator for testing the
    104106 *                  return value.
    105107 */
    106 function has_filter($tag, $function_to_check = false) {
    107         // Don't reset the internal array pointer
    108         $wp_filter = $GLOBALS['wp_filter'];
     108function has_filter( $tag, $function_to_check = false ) {
     109        global $wp_filter;
    109110
    110         $has = ! empty( $wp_filter[ $tag ] );
     111        $has = isset( $wp_filter[ $tag ] ) && $wp_filter[ $tag ]->has_filters();
    111112
    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                 }
     113        if ( ! $function_to_check || ! $has ) {
     114                return $has;
    125115        }
    126116
    127         if ( false === $function_to_check || false == $has )
    128                 return $has;
    129 
    130         if ( !$idx = _wp_filter_build_unique_id($tag, $function_to_check, false) )
     117        if ( ! $idx = _wp_filter_build_unique_id( $tag, $function_to_check, false ) ) {
    131118                return false;
    132 
    133         foreach ( (array) array_keys($wp_filter[$tag]) as $priority ) {
    134                 if ( isset($wp_filter[$tag][$priority][$idx]) )
    135                         return $priority;
    136119        }
    137120
    138         return false;
     121        return $wp_filter[ $tag ]->has_filter( $idx );
    139122}
    140123
    141124/**
     
    166149 * @since 0.71
    167150 *
    168151 * @global array $wp_filter         Stores all of the filters.
    169  * @global array $merged_filters    Merges the filter hooks using this function.
    170152 * @global array $wp_current_filter Stores the list of current filters with the current one last.
    171153 *
    172154 * @param string $tag   The name of the filter hook.
     
    175157 * @return mixed The filtered value after all hooked functions are applied to it.
    176158 */
    177159function apply_filters( $tag, $value ) {
    178         global $wp_filter, $merged_filters, $wp_current_filter;
     160        global $wp_filter, $wp_current_filter;
    179161
    180162        $args = array();
    181163
    182164        // Do 'all' actions first.
    183         if ( isset($wp_filter['all']) ) {
     165        if ( isset( $wp_filter['all'] ) ) {
    184166                $wp_current_filter[] = $tag;
    185167                $args = func_get_args();
    186                 _wp_call_all_hook($args);
     168                _wp_call_all_hook( $args );
     169                array_shift( $args );
    187170        }
    188171
    189         if ( !isset($wp_filter[$tag]) ) {
    190                 if ( isset($wp_filter['all']) )
    191                         array_pop($wp_current_filter);
     172        if ( ! isset( $wp_filter[ $tag ] ) ) {
     173                if ( isset( $wp_filter['all'] ) ) {
     174                        array_pop( $wp_current_filter );
     175                }
    192176                return $value;
    193177        }
    194178
    195         if ( !isset($wp_filter['all']) )
     179        if ( ! isset( $wp_filter['all'] ) ) {
    196180                $wp_current_filter[] = $tag;
    197 
    198         // Sort.
    199         if ( !isset( $merged_filters[ $tag ] ) ) {
    200                 ksort($wp_filter[$tag]);
    201                 $merged_filters[ $tag ] = true;
    202181        }
    203182
    204         reset( $wp_filter[ $tag ] );
    205 
    206         if ( empty($args) )
     183        if ( empty( $args ) ) {
    207184                $args = func_get_args();
     185                array_shift( $args );
     186        }
    208187
    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                         }
     188        $filtered = $wp_filter[ $tag ]->apply_filters( $value, $args );
    215189
    216         } while ( next($wp_filter[$tag]) !== false );
    217 
    218190        array_pop( $wp_current_filter );
    219191
    220         return $value;
     192        return $filtered;
    221193}
    222194
    223195/**
     
    229201 * functions hooked to `$tag` are supplied using an array.
    230202 *
    231203 * @global array $wp_filter         Stores all of the filters
    232  * @global array $merged_filters    Merges the filter hooks using this function.
    233204 * @global array $wp_current_filter Stores the list of current filters with the current one last
    234205 *
    235206 * @param string $tag  The name of the filter hook.
     
    236207 * @param array  $args The arguments supplied to the functions hooked to $tag.
    237208 * @return mixed The filtered value after all hooked functions are applied to it.
    238209 */
    239 function apply_filters_ref_array($tag, $args) {
    240         global $wp_filter, $merged_filters, $wp_current_filter;
     210function apply_filters_ref_array( $tag, $args ) {
     211        global $wp_filter, $wp_current_filter;
    241212
    242213        // Do 'all' actions first
    243         if ( isset($wp_filter['all']) ) {
     214        if ( isset( $wp_filter['all'] ) ) {
    244215                $wp_current_filter[] = $tag;
    245216                $all_args = func_get_args();
    246                 _wp_call_all_hook($all_args);
     217                _wp_call_all_hook( $all_args );
    247218        }
    248219
    249         if ( !isset($wp_filter[$tag]) ) {
    250                 if ( isset($wp_filter['all']) )
    251                         array_pop($wp_current_filter);
     220        if ( ! isset( $wp_filter[$tag] ) ) {
     221                if ( isset( $wp_filter['all'] ) ) {
     222                        array_pop( $wp_current_filter );
     223                }
    252224                return $args[0];
    253225        }
    254226
    255         if ( !isset($wp_filter['all']) )
     227        if ( ! isset( $wp_filter['all'] ) ) {
    256228                $wp_current_filter[] = $tag;
    257 
    258         // Sort
    259         if ( !isset( $merged_filters[ $tag ] ) ) {
    260                 ksort($wp_filter[$tag]);
    261                 $merged_filters[ $tag ] = true;
    262229        }
    263230
    264         reset( $wp_filter[ $tag ] );
     231        $filtered = $wp_filter[ $tag ]->apply_filters( $args[0], $args );
    265232
    266         do {
    267                 foreach( (array) current($wp_filter[$tag]) as $the_ )
    268                         if ( !is_null($the_['function']) )
    269                                 $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    270 
    271         } while ( next($wp_filter[$tag]) !== false );
    272 
    273233        array_pop( $wp_current_filter );
    274234
    275         return $args[0];
     235        return $filtered;
    276236}
    277237
    278238/**
     
    294254 * @return boolean Whether the function existed before it was removed.
    295255 */
    296256function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
    297         $function_to_remove = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
     257        global $wp_filter;
    298258
    299         $r = isset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
    300 
    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 ] );
     259        $r = false;
     260        if ( isset( $wp_filter[ $tag ] ) ) {
     261                $function_to_remove = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
     262                $r = $wp_filter[ $tag ]->remove_filter( $function_to_remove, $priority );
     263                if ( empty( $wp_filter[ $tag ]->callbacks ) ) {
     264                        unset( $wp_filter[ $tag ] );
    305265                }
    306                 if ( empty( $GLOBALS['wp_filter'][ $tag ] ) ) {
    307                         $GLOBALS['wp_filter'][ $tag ] = array();
    308                 }
    309                 unset( $GLOBALS['merged_filters'][ $tag ] );
    310266        }
    311267
    312268        return $r;
     
    322278 * @return bool True when finished.
    323279 */
    324280function remove_all_filters( $tag, $priority = false ) {
    325         global $wp_filter, $merged_filters;
     281        global $wp_filter;
    326282
    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();
    332                 }
     283        if ( isset( $wp_filter[ $tag ] ) ) {
     284                $wp_filter[ $tag ]->remove_all_filters( $priority );
     285                unset( $wp_filter[ $tag ] );
    333286        }
    334287
    335         if ( isset( $merged_filters[ $tag ] ) ) {
    336                 unset( $merged_filters[ $tag ] );
    337         }
    338 
    339288        return true;
    340289}
    341290
     
    450399 *                    functions hooked to the action. Default empty.
    451400 * @return null Will return null if $tag does not exist in $wp_filter array.
    452401 */
    453 function do_action($tag, $arg = '') {
    454         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     402function do_action( $tag, $arg = '' ) {
     403        global $wp_filter, $wp_actions, $wp_current_filter;
    455404
    456         if ( ! isset($wp_actions[$tag]) )
    457                 $wp_actions[$tag] = 1;
    458         else
    459                 ++$wp_actions[$tag];
    460 
     405        if ( ! isset( $wp_actions[ $tag ] ) ) {
     406                $wp_actions[ $tag ] = 1;
     407        } else {
     408                ++$wp_actions[ $tag ];
     409        }
    461410        // Do 'all' actions first
    462         if ( isset($wp_filter['all']) ) {
     411        if ( isset( $wp_filter['all'] ) ) {
    463412                $wp_current_filter[] = $tag;
    464413                $all_args = func_get_args();
    465                 _wp_call_all_hook($all_args);
     414                _wp_call_all_hook( $all_args );
    466415        }
    467416
    468         if ( !isset($wp_filter[$tag]) ) {
    469                 if ( isset($wp_filter['all']) )
    470                         array_pop($wp_current_filter);
     417        if ( ! isset( $wp_filter[ $tag ] ) ) {
     418                if ( isset( $wp_filter['all'] ) ) {
     419                        array_pop( $wp_current_filter );
     420                }
    471421                return;
    472422        }
    473423
    474         if ( !isset($wp_filter['all']) )
     424        if ( ! isset( $wp_filter['all'] ) ) {
    475425                $wp_current_filter[] = $tag;
     426        }
    476427
    477428        $args = array();
    478         if ( is_array($arg) && 1 == count($arg) && isset($arg[0]) && is_object($arg[0]) ) // array(&$this)
     429        // array(&$this)
     430        if ( is_array( $arg ) && 1 == count( $arg ) && isset( $arg[0] ) && is_object( $arg[0] ) ) {
    479431                $args[] =& $arg[0];
    480         else
     432        } else {
    481433                $args[] = $arg;
    482         for ( $a = 2; $a < func_num_args(); $a++ )
     434        }
     435        for ( $a = 2; $a < func_num_args(); $a++ ) {
    483436                $args[] = func_get_arg($a);
    484 
    485         // Sort
    486         if ( !isset( $merged_filters[ $tag ] ) ) {
    487                 ksort($wp_filter[$tag]);
    488                 $merged_filters[ $tag ] = true;
    489437        }
    490438
    491         reset( $wp_filter[ $tag ] );
     439        $wp_filter[ $tag ]->do_action( $args );
    492440
    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 );
    499 
    500         array_pop($wp_current_filter);
     441        array_pop( $wp_current_filter );
    501442}
    502443
    503444/**
     
    533474 * @param array  $args The arguments supplied to the functions hooked to `$tag`.
    534475 * @return null Will return null if `$tag` does not exist in `$wp_filter` array.
    535476 */
    536 function do_action_ref_array($tag, $args) {
    537         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     477function do_action_ref_array( $tag, $args ) {
     478        global $wp_filter, $wp_actions, $wp_current_filter;
    538479
    539         if ( ! isset($wp_actions[$tag]) )
    540                 $wp_actions[$tag] = 1;
    541         else
    542                 ++$wp_actions[$tag];
    543 
     480        if ( ! isset( $wp_actions[ $tag ] ) ) {
     481                $wp_actions[ $tag ] = 1;
     482        } else {
     483                ++$wp_actions[ $tag ];
     484        }
    544485        // Do 'all' actions first
    545         if ( isset($wp_filter['all']) ) {
     486        if ( isset( $wp_filter['all'] ) ) {
    546487                $wp_current_filter[] = $tag;
    547488                $all_args = func_get_args();
    548489                _wp_call_all_hook($all_args);
    549490        }
    550491
    551         if ( !isset($wp_filter[$tag]) ) {
    552                 if ( isset($wp_filter['all']) )
    553                         array_pop($wp_current_filter);
     492        if ( ! isset( $wp_filter[ $tag ] ) ) {
     493                if ( isset( $wp_filter['all'] ) ) {
     494                        array_pop( $wp_current_filter );
     495                }
    554496                return;
    555497        }
    556498
    557         if ( !isset($wp_filter['all']) )
     499        if ( ! isset( $wp_filter['all'] ) ) {
    558500                $wp_current_filter[] = $tag;
    559 
    560         // Sort
    561         if ( !isset( $merged_filters[ $tag ] ) ) {
    562                 ksort($wp_filter[$tag]);
    563                 $merged_filters[ $tag ] = true;
    564501        }
    565502
    566         reset( $wp_filter[ $tag ] );
     503        $wp_filter[ $tag ]->do_action( $args );
    567504
    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 );
    574 
    575         array_pop($wp_current_filter);
     505        array_pop( $wp_current_filter );
    576506}
    577507
    578508/**
     
    823753 *
    824754 * @param array $args The collected parameters from the hook that was called.
    825755 */
    826 function _wp_call_all_hook($args) {
     756function _wp_call_all_hook( $args ) {
    827757        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 );
     758        $wp_filter['all']->do_all_hook( $args );
    836759}
    837760
    838761/**
  • src/wp-settings.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

     
    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

     
    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', 'wp_filter' );
    134134                foreach ( $globals as $key ) {
    135135                        self::$hooks_saved[ $key ] = $GLOBALS[ $key ];
    136136                }
     
    147147         * @return void
    148148         */
    149149        protected function _restore_hooks() {
    150                 $globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
     150                $globals = array( 'wp_actions', 'wp_current_filter', 'wp_filter' );
    151151                foreach ( $globals as $key ) {
    152152                        if ( isset( self::$hooks_saved[ $key ] ) ) {
    153153                                $GLOBALS[ $key ] = self::$hooks_saved[ $key ];
  • tests/phpunit/tests/actions.php

     
    114114                $this->assertEquals( array( $val1 ), array_pop( $argsvar2 ) );
    115115        }
    116116
     117        /**
     118         * Test that multiple callbacks receive the correct number of args even when the number
     119         * is less than, or greater than previous hooks.
     120         *
     121         * @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();
     
    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(
     408                        '__return_null' => array(
     409                                'function' => '__return_null',
     410                                'accepted_args' => 1
     411                        )
     412                );
     413                $this->assertEquals( 11, has_action( $tag, '__return_null' ) );
     414
     415        }
     416
     417        /**
    261418         * Make sure current_action() behaves as current_filter()
    262419         *
    263420         * @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}