Wordpress 4.6
[autoinstalls/wordpress.git] / wp-includes / class-wp-network-query.php
1 <?php
2 /**
3  * Network API: WP_Network_Query class
4  *
5  * @package WordPress
6  * @subpackage Multisite
7  * @since 4.6.0
8  */
9
10 /**
11  * Core class used for querying networks.
12  *
13  * @since 4.6.0
14  *
15  * @see WP_Network_Query::__construct() for accepted arguments.
16  */
17 class WP_Network_Query {
18
19         /**
20          * SQL for database query.
21          *
22          * @since 4.6.0
23          * @access public
24          * @var string
25          */
26         public $request;
27
28         /**
29          * SQL query clauses.
30          *
31          * @since 4.6.0
32          * @access protected
33          * @var array
34          */
35         protected $sql_clauses = array(
36                 'select'  => '',
37                 'from'    => '',
38                 'where'   => array(),
39                 'groupby' => '',
40                 'orderby' => '',
41                 'limits'  => '',
42         );
43
44         /**
45          * Query vars set by the user.
46          *
47          * @since 4.6.0
48          * @access public
49          * @var array
50          */
51         public $query_vars;
52
53         /**
54          * Default values for query vars.
55          *
56          * @since 4.6.0
57          * @access public
58          * @var array
59          */
60         public $query_var_defaults;
61
62         /**
63          * List of networks located by the query.
64          *
65          * @since 4.6.0
66          * @access public
67          * @var array
68          */
69         public $networks;
70
71         /**
72          * The amount of found networks for the current query.
73          *
74          * @since 4.6.0
75          * @access public
76          * @var int
77          */
78         public $found_networks = 0;
79
80         /**
81          * The number of pages.
82          *
83          * @since 4.6.0
84          * @access public
85          * @var int
86          */
87         public $max_num_pages = 0;
88
89         /**
90          * Constructor.
91          *
92          * Sets up the network query, based on the query vars passed.
93          *
94          * @since 4.6.0
95          * @access public
96          *
97          * @param string|array $query {
98          *     Optional. Array or query string of network query parameters. Default empty.
99          *
100          *     @type array        $network__in          Array of network IDs to include. Default empty.
101          *     @type array        $network__not_in      Array of network IDs to exclude. Default empty.
102          *     @type bool         $count                Whether to return a network count (true) or array of network objects.
103          *                                              Default false.
104          *     @type string       $fields               Network fields to return. Accepts 'ids' (returns an array of network IDs)
105          *                                              or empty (returns an array of complete network objects). Default empty.
106          *     @type int          $number               Maximum number of networks to retrieve. Default null (no limit).
107          *     @type int          $offset               Number of networks to offset the query. Used to build LIMIT clause.
108          *                                              Default 0.
109          *     @type bool         $no_found_rows        Whether to disable the `SQL_CALC_FOUND_ROWS` query. Default true.
110          *     @type string|array $orderby              Network status or array of statuses. Accepts 'id', 'domain', 'path',
111          *                                              'domain_length', 'path_length' and 'network__in'. Also accepts false,
112          *                                              an empty array, or 'none' to disable `ORDER BY` clause. Default 'id'.
113          *     @type string       $order                How to order retrieved networks. Accepts 'ASC', 'DESC'. Default 'ASC'.
114          *     @type string       $domain               Limit results to those affiliated with a given network ID.
115          *                                              Default current network ID.
116          *     @type array        $domain__in           Array of domains to include affiliated networks for. Default empty.
117          *     @type array        $domain__not_in       Array of domains to exclude affiliated networks for. Default empty.
118          *     @type string       $path                 Limit results to those affiliated with a given network ID.
119          *                                              Default current network ID.
120          *     @type array        $path__in             Array of paths to include affiliated networks for. Default empty.
121          *     @type array        $path__not_in         Array of paths to exclude affiliated networks for. Default empty.
122          *     @type string       $search               Search term(s) to retrieve matching networks for. Default empty.
123          *     @type bool         $update_network_cache Whether to prime the cache for found networks. Default true.
124          * }
125          */
126         public function __construct( $query = '' ) {
127                 $this->query_var_defaults = array(
128                         'network__in'          => '',
129                         'network__not_in'      => '',
130                         'count'                => false,
131                         'fields'               => '',
132                         'number'               => '',
133                         'offset'               => '',
134                         'no_found_rows'        => true,
135                         'orderby'              => 'id',
136                         'order'                => 'ASC',
137                         'domain'               => '',
138                         'domain__in'           => '',
139                         'domain__not_in'       => '',
140                         'path'                 => '',
141                         'path__in'             => '',
142                         'path__not_in'         => '',
143                         'search'               => '',
144                         'update_network_cache' => true,
145                 );
146
147                 if ( ! empty( $query ) ) {
148                         $this->query( $query );
149                 }
150         }
151
152         /**
153          * Parses arguments passed to the network query with default query parameters.
154          *
155          * @since 4.6.0
156          *
157          * @access public
158          *
159          * @param string|array $query WP_Network_Query arguments. See WP_Network_Query::__construct()
160          */
161         public function parse_query( $query = '' ) {
162                 if ( empty( $query ) ) {
163                         $query = $this->query_vars;
164                 }
165
166                 $this->query_vars = wp_parse_args( $query, $this->query_var_defaults );
167
168                 /**
169                  * Fires after the network query vars have been parsed.
170                  *
171                  * @since 4.6.0
172                  *
173                  * @param WP_Network_Query &$this The WP_Network_Query instance (passed by reference).
174                  */
175                 do_action_ref_array( 'parse_network_query', array( &$this ) );
176         }
177
178         /**
179          * Sets up the WordPress query for retrieving networks.
180          *
181          * @since 4.6.0
182          * @access public
183          *
184          * @param string|array $query Array or URL query string of parameters.
185          * @return array|int List of networks, or number of networks when 'count' is passed as a query var.
186          */
187         public function query( $query ) {
188                 $this->query_vars = wp_parse_args( $query );
189                 return $this->get_networks();
190         }
191
192         /**
193          * Gets a list of networks matching the query vars.
194          *
195          * @since 4.6.0
196          * @access public
197          *
198          * @return int|array The list of networks.
199          */
200         public function get_networks() {
201                 $this->parse_query();
202
203                 /**
204                  * Fires before networks are retrieved.
205                  *
206                  * @since 4.6.0
207                  *
208                  * @param WP_Network_Query &$this Current instance of WP_Network_Query, passed by reference.
209                  */
210                 do_action_ref_array( 'pre_get_networks', array( &$this ) );
211
212                 // $args can include anything. Only use the args defined in the query_var_defaults to compute the key.
213                 $key = md5( serialize( wp_array_slice_assoc( $this->query_vars, array_keys( $this->query_var_defaults ) ) ) );
214                 $last_changed = wp_cache_get( 'last_changed', 'networks' );
215                 if ( ! $last_changed ) {
216                         $last_changed = microtime();
217                         wp_cache_set( 'last_changed', $last_changed, 'networks' );
218                 }
219
220                 $cache_key = "get_network_ids:$key:$last_changed";
221                 $cache_value = wp_cache_get( $cache_key, 'networks' );
222
223                 if ( false === $cache_value ) {
224                         $network_ids = $this->get_network_ids();
225                         if ( $network_ids ) {
226                                 $this->set_found_networks();
227                         }
228
229                         $cache_value = array(
230                                 'network_ids' => $network_ids,
231                                 'found_networks' => $this->found_networks,
232                         );
233                         wp_cache_add( $cache_key, $cache_value, 'networks' );
234                 } else {
235                         $network_ids = $cache_value['network_ids'];
236                         $this->found_networks = $cache_value['found_networks'];
237                 }
238
239                 if ( $this->found_networks && $this->query_vars['number'] ) {
240                         $this->max_num_pages = ceil( $this->found_networks / $this->query_vars['number'] );
241                 }
242
243                 // If querying for a count only, there's nothing more to do.
244                 if ( $this->query_vars['count'] ) {
245                         // $network_ids is actually a count in this case.
246                         return intval( $network_ids );
247                 }
248
249                 $network_ids = array_map( 'intval', $network_ids );
250
251                 if ( 'ids' == $this->query_vars['fields'] ) {
252                         $this->networks = $network_ids;
253                         return $this->networks;
254                 }
255
256                 if ( $this->query_vars['update_network_cache'] ) {
257                         _prime_network_caches( $network_ids );
258                 }
259
260                 // Fetch full network objects from the primed cache.
261                 $_networks = array();
262                 foreach ( $network_ids as $network_id ) {
263                         if ( $_network = get_network( $network_id ) ) {
264                                 $_networks[] = $_network;
265                         }
266                 }
267
268                 /**
269                  * Filters the network query results.
270                  *
271                  * @since 4.6.0
272                  *
273                  * @param array            $results  An array of networks.
274                  * @param WP_Network_Query &$this    Current instance of WP_Network_Query, passed by reference.
275                  */
276                 $_networks = apply_filters_ref_array( 'the_networks', array( $_networks, &$this ) );
277
278                 // Convert to WP_Network instances
279                 $this->networks = array_map( 'get_network', $_networks );
280
281                 return $this->networks;
282         }
283
284         /**
285          * Used internally to get a list of network IDs matching the query vars.
286          *
287          * @since 4.6.0
288          * @access protected
289          *
290          * @return int|array A single count of network IDs if a count query. An array of network IDs if a full query.
291          */
292         protected function get_network_ids() {
293                 global $wpdb;
294
295                 $order = $this->parse_order( $this->query_vars['order'] );
296
297                 // Disable ORDER BY with 'none', an empty array, or boolean false.
298                 if ( in_array( $this->query_vars['orderby'], array( 'none', array(), false ), true ) ) {
299                         $orderby = '';
300                 } elseif ( ! empty( $this->query_vars['orderby'] ) ) {
301                         $ordersby = is_array( $this->query_vars['orderby'] ) ?
302                                 $this->query_vars['orderby'] :
303                                 preg_split( '/[,\s]/', $this->query_vars['orderby'] );
304
305                         $orderby_array = array();
306                         foreach ( $ordersby as $_key => $_value ) {
307                                 if ( ! $_value ) {
308                                         continue;
309                                 }
310
311                                 if ( is_int( $_key ) ) {
312                                         $_orderby = $_value;
313                                         $_order = $order;
314                                 } else {
315                                         $_orderby = $_key;
316                                         $_order = $_value;
317                                 }
318
319                                 $parsed = $this->parse_orderby( $_orderby );
320
321                                 if ( ! $parsed ) {
322                                         continue;
323                                 }
324
325                                 if ( 'network__in' === $_orderby ) {
326                                         $orderby_array[] = $parsed;
327                                         continue;
328                                 }
329
330                                 $orderby_array[] = $parsed . ' ' . $this->parse_order( $_order );
331                         }
332
333                         $orderby = implode( ', ', $orderby_array );
334                 } else {
335                         $orderby = "$wpdb->site.id $order";
336                 }
337
338                 $number = absint( $this->query_vars['number'] );
339                 $offset = absint( $this->query_vars['offset'] );
340
341                 if ( ! empty( $number ) ) {
342                         if ( $offset ) {
343                                 $limits = 'LIMIT ' . $offset . ',' . $number;
344                         } else {
345                                 $limits = 'LIMIT ' . $number;
346                         }
347                 }
348
349                 if ( $this->query_vars['count'] ) {
350                         $fields = 'COUNT(*)';
351                 } else {
352                         $fields = "$wpdb->site.id";
353                 }
354
355                 // Parse network IDs for an IN clause.
356                 if ( ! empty( $this->query_vars['network__in'] ) ) {
357                         $this->sql_clauses['where']['network__in'] = "$wpdb->site.id IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['network__in'] ) ) . ' )';
358                 }
359
360                 // Parse network IDs for a NOT IN clause.
361                 if ( ! empty( $this->query_vars['network__not_in'] ) ) {
362                         $this->sql_clauses['where']['network__not_in'] = "$wpdb->site.id NOT IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['network__not_in'] ) ) . ' )';
363                 }
364
365                 if ( ! empty( $this->query_vars['domain'] ) ) {
366                         $this->sql_clauses['where']['domain'] = $wpdb->prepare( "$wpdb->site.domain = %s", $this->query_vars['domain'] );
367                 }
368
369                 // Parse network domain for an IN clause.
370                 if ( is_array( $this->query_vars['domain__in'] ) ) {
371                         $this->sql_clauses['where']['domain__in'] = "$wpdb->site.domain IN ( '" . implode( "', '", $wpdb->_escape( $this->query_vars['domain__in'] ) ) . "' )";
372                 }
373
374                 // Parse network domain for a NOT IN clause.
375                 if ( is_array( $this->query_vars['domain__not_in'] ) ) {
376                         $this->sql_clauses['where']['domain__not_in'] = "$wpdb->site.domain NOT IN ( '" . implode( "', '", $wpdb->_escape( $this->query_vars['domain__not_in'] ) ) . "' )";
377                 }
378
379                 if ( ! empty( $this->query_vars['path'] ) ) {
380                         $this->sql_clauses['where']['path'] = $wpdb->prepare( "$wpdb->site.path = %s", $this->query_vars['path'] );
381                 }
382
383                 // Parse network path for an IN clause.
384                 if ( is_array( $this->query_vars['path__in'] ) ) {
385                         $this->sql_clauses['where']['path__in'] = "$wpdb->site.path IN ( '" . implode( "', '", $wpdb->_escape( $this->query_vars['path__in'] ) ) . "' )";
386                 }
387
388                 // Parse network path for a NOT IN clause.
389                 if ( is_array( $this->query_vars['path__not_in'] ) ) {
390                         $this->sql_clauses['where']['path__not_in'] = "$wpdb->site.path NOT IN ( '" . implode( "', '", $wpdb->_escape( $this->query_vars['path__not_in'] ) ) . "' )";
391                 }
392
393                 // Falsey search strings are ignored.
394                 if ( strlen( $this->query_vars['search'] ) ) {
395                         $this->sql_clauses['where']['search'] = $this->get_search_sql(
396                                 $this->query_vars['search'],
397                                 array( "$wpdb->site.domain", "$wpdb->site.path" )
398                         );
399                 }
400
401                 $join = '';
402
403                 $where = implode( ' AND ', $this->sql_clauses['where'] );
404
405                 $pieces = array( 'fields', 'join', 'where', 'orderby', 'limits', 'groupby' );
406
407                 /**
408                  * Filters the network query clauses.
409                  *
410                  * @since 4.6.0
411                  *
412                  * @param array            $pieces A compacted array of network query clauses.
413                  * @param WP_Network_Query &$this  Current instance of WP_Network_Query, passed by reference.
414                  */
415                 $clauses = apply_filters_ref_array( 'networks_clauses', array( compact( $pieces ), &$this ) );
416
417                 $fields = isset( $clauses['fields'] ) ? $clauses['fields'] : '';
418                 $join = isset( $clauses['join'] ) ? $clauses['join'] : '';
419                 $where = isset( $clauses['where'] ) ? $clauses['where'] : '';
420                 $orderby = isset( $clauses['orderby'] ) ? $clauses['orderby'] : '';
421                 $limits = isset( $clauses['limits'] ) ? $clauses['limits'] : '';
422                 $groupby = isset( $clauses['groupby'] ) ? $clauses['groupby'] : '';
423
424                 if ( $where ) {
425                         $where = 'WHERE ' . $where;
426                 }
427
428                 if ( $groupby ) {
429                         $groupby = 'GROUP BY ' . $groupby;
430                 }
431
432                 if ( $orderby ) {
433                         $orderby = "ORDER BY $orderby";
434                 }
435
436                 $found_rows = '';
437                 if ( ! $this->query_vars['no_found_rows'] ) {
438                         $found_rows = 'SQL_CALC_FOUND_ROWS';
439                 }
440
441                 $this->sql_clauses['select']  = "SELECT $found_rows $fields";
442                 $this->sql_clauses['from']    = "FROM $wpdb->site $join";
443                 $this->sql_clauses['groupby'] = $groupby;
444                 $this->sql_clauses['orderby'] = $orderby;
445                 $this->sql_clauses['limits']  = $limits;
446
447                 $this->request = "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$where} {$this->sql_clauses['groupby']} {$this->sql_clauses['orderby']} {$this->sql_clauses['limits']}";
448
449                 if ( $this->query_vars['count'] ) {
450                         return intval( $wpdb->get_var( $this->request ) );
451                 }
452
453                 $network_ids = $wpdb->get_col( $this->request );
454
455                 return array_map( 'intval', $network_ids );
456         }
457
458         /**
459          * Populates found_networks and max_num_pages properties for the current query
460          * if the limit clause was used.
461          *
462          * @since 4.6.0
463          * @access private
464          *
465          * @global wpdb $wpdb WordPress database abstraction object.
466          */
467         private function set_found_networks() {
468                 global $wpdb;
469
470                 if ( $this->query_vars['number'] && ! $this->query_vars['no_found_rows'] ) {
471                         /**
472                          * Filters the query used to retrieve found network count.
473                          *
474                          * @since 4.6.0
475                          *
476                          * @param string           $found_networks_query SQL query. Default 'SELECT FOUND_ROWS()'.
477                          * @param WP_Network_Query $network_query        The `WP_Network_Query` instance.
478                          */
479                         $found_networks_query = apply_filters( 'found_networks_query', 'SELECT FOUND_ROWS()', $this );
480
481                         $this->found_networks = (int) $wpdb->get_var( $found_networks_query );
482                 }
483         }
484
485         /**
486          * Used internally to generate an SQL string for searching across multiple columns.
487          *
488          * @since 4.6.0
489          * @access protected
490          *
491          * @global wpdb  $wpdb WordPress database abstraction object.
492          *
493          * @param string $string  Search string.
494          * @param array  $columns Columns to search.
495          *
496          * @return string Search SQL.
497          */
498         protected function get_search_sql( $string, $columns ) {
499                 global $wpdb;
500
501                 $like = '%' . $wpdb->esc_like( $string ) . '%';
502
503                 $searches = array();
504                 foreach ( $columns as $column ) {
505                         $searches[] = $wpdb->prepare( "$column LIKE %s", $like );
506                 }
507
508                 return '(' . implode( ' OR ', $searches ) . ')';
509         }
510
511         /**
512          * Parses and sanitizes 'orderby' keys passed to the network query.
513          *
514          * @since 4.6.0
515          * @access protected
516          *
517          * @global wpdb $wpdb WordPress database abstraction object.
518          *
519          * @param string $orderby Alias for the field to order by.
520          * @return string|false Value to used in the ORDER clause. False otherwise.
521          */
522         protected function parse_orderby( $orderby ) {
523                 global $wpdb;
524
525                 $allowed_keys = array(
526                         'id',
527                         'domain',
528                         'path',
529                 );
530
531                 $parsed = false;
532                 if ( $orderby == 'network__in' ) {
533                         $network__in = implode( ',', array_map( 'absint', $this->query_vars['network__in'] ) );
534                         $parsed = "FIELD( {$wpdb->site}.id, $network__in )";
535                 } elseif ( $orderby == 'domain_length' || $orderby == 'path_length' ) {
536                         $field = substr( $orderby, 0, -7 );
537                         $parsed = "CHAR_LENGTH($wpdb->site.$field)";
538                 } elseif ( in_array( $orderby, $allowed_keys ) ) {
539                         $parsed = "$wpdb->site.$orderby";
540                 }
541
542                 return $parsed;
543         }
544
545         /**
546          * Parses an 'order' query variable and cast it to 'ASC' or 'DESC' as necessary.
547          *
548          * @since 4.6.0
549          * @access protected
550          *
551          * @param string $order The 'order' query variable.
552          * @return string The sanitized 'order' query variable.
553          */
554         protected function parse_order( $order ) {
555                 if ( ! is_string( $order ) || empty( $order ) ) {
556                         return 'ASC';
557                 }
558
559                 if ( 'ASC' === strtoupper( $order ) ) {
560                         return 'ASC';
561                 } else {
562                         return 'DESC';
563                 }
564         }
565 }