Issues (4967)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/wp-includes/class-wp-user-query.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * User API: WP_User_Query class
4
 *
5
 * @package WordPress
6
 * @subpackage Users
7
 * @since 4.4.0
8
 */
9
10
/**
11
 * Core class used for querying users.
12
 *
13
 * @since 3.1.0
14
 *
15
 * @see WP_User_Query::prepare_query() for information on accepted arguments.
16
 */
17
class WP_User_Query {
18
19
	/**
20
	 * Query vars, after parsing
21
	 *
22
	 * @since 3.5.0
23
	 * @access public
24
	 * @var array
25
	 */
26
	public $query_vars = array();
27
28
	/**
29
	 * List of found user ids
30
	 *
31
	 * @since 3.1.0
32
	 * @access private
33
	 * @var array
34
	 */
35
	private $results;
36
37
	/**
38
	 * Total number of found users for the current query
39
	 *
40
	 * @since 3.1.0
41
	 * @access private
42
	 * @var int
43
	 */
44
	private $total_users = 0;
45
46
	/**
47
	 * Metadata query container.
48
	 *
49
	 * @since 4.2.0
50
	 * @access public
51
	 * @var WP_Meta_Query
52
	 */
53
	public $meta_query = false;
54
55
	/**
56
	 * The SQL query used to fetch matching users.
57
	 *
58
	 * @since 4.4.0
59
	 * @access public
60
	 * @var string
61
	 */
62
	public $request;
63
64
	private $compat_fields = array( 'results', 'total_users' );
65
66
	// SQL clauses
67
	public $query_fields;
68
	public $query_from;
69
	public $query_where;
70
	public $query_orderby;
71
	public $query_limit;
72
73
	/**
74
	 * PHP5 constructor.
75
	 *
76
	 * @since 3.1.0
77
	 *
78
	 * @param null|string|array $query Optional. The query variables.
79
	 */
80
	public function __construct( $query = null ) {
81
		if ( ! empty( $query ) ) {
82
			$this->prepare_query( $query );
83
			$this->query();
84
		}
85
	}
86
87
	/**
88
	 * Fills in missing query variables with default values.
89
	 *
90
	 * @since 4.4.0
91
	 * @access public
92
	 *
93
	 * @param array $args Query vars, as passed to `WP_User_Query`.
94
	 * @return array Complete query variables with undefined ones filled in with defaults.
95
	 */
96
	public static function fill_query_vars( $args ) {
97
		$defaults = array(
98
			'blog_id' => get_current_blog_id(),
99
			'role' => '',
100
			'role__in' => array(),
101
			'role__not_in' => array(),
102
			'meta_key' => '',
103
			'meta_value' => '',
104
			'meta_compare' => '',
105
			'include' => array(),
106
			'exclude' => array(),
107
			'search' => '',
108
			'search_columns' => array(),
109
			'orderby' => 'login',
110
			'order' => 'ASC',
111
			'offset' => '',
112
			'number' => '',
113
			'paged' => 1,
114
			'count_total' => true,
115
			'fields' => 'all',
116
			'who' => '',
117
			'has_published_posts' => null,
118
			'nicename' => '',
119
			'nicename__in' => array(),
120
			'nicename__not_in' => array(),
121
			'login' => '',
122
			'login__in' => array(),
123
			'login__not_in' => array()
124
		);
125
126
		return wp_parse_args( $args, $defaults );
127
	}
128
129
	/**
130
	 * Prepare the query variables.
131
	 *
132
	 * @since 3.1.0
133
	 * @since 4.1.0 Added the ability to order by the `include` value.
134
	 * @since 4.2.0 Added 'meta_value_num' support for `$orderby` parameter. Added multi-dimensional array syntax
135
	 *              for `$orderby` parameter.
136
	 * @since 4.3.0 Added 'has_published_posts' parameter.
137
	 * @since 4.4.0 Added 'paged', 'role__in', and 'role__not_in' parameters. The 'role' parameter was updated to
138
	 *              permit an array or comma-separated list of values. The 'number' parameter was updated to support
139
	 *              querying for all users with using -1.
140
	 * @since 4.7.0 Added 'nicename', 'nicename__in', 'nicename__not_in', 'login', 'login__in',
141
	 *              and 'login__not_in' parameters.
142
	 *
143
	 * @access public
144
	 *
145
	 * @global wpdb $wpdb WordPress database abstraction object.
146
	 * @global int  $blog_id
147
	 *
148
	 * @param string|array $query {
149
	 *     Optional. Array or string of Query parameters.
150
	 *
151
	 *     @type int          $blog_id             The site ID. Default is the current site.
152
	 *     @type string|array $role                An array or a comma-separated list of role names that users must match
153
	 *                                             to be included in results. Note that this is an inclusive list: users
154
	 *                                             must match *each* role. Default empty.
155
	 *     @type array        $role__in            An array of role names. Matched users must have at least one of these
156
	 *                                             roles. Default empty array.
157
	 *     @type array        $role__not_in        An array of role names to exclude. Users matching one or more of these
158
	 *                                             roles will not be included in results. Default empty array.
159
	 *     @type string       $meta_key            User meta key. Default empty.
160
	 *     @type string       $meta_value          User meta value. Default empty.
161
	 *     @type string       $meta_compare        Comparison operator to test the `$meta_value`. Accepts '=', '!=',
162
	 *                                             '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN',
163
	 *                                             'BETWEEN', 'NOT BETWEEN', 'EXISTS', 'NOT EXISTS', 'REGEXP',
164
	 *                                             'NOT REGEXP', or 'RLIKE'. Default '='.
165
	 *     @type array        $include             An array of user IDs to include. Default empty array.
166
	 *     @type array        $exclude             An array of user IDs to exclude. Default empty array.
167
	 *     @type string       $search              Search keyword. Searches for possible string matches on columns.
168
	 *                                             When `$search_columns` is left empty, it tries to determine which
169
	 *                                             column to search in based on search string. Default empty.
170
	 *     @type array        $search_columns      Array of column names to be searched. Accepts 'ID', 'login',
171
	 *                                             'nicename', 'email', 'url'. Default empty array.
172
	 *     @type string|array $orderby             Field(s) to sort the retrieved users by. May be a single value,
173
	 *                                             an array of values, or a multi-dimensional array with fields as
174
	 *                                             keys and orders ('ASC' or 'DESC') as values. Accepted values are
175
	 *                                             'ID', 'display_name' (or 'name'), 'include', 'user_login'
176
	 *                                             (or 'login'), 'login__in', 'user_nicename' (or 'nicename'),
177
	 *                                             'nicename__in', 'user_email (or 'email'), 'user_url' (or 'url'),
178
	 *                                             'user_registered' (or 'registered'), 'post_count', 'meta_value',
179
	 *                                             'meta_value_num', the value of `$meta_key`, or an array key of
180
	 *                                             `$meta_query`. To use 'meta_value' or 'meta_value_num', `$meta_key`
181
	 *                                             must be also be defined. Default 'user_login'.
182
	 *     @type string       $order               Designates ascending or descending order of users. Order values
183
	 *                                             passed as part of an `$orderby` array take precedence over this
184
	 *                                             parameter. Accepts 'ASC', 'DESC'. Default 'ASC'.
185
	 *     @type int          $offset              Number of users to offset in retrieved results. Can be used in
186
	 *                                             conjunction with pagination. Default 0.
187
	 *     @type int          $number              Number of users to limit the query for. Can be used in
188
	 *                                             conjunction with pagination. Value -1 (all) is supported, but
189
	 *                                             should be used with caution on larger sites.
190
	 *                                             Default empty (all users).
191
	 *     @type int          $paged               When used with number, defines the page of results to return.
192
	 *                                             Default 1.
193
	 *     @type bool         $count_total         Whether to count the total number of users found. If pagination
194
	 *                                             is not needed, setting this to false can improve performance.
195
	 *                                             Default true.
196
	 *     @type string|array $fields              Which fields to return. Single or all fields (string), or array
197
	 *                                             of fields. Accepts 'ID', 'display_name', 'user_login',
198
	 *                                             'user_nicename', 'user_email', 'user_url', 'user_registered'.
199
	 *                                             Use 'all' for all fields and 'all_with_meta' to include
200
	 *                                             meta fields. Default 'all'.
201
	 *     @type string       $who                 Type of users to query. Accepts 'authors'.
202
	 *                                             Default empty (all users).
203
	 *     @type bool|array   $has_published_posts Pass an array of post types to filter results to users who have
204
	 *                                             published posts in those post types. `true` is an alias for all
205
	 *                                             public post types.
206
	 *     @type string       $nicename            The user nicename. Default empty.
207
	 *     @type array        $nicename__in        An array of nicenames to include. Users matching one of these
208
	 *                                             nicenames will be included in results. Default empty array.
209
	 *     @type array        $nicename__not_in    An array of nicenames to exclude. Users matching one of these
210
	 *                                             nicenames will not be included in results. Default empty array.
211
	 *     @type string       $login               The user login. Default empty.
212
	 *     @type array        $login__in           An array of logins to include. Users matching one of these
213
	 *                                             logins will be included in results. Default empty array.
214
	 *     @type array        $login__not_in       An array of logins to exclude. Users matching one of these
215
	 *                                             logins will not be included in results. Default empty array.
216
	 * }
217
	 */
218
	public function prepare_query( $query = array() ) {
219
		global $wpdb;
220
221
		if ( empty( $this->query_vars ) || ! empty( $query ) ) {
222
			$this->query_limit = null;
223
			$this->query_vars = $this->fill_query_vars( $query );
0 ignored issues
show
It seems like $query defined by parameter $query on line 218 can also be of type string; however, WP_User_Query::fill_query_vars() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
224
		}
225
226
		/**
227
		 * Fires before the WP_User_Query has been parsed.
228
		 *
229
		 * The passed WP_User_Query object contains the query variables, not
230
		 * yet passed into SQL.
231
		 *
232
		 * @since 4.0.0
233
		 *
234
		 * @param WP_User_Query $this The current WP_User_Query instance,
235
		 *                            passed by reference.
236
		 */
237
		do_action( 'pre_get_users', $this );
238
239
		// Ensure that query vars are filled after 'pre_get_users'.
240
		$qv =& $this->query_vars;
241
		$qv =  $this->fill_query_vars( $qv );
242
243
		if ( is_array( $qv['fields'] ) ) {
244
			$qv['fields'] = array_unique( $qv['fields'] );
245
246
			$this->query_fields = array();
247
			foreach ( $qv['fields'] as $field ) {
248
				$field = 'ID' === $field ? 'ID' : sanitize_key( $field );
249
				$this->query_fields[] = "$wpdb->users.$field";
250
			}
251
			$this->query_fields = implode( ',', $this->query_fields );
252
		} elseif ( 'all' == $qv['fields'] ) {
253
			$this->query_fields = "$wpdb->users.*";
254
		} else {
255
			$this->query_fields = "$wpdb->users.ID";
256
		}
257
258
		if ( isset( $qv['count_total'] ) && $qv['count_total'] )
259
			$this->query_fields = 'SQL_CALC_FOUND_ROWS ' . $this->query_fields;
260
261
		$this->query_from = "FROM $wpdb->users";
262
		$this->query_where = "WHERE 1=1";
263
264
		// Parse and sanitize 'include', for use by 'orderby' as well as 'include' below.
265
		if ( ! empty( $qv['include'] ) ) {
266
			$include = wp_parse_id_list( $qv['include'] );
267
		} else {
268
			$include = false;
269
		}
270
271
		$blog_id = 0;
272
		if ( isset( $qv['blog_id'] ) ) {
273
			$blog_id = absint( $qv['blog_id'] );
274
		}
275
276
		if ( $qv['has_published_posts'] && $blog_id ) {
277
			if ( true === $qv['has_published_posts'] ) {
278
				$post_types = get_post_types( array( 'public' => true ) );
279
			} else {
280
				$post_types = (array) $qv['has_published_posts'];
281
			}
282
283
			foreach ( $post_types as &$post_type ) {
284
				$post_type = $wpdb->prepare( '%s', $post_type );
285
			}
286
287
			$posts_table = $wpdb->get_blog_prefix( $blog_id ) . 'posts';
288
			$this->query_where .= " AND $wpdb->users.ID IN ( SELECT DISTINCT $posts_table.post_author FROM $posts_table WHERE $posts_table.post_status = 'publish' AND $posts_table.post_type IN ( " . join( ", ", $post_types ) . " ) )";
289
		}
290
291
		// nicename
292
		if ( '' !== $qv['nicename']) {
293
			$this->query_where .= $wpdb->prepare( ' AND user_nicename = %s', $qv['nicename'] );
294
		}
295
296
		if ( ! empty( $qv['nicename__in'] ) ) {
297
			$sanitized_nicename__in = array_map( 'esc_sql', $qv['nicename__in'] );
298
			$nicename__in = implode( "','", $sanitized_nicename__in );
299
			$this->query_where .= " AND user_nicename IN ( '$nicename__in' )";
300
		}
301
302
		if ( ! empty( $qv['nicename__not_in'] ) ) {
303
			$sanitized_nicename__not_in = array_map( 'esc_sql', $qv['nicename__not_in'] );
304
			$nicename__not_in = implode( "','", $sanitized_nicename__not_in );
305
			$this->query_where .= " AND user_nicename NOT IN ( '$nicename__not_in' )";
306
		}
307
308
		// login
309
		if ( '' !== $qv['login']) {
310
			$this->query_where .= $wpdb->prepare( ' AND user_login = %s', $qv['login'] );
311
		}
312
313 View Code Duplication
		if ( ! empty( $qv['login__in'] ) ) {
314
			$sanitized_login__in = array_map( 'esc_sql', $qv['login__in'] );
315
			$login__in = implode( "','", $sanitized_login__in );
316
			$this->query_where .= " AND user_login IN ( '$login__in' )";
317
		}
318
319
		if ( ! empty( $qv['login__not_in'] ) ) {
320
			$sanitized_login__not_in = array_map( 'esc_sql', $qv['login__not_in'] );
321
			$login__not_in = implode( "','", $sanitized_login__not_in );
322
			$this->query_where .= " AND user_login NOT IN ( '$login__not_in' )";
323
		}
324
325
		// Meta query.
326
		$this->meta_query = new WP_Meta_Query();
327
		$this->meta_query->parse_query_vars( $qv );
328
329
		if ( isset( $qv['who'] ) && 'authors' == $qv['who'] && $blog_id ) {
330
			$who_query = array(
331
				'key' => $wpdb->get_blog_prefix( $blog_id ) . 'user_level',
332
				'value' => 0,
333
				'compare' => '!=',
334
			);
335
336
			// Prevent extra meta query.
337
			$qv['blog_id'] = $blog_id = 0;
338
339 View Code Duplication
			if ( empty( $this->meta_query->queries ) ) {
340
				$this->meta_query->queries = array( $who_query );
341
			} else {
342
				// Append the cap query to the original queries and reparse the query.
343
				$this->meta_query->queries = array(
344
					'relation' => 'AND',
345
					array( $this->meta_query->queries, $who_query ),
346
				);
347
			}
348
349
			$this->meta_query->parse_query_vars( $this->meta_query->queries );
350
		}
351
352
		$roles = array();
353
		if ( isset( $qv['role'] ) ) {
354 View Code Duplication
			if ( is_array( $qv['role'] ) ) {
355
				$roles = $qv['role'];
356
			} elseif ( is_string( $qv['role'] ) && ! empty( $qv['role'] ) ) {
357
				$roles = array_map( 'trim', explode( ',', $qv['role'] ) );
358
			}
359
		}
360
361
		$role__in = array();
362
		if ( isset( $qv['role__in'] ) ) {
363
			$role__in = (array) $qv['role__in'];
364
		}
365
366
		$role__not_in = array();
367
		if ( isset( $qv['role__not_in'] ) ) {
368
			$role__not_in = (array) $qv['role__not_in'];
369
		}
370
371
		if ( $blog_id && ( ! empty( $roles ) || ! empty( $role__in ) || ! empty( $role__not_in ) || is_multisite() ) ) {
372
			$role_queries  = array();
373
374
			$roles_clauses = array( 'relation' => 'AND' );
375 View Code Duplication
			if ( ! empty( $roles ) ) {
376
				foreach ( $roles as $role ) {
377
					$roles_clauses[] = array(
378
						'key'     => $wpdb->get_blog_prefix( $blog_id ) . 'capabilities',
379
						'value'   => '"' . $role . '"',
380
						'compare' => 'LIKE',
381
					);
382
				}
383
384
				$role_queries[] = $roles_clauses;
385
			}
386
387
			$role__in_clauses = array( 'relation' => 'OR' );
388 View Code Duplication
			if ( ! empty( $role__in ) ) {
389
				foreach ( $role__in as $role ) {
390
					$role__in_clauses[] = array(
391
						'key'     => $wpdb->get_blog_prefix( $blog_id ) . 'capabilities',
392
						'value'   => '"' . $role . '"',
393
						'compare' => 'LIKE',
394
					);
395
				}
396
397
				$role_queries[] = $role__in_clauses;
398
			}
399
400
			$role__not_in_clauses = array( 'relation' => 'AND' );
401 View Code Duplication
			if ( ! empty( $role__not_in ) ) {
402
				foreach ( $role__not_in as $role ) {
403
					$role__not_in_clauses[] = array(
404
						'key'     => $wpdb->get_blog_prefix( $blog_id ) . 'capabilities',
405
						'value'   => '"' . $role . '"',
406
						'compare' => 'NOT LIKE',
407
					);
408
				}
409
410
				$role_queries[] = $role__not_in_clauses;
411
			}
412
413
			// If there are no specific roles named, make sure the user is a member of the site.
414
			if ( empty( $role_queries ) ) {
415
				$role_queries[] = array(
416
					'key' => $wpdb->get_blog_prefix( $blog_id ) . 'capabilities',
417
					'compare' => 'EXISTS',
418
				);
419
			}
420
421
			// Specify that role queries should be joined with AND.
422
			$role_queries['relation'] = 'AND';
423
424 View Code Duplication
			if ( empty( $this->meta_query->queries ) ) {
425
				$this->meta_query->queries = $role_queries;
426
			} else {
427
				// Append the cap query to the original queries and reparse the query.
428
				$this->meta_query->queries = array(
429
					'relation' => 'AND',
430
					array( $this->meta_query->queries, $role_queries ),
431
				);
432
			}
433
434
			$this->meta_query->parse_query_vars( $this->meta_query->queries );
435
		}
436
437
		if ( ! empty( $this->meta_query->queries ) ) {
438
			$clauses = $this->meta_query->get_sql( 'user', $wpdb->users, 'ID', $this );
439
			$this->query_from .= $clauses['join'];
440
			$this->query_where .= $clauses['where'];
441
442
			if ( $this->meta_query->has_or_relation() ) {
443
				$this->query_fields = 'DISTINCT ' . $this->query_fields;
444
			}
445
		}
446
447
		// sorting
448
		$qv['order'] = isset( $qv['order'] ) ? strtoupper( $qv['order'] ) : '';
449
		$order = $this->parse_order( $qv['order'] );
450
451
		if ( empty( $qv['orderby'] ) ) {
452
			// Default order is by 'user_login'.
453
			$ordersby = array( 'user_login' => $order );
454
		} elseif ( is_array( $qv['orderby'] ) ) {
455
			$ordersby = $qv['orderby'];
456
		} else {
457
			// 'orderby' values may be a comma- or space-separated list.
458
			$ordersby = preg_split( '/[,\s]+/', $qv['orderby'] );
459
		}
460
461
		$orderby_array = array();
462
		foreach ( $ordersby as $_key => $_value ) {
463
			if ( ! $_value ) {
464
				continue;
465
			}
466
467
			if ( is_int( $_key ) ) {
468
				// Integer key means this is a flat array of 'orderby' fields.
469
				$_orderby = $_value;
470
				$_order = $order;
471
			} else {
472
				// Non-integer key means this the key is the field and the value is ASC/DESC.
473
				$_orderby = $_key;
474
				$_order = $_value;
475
			}
476
477
			$parsed = $this->parse_orderby( $_orderby );
478
479
			if ( ! $parsed ) {
480
				continue;
481
			}
482
483
			if ( 'nicename__in' === $_orderby || 'login__in' === $_orderby ) {
484
				$orderby_array[] = $parsed;
485
			} else {
486
				$orderby_array[] = $parsed . ' ' . $this->parse_order( $_order );
487
			}
488
		}
489
490
		// If no valid clauses were found, order by user_login.
491
		if ( empty( $orderby_array ) ) {
492
			$orderby_array[] = "user_login $order";
493
		}
494
495
		$this->query_orderby = 'ORDER BY ' . implode( ', ', $orderby_array );
496
497
		// limit
498
		if ( isset( $qv['number'] ) && $qv['number'] > 0 ) {
499
			if ( $qv['offset'] ) {
500
				$this->query_limit = $wpdb->prepare("LIMIT %d, %d", $qv['offset'], $qv['number']);
501
			} else {
502
				$this->query_limit = $wpdb->prepare( "LIMIT %d, %d", $qv['number'] * ( $qv['paged'] - 1 ), $qv['number'] );
503
			}
504
		}
505
506
		$search = '';
507
		if ( isset( $qv['search'] ) )
508
			$search = trim( $qv['search'] );
509
510
		if ( $search ) {
511
			$leading_wild = ( ltrim($search, '*') != $search );
512
			$trailing_wild = ( rtrim($search, '*') != $search );
513
			if ( $leading_wild && $trailing_wild )
514
				$wild = 'both';
515
			elseif ( $leading_wild )
516
				$wild = 'leading';
517
			elseif ( $trailing_wild )
518
				$wild = 'trailing';
519
			else
520
				$wild = false;
521
			if ( $wild )
0 ignored issues
show
Bug Best Practice introduced by
The expression $wild of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
522
				$search = trim($search, '*');
523
524
			$search_columns = array();
525
			if ( $qv['search_columns'] )
526
				$search_columns = array_intersect( $qv['search_columns'], array( 'ID', 'user_login', 'user_email', 'user_url', 'user_nicename' ) );
527
			if ( ! $search_columns ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $search_columns of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
528
				if ( false !== strpos( $search, '@') )
529
					$search_columns = array('user_email');
530
				elseif ( is_numeric($search) )
531
					$search_columns = array('user_login', 'ID');
532
				elseif ( preg_match('|^https?://|', $search) && ! ( is_multisite() && wp_is_large_network( 'users' ) ) )
533
					$search_columns = array('user_url');
534
				else
535
					$search_columns = array('user_login', 'user_url', 'user_email', 'user_nicename', 'display_name');
536
			}
537
538
			/**
539
			 * Filters the columns to search in a WP_User_Query search.
540
			 *
541
			 * The default columns depend on the search term, and include 'user_email',
542
			 * 'user_login', 'ID', 'user_url', 'display_name', and 'user_nicename'.
543
			 *
544
			 * @since 3.6.0
545
			 *
546
			 * @param array         $search_columns Array of column names to be searched.
547
			 * @param string        $search         Text being searched.
548
			 * @param WP_User_Query $this           The current WP_User_Query instance.
549
			 */
550
			$search_columns = apply_filters( 'user_search_columns', $search_columns, $search, $this );
551
552
			$this->query_where .= $this->get_search_sql( $search, $search_columns, $wild );
0 ignored issues
show
It seems like $wild can also be of type string; however, WP_User_Query::get_search_sql() does only seem to accept boolean, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
553
		}
554
555
		if ( ! empty( $include ) ) {
556
			// Sanitized earlier.
557
			$ids = implode( ',', $include );
558
			$this->query_where .= " AND $wpdb->users.ID IN ($ids)";
559 View Code Duplication
		} elseif ( ! empty( $qv['exclude'] ) ) {
560
			$ids = implode( ',', wp_parse_id_list( $qv['exclude'] ) );
561
			$this->query_where .= " AND $wpdb->users.ID NOT IN ($ids)";
562
		}
563
564
		// Date queries are allowed for the user_registered field.
565
		if ( ! empty( $qv['date_query'] ) && is_array( $qv['date_query'] ) ) {
566
			$date_query = new WP_Date_Query( $qv['date_query'], 'user_registered' );
567
			$this->query_where .= $date_query->get_sql();
568
		}
569
570
		/**
571
		 * Fires after the WP_User_Query has been parsed, and before
572
		 * the query is executed.
573
		 *
574
		 * The passed WP_User_Query object contains SQL parts formed
575
		 * from parsing the given query.
576
		 *
577
		 * @since 3.1.0
578
		 *
579
		 * @param WP_User_Query $this The current WP_User_Query instance,
580
		 *                            passed by reference.
581
		 */
582
		do_action_ref_array( 'pre_user_query', array( &$this ) );
583
	}
584
585
	/**
586
	 * Execute the query, with the current variables.
587
	 *
588
	 * @since 3.1.0
589
	 *
590
	 * @global wpdb $wpdb WordPress database abstraction object.
591
	 */
592
	public function query() {
593
		global $wpdb;
594
595
		$qv =& $this->query_vars;
596
597
		$this->request = "SELECT $this->query_fields $this->query_from $this->query_where $this->query_orderby $this->query_limit";
598
599
		if ( is_array( $qv['fields'] ) || 'all' == $qv['fields'] ) {
600
			$this->results = $wpdb->get_results( $this->request );
601
		} else {
602
			$this->results = $wpdb->get_col( $this->request );
603
		}
604
605
		/**
606
		 * Filters SELECT FOUND_ROWS() query for the current WP_User_Query instance.
607
		 *
608
		 * @since 3.2.0
609
		 *
610
		 * @global wpdb $wpdb WordPress database abstraction object.
611
		 *
612
		 * @param string $sql The SELECT FOUND_ROWS() query for the current WP_User_Query.
613
		 */
614
		if ( isset( $qv['count_total'] ) && $qv['count_total'] )
615
			$this->total_users = (int) $wpdb->get_var( apply_filters( 'found_users_query', 'SELECT FOUND_ROWS()' ) );
616
617
		if ( !$this->results )
618
			return;
619
620
		if ( 'all_with_meta' == $qv['fields'] ) {
621
			cache_users( $this->results );
622
623
			$r = array();
624
			foreach ( $this->results as $userid )
625
				$r[ $userid ] = new WP_User( $userid, '', $qv['blog_id'] );
626
627
			$this->results = $r;
628
		} elseif ( 'all' == $qv['fields'] ) {
629
			foreach ( $this->results as $key => $user ) {
630
				$this->results[ $key ] = new WP_User( $user, '', $qv['blog_id'] );
631
			}
632
		}
633
	}
634
635
	/**
636
	 * Retrieve query variable.
637
	 *
638
	 * @since 3.5.0
639
	 * @access public
640
	 *
641
	 * @param string $query_var Query variable key.
642
	 * @return mixed
643
	 */
644
	public function get( $query_var ) {
645
		if ( isset( $this->query_vars[$query_var] ) )
646
			return $this->query_vars[$query_var];
647
648
		return null;
649
	}
650
651
	/**
652
	 * Set query variable.
653
	 *
654
	 * @since 3.5.0
655
	 * @access public
656
	 *
657
	 * @param string $query_var Query variable key.
658
	 * @param mixed $value Query variable value.
659
	 */
660
	public function set( $query_var, $value ) {
661
		$this->query_vars[$query_var] = $value;
662
	}
663
664
	/**
665
	 * Used internally to generate an SQL string for searching across multiple columns
666
	 *
667
	 * @access protected
668
	 * @since 3.1.0
669
	 *
670
	 * @global wpdb $wpdb WordPress database abstraction object.
671
	 *
672
	 * @param string $string
673
	 * @param array  $cols
674
	 * @param bool   $wild   Whether to allow wildcard searches. Default is false for Network Admin, true for single site.
675
	 *                       Single site allows leading and trailing wildcards, Network Admin only trailing.
676
	 * @return string
677
	 */
678
	protected function get_search_sql( $string, $cols, $wild = false ) {
679
		global $wpdb;
680
681
		$searches = array();
682
		$leading_wild = ( 'leading' == $wild || 'both' == $wild ) ? '%' : '';
683
		$trailing_wild = ( 'trailing' == $wild || 'both' == $wild ) ? '%' : '';
684
		$like = $leading_wild . $wpdb->esc_like( $string ) . $trailing_wild;
685
686
		foreach ( $cols as $col ) {
687
			if ( 'ID' == $col ) {
688
				$searches[] = $wpdb->prepare( "$col = %s", $string );
689
			} else {
690
				$searches[] = $wpdb->prepare( "$col LIKE %s", $like );
691
			}
692
		}
693
694
		return ' AND (' . implode(' OR ', $searches) . ')';
695
	}
696
697
	/**
698
	 * Return the list of users.
699
	 *
700
	 * @since 3.1.0
701
	 * @access public
702
	 *
703
	 * @return array Array of results.
704
	 */
705
	public function get_results() {
706
		return $this->results;
707
	}
708
709
	/**
710
	 * Return the total number of users for the current query.
711
	 *
712
	 * @since 3.1.0
713
	 * @access public
714
	 *
715
	 * @return int Number of total users.
716
	 */
717
	public function get_total() {
718
		return $this->total_users;
719
	}
720
721
	/**
722
	 * Parse and sanitize 'orderby' keys passed to the user query.
723
	 *
724
	 * @since 4.2.0
725
	 * @access protected
726
	 *
727
	 * @global wpdb $wpdb WordPress database abstraction object.
728
	 *
729
	 * @param string $orderby Alias for the field to order by.
730
	 * @return string Value to used in the ORDER clause, if `$orderby` is valid.
731
	 */
732
	protected function parse_orderby( $orderby ) {
733
		global $wpdb;
734
735
		$meta_query_clauses = $this->meta_query->get_clauses();
736
737
		$_orderby = '';
738
		if ( in_array( $orderby, array( 'login', 'nicename', 'email', 'url', 'registered' ) ) ) {
739
			$_orderby = 'user_' . $orderby;
740
		} elseif ( in_array( $orderby, array( 'user_login', 'user_nicename', 'user_email', 'user_url', 'user_registered' ) ) ) {
741
			$_orderby = $orderby;
742
		} elseif ( 'name' == $orderby || 'display_name' == $orderby ) {
743
			$_orderby = 'display_name';
744
		} elseif ( 'post_count' == $orderby ) {
745
			// todo: avoid the JOIN
746
			$where = get_posts_by_author_sql( 'post' );
747
			$this->query_from .= " LEFT OUTER JOIN (
748
				SELECT post_author, COUNT(*) as post_count
749
				FROM $wpdb->posts
750
				$where
751
				GROUP BY post_author
752
			) p ON ({$wpdb->users}.ID = p.post_author)
753
			";
754
			$_orderby = 'post_count';
755
		} elseif ( 'ID' == $orderby || 'id' == $orderby ) {
756
			$_orderby = 'ID';
757
		} elseif ( 'meta_value' == $orderby || $this->get( 'meta_key' ) == $orderby ) {
758
			$_orderby = "$wpdb->usermeta.meta_value";
759
		} elseif ( 'meta_value_num' == $orderby ) {
760
			$_orderby = "$wpdb->usermeta.meta_value+0";
761
		} elseif ( 'include' === $orderby && ! empty( $this->query_vars['include'] ) ) {
762
			$include = wp_parse_id_list( $this->query_vars['include'] );
763
			$include_sql = implode( ',', $include );
764
			$_orderby = "FIELD( $wpdb->users.ID, $include_sql )";
765 View Code Duplication
		} elseif ( 'nicename__in' === $orderby ) {
766
			$sanitized_nicename__in = array_map( 'esc_sql', $this->query_vars['nicename__in'] );
767
			$nicename__in = implode( "','", $sanitized_nicename__in );
768
			$_orderby = "FIELD( user_nicename, '$nicename__in' )";
769
		} elseif ( 'login__in' === $orderby ) {
770
			$sanitized_login__in = array_map( 'esc_sql', $this->query_vars['login__in'] );
771
			$login__in = implode( "','", $sanitized_login__in );
772
			$_orderby = "FIELD( user_login, '$login__in' )";
773
		} elseif ( isset( $meta_query_clauses[ $orderby ] ) ) {
774
			$meta_clause = $meta_query_clauses[ $orderby ];
775
			$_orderby = sprintf( "CAST(%s.meta_value AS %s)", esc_sql( $meta_clause['alias'] ), esc_sql( $meta_clause['cast'] ) );
776
		}
777
778
		return $_orderby;
779
	}
