Index: tests/phpunit/includes/functions.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- tests/phpunit/includes/functions.php	(revision 26243)
+++ tests/phpunit/includes/functions.php	(revision )
@@ -1,37 +1,5 @@
 <?php
 
-// For adding hooks before loading WP
-function tests_add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
-	global $wp_filter, $merged_filters;
-
-	$idx = _test_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 ] );
-	return true;
-}
-
-function _test_filter_build_unique_id($tag, $function, $priority) {
-	global $wp_filter;
-	static $filter_id_count = 0;
-
-	if ( is_string($function) )
-		return $function;
-
-	if ( is_object($function) ) {
-		// Closures are currently implemented as objects
-		$function = array( $function, '' );
-	} else {
-		$function = (array) $function;
-	}
-
-	if (is_object($function[0]) ) {
-		return spl_object_hash($function[0]) . $function[1];
-	} else if ( is_string($function[0]) ) {
-		// Static Calling
-		return $function[0].$function[1];
-	}
-}
-
 function _delete_all_posts() {
 	global $wpdb;
 
Index: src/wp-includes/class-wp-hook.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/wp-includes/class-wp-hook.php	(revision )
+++ src/wp-includes/class-wp-hook.php	(revision )
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * Class WP_Hook
+ *
+ * TODO: This implements SplSubject. Make that explicit
+ *       when we can depend on SPL's presence.
+ */
+class WP_Hook {
+	// TODO: When PHP 5.3 is the minimum version, use SplObjectStorage
+	/** @var SplObserver[] */
+	private $observers = array();
+	public $callbacks = array();
+
+	public function add_filter( $function_to_add, $priority, $accepted_args, $tag ) {
+		global $wp_filter;
+		$idx = $wp_filter->build_unique_id($function_to_add, $tag, $priority);
+		$this->callbacks[$priority][$idx] = array( 'function' => $function_to_add, 'accepted_args' => $accepted_args );
+		$this->notify();
+	}
+
+	/**
+	 * @param string $function_key
+	 * @param int $priority
+	 *
+	 * @return bool Whether the callback existed before it was removed
+	 */
+	public function remove_filter( $function_key, $priority ) {
+		$exists = isset($this->callbacks[$priority][$function_key]);
+		if ( $exists ) {
+			unset($this->callbacks[$priority][$function_key]);
+			if ( empty($this->callbacks[$priority]) ) {
+				unset($this->callbacks[$priority]);
+			}
+			$this->notify();
+		}
+		return $exists;
+	}
+
+	/**
+	 * Check if any action has been registered for a hook.
+	 *
+	 * @param string $function_key The hashed index of the filter
+	 * @return mixed The priority of that hook is returned, or false if the function is not attached.
+	 */
+	public function has_filter( $function_key ) {
+		foreach ( $this->callbacks as $priority => &$callbacks ) {
+			if ( isset($callbacks[$function_key]) ) {
+				return $priority;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Remove all of the callbacks from the filter.
+	 *
+	 * @param int|bool $priority The priority number to remove.
+	 * @return void
+	 */
+	public function remove_all_filters( $priority = false ) {
+		if ( empty($this->callbacks) ) {
+			return;
+		}
+		if( false !== $priority && isset($this->callbacks[$priority]) ) {
+			unset($this->callbacks[$priority]);
+		} else {
+			$this->callbacks = array();
+		}
+		$this->notify();
+	}
+
+	public function get_iterator() {
+		$iterator = new WP_Hook_Iterator( $this );
+		return $iterator;
+	}
+
+	public function attach( $observer ) {
+		$this->observers[] = $observer;
+	}
+
+	public function detach( $observer ) {
+		foreach ( $this->observers as $key => $o ) {
+			if ( $observer === $o ) {
+				unset($this->observers[$key]);
+			}
+		}
+	}
+
+	public function notify() {
+		foreach ( $this->observers as $o ) {
+			$o->update($this);
+		}
+	}
+}
+ 
\ No newline at end of file
Index: src/wp-includes/class-wp-filters.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/wp-includes/class-wp-filters.php	(revision )
+++ src/wp-includes/class-wp-filters.php	(revision )
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * Class WP_Filters
+ */
+class WP_Filters implements ArrayAccess {
+	/** @var WP_Hook[] */
+	private $hooks = array();
+	/** @var callable */
+	private $hasher;
+
+	public function __construct() {
+		$this->setup_hasher();
+	}
+
+	/**
+	 * Decide how we're going to hash object methods
+	 * @return void
+	 */
+	private function setup_hasher() {
+		if ( function_exists('spl_object_hash') ) {
+			$this->hasher = array( $this, 'spl_hasher' );
+		} else {
+			$this->hasher = array( $this, 'nospl_hasher' );
+		}
+	}
+
+	/**
+	 * Build a unique ID for storage and retrieval of a callback.
+	 *
+	 * @param callback $function Used for creating unique id
+	 * @param string $tag Used when SPL is unavailable for counting how many hooks were applied
+	 * @param int|bool $priority Used when SPL is unavailable for counting how many hooks were applied. If === false and $function is an object reference, we return the unique id only if it already has one, false otherwise.
+	 * @return string|bool Unique ID for usage as array key or false if $priority === false and $function is an object reference, and it does not already have a unique id.
+	 */
+	public function build_unique_id( $function, $tag, $priority ) {
+		if ( is_string($function) )
+			return $function;
+
+		if ( is_object($function) ) {
+			// Closures are currently implemented as objects
+			$function = array( $function, '' );
+		} else {
+			$function = (array) $function;
+		}
+
+		if (is_object($function[0]) ) {
+			return call_user_func( $this->hasher, $function, $tag, $priority );
+		} else if ( is_string($function[0]) ) {
+			// Static Calling
+			return $function[0] . '::' . $function[1];
+		}
+	}
+
+	private function spl_hasher( $function ) {
+		return spl_object_hash($function[0]) . $function[1];
+	}
+
+	private function nospl_hasher( $function, $tag, $priority ) {
+		static $filter_id_count = 0;
+
+		$obj_idx = get_class($function[0]).$function[1];
+		if ( !isset($function[0]->wp_filter_id) ) {
+			if ( false === $priority )
+				return false;
+			$obj_idx .= isset($this->hooks[$tag]->callbacks[$priority]) ? count((array)$this->hooks[$tag]->callbacks[$priority]) : $filter_id_count;
+			$function[0]->wp_filter_id = $filter_id_count;
+			++$filter_id_count;
+		} else {
+			$obj_idx .= $function[0]->wp_filter_id;
+		}
+
+		return $obj_idx;
+	}
+
+	/**
+	 * Whether a offset exists
+	 * @link http://php.net/manual/en/arrayaccess.offsetexists.php
+	 *
+	 * @param mixed $offset An offset to check for.
+	 *
+	 * @return boolean true on success or false on failure.
+	 */
+	public function offsetExists( $offset ) {
+		return isset($this->hooks[$offset]);
+	}
+
+	/**
+	 * Offset to retrieve
+	 *
+	 * @link http://php.net/manual/en/arrayaccess.offsetget.php
+	 *
+	 * @param mixed $offset The offset to retrieve.
+	 *
+	 * @return WP_Hook Can return all value types.
+	 */
+	public function offsetGet( $offset ) {
+		return isset($this->hooks[$offset]) ? $this->hooks[$offset] : null;
+	}
+
+	/**
+	 * Offset to set
+	 *
+	 * @link http://php.net/manual/en/arrayaccess.offsetset.php
+	 *
+	 * @param mixed $offset The offset to assign the value to.
+	 * @param WP_Hook $value The value to set.
+	 *
+	 * @return void
+	 */
+	public function offsetSet( $offset, $value ) {
+		if ( is_null($offset) ) {
+			$this->hooks[] = $value;
+		} else {
+			$this->hooks[$offset] = $value;
+		}
+	}
+
+	/**
+	 * Offset to unset
+	 *
+	 * @link http://php.net/manual/en/arrayaccess.offsetunset.php
+	 *
+	 * @param mixed $offset The offset to unset.
+	 *
+	 * @return void
+	 */
+	public function offsetUnset( $offset ) {
+		unset($this->hooks[$offset]);
+	}
+}
+ 
\ No newline at end of file
Index: src/wp-includes/plugin.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/wp-includes/plugin.php	(revision 26243)
+++ src/wp-includes/plugin.php	(revision )
@@ -20,17 +20,17 @@
  */
 
 // Initialize the filter globals.
-global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
+require( ABSPATH . '/wp-includes/class-wp-filters.php' );
+require( ABSPATH . '/wp-includes/class-wp-hook.php' );
+require( ABSPATH . '/wp-includes/class-wp-hook-iterator.php' );
+global $wp_filter, $wp_actions, $wp_current_filter;
 
 if ( ! isset( $wp_filter ) )
-	$wp_filter = array();
+	$wp_filter = new WP_Filters();
 
 if ( ! isset( $wp_actions ) )
 	$wp_actions = array();
 
-if ( ! isset( $merged_filters ) )
-	$merged_filters = array();
-
 if ( ! isset( $wp_current_filter ) )
 	$wp_current_filter = array();
 
@@ -67,7 +67,6 @@
  * @subpackage Plugin
  *
  * @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.
  *
  * @since 0.71
  *
@@ -80,11 +79,12 @@
  * @return boolean true
  */
 function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
-	global $wp_filter, $merged_filters;
+	global $wp_filter;
+	if ( !isset($wp_filter[$tag]) ) {
+		$wp_filter[$tag] = new WP_Hook();
+	}
+	$wp_filter[$tag]->add_filter( $function_to_add, $priority, $accepted_args, $tag );
 
-	$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 ] );
 	return true;
 }
 
@@ -106,21 +106,16 @@
 function has_filter($tag, $function_to_check = false) {
 	global $wp_filter;
 
-	$has = !empty($wp_filter[$tag]);
+	$has = isset($wp_filter[$tag]) && !empty($wp_filter[$tag]->callbacks);
 	if ( false === $function_to_check || false == $has )
 		return $has;
 
 	if ( !$idx = _wp_filter_build_unique_id($tag, $function_to_check, false) )
 		return false;
 
-	foreach ( (array) array_keys($wp_filter[$tag]) as $priority ) {
-		if ( isset($wp_filter[$tag][$priority][$idx]) )
-			return $priority;
+	return $wp_filter[$tag]->has_filter($idx);
-	}
+}
 
-	return false;
-}
-
 /**
  * Call the functions added to a filter hook.
  *
@@ -150,7 +145,6 @@
  * @subpackage Plugin
  *
  * @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
  *
  * @since 0.71
@@ -161,7 +155,7 @@
  * @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();
 
@@ -181,26 +175,16 @@
 	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']) ){
+	$iterator = $wp_filter[$tag]->get_iterator();
+	foreach ( $iterator as $the_ ) {
-				$args[1] = $value;
-				$value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
-			}
+		$args[1] = $value;
+		$value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
+	}
+	$iterator->cleanup();
 
-	} while ( next($wp_filter[$tag]) !== false );
-
 	array_pop( $wp_current_filter );
 
 	return $value;
@@ -216,7 +200,6 @@
  * @subpackage Plugin
  * @since 3.0.0
  * @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.
@@ -224,7 +207,7 @@
  * @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']) ) {
@@ -242,21 +225,12 @@
 	if ( !isset($wp_filter['all']) )
 		$wp_current_filter[] = $tag;
 
-	// Sort
-	if ( !isset( $merged_filters[ $tag ] ) ) {
-		ksort($wp_filter[$tag]);
-		$merged_filters[ $tag ] = true;
+	$iterator = $wp_filter[$tag]->get_iterator();
+	foreach ( $iterator as $the_ ) {
+		$args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
 	}
+	$iterator->cleanup();
 
-	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 );
-
 	array_pop( $wp_current_filter );
 
 	return $args[0];
@@ -284,15 +258,12 @@
  * @return boolean 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]);
-		unset($GLOBALS['merged_filters'][$tag]);
+	$r = false;
+	if ( isset($wp_filter[$tag]) ) {
+		$function_to_remove = $wp_filter->build_unique_id($function_to_remove, $tag, $priority);
+		$r = $wp_filter[$tag]->remove_filter($function_to_remove, $priority);
 	}
 
 	return $r;
@@ -308,18 +279,12 @@
  * @return bool 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 && isset($wp_filter[$tag][$priority]) )
-			unset($wp_filter[$tag][$priority]);
-		else
-			unset($wp_filter[$tag]);
+		$wp_filter[$tag]->remove_all_filters($priority);
 	}
 
-	if( isset($merged_filters[$tag]) )
-		unset($merged_filters[$tag]);
-
 	return true;
 }
 
@@ -384,7 +349,7 @@
  * @return null Will return null if $tag does not exist in $wp_filter array
  */
 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;
