Index: src/wp-includes/class-wp-network-query.php
===================================================================
--- src/wp-includes/class-wp-network-query.php	(revision 0)
+++ src/wp-includes/class-wp-network-query.php	(working copy)
@@ -0,0 +1,617 @@
+<?php
+/**
+ * Network API: WP_Network_Query class
+ *
+ * @package WordPress
+ * @subpackage Multisite
+ * @since 4.6.0
+ */
+
+/**
+ * Core class used for querying networks.
+ *
+ * @since 3.1.0
+ *
+ * @see WP_Network_Query::__construct() for accepted arguments.
+ */
+class WP_Network_Query {
+
+	/**
+	 * SQL for database query.
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 * @var string
+	 */
+	public $request;
+
+	/**
+	 * Metadata query container
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 * @var object WP_Meta_Query
+	 */
+	public $meta_query = false;
+
+	/**
+	 * Metadata query clauses.
+	 *
+	 * @since 4.6.0
+	 * @access protected
+	 * @var array
+	 */
+	protected $meta_query_clauses;
+
+	/**
+	 * SQL query clauses.
+	 *
+	 * @since 4.6.0
+	 * @access protected
+	 * @var array
+	 */
+	protected $sql_clauses = array(
+		'select'  => '',
+		'from'    => '',
+		'where'   => array(),
+		'groupby' => '',
+		'orderby' => '',
+		'limits'  => '',
+	);
+
+	/**
+	 * Query vars set by the user.
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 * @var array
+	 */
+	public $query_vars;
+
+	/**
+	 * Default values for query vars.
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 * @var array
+	 */
+	public $query_var_defaults;
+
+	/**
+	 * List of networks located by the query.
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 * @var array
+	 */
+	public $networks;
+
+	/**
+	 * The amount of found networks for the current query.
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 * @var int
+	 */
+	public $found_networks = 0;
+
+	/**
+	 * The number of pages.
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 * @var int
+	 */
+	public $max_num_pages = 0;
+
+	/**
+	 * Constructor.
+	 *
+	 * Sets up the network query, based on the query vars passed.
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 *
+	 * @param string|array $query {
+	 *     Optional. Array or query string of network query parameters. Default empty.
+	 *
+	 *     @type array        $network__in               Array of network IDs to include. Default empty.
+	 *     @type array        $network__not_in           Array of network IDs to exclude. Default empty.
+ 	 *     @type bool         $count                     Whether to return a network count (true) or array of network objects.
+ 	 *                                                   Default false.
+ 	 *     @type string       $fields                    Network fields to return. Accepts 'ids' for network IDs only or empty
+ 	 *                                                   for all fields. Default empty.
+ 	 *     @type int          $number                    Maximum number of networks to retrieve. Default null (no limit).
+ 	 *     @type int          $offset                    Number of networks to offset the query. Used to build LIMIT clause.
+ 	 *                                                   Default 0.
+ 	 *     @type bool         $no_found_rows             Whether to disable the `SQL_CALC_FOUND_ROWS` query. Default true.
+ 	 *     @type string|array $orderby                   Network status or array of statuses. To use 'meta_value' or
+ 	 *                                                   'meta_value_num', `$meta_key` must also be defined. To sort by a
+ 	 *                                                   specific `$meta_query` clause, use that clause's array key. Accepts
+ 	 *                                                   'id', 'domain', 'path', 'domain_length', 'path_length', 'network__in',
+ 	 *                                                   'meta_value', 'meta_value_num', the value of $meta_key, and the array keys
+ 	 *                                                   of `$meta_query`. Also accepts false, an empty array, or 'none' to disable
+ 	 *                                                   `ORDER BY` clause. Default 'id'.
+ 	 *     @type string       $order                     How to order retrieved networks. Accepts 'ASC', 'DESC'. Default 'ASC'.
+ 	 *     @type string       $domain                    Limit results to those affiliated with a given network ID.
+ 	 *                                                   Default current network ID.
+ 	 *     @type array        $domain__in                Array of domains to include affiliated networks for. Default empty.
+ 	 *     @type array        $domain__not_in            Array of domains to exclude affiliated networks for. Default empty.
+ 	 *     @type string       $path                      Limit results to those affiliated with a given network ID.
+ 	 *                                                   Default current network ID.
+ 	 *     @type array        $path__in                  Array of paths to include affiliated networks for. Default empty.
+ 	 *     @type array        $path__not_in              Array of paths to exclude affiliated networks for. Default empty.
+ 	 *     @type string       $search                    Search term(s) to retrieve matching networks for. Default empty.
+	 *     @type string       $meta_key                  Include networks with a matching network meta key.
+	 *                                                   Default empty.
+	 *     @type string       $meta_value                Include networks with a matching network meta value.
+	 *                                                   Requires `$meta_key` to be set. Default empty.
+	 *     @type array        $meta_query                Meta query clauses to limit retrieved networks by.
+	 *                                                   See WP_Meta_Query. Default empty.
+	 *     @type bool         $update_network_cache      Whether to prime the cache for found networks. Default true.
+ 	 *     @type bool         $update_network_meta_cache Whether to prime metadata cache for found networks. Default true.
+	 * }
+	 */
+	public function __construct( $query = '' ) {
+		$this->query_var_defaults = array(
+			'network__in'               => '',
+			'network__not_in'           => '',
+			'count'                     => false,
+			'fields'                    => '',
+			'number'                    => '',
+			'offset'                    => '',
+			'no_found_rows'             => true,
+			'orderby'                   => '',
+			'order'                     => 'ASC',
+			'domain'                    => '',
+			'domain__in'                => '',
+			'domain__not_in'            => '',
+			'path'                      => '',
+			'path__in'                  => '',
+			'path__not_in'              => '',
+			'search'                    => '',
+			'meta_key'                  => '',
+			'meta_value'                => '',
+			'meta_query'                => '',
+			'update_network_cache'      => true,
+			'update_network_meta_cache' => true,
+		);
+
+		if ( ! empty( $query ) ) {
+			$this->query( $query );
+		}
+	}
+
+	/**
+	 * Parse arguments passed to the network query with default query parameters.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @access public
+	 *
+	 * @param string|array $query WP_Network_Query arguments. See WP_Network_Query::__construct()
+	 */
+	public function parse_query( $query = '' ) {
+		if ( empty( $query ) ) {
+			$query = $this->query_vars;
+		}
+
+		$this->query_vars = wp_parse_args( $query, $this->query_var_defaults );
+
+		/**
+		 * Fires after the network query vars have been parsed.
+		 *
+		 * @since 4.6.0
+		 *
+		 * @param WP_Network_Query &$this The WP_Network_Query instance (passed by reference).
+		 */
+		do_action_ref_array( 'parse_network_query', array( &$this ) );
+	}
+
+	/**
+	 * Sets up the WordPress query for retrieving networks.
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 *
+	 * @param string|array $query Array or URL query string of parameters.
+	 * @return array|int List of networks, or number of networks when 'count' is passed as a query var.
+	 */
+	public function query( $query ) {
+		$this->query_vars = wp_parse_args( $query );
+		return $this->get_networks();
+	}
+
+	/**
+	 * Get a list of networks matching the query vars.
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 *
+	 * @global wpdb $wpdb WordPress database abstraction object.
+	 *
+	 * @return int|array The list of networks.
+	 */
+	public function get_networks() {
+		global $wpdb;
+
+		$this->parse_query();
+
+		// Parse meta query
+		$this->meta_query = new WP_Meta_Query();
+		$this->meta_query->parse_query_vars( $this->query_vars );
+
+		/**
+		 * Fires before networks are retrieved.
+		 *
+		 * @since 4.6.0
+		 *
+		 * @param WP_Network_Query &$this Current instance of WP_Network_Query, passed by reference.
+		 */
+		do_action_ref_array( 'pre_get_networks', array( &$this ) );
+
+		// Reparse query vars, in case they were modified in a 'pre_get_networks' callback.
+		$this->meta_query->parse_query_vars( $this->query_vars );
+		if ( ! empty( $this->meta_query->queries ) ) {
+			$this->meta_query_clauses = $this->meta_query->get_sql( 'site', $wpdb->site, 'id', $this );
+		}
+
+		// $args can include anything. Only use the args defined in the query_var_defaults to compute the key.
+		$key = md5( serialize( wp_array_slice_assoc( $this->query_vars, array_keys( $this->query_var_defaults ) ) ) );
+		$last_changed = wp_cache_get( 'last_changed', 'networks' );
+		if ( ! $last_changed ) {
+			$last_changed = microtime();
+			wp_cache_set( 'last_changed', $last_changed, 'networks' );
+		}
+		$cache_key = "get_network_ids:$key:$last_changed";
+
+		$network_ids = wp_cache_get( $cache_key, 'networks' );
+		if ( false === $network_ids ) {
+			$network_ids = $this->get_network_ids();
+			wp_cache_add( $cache_key, $network_ids, 'networks' );
+		}
+
+		// If querying for a count only, there's nothing more to do.
+		if ( $this->query_vars['count'] ) {
+			// $network_ids is actually a count in this case.
+			return intval( $network_ids );
+		}
+
+		$network_ids = array_map( 'intval', $network_ids );
+
+		$this->network_count = count( $this->networks );
+
+		if ( $network_ids && $this->query_vars['number'] && ! $this->query_vars['no_found_rows'] ) {
+			/**
+			 * Filter the query used to retrieve found network count.
+			 *
+			 * @since 4.6.0
+			 *
+			 * @param string           $found_networks_query SQL query. Default 'SELECT FOUND_ROWS()'.
+			 * @param WP_Network_Query $network_query        The `WP_Network_Query` instance.
+			 */
+			$found_networks_query = apply_filters( 'found_networks_query', 'SELECT FOUND_ROWS()', $this );
+			$this->found_networks = (int) $wpdb->get_var( $found_networks_query );
+
+			$this->max_num_pages = ceil( $this->found_networks / $this->query_vars['number'] );
+		}
+
+		if ( 'ids' == $this->query_vars['fields'] ) {
+			$this->networks = $network_ids;
+			return $this->networks;
+		}
+
+		if ( $this->query_vars['update_network_cache'] ) {
+			_prime_network_caches( $network_ids, $this->query_vars['update_network_meta_cache'] );
+		}
+
+		// Fetch full network objects from the primed cache.
+		$_networks = array();
+		foreach ( $network_ids as $network_id ) {
+			if ( $_network = get_network( $network_id ) ) {
+				$_networks[] = $_network;
+			}
+		}
+
+		/**
+		 * Filter the network query results.
+		 *
+		 * @since 4.6.0
+		 *
+		 * @param array            $results  An array of networks.
+		 * @param WP_Network_Query &$this    Current instance of WP_Network_Query, passed by reference.
+		 */
+		$_networks = apply_filters_ref_array( 'the_networks', array( $_networks, &$this ) );
+
+		// Convert to WP_Network instances
+		$this->networks = array_map( 'get_network', $_networks );
+
+		return $this->networks;
+	}
+
+	/**
+	 * Used internally to get a list of network IDs matching the query vars.
+	 *
+	 * @since 4.6.0
+	 * @access protected
+	 *
+	 * @global wpdb $wpdb WordPress database abstraction object.
+	 */
+	protected function get_network_ids() {
+		global $wpdb;
+
+		$order = $this->parse_order( $this->query_vars['order'] );
+
+		// Disable ORDER BY with 'none', an empty array, or boolean false.
+		if ( in_array( $this->query_vars['orderby'], array( 'none', array(), false ), true ) ) {
+			$orderby = '';
+		} elseif ( ! empty( $this->query_vars['orderby'] ) ) {
+			$ordersby = is_array( $this->query_vars['orderby'] ) ?
+				$this->query_vars['orderby'] :
+				preg_split( '/[,\s]/', $this->query_vars['orderby'] );
+
+			$orderby_array         = array();
+			foreach ( $ordersby as $_key => $_value ) {
+				if ( ! $_value ) {
+					continue;
+				}
+
+				if ( is_int( $_key ) ) {
+					$_orderby = $_value;
+					$_order   = $order;
+				} else {
+					$_orderby = $_key;
+					$_order   = $_value;
+				}
+
+				$parsed = $this->parse_orderby( $_orderby );
+
+				if ( ! $parsed ) {
+					continue;
+				}
+
+				if ( 'network__in' === $_orderby ) {
+					$orderby_array[] = $parsed;
+					continue;
+				}
+
+				$orderby_array[] = $parsed . ' ' . $this->parse_order( $_order );
+			}
+
+			$orderby = implode( ', ', $orderby_array );
+		} else {
+			$orderby = "$wpdb->site.id $order";
+		}
+
+		$number = absint( $this->query_vars['number'] );
+		$offset = absint( $this->query_vars['offset'] );
+
+		if ( ! empty( $number ) ) {
+			if ( $offset ) {
+				$limits = 'LIMIT ' . $offset . ',' . $number;
+			} else {
+				$limits = 'LIMIT ' . $number;
+			}
+		}
+
+		if ( $this->query_vars['count'] ) {
+			$fields = 'COUNT(*)';
+		} else {
+			$fields = "$wpdb->site.id";
+		}
+
+		// Parse network IDs for an IN clause.
+		if ( ! empty( $this->query_vars['network__in'] ) ) {
+			$this->sql_clauses['where']['network__in'] = "$wpdb->site.id IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['network__in'] ) ) . ' )';
+		}
+
+		// Parse network IDs for a NOT IN clause.
+		if ( ! empty( $this->query_vars['network__not_in'] ) ) {
+			$this->sql_clauses['where']['network__not_in'] = "$wpdb->site.id NOT IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['network__not_in'] ) ) . ' )';
+		}
+
+		if ( ! empty( $this->query_vars['domain'] ) ) {
+			$this->sql_clauses['where']['domain'] = $wpdb->prepare( "$wpdb->site.domain = %s", $this->query_vars['domain'] );
+		}
+
+		// Parse network domain for an IN clause.
+		if ( is_array( $this->query_vars['domain__in'] ) ) {
+			$this->sql_clauses['where']['domain__in'] = "$wpdb->site.domain IN ( '" . implode( "', '", $wpdb->_escape( $this->query_vars['domain__in'] ) ) . "' )";
+		}
+
+		// Parse network domain for a NOT IN clause.
+		if ( is_array( $this->query_vars['domain__not_in'] ) ) {
+			$this->sql_clauses['where']['domain__not_in'] = "$wpdb->site.domain NOT IN ( '" . implode( "', '", $wpdb->_escape( $this->query_vars['domain__not_in'] ) ) . "' )";
+		}
+
+		if ( ! empty( $this->query_vars['path'] ) ) {
+			$this->sql_clauses['where']['path'] = $wpdb->prepare( "$wpdb->site.path = %s", $this->query_vars['path'] );
+		}
+
+		// Parse network path for an IN clause.
+		if ( is_array( $this->query_vars['path__in'] ) ) {
+			$this->sql_clauses['where']['path__in'] = "$wpdb->site.path IN ( '" . implode( "', '", $wpdb->_escape( $this->query_vars['path__in'] ) ) . "' )";
+		}
+
+		// Parse network path for a NOT IN clause.
+		if ( is_array( $this->query_vars['path__not_in'] ) ) {
+			$this->sql_clauses['where']['path__not_in'] = "$wpdb->site.path NOT IN ( '" . implode( "', '", $wpdb->_escape( $this->query_vars['path__not_in'] ) ) . "' )";
+		}
+
+		// Falsey search strings are ignored.
+		if ( strlen( $this->query_vars['search'] ) ) {
+			$this->sql_clauses['where']['search'] = $this->get_search_sql(
+				$this->query_vars['search'],
+				array( "$wpdb->site.domain", "$wpdb->site.path" )
+			);
+		}
+
+		$join = '';
+
+		if ( ! empty( $this->meta_query_clauses ) ) {
+			$join .= $this->meta_query_clauses['join'];
+
+			// Strip leading 'AND'.
+			$this->sql_clauses['where']['meta_query'] = preg_replace( '/^\s*AND\s*/', '', $this->meta_query_clauses['where'] );
+
+			if ( ! $this->query_vars['count'] ) {
+				$groupby = "{$wpdb->site}.id";
+			}
+		}
+
+		$where = implode( ' AND ', $this->sql_clauses['where'] );
+
+		$pieces = array( 'fields', 'join', 'where', 'orderby', 'limits', 'groupby' );
+
+		/**
+		 * Filters the network query clauses.
+		 *
+		 * @since 4.6.0
+		 *
+		 * @param array            $pieces A compacted array of network query clauses.
+		 * @param WP_Network_Query &$this  Current instance of WP_Network_Query, passed by reference.
+		 */
+		$clauses = apply_filters_ref_array( 'networks_clauses', array( compact( $pieces ), &$this ) );
+
+		$fields  = isset( $clauses['fields'] ) ? $clauses['fields'] : '';
+		$join    = isset( $clauses['join'] ) ? $clauses['join'] : '';
+		$where   = isset( $clauses['where'] ) ? $clauses['where'] : '';
+		$orderby = isset( $clauses['orderby'] ) ? $clauses['orderby'] : '';
+		$limits  = isset( $clauses['limits'] ) ? $clauses['limits'] : '';
+		$groupby = isset( $clauses['groupby'] ) ? $clauses['groupby'] : '';
+
+		if ( $where ) {
+			$where = 'WHERE ' . $where;
+		}
+
+		if ( $groupby ) {
+			$groupby = 'GROUP BY ' . $groupby;
+		}
+
+		if ( $orderby ) {
+			$orderby = "ORDER BY $orderby";
+		}
+
+		$found_rows = '';
+		if ( ! $this->query_vars['no_found_rows'] ) {
+			$found_rows = 'SQL_CALC_FOUND_ROWS';
+		}
+
+		$this->sql_clauses['select']  = "SELECT $found_rows $fields";
+		$this->sql_clauses['from']    = "FROM $wpdb->site $join";
+		$this->sql_clauses['groupby'] = $groupby;
+		$this->sql_clauses['orderby'] = $orderby;
+		$this->sql_clauses['limits']  = $limits;
+
+		$this->request = "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$where} {$this->sql_clauses['groupby']} {$this->sql_clauses['orderby']} {$this->sql_clauses['limits']}";
+
+		if ( $this->query_vars['count'] ) {
+			return intval( $wpdb->get_var( $this->request ) );
+		}
+
+		$network_ids = $wpdb->get_col( $this->request );
+
+		return array_map( 'intval', $network_ids );
+	}
+
+	/**
+	 * Used internally to generate an SQL string for searching across multiple columns
+	 *
+	 * @since 4.6.0
+	 * @access protected
+	 *
+	 * @global wpdb  $wpdb WordPress database abstraction object.
+	 *
+	 * @param string $string  Search string.
+	 * @param array  $columns Columns to search.
+	 *
+	 * @return string Search SQL.
+	 */
+	protected function get_search_sql( $string, $columns ) {
+		global $wpdb;
+
+		$like = '%' . $wpdb->esc_like( $string ) . '%';
+
+		$searches = array();
+		foreach ( $columns as $column ) {
+			$searches[] = $wpdb->prepare( "$column LIKE %s", $like );
+		}
+
+		return '(' . implode( ' OR ', $searches ) . ')';
+	}
+
+	/**
+	 * Parses and sanitizes 'orderby' keys passed to the network query.
+	 *
+	 * @since 4.6.0
+	 * @access protected
+	 *
+	 * @global wpdb $wpdb WordPress database abstraction object.
+	 *
+	 * @param string $orderby Alias for the field to order by.
+	 * @return string|false Value to used in the ORDER clause. False otherwise.
+	 */
+	protected function parse_orderby( $orderby ) {
+		global $wpdb;
+
+		$allowed_keys = array(
+			'id',
+			'domain',
+			'path',
+		);
+
+		if ( ! empty( $this->query_vars['meta_key'] ) ) {
+			$allowed_keys[] = $this->query_vars['meta_key'];
+			$allowed_keys[] = 'meta_value';
+			$allowed_keys[] = 'meta_value_num';
+		}
+
+		$meta_query_clauses = $this->meta_query->get_clauses();
+		if ( $meta_query_clauses ) {
+			$allowed_keys = array_merge( $allowed_keys, array_keys( $meta_query_clauses ) );
+		}
+
+		$parsed = false;
+		if ( $orderby == $this->query_vars['meta_key'] || $orderby == 'meta_value' ) {
+			$parsed = "$wpdb->sitemeta.meta_value";
+		} elseif ( $orderby == 'meta_value_num' ) {
+			$parsed = "$wpdb->sitemeta.meta_value+0";
+		} elseif ( $orderby == 'network__in' ) {
+			$network__in = implode( ',', array_map( 'absint', $this->query_vars['network__in'] ) );
+			$parsed = "FIELD( {$wpdb->site}.id, $network__in )";
+		} elseif ( $orderby == 'domain_length' || $orderby == 'path_length' ) {
+			$field = substr( $orderby, 0, -7 );
+			$parsed = "CHAR_LENGTH($wpdb->site.$field)";
+		} elseif ( in_array( $orderby, $allowed_keys ) ) {
+			if ( isset( $meta_query_clauses[ $orderby ] ) ) {
+				$meta_clause = $meta_query_clauses[ $orderby ];
+				$parsed = sprintf( "CAST(%s.meta_value AS %s)", esc_sql( $meta_clause['alias'] ), esc_sql( $meta_clause['cast'] ) );
+			} else {
+				$parsed = "$wpdb->site.$orderby";
+			}
+		}
+
+		return $parsed;
+	}
+
+	/**
+	 * Parses an 'order' query variable and cast it to 'ASC' or 'DESC' as necessary.
+	 *
+	 * @since 4.6.0
+	 * @access protected
+	 *
+	 * @param string $order The 'order' query variable.
+	 * @return string The sanitized 'order' query variable.
+	 */
+	protected function parse_order( $order ) {
+		if ( ! is_string( $order ) || empty( $order ) ) {
+			return 'ASC';
+		}
+
+		if ( 'ASC' === strtoupper( $order ) ) {
+			return 'ASC';
+		} else {
+			return 'DESC';
+		}
+	}
+}