780
781
	/**
782
	 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
783
	 *
784
	 * @since 4.2.0
785
	 * @access protected
786
	 *
787
	 * @param string $order The 'order' query variable.
788
	 * @return string The sanitized 'order' query variable.
789
	 */
790 View Code Duplication
	protected function parse_order( $order ) {
791
		if ( ! is_string( $order ) || empty( $order ) ) {
792
			return 'DESC';
793
		}
794
795
		if ( 'ASC' === strtoupper( $order ) ) {
796
			return 'ASC';
797
		} else {
798
			return 'DESC';
799
		}
800
	}
801
802
	/**
803
	 * Make private properties readable for backward compatibility.
804
	 *
805
	 * @since 4.0.0
806
	 * @access public
807
	 *
808
	 * @param string $name Property to get.
809
	 * @return mixed Property.
810
	 */
811
	public function __get( $name ) {
812
		if ( in_array( $name, $this->compat_fields ) ) {
813
			return $this->$name;
814
		}
815
	}
816
817
	/**
818
	 * Make private properties settable for backward compatibility.
819
	 *
820
	 * @since 4.0.0
821
	 * @access public
822
	 *
823
	 * @param string $name  Property to check if set.
824
	 * @param mixed  $value Property value.
825
	 * @return mixed Newly-set property.
826
	 */