@@ -415,21 +380,12 @@
 	for ( $a = 2; $a < func_num_args(); $a++ )
 		$args[] = func_get_arg($a);
 
-	// Sort
-	if ( !isset( $merged_filters[ $tag ] ) ) {
-		ksort($wp_filter[$tag]);
-		$merged_filters[ $tag ] = true;
+	$iterator = $wp_filter[$tag]->get_iterator();
+	foreach ( $iterator as $the_ ) {
+		call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
 	}
+	$iterator->cleanup();
 
-	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 );
-
 	array_pop($wp_current_filter);
 }
 
@@ -470,7 +426,7 @@
  * @return null Will return null if $tag does not exist in $wp_filter array
  */
 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;
@@ -493,21 +449,12 @@
 	if ( !isset($wp_filter['all']) )
 		$wp_current_filter[] = $tag;
 
-	// Sort
-	if ( !isset( $merged_filters[ $tag ] ) ) {
-		ksort($wp_filter[$tag]);
-		$merged_filters[ $tag ] = true;
+	$iterator = $wp_filter[$tag]->get_iterator();
+	foreach ( $iterator as $the_ ) {
+		call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
 	}
+	$iterator->cleanup();
 
-	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 );
-
 	array_pop($wp_current_filter);
 }
 
@@ -731,15 +678,14 @@
  */
 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']) )