Property changes on: src/wp-includes/class-wp-network-query.php
___________________________________________________________________
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Index: src/wp-includes/class-wp-network.php
===================================================================
--- src/wp-includes/class-wp-network.php	(revision 37473)
+++ src/wp-includes/class-wp-network.php	(working copy)
@@ -138,6 +138,18 @@
 	}
 
 	/**
+	 * Convert object to array.
+	 *
+	 * @since 4.6.0
+	 * @access public
+	 *
+	 * @return array Object as array.
+	 */
+	public function to_array() {
+		return get_object_vars( $this );
+	}
+
+	/**
 	 * Set the site name assigned to the network if one has not been populated.
 	 *
 	 * @since 4.4.0
Index: src/wp-includes/ms-blogs.php
===================================================================
--- src/wp-includes/ms-blogs.php	(revision 37473)
+++ src/wp-includes/ms-blogs.php	(working copy)
@@ -973,6 +973,146 @@
 }
 
 /**
+ * Retrieve a list of networks.
+ *
+ * @since 4.6.0
+ *
+ * @param string|array $args Optional. Array or string of arguments. See {@see WP_Network_Query::parse_query()}
+ *                           for information on accepted arguments. Default empty.
+ *
+ * @return int|array List of networks or number of found networks if `$count` argument is true.
+ */
+function get_networks( $args = '' ) {
+	$query = new WP_Network_Query();
+	return $query->query( $args );
+}
+
+/**
+ * Retrieves network data given a network ID or network object.
+ *
+ * If an object is passed then the network data will be cached and then returned
+ * after being passed through a filter. If the network is empty, then the
+ * current network will be used, if it is set.
+ *
+ * @since 4.6.0
+ *
+ * @global WP_Network $current_site
+ *
+ * @param WP_Network|string|int $network Network to retrieve.
+ * @param string                $output  Optional. OBJECT or ARRAY_A or ARRAY_N constants.
+ *
+ * @return WP_Network|array|null Depends on $output value.
+ */
+function get_network( &$network = null, $output = OBJECT ) {
+	if ( empty( $network ) && isset( $GLOBALS['current_site'] ) ) {
+		$network = $GLOBALS['current_site'];
+	}
+
+	if ( $network instanceof WP_Network ) {
+		$_network = $network;
+	} elseif ( is_object( $network ) ) {
+		$_network = new WP_Network( $network );
+	} else {
+		$_network = WP_Network::get_instance( $network );
+	}
+
+	if ( ! $_network ) {
+		return null;
+	}
+
+	/**
+	 * Fires after a network is retrieved.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param mixed $_network Network data.
+	 */
+	$_network = apply_filters( 'get_network', $_network );
+
+	if ( $output == OBJECT ) {
+		return $_network;
+	} elseif ( $output == ARRAY_A ) {
+		return $_network->to_array();
+	} elseif ( $output == ARRAY_N ) {
+		return array_values( $_network->to_array() );
+	}
+	return $_network;
+}
+
+/**
+ * Removes a network from the object cache.
+ *
+ * @since 4.6.0
+ *
+ * @param int|array $ids Network ID or an array of network IDs to remove from cache.
+ */
+function clean_network_cache( $ids ) {
+	foreach ( (array) $ids as $id ) {
+		wp_cache_delete( $id, 'networks' );
+
+		/**
+		 * Fires immediately after a network has been removed from the object cache.
+		 *
+		 * @since 4.6.0
+		 *
+		 * @param int $id Network ID.
+		 */
+		do_action( 'clean_network_cache', $id );
+	}
+
+	wp_cache_set( 'last_changed', microtime(), 'networks' );
+}
+
+/**
+ * Updates the network cache of given networks.
+ *
+ * Will add the networks in $networks to the cache. If network ID already exists
+ * in the network cache then it will not be updated. The network is added to the
+ * cache using the network group with the key using the ID of the networks.
+ *
+ * @since 4.6.0
+ *
+ * @param array $networks          Array of network row objects
+ * @param bool  $update_meta_cache Whether to update network meta cache. Default true.
+ */
+function update_network_cache( $networks, $update_meta_cache = true ) {
+	foreach ( (array) $networks as $network )
+		wp_cache_add( $network->id, $network, 'networks' );
+
+	if ( $update_meta_cache ) {
+		// Avoid `wp_list_pluck()` in case `$networks` is passed by reference.
+		$network_ids = array();
+		foreach ( $networks as $network ) {
+			$network_ids[] = $network->id;
+		}
+		update_meta_cache( 'site', $network_ids );
+	}
+}
+
+/**
+ * Adds any networks from the given IDs to the cache that do not already exist in cache.
+ *
+ * @since 4.6.0
+ * @access private
+ *
+ * @see update_network_cache()
+ * @global wpdb $wpdb WordPress database abstraction object.
+ *
+ * @param array $network_ids       Array of network IDs.
+ * @param bool  $update_meta_cache Optional. Whether to update the meta cache. Default true.
+ */
+function _prime_network_caches( $network_ids, $update_meta_cache = true ) {
+	global $wpdb;
+
+	$non_cached_ids = _get_non_cached_ids( $network_ids, 'networks' );
+	if ( !empty( $non_cached_ids ) ) {
+		$fresh_networks = $wpdb->get_results( sprintf( "SELECT $wpdb->site.* FROM $wpdb->site WHERE id IN (%s)", join( ",", array_map( 'intval', $non_cached_ids ) ) ) );
+
+		update_network_cache( $fresh_networks, $update_meta_cache );
+	}
+}
+
+/**
  * Handler for updating the blog date when a post is published or an already published post is changed.
  *
  * @since 3.3.0
Index: src/wp-includes/ms-settings.php
===================================================================
--- src/wp-includes/ms-settings.php	(revision 37473)
+++ src/wp-includes/ms-settings.php	(working copy)
@@ -28,6 +28,9 @@
 /** WP_Site class */
 require_once( ABSPATH . WPINC . '/class-wp-site.php' );
 