827
	public function __set( $name, $value ) {
828
		if ( in_array( $name, $this->compat_fields ) ) {
829
			return $this->$name = $value;
830
		}
831
	}
832
833
	/**
834
	 * Make private properties checkable for backward compatibility.
835
	 *
836
	 * @since 4.0.0
837
	 * @access public
838
	 *
839
	 * @param string $name Property to check if set.
840
	 * @return bool Whether the property is set.
0 ignored issues
show
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
841
	 */
842
	public function __isset( $name ) {
843
		if ( in_array( $name, $this->compat_fields ) ) {
844
			return isset( $this->$name );
845
		}
846
	}
847
848
	/**
849
	 * Make private properties un-settable for backward compatibility.
850
	 *
851
	 * @since 4.0.0
852
	 * @access public
853
	 *
854
	 * @param string $name Property to unset.
855
	 */
856
	public function __unset( $name ) {
857
		if ( in_array( $name, $this->compat_fields ) ) {
858
			unset( $this->$name );
859
		}
860
	}
861
862
	/**
863
	 * Make private/protected methods readable for backward compatibility.
864
	 *
865
	 * @since 4.0.0
866
	 * @access public
867
	 *
868
	 * @param callable $name      Method to call.
869
	 * @param array    $arguments Arguments to pass when calling.
870
	 * @return mixed Return value of the callback, false otherwise.
871
	 */
872
	public function __call( $name, $arguments ) {
873
		if ( 'get_search_sql' === $name ) {
874
			return call_user_func_array( array( $this, $name ), $arguments );
875
		}
876
		return false;
877
	}
878
}
879