+	$iterator = $wp_filter['all']->get_iterator();
+	foreach ( $iterator as $the_ ) {
+		if ( !is_null($the_['function']) ) {
-				call_user_func_array($the_['function'], $args);
+			call_user_func_array($the_['function'], $args);
-
-	} while ( next($wp_filter['all']) !== false );
-}
+		}
+	}
+	$iterator->cleanup();
+}
 
 /**
  * Build Unique ID for storage and retrieval.
@@ -772,38 +718,5 @@
  */
 function _wp_filter_build_unique_id($tag, $function, $priority) {
 	global $wp_filter;
-	static $filter_id_count = 0;
-
-	if ( is_string($function) )
-		return $function;
-
-	if ( is_object($function) ) {
-		// Closures are currently implemented as objects
-		$function = array( $function, '' );
-	} else {
-		$function = (array) $function;
-	}
-
-	if (is_object($function[0]) ) {
-		// Object Class Calling
-		if ( function_exists('spl_object_hash') ) {
-			return spl_object_hash($function[0]) . $function[1];
-		} else {
-			$obj_idx = get_class($function[0]).$function[1];
-			if ( !isset($function[0]->wp_filter_id) ) {
-				if ( false === $priority )
-					return false;
-				$obj_idx .= isset($wp_filter[$tag][$priority]) ? count((array)$wp_filter[$tag][$priority]) : $filter_id_count;
-				$function[0]->wp_filter_id = $filter_id_count;
-				++$filter_id_count;
-			} else {
-				$obj_idx .= $function[0]->wp_filter_id;
-			}
-
-			return $obj_idx;
-		}
-	} else if ( is_string($function[0]) ) {
-		// Static Calling
-		return $function[0] . '::' . $function[1];
-	}
+	return $wp_filter->build_unique_id($function, $tag, $priority);
 }