+/** WP_Network_Query class */
+require_once( ABSPATH . WPINC . '/class-wp-network-query.php' );
+
 /** Multisite loader */
 require_once( ABSPATH . WPINC . '/ms-load.php' );
 
Index: tests/phpunit/tests/multisite/networkQuery.php
===================================================================
--- tests/phpunit/tests/multisite/networkQuery.php	(revision 0)
+++ tests/phpunit/tests/multisite/networkQuery.php	(working copy)
@@ -0,0 +1,498 @@
+<?php
+
+if ( is_multisite() ) :
+
+/**
+ * Test network query functionality in multisite.
+ *
+ * @group ms-network
+ * @group ms-network-query
+ * @group multisite
+ */
+class Tests_Multisite_Network_Query extends WP_UnitTestCase {
+	protected static $network_ids;
+
+	protected $suppress = false;
+
+	function setUp() {
+		global $wpdb;
+		parent::setUp();
+		$this->suppress = $wpdb->suppress_errors();
+	}
+
+	function tearDown() {
+		global $wpdb;
+		$wpdb->suppress_errors( $this->suppress );
+		parent::tearDown();
+	}
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$network_ids = array(
+			'wordpress.org/'         => array( 'domain' => 'wordpress.org',      'path' => '/' ),
+			'make.wordpress.org/'    => array( 'domain' => 'make.wordpress.org', 'path' => '/' ),
+			'www.wordpress.net/'     => array( 'domain' => 'www.wordpress.net',  'path' => '/' ),
+			'www.w.org/foo/'         => array( 'domain' => 'www.w.org',          'path' => '/foo/' ),
+		);
+
+		foreach ( self::$network_ids as &$id ) {
+			$id = $factory->network->create( $id );
+		}
+		unset( $id );
+	}
+
+	public static function wpTearDownAfterClass() {
+		global $wpdb;
+
+		foreach( self::$network_ids as $id ) {
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->sitemeta} WHERE site_id = %d", $id ) );
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->site} WHERE id= %d", $id ) );
+		}
+	}
+
+	public function test_wp_network_query_by_number() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'   => 'ids',
+			'number'   => 3,
+		) );
+
+		$this->assertEquals( 3, count( $found ) );
+	}
+
+	public function test_wp_network_query_by_network__in_with_single_id() {
+		$expected = array( self::$network_ids['wordpress.org/'] );
+
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'      => 'ids',
+			'network__in' => $expected,
+		) );
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_network__in_with_multiple_ids() {
+		$expected = array( self::$network_ids['wordpress.org/'], self::$network_ids['www.wordpress.net/'] );
+
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'      => 'ids',
+			'network__in' => $expected,
+		) );
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_network__in_and_count_with_multiple_ids() {
+		$expected = array( self::$network_ids['wordpress.org/'], self::$network_ids['make.wordpress.org/'] );
+
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'      => 'ids',
+			'count'       => true,
+			'network__in' => $expected,
+		) );
+
+		$this->assertEquals( 2, $found );
+	}
+
+	public function test_wp_network_query_by_network__not_in_with_single_id() {
+		$excluded = array( self::$network_ids['wordpress.org/'] );
+		$expected = array_diff( self::$network_ids, $excluded );
+
+		// Exclude main network since we don't have control over it here.
+		$excluded[] = 1;
+
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'          => 'ids',
+			'network__not_in' => $excluded,
+		) );
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_network__not_in_with_multiple_ids() {
+		$excluded = array( self::$network_ids['wordpress.org/'], self::$network_ids['www.w.org/foo/'] );
+		$expected = array_diff( self::$network_ids, $excluded );
+
+		// Exclude main network since we don't have control over it here.
+		$excluded[] = 1;
+
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'          => 'ids',
+			'network__not_in' => $excluded,
+		) );
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_domain() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'       => 'ids',
+			'domain'       => 'www.w.org',
+		) );
+
+		$expected = array(
+			self::$network_ids['www.w.org/foo/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_domain__in_with_single_domain() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'     => 'ids',
+			'domain__in' => array( 'make.wordpress.org' ),
+		));
+
+		$expected = array(
+			self::$network_ids['make.wordpress.org/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_domain__in_with_multiple_domains() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'     => 'ids',
+			'domain__in' => array( 'wordpress.org', 'make.wordpress.org' ),
+		));
+
+		$expected = array(
+			self::$network_ids['wordpress.org/'],
+			self::$network_ids['make.wordpress.org/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_domain__in_with_multiple_domains_and_number() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'     => 'ids',
+			'number'     => 1,
+			'domain__in' => array( 'wordpress.org', 'make.wordpress.org' ),
+		));
+
+		$expected = array(
+			self::$network_ids['wordpress.org/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_domain__in_with_multiple_domains_and_number_and_offset() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'     => 'ids',
+			'number'     => 1,
+			'offset'     => 1,
+			'domain__in' => array( 'wordpress.org', 'make.wordpress.org' ),
+		));
+
+		$expected = array(
+			self::$network_ids['make.wordpress.org/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_domain__not_in_with_single_domain() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'         => 'ids',
+			'domain__not_in' => array( 'www.w.org' ),
+		));
+
+		$expected = array(
+			get_current_site()->id, // Account for the initial network added by the test suite.
+			self::$network_ids['wordpress.org/'],
+			self::$network_ids['make.wordpress.org/'],
+			self::$network_ids['www.wordpress.net/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_domain__not_in_with_multiple_domains() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'         => 'ids',
+			'domain__not_in' => array( 'wordpress.org', 'www.w.org' ),
+		));
+
+		$expected = array(
+			get_current_site()->id, // Account for the initial network added by the test suite.
+			self::$network_ids['make.wordpress.org/'],
+			self::$network_ids['www.wordpress.net/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_domain__not_in_with_multiple_domains_and_number() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'         => 'ids',
+			'number'         => 2,
+			'domain__not_in' => array( 'wordpress.org', 'www.w.org' ),
+		));
+
+		$expected = array(
+			get_current_site()->id, // Account for the initial network added by the test suite.
+			self::$network_ids['make.wordpress.org/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_domain__not_in_with_multiple_domains_and_number_and_offset() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'         => 'ids',
+			'number'         => 2,
+			'offset'         => 1,
+			'domain__not_in' => array( 'wordpress.org', 'www.w.org' ),
+		));
+
+		$expected = array(
+			self::$network_ids['make.wordpress.org/'],
+			self::$network_ids['www.wordpress.net/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_path_with_expected_results() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'          => 'ids',
+			'path'            => '/',
+			'network__not_in' => get_current_site()->id, // Exclude the initial network added by the test suite.
+		) );
+
+		$expected = array(
+			self::$network_ids['wordpress.org/'],
+			self::$network_ids['make.wordpress.org/'],
+			self::$network_ids['www.wordpress.net/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_path_and_number_and_offset_with_expected_results() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'          => 'ids',
+			'number'          => 1,
+			'offset'          => 2,
+			'path'            => '/',
+			'network__not_in' => get_current_site()->id, // Exclude the initial network added by the test suite.
+		) );
+
+		$expected = array(
+			self::$network_ids['www.wordpress.net/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_path_with_no_expected_results() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'       => 'ids',
+			'path'         => '/bar/',
+		) );
+
+		$this->assertEmpty( $found );
+	}
+
+	public function test_wp_network_query_by_search_with_text_in_domain() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'       => 'ids',
+			'search'       => 'ww.word',
+		) );
+
+		$expected = array(
+			self::$network_ids['www.wordpress.net/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_search_with_text_in_path() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'       => 'ids',
+			'search'       => 'foo',
+		) );
+
+		$expected = array(
+			self::$network_ids['www.w.org/foo/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_path_order_by_domain_desc() {
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'          => 'ids',
+			'path'            => '/',
+			'network__not_in' => get_current_site()->id, // Exclude the initial network added by the test suite.
+			'order'           => 'DESC',
+			'orderby'         => 'domain',
+		) );
+
+		$expected = array(
+			self::$network_ids['www.wordpress.net/'],
+			self::$network_ids['wordpress.org/'],
+			self::$network_ids['make.wordpress.org/'],
+		);
+
+		$this->assertEquals( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_meta_query_key_value_combination() {
+		update_network_option( self::$network_ids['wordpress.org/'], 'key1', 'value1' );
+		update_network_option( self::$network_ids['www.w.org/foo/'], 'key1', 'value1' );
+		update_network_option( self::$network_ids['make.wordpress.org/'], 'key1', 'value2' );
+		update_network_option( self::$network_ids['make.wordpress.org/'], 'key2', 'value1' );
+
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'     => 'ids',
+			'meta_query' => array(
+				'relation' => 'AND',
+				array(
+					'key'   => 'key1',
+					'value' => 'value1',
+				),
+			),
+		) );
+
+		$expected = array(
+			self::$network_ids['wordpress.org/'],
+			self::$network_ids['www.w.org/foo/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_meta_query_key_exists() {
+		update_network_option( self::$network_ids['wordpress.org/'], 'key1', 'value1' );
+		update_network_option( self::$network_ids['www.w.org/foo/'], 'key1', 'value1' );
+		update_network_option( self::$network_ids['make.wordpress.org/'], 'key1', 'value2' );
+		update_network_option( self::$network_ids['make.wordpress.org/'], 'key2', 'value1' );
+
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'     => 'ids',
+			'meta_query' => array(
+				'relation' => 'AND',
+				array(
+					'key'     => 'key1',
+					'compare' => 'EXISTS',
+				),
+			),
+		) );
+
+		$expected = array(
+			self::$network_ids['wordpress.org/'],
+			self::$network_ids['make.wordpress.org/'],
+			self::$network_ids['www.w.org/foo/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_meta_query_key_value_combination_and_other_key_exists() {
+		update_network_option( self::$network_ids['wordpress.org/'], 'key1', 'value1' );
+		update_network_option( self::$network_ids['www.w.org/foo/'], 'key1', 'value1' );
+		update_network_option( self::$network_ids['make.wordpress.org/'], 'key1', 'value2' );
+		update_network_option( self::$network_ids['make.wordpress.org/'], 'key2', 'value1' );
+
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'     => 'ids',
+			'meta_query' => array(
+				'relation' => 'AND',
+				array(
+					'key'     => 'key1',
+					'value'   => 'value2',
+				),
+				array(
+					'key'     => 'key2',
+					'compare' => 'EXISTS',
+				),
+			),
+		) );
+
+		$expected = array(
+			self::$network_ids['make.wordpress.org/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_by_meta_query_key_value_combination_or_other_key_exists() {
+		update_network_option( self::$network_ids['wordpress.org/'], 'key1', 'value1' );
+		update_network_option( self::$network_ids['www.w.org/foo/'], 'key1', 'value1' );
+		update_network_option( self::$network_ids['make.wordpress.org/'], 'key1', 'value2' );
+		update_network_option( self::$network_ids['make.wordpress.org/'], 'key2', 'value1' );
+
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'     => 'ids',
+			'meta_query' => array(
+				'relation' => 'OR',
+				array(
+					'key'     => 'key1',
+					'value'   => 'value1',
+				),
+				array(
+					'key'     => 'key2',
+					'compare' => 'EXISTS',
+				),
+			),
+		) );
+
+		$expected = array(
+			self::$network_ids['wordpress.org/'],
+			self::$network_ids['make.wordpress.org/'],
+			self::$network_ids['www.w.org/foo/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_network_query_orderby_meta() {
+		update_network_option( self::$network_ids['wordpress.org/'], 'key', 'value1' );
+		update_network_option( self::$network_ids['www.w.org/foo/'], 'key', 'value4' );
+		update_network_option( self::$network_ids['make.wordpress.org/'], 'key', 'something' );
+
+		$q = new WP_Network_Query();
+		$found = $q->query( array(
+			'fields'   => 'ids',
+			'meta_key' => 'key',
+			'orderby'  => array( 'meta_value' ),
+			'order'    => 'DESC',
+		) );
+
+		$expected = array(
+			self::$network_ids['www.w.org/foo/'],
+			self::$network_ids['wordpress.org/'],
+			self::$network_ids['make.wordpress.org/'],
+		);
+
+		$this->assertEquals( $expected, $found );
+	}
+}
+
+endif;

Property changes on: tests/phpunit/tests/multisite/networkQuery.php
___________________________________________________________________
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
