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