Index: tests/phpunit/tests/actions.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- tests/phpunit/tests/actions.php	(revision 26243)
+++ tests/phpunit/tests/actions.php	(revision )
@@ -256,4 +256,62 @@
 	function action_self_removal() {
 		remove_action( 'test_action_self_removal', array( $this, 'action_self_removal' ) );
 	}
+
+	/**
+	 * @ticket 17817
+	 */
+	function test_action_recursion() {
+		$tag = rand_str();
+		$a = new MockAction();
+		$b = new MockAction();
+
+		add_action( $tag, array($a, 'action'), 11, 1 );
+		add_action( $tag, array($b, 'action'), 13, 1 );
+		add_action( $tag, array($this, 'action_that_causes_recursion'), 12, 1 );
+		do_action( $tag, $tag );
+
+		$this->assertEquals( 2, $a->get_call_count(), 'recursive actions should call all callbacks with earlier priority' );
+		$this->assertEquals( 2, $b->get_call_count(), 'recursive actions should call callbacks with later priority' );
+	}
+
+	function action_that_causes_recursion( $tag ) {
+		static $recursing = FALSE;
+		if ( !$recursing ) {
+			$recursing = TRUE;
+			do_action( $tag, $tag );
+		}
+		$recursing = FALSE;
+	}
+
+	/**
+	 * @ticket 9968
+	 */
+	function test_action_callback_manipulation_while_running() {
+		$tag = rand_str();
+		$a = new MockAction();
+		$b = new MockAction();
+		$c = new MockAction();
+		$d = new MockAction();
+		$e = new MockAction();
+
+		add_action( $tag, array($a, 'action'), 11, 2 );
+		add_action( $tag, array($this, 'action_that_manipulates_a_running_hook'), 12, 2 );
+		add_action( $tag, array($b, 'action'), 12, 2 );
+
+		do_action( $tag, $tag, array($a,$b,$c,$d,$e) );
+		do_action( $tag, $tag, array($a,$b,$c,$d,$e) );
+
+		$this->assertEquals( 2, $a->get_call_count(), 'callbacks should run unless otherwise instructed' );
+		$this->assertEquals( 1, $b->get_call_count(), 'callback removed by same priority callback should still get called' );
+		$this->assertEquals( 1, $c->get_call_count(), 'callback added by same priority callback should not get called' );
+		$this->assertEquals( 2, $d->get_call_count(), 'callback added by earlier priority callback should get called' );
+		$this->assertEquals( 1, $e->get_call_count(), 'callback added by later priority callback should not get called' );
+	}
+
+	function action_that_manipulates_a_running_hook( $tag, $mocks ) {
+		remove_action( $tag, array($mocks[1], 'action'), 12, 2 );
+		add_action( $tag, array($mocks[2], 'action' ), 12, 2 );
+		add_action( $tag, array($mocks[3], 'action' ), 13, 2 );
+		add_action( $tag, array($mocks[4], 'action' ), 10, 2 );
+	}
 }
