diff --git a/wp-includes/class-wp-hook.php b/wp-includes/class-wp-hook.php new file mode 100644 index 0000000000..0bdde1d434 --- /dev/null +++ b/wp-includes/class-wp-hook.php @@ -0,0 +1,516 @@ +callbacks[ $priority ] ); + + $this->callbacks[ $priority ][ $idx ] = array( + 'function' => $function_to_add, + 'accepted_args' => $accepted_args + ); + + // if we're adding a new priority to the list, put them back in sorted order + if ( ! $priority_existed && count( $this->callbacks ) > 1 ) { + ksort( $this->callbacks, SORT_NUMERIC ); + } + + if ( $this->nesting_level > 0 ) { + $this->resort_active_iterations( $priority, $priority_existed ); + } + } + + /** + * Handles reseting callback priority keys mid-iteration. + * + * @since 4.7.0 + * @access private + * + * @param bool|int $new_priority The priority of the new filter being added. Default false, for no priority being added. + * @param bool $priority_existed Flag for whether the priority already existed before the new filter was added. + */ + private function resort_active_iterations( $new_priority = false, $priority_existed = false ) { + $new_priorities = array_keys( $this->callbacks ); + + // If there are no remaining hooks, clear out all running iterations. + if ( ! $new_priorities ) { + foreach ( $this->iterations as $index => $iteration ) { + $this->iterations[ $index ] = $new_priorities; + } + return; + } + + $min = min( $new_priorities ); + foreach ( $this->iterations as $index => &$iteration ) { + $current = current( $iteration ); + // If we're already at the end of this iteration, just leave the array pointer where it is. + if ( false === $current ) { + continue; + } + + $iteration = $new_priorities; + + if ( $current < $min ) { + array_unshift( $iteration, $current ); + continue; + } + + while ( current( $iteration ) < $current ) { + if ( false === next( $iteration ) ) { + break; + } + } + + // If we have a new priority that didn't exist, but ::apply_filters() or ::do_action() thinks it's the current priority... + if ( $new_priority === $this->current_priority[ $index ] && ! $priority_existed ) { + // ... and the new priority is the same as what $this->iterations thinks is the previous priority, + // We need to move back to it. + + if ( false === current( $iteration ) ) { + // If we've already moved off the end of the array, go back to the last element. + $prev = end( $iteration ); + } else { + // Otherwise, just go back to the previous element. + $prev = prev( $iteration ); + } + if ( false === $prev ) { + // Start of the array. Reset, and go about our day. + reset( $iteration ); + } elseif ( $new_priority !== $prev ) { + // Previous wasn't the same. Move forward again. + next( $iteration ); + } + } + } + unset( $iteration ); + } + + /** + * Unhooks a function or method from a specific filter action. + * + * @since 4.7.0 + * @access public + * + * @param string $tag The filter hook to which the function to be removed is hooked. Used + * for building the callback ID when SPL is not available. + * @param callable $function_to_remove The callback to be removed from running when the filter is applied. + * @param int $priority The exact priority used when adding the original filter callback. + * @return bool Whether the callback existed before it was removed. + */ + public function remove_filter( $tag, $function_to_remove, $priority ) { + $function_key = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority ); + + $exists = isset( $this->callbacks[ $priority ][ $function_key ] ); + if ( $exists ) { + unset( $this->callbacks[ $priority ][ $function_key ] ); + if ( ! $this->callbacks[ $priority ] ) { + unset( $this->callbacks[ $priority ] ); + if ( $this->nesting_level > 0 ) { + $this->resort_active_iterations(); + } + } + } + return $exists; + } + + /** + * Checks if a specific action has been registered for this hook. + * + * @since 4.7.0 + * @access public + * + * @param callable|bool $function_to_check Optional. The callback to check for. Default false. + * @param string $tag Optional. The name of the filter hook. Default empty. + * Used for building the callback ID when SPL is not available. + * @return bool|int The priority of that hook is returned, or false if the function is not attached. + */ + public function has_filter( $tag = '', $function_to_check = false ) { + if ( false === $function_to_check ) { + return $this->has_filters(); + } + + $function_key = _wp_filter_build_unique_id( $tag, $function_to_check, false ); + if ( ! $function_key ) { + return false; + } + + foreach ( $this->callbacks as $priority => $callbacks ) { + if ( isset( $callbacks[ $function_key ] ) ) { + return $priority; + } + } + + return false; + } + + /** + * Checks if any callbacks have been registered for this hook. + * + * @since 4.7.0 + * @access public + * + * @return bool True if callbacks have been registered for the current hook, false otherwise. + */ + public function has_filters() { + foreach ( $this->callbacks as $callbacks ) { + if ( $callbacks ) { + return true; + } + } + return false; + } + + /** + * Removes all callbacks from the current filter. + * + * @since 4.7.0 + * @access public + * + * @param int|bool $priority Optional. The priority number to remove. Default false. + */ + public function remove_all_filters( $priority = false ) { + if ( ! $this->callbacks ) { + return; + } + + if ( false === $priority ) { + $this->callbacks = array(); + } else if ( isset( $this->callbacks[ $priority ] ) ) { + unset( $this->callbacks[ $priority ] ); + } + + if ( $this->nesting_level > 0 ) { + $this->resort_active_iterations(); + } + } + + /** + * Calls the callback functions added to a filter hook. + * + * @since 4.7.0 + * @access public + * + * @param mixed $value The value to filter. + * @param array $args Arguments to pass to callbacks. + * @return mixed The filtered value after all hooked functions are applied to it. + */ + public function apply_filters( $value, $args ) { + if ( ! $this->callbacks ) { + return $value; + } + + $nesting_level = $this->nesting_level++; + + $this->iterations[ $nesting_level ] = array_keys( $this->callbacks ); + $num_args = count( $args ); + + do { + $this->current_priority[ $nesting_level ] = $priority = current( $this->iterations[ $nesting_level ] ); + + foreach ( $this->callbacks[ $priority ] as $the_ ) { + if( ! $this->doing_action ) { + $args[ 0 ] = $value; + } + + // Avoid the array_slice if possible. + if ( $the_['accepted_args'] == 0 ) { + $value = call_user_func_array( $the_['function'], array() ); + } elseif ( $the_['accepted_args'] >= $num_args ) { + $value = call_user_func_array( $the_['function'], $args ); + } else { + $value = call_user_func_array( $the_['function'], array_slice( $args, 0, (int)$the_['accepted_args'] ) ); + } + } + } while ( false !== next( $this->iterations[ $nesting_level ] ) ); + + unset( $this->iterations[ $nesting_level ] ); + unset( $this->current_priority[ $nesting_level ] ); + + $this->nesting_level--; + + return $value; + } + + /** + * Executes the callback functions hooked on a specific action hook. + * + * @since 4.7.0 + * @access public + * + * @param mixed $args Arguments to pass to the hook callbacks. + */ + public function do_action( $args ) { + $this->doing_action = true; + $this->apply_filters( '', $args ); + + // If there are recursive calls to the current action, we haven't finished it until we get to the last one. + if ( ! $this->nesting_level ) { + $this->doing_action = false; + } + } + + /** + * Processes the functions hooked into the 'all' hook. + * + * @since 4.7.0 + * @access public + * + * @param array $args Arguments to pass to the hook callbacks. Passed by reference. + */ + public function do_all_hook( &$args ) { + $nesting_level = $this->nesting_level++; + $this->iterations[ $nesting_level ] = array_keys( $this->callbacks ); + + do { + $priority = current( $this->iterations[ $nesting_level ] ); + foreach ( $this->callbacks[ $priority ] as $the_ ) { + call_user_func_array( $the_['function'], $args ); + } + } while ( false !== next( $this->iterations[ $nesting_level ] ) ); + + unset( $this->iterations[ $nesting_level ] ); + $this->nesting_level--; + } + + /** + * Normalizes filters setup before WordPress has initialized to WP_Hook objects. + * + * @since 4.7.0 + * @access public + * @static + * + * @param array $filters Filters to normalize. + * @return WP_Hook[] Array of normalized filters. + */ + public static function build_preinitialized_hooks( $filters ) { + /** @var WP_Hook[] $normalized */ + $normalized = array(); + + foreach ( $filters as $tag => $callback_groups ) { + if ( is_object( $callback_groups ) && $callback_groups instanceof WP_Hook ) { + $normalized[ $tag ] = $callback_groups; + continue; + } + $hook = new WP_Hook(); + + // Loop through callback groups. + foreach ( $callback_groups as $priority => $callbacks ) { + + // Loop through callbacks. + foreach ( $callbacks as $cb ) { + $hook->add_filter( $tag, $cb['function'], $priority, $cb['accepted_args'] ); + } + } + $normalized[ $tag ] = $hook; + } + return $normalized; + } + + /** + * Determines whether an offset value exists. + * + * @since 4.7.0 + * @access public + * + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * + * @param mixed $offset An offset to check for. + * @return bool True if the offset exists, false otherwise. + */ + public function offsetExists( $offset ) { + return isset( $this->callbacks[ $offset ] ); + } + + /** + * Retrieves a value at a specified offset. + * + * @since 4.7.0 + * @access public + * + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * + * @param mixed $offset The offset to retrieve. + * @return mixed If set, the value at the specified offset, null otherwise. + */ + public function offsetGet( $offset ) { + return isset( $this->callbacks[ $offset ] ) ? $this->callbacks[ $offset ] : null; + } + + /** + * Sets a value at a specified offset. + * + * @since 4.7.0 + * @access public + * + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * + * @param mixed $offset The offset to assign the value to. + * @param mixed $value The value to set. + */ + public function offsetSet( $offset, $value ) { + if ( is_null( $offset ) ) { + $this->callbacks[] = $value; + } else { + $this->callbacks[ $offset ] = $value; + } + } + + /** + * Unsets a specified offset. + * + * @since 4.7.0 + * @access public + * + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * + * @param mixed $offset The offset to unset. + */ + public function offsetUnset( $offset ) { + unset( $this->callbacks[ $offset ] ); + } + + /** + * Return the current element + * + * @since 4.7.0 + * @access public + * + * @link http://php.net/manual/en/iterator.current.php + * + * @return array Of callbacks at current priority. + */ + public function current() { + return current( $this->callbacks ); + } + + /** + * Move forward to the next element + * + * @since 4.7.0 + * @access public + * + * @link http://php.net/manual/en/iterator.next.php + * + * @return array Of callbacks at next priority. + */ + public function next() { + return next( $this->callbacks ); + } + + /** + * Return the key of the current element + * + * @since 4.7.0 + * @access public + * + * @link http://php.net/manual/en/iterator.key.php + * + * @return mixed Returns current priority on success, or NULL on failure + */ + public function key() { + return key( $this->callbacks ); + } + + /** + * Checks if current position is valid + * + * @since 4.7.0 + * @access public + * + * @link http://php.net/manual/en/iterator.valid.php + * + * @return boolean + */ + public function valid() { + return key( $this->callbacks ) !== null; + } + + /** + * Rewind the Iterator to the first element + * + * @since 4.7.0 + * @access public + * + * @link http://php.net/manual/en/iterator.rewind.php + */ + public function rewind() { + reset( $this->callbacks ); + } + + +} diff --git a/wp-includes/plugin.php b/wp-includes/plugin.php index bdb9ebd17d..f2585c5a9e 100644 --- a/wp-includes/plugin.php +++ b/wp-includes/plugin.php @@ -22,17 +22,20 @@ */ // Initialize the filter globals. -global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter; +require( ABSPATH . WPINC . '/class-wp-hook.php' ); -if ( ! isset( $wp_filter ) ) +/** @var WP_Hook[] $wp_filter */ +global $wp_filter, $wp_actions, $wp_current_filter; + +if ( $wp_filter ) { + $wp_filter = WP_Hook::build_preinitialized_hooks( $wp_filter ); +} else { $wp_filter = array(); +} if ( ! isset( $wp_actions ) ) $wp_actions = array(); -if ( ! isset( $merged_filters ) ) - $merged_filters = array(); - if ( ! isset( $wp_current_filter ) ) $wp_current_filter = array(); @@ -89,8 +92,6 @@ if ( ! isset( $wp_current_filter ) ) * @since 0.71 * * @global array $wp_filter A multidimensional array of all hooks and the callbacks hooked to them. - * @global array $merged_filters Tracks the tags that need to be merged for later. If the hook is added, - * it doesn't need to run through that process. * * @param string $tag The name of the filter to hook the $function_to_add callback to. * @param callable $function_to_add The callback to be run when the filter is applied. @@ -103,11 +104,11 @@ if ( ! isset( $wp_current_filter ) ) * @return true */ function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) { - global $wp_filter, $merged_filters; - - $idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority); - $wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args); - unset( $merged_filters[ $tag ] ); + global $wp_filter; + if ( ! isset( $wp_filter[ $tag ] ) ) { + $wp_filter[ $tag ] = new WP_Hook(); + } + $wp_filter[ $tag ]->add_filter( $tag, $function_to_add, $priority, $accepted_args ); return true; } @@ -128,38 +129,13 @@ function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 * return value. */ function has_filter($tag, $function_to_check = false) { - // Don't reset the internal array pointer - $wp_filter = $GLOBALS['wp_filter']; + global $wp_filter; - $has = ! empty( $wp_filter[ $tag ] ); - - // Make sure at least one priority has a filter callback - if ( $has ) { - $exists = false; - foreach ( $wp_filter[ $tag ] as $callbacks ) { - if ( ! empty( $callbacks ) ) { - $exists = true; - break; - } - } - - if ( ! $exists ) { - $has = false; - } - } - - if ( false === $function_to_check || false === $has ) - return $has; - - if ( !$idx = _wp_filter_build_unique_id($tag, $function_to_check, false) ) + if ( ! isset( $wp_filter[ $tag ] ) ) { return false; - - foreach ( (array) array_keys($wp_filter[$tag]) as $priority ) { - if ( isset($wp_filter[$tag][$priority][$idx]) ) - return $priority; } - return false; + return $wp_filter[ $tag ]->has_filter( $tag, $function_to_check ); } /** @@ -190,7 +166,6 @@ function has_filter($tag, $function_to_check = false) { * @since 0.71 * * @global array $wp_filter Stores all of the filters. - * @global array $merged_filters Merges the filter hooks using this function. * @global array $wp_current_filter Stores the list of current filters with the current one last. * * @param string $tag The name of the filter hook. @@ -199,7 +174,7 @@ function has_filter($tag, $function_to_check = false) { * @return mixed The filtered value after all hooked functions are applied to it. */ function apply_filters( $tag, $value ) { - global $wp_filter, $merged_filters, $wp_current_filter; + global $wp_filter, $wp_current_filter; $args = array(); @@ -219,29 +194,17 @@ function apply_filters( $tag, $value ) { if ( !isset($wp_filter['all']) ) $wp_current_filter[] = $tag; - // Sort. - if ( !isset( $merged_filters[ $tag ] ) ) { - ksort($wp_filter[$tag]); - $merged_filters[ $tag ] = true; - } - - reset( $wp_filter[ $tag ] ); - if ( empty($args) ) $args = func_get_args(); - do { - foreach ( (array) current($wp_filter[$tag]) as $the_ ) - if ( !is_null($the_['function']) ){ - $args[1] = $value; - $value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args'])); - } + // don't pass the tag name to WP_Hook + array_shift( $args ); - } while ( next($wp_filter[$tag]) !== false ); + $filtered = $wp_filter[ $tag ]->apply_filters( $value, $args ); array_pop( $wp_current_filter ); - return $value; + return $filtered; } /** @@ -253,7 +216,6 @@ function apply_filters( $tag, $value ) { * functions hooked to `$tag` are supplied using an array. * * @global array $wp_filter Stores all of the filters - * @global array $merged_filters Merges the filter hooks using this function. * @global array $wp_current_filter Stores the list of current filters with the current one last * * @param string $tag The name of the filter hook. @@ -261,7 +223,7 @@ function apply_filters( $tag, $value ) { * @return mixed The filtered value after all hooked functions are applied to it. */ function apply_filters_ref_array($tag, $args) { - global $wp_filter, $merged_filters, $wp_current_filter; + global $wp_filter, $wp_current_filter; // Do 'all' actions first if ( isset($wp_filter['all']) ) { @@ -279,24 +241,11 @@ function apply_filters_ref_array($tag, $args) { if ( !isset($wp_filter['all']) ) $wp_current_filter[] = $tag; - // Sort - if ( !isset( $merged_filters[ $tag ] ) ) { - ksort($wp_filter[$tag]); - $merged_filters[ $tag ] = true; - } - - reset( $wp_filter[ $tag ] ); - - do { - foreach ( (array) current($wp_filter[$tag]) as $the_ ) - if ( !is_null($the_['function']) ) - $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args'])); - - } while ( next($wp_filter[$tag]) !== false ); + $filtered = $wp_filter[ $tag ]->apply_filters( $args[0], $args ); array_pop( $wp_current_filter ); - return $args[0]; + return $filtered; } /** @@ -313,7 +262,6 @@ function apply_filters_ref_array($tag, $args) { * @since 1.2.0 * * @global array $wp_filter Stores all of the filters - * @global array $merged_filters Merges the filter hooks using this function. * * @param string $tag The filter hook to which the function to be removed is hooked. * @param callable $function_to_remove The name of the function which should be removed. @@ -321,19 +269,14 @@ function apply_filters_ref_array($tag, $args) { * @return bool Whether the function existed before it was removed. */ function remove_filter( $tag, $function_to_remove, $priority = 10 ) { - $function_to_remove = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority ); + global $wp_filter; - $r = isset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] ); - - if ( true === $r ) { - unset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] ); - if ( empty( $GLOBALS['wp_filter'][ $tag ][ $priority ] ) ) { - unset( $GLOBALS['wp_filter'][ $tag ][ $priority ] ); + $r = false; + if ( isset( $wp_filter[ $tag ] ) ) { + $r = $wp_filter[ $tag ]->remove_filter( $tag, $function_to_remove, $priority ); + if ( ! $wp_filter[ $tag ]->callbacks ) { + unset( $wp_filter[ $tag ] ); } - if ( empty( $GLOBALS['wp_filter'][ $tag ] ) ) { - $GLOBALS['wp_filter'][ $tag ] = array(); - } - unset( $GLOBALS['merged_filters'][ $tag ] ); } return $r; @@ -344,26 +287,22 @@ function remove_filter( $tag, $function_to_remove, $priority = 10 ) { * * @since 2.7.0 * - * @global array $wp_filter Stores all of the filters - * @global array $merged_filters Merges the filter hooks using this function. + * @global array $wp_filter Stores all of the filters * * @param string $tag The filter to remove hooks from. * @param int|bool $priority Optional. The priority number to remove. Default false. * @return true True when finished. */ function remove_all_filters( $tag, $priority = false ) { - global $wp_filter, $merged_filters; + global $wp_filter; if ( isset( $wp_filter[ $tag ]) ) { - if ( false === $priority ) { - $wp_filter[ $tag ] = array(); - } elseif ( isset( $wp_filter[ $tag ][ $priority ] ) ) { - $wp_filter[ $tag ][ $priority ] = array(); + $wp_filter[ $tag ]->remove_all_filters( $priority ); + if ( ! $wp_filter[ $tag ]->has_filters() ) { + unset( $wp_filter[ $tag ] ); } } - unset( $merged_filters[ $tag ] ); - return true; } @@ -473,7 +412,6 @@ function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1) * * @global array $wp_filter Stores all of the filters * @global array $wp_actions Increments the amount of times action was triggered. - * @global array $merged_filters Merges the filter hooks using this function. * @global array $wp_current_filter Stores the list of current filters with the current one last * * @param string $tag The name of the action to be executed. @@ -481,7 +419,7 @@ function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1) * functions hooked to the action. Default empty. */ function do_action($tag, $arg = '') { - global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter; + global $wp_filter, $wp_actions, $wp_current_filter; if ( ! isset($wp_actions[$tag]) ) $wp_actions[$tag] = 1; @@ -512,20 +450,7 @@ function do_action($tag, $arg = '') { for ( $a = 2, $num = func_num_args(); $a < $num; $a++ ) $args[] = func_get_arg($a); - // Sort - if ( !isset( $merged_filters[ $tag ] ) ) { - ksort($wp_filter[$tag]); - $merged_filters[ $tag ] = true; - } - - reset( $wp_filter[ $tag ] ); - - do { - foreach ( (array) current($wp_filter[$tag]) as $the_ ) - if ( !is_null($the_['function']) ) - call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args'])); - - } while ( next($wp_filter[$tag]) !== false ); + $wp_filter[ $tag ]->do_action( $args ); array_pop($wp_current_filter); } @@ -558,14 +483,13 @@ function did_action($tag) { * functions hooked to $tag< are supplied using an array. * @global array $wp_filter Stores all of the filters * @global array $wp_actions Increments the amount of times action was triggered. - * @global array $merged_filters Merges the filter hooks using this function. * @global array $wp_current_filter Stores the list of current filters with the current one last * * @param string $tag The name of the action to be executed. * @param array $args The arguments supplied to the functions hooked to `$tag`. */ function do_action_ref_array($tag, $args) { - global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter; + global $wp_filter, $wp_actions, $wp_current_filter; if ( ! isset($wp_actions[$tag]) ) $wp_actions[$tag] = 1; @@ -588,20 +512,7 @@ function do_action_ref_array($tag, $args) { if ( !isset($wp_filter['all']) ) $wp_current_filter[] = $tag; - // Sort - if ( !isset( $merged_filters[ $tag ] ) ) { - ksort($wp_filter[$tag]); - $merged_filters[ $tag ] = true; - } - - reset( $wp_filter[ $tag ] ); - - do { - foreach ( (array) current($wp_filter[$tag]) as $the_ ) - if ( !is_null($the_['function']) ) - call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args'])); - - } while ( next($wp_filter[$tag]) !== false ); + $wp_filter[ $tag ]->do_action( $args ); array_pop($wp_current_filter); } @@ -923,13 +834,7 @@ function register_uninstall_hook( $file, $callback ) { function _wp_call_all_hook($args) { global $wp_filter; - reset( $wp_filter['all'] ); - do { - foreach ( (array) current($wp_filter['all']) as $the_ ) - if ( !is_null($the_['function']) ) - call_user_func_array($the_['function'], $args); - - } while ( next($wp_filter['all']) !== false ); + $wp_filter['all']->do_all_hook( $args ); } /** diff --git a/wp-includes/version.php b/wp-includes/version.php index 06477ed425..34040bc198 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -4,7 +4,7 @@ * * @global string $wp_version */ -$wp_version = '4.7-alpha-38559'; +$wp_version = '4.7-alpha-38571'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.