wp/wp-includes/class-wp-user-query.php
changeset 21 48c4eec2b7e6
parent 19 3d72ae0968f4
child 22 8c2e4d02f4ef
--- a/wp/wp-includes/class-wp-user-query.php	Thu Sep 29 08:06:27 2022 +0200
+++ b/wp/wp-includes/class-wp-user-query.php	Fri Sep 05 18:40:08 2025 +0200
@@ -14,6 +14,7 @@
  *
  * @see WP_User_Query::prepare_query() for information on accepted arguments.
  */
+#[AllowDynamicProperties]
 class WP_User_Query {
 
 	/**
@@ -66,11 +67,12 @@
 	public $query_limit;
 
 	/**
-	 * PHP5 constructor.
+	 * Constructor.
 	 *
 	 * @since 3.1.0
 	 *
 	 * @param null|string|array $query Optional. The query variables.
+	 *                                 See WP_User_Query::prepare_query() for information on accepted arguments.
 	 */
 	public function __construct( $query = null ) {
 		if ( ! empty( $query ) ) {
@@ -84,7 +86,7 @@
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $args Query vars, as passed to `WP_User_Query`.
+	 * @param string|array $args Query vars, as passed to `WP_User_Query`.
 	 * @return array Complete query variables with undefined ones filled in with defaults.
 	 */
 	public static function fill_query_vars( $args ) {
@@ -118,6 +120,7 @@
 			'login'               => '',
 			'login__in'           => array(),
 			'login__not_in'       => array(),
+			'cache_results'       => true,
 		);
 
 		return wp_parse_args( $args, $defaults );
@@ -139,46 +142,48 @@
 	 * @since 5.1.0 Introduced the 'meta_compare_key' parameter.
 	 * @since 5.3.0 Introduced the 'meta_type_key' parameter.
 	 * @since 5.9.0 Added 'capability', 'capability__in', and 'capability__not_in' parameters.
+	 *              Deprecated the 'who' parameter.
+	 * @since 6.3.0 Added 'cache_results' parameter.
 	 *
-	 * @global wpdb $wpdb WordPress database abstraction object.
-	 * @global int  $blog_id
+	 * @global wpdb     $wpdb     WordPress database abstraction object.
+	 * @global WP_Roles $wp_roles WordPress role management object.
 	 *
 	 * @param string|array $query {
-	 *     Optional. Array or string of Query parameters.
+	 *     Optional. Array or string of query parameters.
 	 *
 	 *     @type int             $blog_id             The site ID. Default is the current site.
-	 *     @type string|string[] $role                An array or a comma-separated list of role names that users must match
-	 *                                                to be included in results. Note that this is an inclusive list: users
-	 *                                                must match *each* role. Default empty.
-	 *     @type string[]        $role__in            An array of role names. Matched users must have at least one of these
-	 *                                                roles. Default empty array.
-	 *     @type string[]        $role__not_in        An array of role names to exclude. Users matching one or more of these
-	 *                                                roles will not be included in results. Default empty array.
+	 *     @type string|string[] $role                An array or a comma-separated list of role names that users
+	 *                                                must match to be included in results. Note that this is
+	 *                                                an inclusive list: users must match *each* role. Default empty.
+	 *     @type string[]        $role__in            An array of role names. Matched users must have at least one
+	 *                                                of these roles. Default empty array.
+	 *     @type string[]        $role__not_in        An array of role names to exclude. Users matching one or more
+	 *                                                of these roles will not be included in results. Default empty array.
 	 *     @type string|string[] $meta_key            Meta key or keys to filter by.
 	 *     @type string|string[] $meta_value          Meta value or values to filter by.
 	 *     @type string          $meta_compare        MySQL operator used for comparing the meta value.
-	 *                                                See WP_Meta_Query::__construct for accepted values and default value.
+	 *                                                See WP_Meta_Query::__construct() for accepted values and default value.
 	 *     @type string          $meta_compare_key    MySQL operator used for comparing the meta key.
-	 *                                                See WP_Meta_Query::__construct for accepted values and default value.
+	 *                                                See WP_Meta_Query::__construct() for accepted values and default value.
 	 *     @type string          $meta_type           MySQL data type that the meta_value column will be CAST to for comparisons.
-	 *                                                See WP_Meta_Query::__construct for accepted values and default value.
+	 *                                                See WP_Meta_Query::__construct() for accepted values and default value.
 	 *     @type string          $meta_type_key       MySQL data type that the meta_key column will be CAST to for comparisons.
-	 *                                                See WP_Meta_Query::__construct for accepted values and default value.
+	 *                                                See WP_Meta_Query::__construct() for accepted values and default value.
 	 *     @type array           $meta_query          An associative array of WP_Meta_Query arguments.
-	 *                                                See WP_Meta_Query::__construct for accepted values.
-	 *     @type string|string[] $capability          An array or a comma-separated list of capability names that users must match
-	 *                                                to be included in results. Note that this is an inclusive list: users
-	 *                                                must match *each* capability.
-	 *                                                Does NOT work for capabilities not in the database or filtered via {@see 'map_meta_cap'}.
-	 *                                                Default empty.
-	 *     @type string[]        $capability__in      An array of capability names. Matched users must have at least one of these
-	 *                                                capabilities.
-	 *                                                Does NOT work for capabilities not in the database or filtered via {@see 'map_meta_cap'}.
-	 *                                                Default empty array.
-	 *     @type string[]        $capability__not_in  An array of capability names to exclude. Users matching one or more of these
-	 *                                                capabilities will not be included in results.
-	 *                                                Does NOT work for capabilities not in the database or filtered via {@see 'map_meta_cap'}.
-	 *                                                Default empty array.
+	 *                                                See WP_Meta_Query::__construct() for accepted values.
+	 *     @type string|string[] $capability          An array or a comma-separated list of capability names that users
+	 *                                                must match to be included in results. Note that this is
+	 *                                                an inclusive list: users must match *each* capability.
+	 *                                                Does NOT work for capabilities not in the database or filtered
+	 *                                                via {@see 'map_meta_cap'}. Default empty.
+	 *     @type string[]        $capability__in      An array of capability names. Matched users must have at least one
+	 *                                                of these capabilities.
+	 *                                                Does NOT work for capabilities not in the database or filtered
+	 *                                                via {@see 'map_meta_cap'}. Default empty array.
+	 *     @type string[]        $capability__not_in  An array of capability names to exclude. Users matching one or more
+	 *                                                of these capabilities will not be included in results.
+	 *                                                Does NOT work for capabilities not in the database or filtered
+	 *                                                via {@see 'map_meta_cap'}. Default empty array.
 	 *     @type int[]           $include             An array of user IDs to include. Default empty array.
 	 *     @type int[]           $exclude             An array of user IDs to exclude. Default empty array.
 	 *     @type string          $search              Search keyword. Searches for possible string matches on columns.
@@ -195,13 +200,13 @@
 	 *                                                - 'include'
 	 *                                                - 'user_login' (or 'login')
 	 *                                                - 'login__in'
-	 *                                                - 'user_nicename' (or 'nicename'),
+	 *                                                - 'user_nicename' (or 'nicename')
 	 *                                                - 'nicename__in'
 	 *                                                - 'user_email (or 'email')
-	 *                                                - 'user_url' (or 'url'),
+	 *                                                - 'user_url' (or 'url')
 	 *                                                - 'user_registered' (or 'registered')
 	 *                                                - 'post_count'
-	 *                                                - 'meta_value',
+	 *                                                - 'meta_value'
 	 *                                                - 'meta_value_num'
 	 *                                                - The value of `$meta_key`
 	 *                                                - An array key of `$meta_query`
@@ -235,10 +240,11 @@
 	 *                                                - 'user_status'
 	 *                                                - 'spam' (only available on multisite installs)
 	 *                                                - 'deleted' (only available on multisite installs)
-	 *                                                - 'all' for all fields
-	 *                                                - 'all_with_meta' to include meta fields.
+	 *                                                - 'all' for all fields and loads user meta.
+	 *                                                - 'all_with_meta' Deprecated. Use 'all'.
 	 *                                                Default 'all'.
-	 *     @type string          $who                 Type of users to query. Accepts 'authors'.
+	 *     @type string          $who                 Deprecated, use `$capability` instead.
+	 *                                                Type of users to query. Accepts 'authors'.
 	 *                                                Default empty (all users).
 	 *     @type bool|string[]   $has_published_posts Pass an array of post types to filter results to users who have
 	 *                                                published posts in those post types. `true` is an alias for all
@@ -253,10 +259,11 @@
 	 *                                                logins will be included in results. Default empty array.
 	 *     @type string[]        $login__not_in       An array of logins to exclude. Users matching one of these
 	 *                                                logins will not be included in results. Default empty array.
+	 *     @type bool            $cache_results       Whether to cache user information. Default true.
 	 * }
 	 */
 	public function prepare_query( $query = array() ) {
-		global $wpdb;
+		global $wpdb, $wp_roles;
 
 		if ( empty( $this->query_vars ) || ! empty( $query ) ) {
 			$this->query_limit = null;
@@ -310,9 +317,7 @@
 				$this->query_fields[] = "$wpdb->users.$field";
 			}
 			$this->query_fields = implode( ',', $this->query_fields );
-		} elseif ( 'all' === $qv['fields'] ) {
-			$this->query_fields = "$wpdb->users.*";
-		} elseif ( ! in_array( $qv['fields'], $allowed_fields, true ) ) {
+		} elseif ( 'all_with_meta' === $qv['fields'] || 'all' === $qv['fields'] || ! in_array( $qv['fields'], $allowed_fields, true ) ) {
 			$this->query_fields = "$wpdb->users.ID";
 		} else {
 			$field              = 'id' === strtolower( $qv['fields'] ) ? 'ID' : sanitize_key( $qv['fields'] );
@@ -450,8 +455,6 @@
 		$available_roles = array();
 
 		if ( ! empty( $qv['capability'] ) || ! empty( $qv['capability__in'] ) || ! empty( $qv['capability__not_in'] ) ) {
-			global $wp_roles;
-
 			$wp_roles->for_site( $blog_id );
 			$available_roles = $wp_roles->roles;
 		}
@@ -692,8 +695,8 @@
 		}
 
 		if ( $search ) {
-			$leading_wild  = ( ltrim( $search, '*' ) != $search );
-			$trailing_wild = ( rtrim( $search, '*' ) != $search );
+			$leading_wild  = ( ltrim( $search, '*' ) !== $search );
+			$trailing_wild = ( rtrim( $search, '*' ) !== $search );
 			if ( $leading_wild && $trailing_wild ) {
 				$wild = 'both';
 			} elseif ( $leading_wild ) {
@@ -712,7 +715,7 @@
 				$search_columns = array_intersect( $qv['search_columns'], array( 'ID', 'user_login', 'user_email', 'user_url', 'user_nicename', 'display_name' ) );
 			}
 			if ( ! $search_columns ) {
-				if ( false !== strpos( $search, '@' ) ) {
+				if ( str_contains( $search, '@' ) ) {
 					$search_columns = array( 'user_email' );
 				} elseif ( is_numeric( $search ) ) {
 					$search_columns = array( 'user_login', 'ID' );
@@ -779,8 +782,25 @@
 	public function query() {
 		global $wpdb;
 
+		if ( ! did_action( 'plugins_loaded' ) ) {
+			_doing_it_wrong(
+				'WP_User_Query::query',
+				sprintf(
+				/* translators: %s: plugins_loaded */
+					__( 'User queries should not be run before the %s hook.' ),
+					'<code>plugins_loaded</code>'
+				),
+				'6.1.1'
+			);
+		}
+
 		$qv =& $this->query_vars;
 
+		// Do not cache results if more than 3 fields are requested.
+		if ( is_array( $qv['fields'] ) && count( $qv['fields'] ) > 3 ) {
+			$qv['cache_results'] = false;
+		}
+
 		/**
 		 * Filters the users array before the query takes place.
 		 *
@@ -800,35 +820,54 @@
 		$this->results = apply_filters_ref_array( 'users_pre_query', array( null, &$this ) );
 
 		if ( null === $this->results ) {
-			$this->request = "
-				SELECT {$this->query_fields}
-				{$this->query_from}
-				{$this->query_where}
-				{$this->query_orderby}
-				{$this->query_limit}
-			";
+			// Beginning of the string is on a new line to prevent leading whitespace. See https://core.trac.wordpress.org/ticket/56841.
+			$this->request =
+				"SELECT {$this->query_fields}
+				 {$this->query_from}
+				 {$this->query_where}
+				 {$this->query_orderby}
+				 {$this->query_limit}";
+			$cache_value   = false;
+			$cache_key     = $this->generate_cache_key( $qv, $this->request );
+			$cache_group   = 'user-queries';
+			if ( $qv['cache_results'] ) {
+				$cache_value = wp_cache_get( $cache_key, $cache_group );
+			}
+			if ( false !== $cache_value ) {
+				$this->results     = $cache_value['user_data'];
+				$this->total_users = $cache_value['total_users'];
+			} else {
 
-			if ( is_array( $qv['fields'] ) || 'all' === $qv['fields'] ) {
-				$this->results = $wpdb->get_results( $this->request );
-			} else {
-				$this->results = $wpdb->get_col( $this->request );
-			}
+				if ( is_array( $qv['fields'] ) ) {
+					$this->results = $wpdb->get_results( $this->request );
+				} else {
+					$this->results = $wpdb->get_col( $this->request );
+				}
 
-			if ( isset( $qv['count_total'] ) && $qv['count_total'] ) {
-				/**
-				 * Filters SELECT FOUND_ROWS() query for the current WP_User_Query instance.
-				 *
-				 * @since 3.2.0
-				 * @since 5.1.0 Added the `$this` parameter.
-				 *
-				 * @global wpdb $wpdb WordPress database abstraction object.
-				 *
-				 * @param string        $sql   The SELECT FOUND_ROWS() query for the current WP_User_Query.
-				 * @param WP_User_Query $query The current WP_User_Query instance.
-				 */
-				$found_users_query = apply_filters( 'found_users_query', 'SELECT FOUND_ROWS()', $this );
+				if ( isset( $qv['count_total'] ) && $qv['count_total'] ) {
+					/**
+					 * Filters SELECT FOUND_ROWS() query for the current WP_User_Query instance.
+					 *
+					 * @since 3.2.0
+					 * @since 5.1.0 Added the `$this` parameter.
+					 *
+					 * @global wpdb $wpdb WordPress database abstraction object.
+					 *
+					 * @param string        $sql   The SELECT FOUND_ROWS() query for the current WP_User_Query.
+					 * @param WP_User_Query $query The current WP_User_Query instance.
+					 */
+					$found_users_query = apply_filters( 'found_users_query', 'SELECT FOUND_ROWS()', $this );
 
-				$this->total_users = (int) $wpdb->get_var( $found_users_query );
+					$this->total_users = (int) $wpdb->get_var( $found_users_query );
+				}
+
+				if ( $qv['cache_results'] ) {
+					$cache_value = array(
+						'user_data'   => $this->results,
+						'total_users' => $this->total_users,
+					);
+					wp_cache_add( $cache_key, $cache_value, $cache_group );
+				}
 			}
 		}
 
@@ -842,19 +881,21 @@
 			foreach ( $this->results as $result ) {
 				$result->id = $result->ID;
 			}
-		} elseif ( 'all_with_meta' === $qv['fields'] ) {
-			cache_users( $this->results );
+		} elseif ( 'all_with_meta' === $qv['fields'] || 'all' === $qv['fields'] ) {
+			if ( function_exists( 'cache_users' ) ) {
+				cache_users( $this->results );
+			}
 
 			$r = array();
 			foreach ( $this->results as $userid ) {
-				$r[ $userid ] = new WP_User( $userid, '', $qv['blog_id'] );
+				if ( 'all_with_meta' === $qv['fields'] ) {
+					$r[ $userid ] = new WP_User( $userid, '', $qv['blog_id'] );
+				} else {
+					$r[] = new WP_User( $userid, '', $qv['blog_id'] );
+				}
 			}
 
 			$this->results = $r;
-		} elseif ( 'all' === $qv['fields'] ) {
-			foreach ( $this->results as $key => $user ) {
-				$this->results[ $key ] = new WP_User( $user, '', $qv['blog_id'] );
-			}
 		}
 	}
 
@@ -970,12 +1011,11 @@
 				FROM $wpdb->posts
 				$where
 				GROUP BY post_author
-			) p ON ({$wpdb->users}.ID = p.post_author)
-			";
+			) p ON ({$wpdb->users}.ID = p.post_author)";
 			$_orderby          = 'post_count';
 		} elseif ( 'ID' === $orderby || 'id' === $orderby ) {
 			$_orderby = 'ID';
-		} elseif ( 'meta_value' === $orderby || $this->get( 'meta_key' ) == $orderby ) {
+		} elseif ( 'meta_value' === $orderby || $this->get( 'meta_key' ) === $orderby ) {
 			$_orderby = "$wpdb->usermeta.meta_value";
 		} elseif ( 'meta_value_num' === $orderby ) {
 			$_orderby = "$wpdb->usermeta.meta_value+0";
@@ -1000,6 +1040,57 @@
 	}
 
 	/**
+	 * Generate cache key.
+	 *
+	 * @since 6.3.0
+	 *
+	 * @global wpdb $wpdb WordPress database abstraction object.
+	 *
+	 * @param array  $args Query arguments.
+	 * @param string $sql  SQL statement.
+	 * @return string Cache key.
+	 */
+	protected function generate_cache_key( array $args, $sql ) {
+		global $wpdb;
+
+		// Replace wpdb placeholder in the SQL statement used by the cache key.
+		$sql = $wpdb->remove_placeholder_escape( $sql );
+
+		$key          = md5( $sql );
+		$last_changed = wp_cache_get_last_changed( 'users' );
+
+		if ( empty( $args['orderby'] ) ) {
+			// Default order is by 'user_login'.
+			$ordersby = array( 'user_login' => '' );
+		} elseif ( is_array( $args['orderby'] ) ) {
+			$ordersby = $args['orderby'];
+		} else {
+			// 'orderby' values may be a comma- or space-separated list.
+			$ordersby = preg_split( '/[,\s]+/', $args['orderby'] );
+		}
+
+		$blog_id = 0;
+		if ( isset( $args['blog_id'] ) ) {
+			$blog_id = absint( $args['blog_id'] );
+		}
+
+		if ( $args['has_published_posts'] || in_array( 'post_count', $ordersby, true ) ) {
+			$switch = $blog_id && get_current_blog_id() !== $blog_id;
+			if ( $switch ) {
+				switch_to_blog( $blog_id );
+			}
+
+			$last_changed .= wp_cache_get_last_changed( 'posts' );
+
+			if ( $switch ) {
+				restore_current_blog();
+			}
+		}
+
+		return "get_users:$key:$last_changed";
+	}
+
+	/**
 	 * Parses an 'order' query variable and casts it to ASC or DESC as necessary.
 	 *
 	 * @since 4.2.0
@@ -1023,6 +1114,7 @@
 	 * Makes private properties readable for backward compatibility.
 	 *
 	 * @since 4.0.0
+	 * @since 6.4.0 Getting a dynamic property is deprecated.
 	 *
 	 * @param string $name Property to get.
 	 * @return mixed Property.
@@ -1031,27 +1123,44 @@
 		if ( in_array( $name, $this->compat_fields, true ) ) {
 			return $this->$name;
 		}
+
+		wp_trigger_error(
+			__METHOD__,
+			"The property `{$name}` is not declared. Getting a dynamic property is " .
+			'deprecated since version 6.4.0! Instead, declare the property on the class.',
+			E_USER_DEPRECATED
+		);
+		return null;
 	}
 
 	/**
 	 * Makes private properties settable for backward compatibility.
 	 *
 	 * @since 4.0.0
+	 * @since 6.4.0 Setting a dynamic property is deprecated.
 	 *
 	 * @param string $name  Property to check if set.
 	 * @param mixed  $value Property value.
-	 * @return mixed Newly-set property.
 	 */
 	public function __set( $name, $value ) {
 		if ( in_array( $name, $this->compat_fields, true ) ) {
-			return $this->$name = $value;
+			$this->$name = $value;
+			return;
 		}
+
+		wp_trigger_error(
+			__METHOD__,
+			"The property `{$name}` is not declared. Setting a dynamic property is " .
+			'deprecated since version 6.4.0! Instead, declare the property on the class.',
+			E_USER_DEPRECATED
+		);
 	}
 
 	/**
 	 * Makes private properties checkable for backward compatibility.
 	 *
 	 * @since 4.0.0
+	 * @since 6.4.0 Checking a dynamic property is deprecated.
 	 *
 	 * @param string $name Property to check if set.
 	 * @return bool Whether the property is set.
@@ -1060,19 +1169,36 @@
 		if ( in_array( $name, $this->compat_fields, true ) ) {
 			return isset( $this->$name );
 		}
+
+		wp_trigger_error(
+			__METHOD__,
+			"The property `{$name}` is not declared. Checking `isset()` on a dynamic property " .
+			'is deprecated since version 6.4.0! Instead, declare the property on the class.',
+			E_USER_DEPRECATED
+		);
+		return false;
 	}
 
 	/**
 	 * Makes private properties un-settable for backward compatibility.
 	 *
 	 * @since 4.0.0
+	 * @since 6.4.0 Unsetting a dynamic property is deprecated.
 	 *
 	 * @param string $name Property to unset.
 	 */
 	public function __unset( $name ) {
 		if ( in_array( $name, $this->compat_fields, true ) ) {
 			unset( $this->$name );
+			return;
 		}
+
+		wp_trigger_error(
+			__METHOD__,
+			"A property `{$name}` is not declared. Unsetting a dynamic property is " .
+			'deprecated since version 6.4.0! Instead, declare the property on the class.',
+			E_USER_DEPRECATED
+		);
 	}
 
 	/**