Index: tests/phpunit/includes/bootstrap.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- tests/phpunit/includes/bootstrap.php	(revision 26243)
+++ tests/phpunit/includes/bootstrap.php	(revision )
@@ -67,6 +67,10 @@
 
 require_once dirname( __FILE__ ) . '/functions.php';
 
+// load plugin.php out of order so that we can set up
+// some filters in advance
+require_once ABSPATH . '/wp-includes/plugin.php';
+
 // Preset WordPress options defined in bootstrap file.
 // Used to activate themes, plugins, as well as  other settings.
 if(isset($GLOBALS['wp_tests_options'])) {
@@ -76,7 +80,7 @@
 	}
 
 	foreach ( array_keys( $GLOBALS['wp_tests_options'] ) as $key ) {
-		tests_add_filter( 'pre_option_'.$key, 'wp_tests_options' );
+		add_filter( 'pre_option_'.$key, 'wp_tests_options' );
 	}
 }
 
Index: src/wp-settings.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/wp-settings.php	(revision 26243)
+++ src/wp-settings.php	(revision )
@@ -72,7 +72,7 @@
 require( ABSPATH . WPINC . '/functions.php' );
 require( ABSPATH . WPINC . '/class-wp.php' );
 require( ABSPATH . WPINC . '/class-wp-error.php' );
-require( ABSPATH . WPINC . '/plugin.php' );
+require_once( ABSPATH . WPINC . '/plugin.php' );
 require( ABSPATH . WPINC . '/pomo/mo.php' );
 
 // Include the wpdb class and, if present, a db.php database drop-in.
