diff -r 7b1b88e27a20 -r 48c4eec2b7e6 wp/wp-includes/class-wp-user-query.php --- 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.' ), + 'plugins_loaded' + ), + '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 + ); } /**