Index: src/wp-includes/class-wp-hook-iterator.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/wp-includes/class-wp-hook-iterator.php	(revision )
+++ src/wp-includes/class-wp-hook-iterator.php	(revision )
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * Class WP_Hook_Iterator
+ *
+ * TODO: This implements SplObserver. Make that explicit
+ *       when we can depend on SPL's presence.
+ */
+class WP_Hook_Iterator implements Iterator {
+	/** @var WP_Hook  */
+	private $hook = NULL;
+	private $current_callback = NULL;
+	private $current_callback_index = NULL;
+	private $current_priority = NULL;
+	private $current_priority_index = NULL;
+	private $all_priorities = NULL;
+	private $callbacks_for_current_priority = array();
+
+	public function __construct( WP_Hook $hook ) {
+		$this->hook = $hook;
+		$this->hook->attach($this);
+		$this->rewind();
+	}
+
+	public function cleanup() {
+		$this->hook->detach($this);
+	}
+
+	/**
+	 * Return the current element
+	 *
+	 * @link http://php.net/manual/en/iterator.current.php
+	 * @return mixed Can return any type.
+	 */
+	public function current() {
+		return $this->current_callback;
+	}
+
+	/**
+	 * Move forward to next element
+	 * @link http://php.net/manual/en/iterator.next.php
+	 * @return void Any returned value is ignored.
+	 */
+	public function next() {
+		$this->current_callback = NULL;
+		$this->current_callback_index = NULL;
+		$next = next( $this->callbacks_for_current_priority );
+
+		if ( $next === FALSE ) {
+			do {
+				$this->increment_priority();
+			} while ( empty($this->callbacks_for_current_priority) && isset( $this->current_priority) );
+
+			$next = reset( $this->callbacks_for_current_priority );
+		}
+
+		if ( !empty($next) ) {
+			$this->current_callback = $next;
+			$this->current_callback_index = key($this->callbacks_for_current_priority);
+		}
+	}
+
+	/**
+	 * Setup the callbacks array for the next available priority
+	 * @return void
+	 */
+	private function increment_priority() {
+		$this->set_current_priority();
+		if ( isset($this->current_priority) ) {
+			$this->callbacks_for_current_priority = $this->get_callbacks($this->current_priority);
+		} else {
+			$this->callbacks_for_current_priority = array();
+		}
+	}
+
+	/**
+	 * Move to the next available priority
+	 * @return void
+	 */
+	private function set_current_priority() {
+		if ( empty($this->hook->callbacks) ) {
+			$this->all_priorities = array();
+			$this->current_priority = NULL;
+			return;
+		}
+
+		if ( !isset($this->all_priorities) ) {
+			$this->all_priorities = array_keys($this->hook->callbacks);
+			sort($this->all_priorities);
+			if ( isset($this->current_priority) ) { // callbacks were manipulated while we were running
+				foreach ( $this->all_priorities as $index => &$p ) { // go to the next larger priority
+					if ( $p > $this->current_priority ) {
+						$this->current_priority_index = $index;
+						$this->current_priority = $p;
+						break;
+					}
+				}
+			} else { // starting fresh
+				$this->current_priority_index = 0;
+				$this->current_priority = $this->all_priorities[0];
+			}
+			return;
+		}
+
+		if ( !isset($this->current_priority_index) ) {
+			$this->current_priority_index = 0;
+		} else {
+			$this->current_priority_index++;
+		}
+		if ( isset($this->all_priorities[$this->current_priority_index]) ) {
+			$this->current_priority = $this->all_priorities[$this->current_priority_index];
+			return;
+		}
+
+		$this->current_priority = NULL;
+		return;
+	}
+
+	private function get_callbacks( $priority ) {
+		if ( isset($this->hook->callbacks[$priority]) ) {
+			return $this->hook->callbacks[$priority];
+		}
+		return array();
+	}
+
+	/**
+	 * Return the key of the current element
+	 * @link http://php.net/manual/en/iterator.key.php
+	 * @return mixed scalar on success, or null on failure.
+	 */
+	public function key() {
+		return $this->current_callback_index;
+	}
+
+	/**
+	 * Checks if current position is valid
+	 * @link http://php.net/manual/en/iterator.valid.php
+	 * @return boolean The return value will be casted to boolean and then evaluated.
+	 * Returns true on success or false on failure.
+	 */
+	public function valid() {
+		return isset($this->current_callback);
+	}
+
+	/**
+	 * Rewind the Iterator to the first element
+	 * @link http://php.net/manual/en/iterator.rewind.php
+	 * @return void Any returned value is ignored.
+	 */
+	public function rewind() {
+		$this->current_priority = NULL;
+		$this->current_priority_index = NULL;
+		$this->current_callback = NULL;
+		$this->current_callback_index = NULL;
+		$this->callbacks_for_current_priority = array();
+		$this->next();
+	}
+
+	/**
+	 * Receive update from subject
+	 *
+	 * @link http://php.net/manual/en/splobserver.update.php
+	 *
+	 * @param SplSubject $subject The SplSubject notifying the observer of an update.
+	 *
+	 * @return void
+	 */
+	public function update( $subject ) {
+		unset($this->all_priorities);
+		unset($this->current_priority_index);
+	}
+}
