$role_queries[] = $role__not_in_clauses; } // If there are no specific roles named, make sure the user is a member of the site. if ( empty( $role_queries ) ) { $role_queries[] = array( 'key' => $wpdb->get_blog_prefix( $blog_id ) . 'capabilities', 'compare' => 'EXISTS', ); } // Specify that role queries should be joined with AND. $role_queries['relation'] = 'AND'; if ( empty( $this->meta_query->queries ) ) { $this->meta_query->queries = $role_queries; } else { // Append the cap query to the original queries and reparse the query. $this->meta_query->queries = array( 'relation' => 'AND', array( $this->meta_query->queries, $role_queries ), ); } $this->meta_query->parse_query_vars( $this->meta_query->queries ); } if ( ! empty( $this->meta_query->queries ) ) { $clauses = $this->meta_query->get_sql( 'user', $wpdb->users, 'ID', $this ); $this->query_from .= $clauses['join']; $this->query_where .= $clauses['where']; if ( $this->meta_query->has_or_relation() ) { $this->query_fields = 'DISTINCT ' . $this->query_fields; } } // Sorting. $qv['order'] = isset( $qv['order'] ) ? strtoupper( $qv['order'] ) : ''; $order = $this->parse_order( $qv['order'] ); if ( empty( $qv['orderby'] ) ) { // Default order is by 'user_login'. $ordersby = array( 'user_login' => $order ); } elseif ( is_array( $qv['orderby'] ) ) { $ordersby = $qv['orderby']; } else { // 'orderby' values may be a comma- or space-separated list. $ordersby = preg_split( '/[,\s]+/', $qv['orderby'] ); } $orderby_array = array(); foreach ( $ordersby as $_key => $_value ) { if ( ! $_value ) { continue; } if ( is_int( $_key ) ) { // Integer key means this is a flat array of 'orderby' fields. $_orderby = $_value; $_order = $order; } else { // Non-integer key means this the key is the field and the value is ASC/DESC. $_orderby = $_key; $_order = $_value; } $parsed = $this->parse_orderby( $_orderby ); if ( ! $parsed ) { continue; } if ( 'nicename__in' === $_orderby || 'login__in' === $_orderby ) { $orderby_array[] = $parsed; } else { $orderby_array[] = $parsed . ' ' . $this->parse_order( $_order ); } } // If no valid clauses were found, order by user_login. if ( empty( $orderby_array ) ) { $orderby_array[] = "user_login $order"; } $this->query_orderby = 'ORDER BY ' . implode( ', ', $orderby_array ); // Limit. if ( isset( $qv['number'] ) && $qv['number'] > 0 ) { if ( $qv['offset'] ) { $this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['offset'], $qv['number'] ); } else { $this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['number'] * ( $qv['paged'] - 1 ), $qv['number'] ); } } $search = ''; if ( isset( $qv['search'] ) ) { $search = trim( $qv['search'] ); } if ( $search ) { $leading_wild = ( ltrim( $search, '*' ) !== $search ); $trailing_wild = ( rtrim( $search, '*' ) !== $search ); if ( $leading_wild && $trailing_wild ) { $wild = 'both'; } elseif ( $leading_wild ) { $wild = 'leading'; } elseif ( $trailing_wild ) { $wild = 'trailing'; } else { $wild = false; } if ( $wild ) { $search = trim( $search, '*' ); } $search_columns = array(); if ( $qv['search_columns'] ) { $search_columns = array_intersect( $qv['search_columns'], array( 'ID', 'user_login', 'user_email', 'user_url', 'user_nicename', 'display_name' ) ); } if ( ! $search_columns ) { if ( str_contains( $search, '@' ) ) { $search_columns = array( 'user_email' ); } elseif ( is_numeric( $search ) ) { $search_columns = array( 'user_login', 'ID' ); } elseif ( preg_match( '|^https?://|', $search ) && ! ( is_multisite() && wp_is_large_network( 'users' ) ) ) { $search_columns = array( 'user_url' ); } else { $search_columns = array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' ); } } /** * Filters the columns to search in a WP_User_Query search. * * The default columns depend on the search term, and include 'ID', 'user_login', * 'user_email', 'user_url', 'user_nicename', and 'display_name'. * * @since 3.6.0 * * @param string[] $search_columns Array of column names to be searched. * @param string $search Text being searched. * @param WP_User_Query $query The current WP_User_Query instance. */ $search_columns = apply_filters( 'user_search_columns', $search_columns, $search, $this ); $this->query_where .= $this->get_search_sql( $search, $search_columns, $wild ); } if ( ! empty( $include ) ) { // Sanitized earlier. $ids = implode( ',', $include ); $this->query_where .= " AND $wpdb->users.ID IN ($ids)"; } elseif ( ! empty( $qv['exclude'] ) ) { $ids = implode( ',', wp_parse_id_list( $qv['exclude'] ) ); $this->query_where .= " AND $wpdb->users.ID NOT IN ($ids)"; } // Date queries are allowed for the user_registered field. if ( ! empty( $qv['date_query'] ) && is_array( $qv['date_query'] ) ) { $date_query = new WP_Date_Query( $qv['date_query'], 'user_registered' ); $this->query_where .= $date_query->get_sql(); } /** * Fires after the WP_User_Query has been parsed, and before * the query is executed. * * The passed WP_User_Query object contains SQL parts formed * from parsing the given query. * * @since 3.1.0 * * @param WP_User_Query $query Current instance of WP_User_Query (passed by reference). */ do_action_ref_array( 'pre_user_query', array( &$this ) ); } /** * Executes the query, with the current variables. * * @since 3.1.0 * * @global wpdb $wpdb WordPress database abstraction object. */ 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. * * Return a non-null value to bypass WordPress' default user queries. * * Filtering functions that require pagination information are encouraged to set * the `total_users` property of the WP_User_Query object, passed to the filter * by reference. If WP_User_Query does not perform a database query, it will not * have enough information to generate these values itself. * * @since 5.1.0 * * @param array|null $results Return an array of user data to short-circuit WP's user query * or null to allow WP to run its normal queries. * @param WP_User_Query $query The WP_User_Query instance (passed by reference). */ $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} "; $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'] ) ) { $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 ); $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 ); } } } if ( ! $this->results ) { return; } if ( is_array( $qv['fields'] ) && isset( $this->results[0]->ID ) ) { foreach ( $this->results as $result ) { $result->id = $result->ID; } } 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 ) { 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; } } /** * Retrieves query variable. * * @since 3.5.0 * * @param string $query_var Query variable key. * @return mixed */ public function get( $query_var ) { if ( isset( $this->query_vars[ $query_var ] ) ) { return $this->query_vars[ $query_var ]; } return null; } /** * Sets query variable. * * @since 3.5.0 * * @param string $query_var Query variable key. * @param mixed $value Query variable value. */ public function set( $query_var, $value ) { $this->query_vars[ $query_var ] = $value; } /** * Used internally to generate an SQL string for searching across multiple columns. * * @since 3.1.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $search Search string. * @param string[] $columns Array of columns to search. * @param bool $wild Whether to allow wildcard searches. Default is false for Network Admin, true for single site. * Single site allows leading and trailing wildcards, Network Admin only trailing. * @return string */ protected function get_search_sql( $search, $columns, $wild = false ) { global $wpdb; $searches = array(); $leading_wild = ( 'leading' === $wild || 'both' === $wild ) ? '%' : ''; $trailing_wild = ( 'trailing' === $wild || 'both' === $wild ) ? '%' : ''; $like = $leading_wild . $wpdb->esc_like( $search ) . $trailing_wild; foreach ( $columns as $column ) { if ( 'ID' === $column ) { $searches[] = $wpdb->prepare( "$column = %s", $search ); } else { $searches[] = $wpdb->prepare( "$column LIKE %s", $like ); } } return ' AND (' . implode( ' OR ', $searches ) . ')'; } /** * Returns the list of users. * * @since 3.1.0 * * @return array Array of results. */ public function get_results() { return $this->results; } /** * Returns the total number of users for the current query. * * @since 3.1.0 * * @return int Number of total users. */ public function get_total() { return $this->total_users; } /** * Parses and sanitizes 'orderby' keys passed to the user query. * * @since 4.2.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $orderby Alias for the field to order by. * @return string Value to used in the ORDER clause, if `$orderby` is valid. */ protected function parse_orderby( $orderby ) { global $wpdb; $meta_query_clauses = $this->meta_query->get_clauses(); $_orderby = ''; if ( in_array( $orderby, array( 'login', 'nicename', 'email', 'url', 'registered' ), true ) ) { $_orderby = 'user_' . $orderby; } elseif ( in_array( $orderby, array( 'user_login', 'user_nicename', 'user_email', 'user_url', 'user_registered' ), true ) ) { $_orderby = $orderby; } elseif ( 'name' === $orderby || 'display_name' === $orderby ) { $_orderby = 'display_name'; } elseif ( 'post_count' === $orderby ) { // @todo Avoid the JOIN. $where = get_posts_by_author_sql( 'post' ); $this->query_from .= " LEFT OUTER JOIN ( SELECT post_author, COUNT(*) as post_count FROM $wpdb->posts $where GROUP BY 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 ) { $_orderby = "$wpdb->usermeta.meta_value"; } elseif ( 'meta_value_num' === $orderby ) { $_orderby = "$wpdb->usermeta.meta_value+0"; } elseif ( 'include' === $orderby && ! empty( $this->query_vars['include'] ) ) { $include = wp_parse_id_list( $this->query_vars['include'] ); $include_sql = implode( ',', $include ); $_orderby = "FIELD( $wpdb->users.ID, $include_sql )"; } elseif ( 'nicename__in' === $orderby ) { $sanitized_nicename__in = array_map( 'esc_sql', $this->query_vars['nicename__in'] ); $nicename__in = implode( "','", $sanitized_nicename__in ); $_orderby = "FIELD( user_nicename, '$nicename__in' )"; } elseif ( 'login__in' === $orderby ) { $sanitized_login__in = array_map( 'esc_sql', $this->query_vars['login__in'] ); $login__in = implode( "','", $sanitized_login__in ); $_orderby = "FIELD( user_login, '$login__in' )"; } elseif ( isset( $meta_query_clauses[ $orderby ] ) ) { $meta_clause = $meta_query_clauses[ $orderby ]; $_orderby = sprintf( 'CAST(%s.meta_value AS %s)', esc_sql( $meta_clause['alias'] ), esc_sql( $meta_clause['cast'] ) ); } return $_orderby; } /** * 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 * * @param string $order The 'order' query variable. * @return string The sanitized 'order' query variable. */ protected function parse_order( $order ) { if ( ! is_string( $order ) || empty( $order ) ) { return 'DESC'; } if ( 'ASC' === strtoupper( $order ) ) { return 'ASC'; } else { return 'DESC'; } } /** * Makes private properties readable for backward compatibility. * * @since 4.0.0 * * @param string $name Property to get. * @return mixed Property. */ public function __get( $name ) { if ( in_array( $name, $this->compat_fields, true ) ) { return $this->$name; } } /** * Makes private properties settable for backward compatibility. * * @since 4.0.0 * * @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; } } /** * Makes private properties checkable for backward compatibility. * * @since 4.0.0 * * @param string $name Property to check if set. * @return bool Whether the property is set. */ public function __isset( $name ) { if ( in_array( $name, $this->compat_fields, true ) ) { return isset( $this->$name ); } } /** * Makes private properties un-settable for backward compatibility. * * @since 4.0.0 * * @param string $name Property to unset. */ public function __unset( $name ) { if ( in_array( $name, $this->compat_fields, true ) ) { unset( $this->$name ); } } /** * Makes private/protected methods readable for backward compatibility. * * @since 4.0.0 * * @param string $name Method to call. * @param array $arguments Arguments to pass when calling. * @return mixed Return value of the callback, false otherwise. */ public function __call( $name, $arguments ) { if ( 'get_search_sql' === $name ) { return $this->get_search_sql( ...$arguments ); } return false; } } e data value. * @return bool True on success, false on failure. */ public function add_data( $handle, $key, $value ) { if ( ! isset( $this->registered[ $handle ] ) ) { return false; } return $this->registered[ $handle ]->add_data( $key, $value ); } /** * Get extra item data. * * Gets data associated with a registered item. * * @since 3.3.0 * * @param string $handle Name of the item. Should be unique. * @param string $key The data key. * @return mixed Extra item data (string), false otherwise. */ public function get_data( $handle, $key ) { if ( ! isset( $this->registered[ $handle ] ) ) { return false; } if ( ! isset( $this->registered[ $handle ]->extra[ $key ] ) ) { return false; } return $this->registered[ $handle ]->extra[ $key ]; } /** * Un-register an item or items. * * @since 2.1.0 * @since 2.6.0 Moved from `WP_Scripts`. * * @param string|string[] $handles Item handle (string) or item handles (array of strings). */ public function remove( $handles ) { foreach ( (array) $handles as $handle ) { unset( $this->registered[ $handle ] ); } } /** * Queue an item or items. * * Decodes handles and arguments, then queues handles and stores * arguments in the class property $args. For example in extending * classes, $args is appended to the item url as a query string. * Note $args is NOT the $args property of items in the $registered array. * * @since 2.1.0 * @since 2.6.0 Moved from `WP_Scripts`. * * @param string|string[] $handles Item handle (string) or item handles (array of strings). */ public function enqueue( $handles ) { foreach ( (array) $handles as $handle ) { $handle = explode( '?', $handle ); if ( ! in_array( $handle[0], $this->queue, true ) && isset( $this->registered[ $handle[0] ] ) ) { $this->queue[] = $handle[0]; // Reset all dependencies so they must be recalculated in recurse_deps(). $this->all_queued_deps = null; if ( isset( $handle[1] ) ) { $this->args[ $handle[0] ] = $handle[1]; } } elseif ( ! isset( $this->registered[ $handle[0] ] ) ) { $this->queued_before_register[ $handle[0] ] = null; // $args if ( isset( $handle[1] ) ) { $this->queued_before_register[ $handle[0] ] = $handle[1]; } } } } /** * Dequeue an item or items. * * Decodes handles and arguments, then dequeues handles * and removes arguments from the class property $args. * * @since 2.1.0 * @since 2.6.0 Moved from `WP_Scripts`. * * @param string|string[] $handles Item handle (string) or item handles (array of strings). */ public function dequeue( $handles ) { foreach ( (array) $handles as $handle ) { $handle = explode( '?', $handle ); $key = array_search( $handle[0], $this->queue, true ); if ( false !== $key ) { // Reset all dependencies so they must be recalculated in recurse_deps(). $this->all_queued_deps = null; unset( $this->queue[ $key ] ); unset( $this->args[ $handle[0] ] ); } elseif ( array_key_exists( $handle[0], $this->queued_before_register ) ) { unset( $this->queued_before_register[ $handle[0] ] ); } } } /** * Recursively search the passed dependency tree for a handle. * * @since 4.0.0 * * @param string[] $queue An array of queued _WP_Dependency handles. * @param string $handle Name of the item. Should be unique. * @return bool Whether the handle is found after recursively searching the dependency tree. */ protected function recurse_deps( $queue, $handle ) { if ( isset( $this->all_queued_deps ) ) { return isset( $this->all_queued_deps[ $handle ] ); } $all_deps = array_fill_keys( $queue, true ); $queues = array(); $done = array(); while ( $queue ) { foreach ( $queue as $queued ) { if ( ! isset( $done[ $queued ] ) && isset( $this->registered[ $queued ] ) ) { $deps = $this->registered[ $queued ]->deps; if ( $deps ) { $all_deps += array_fill_keys( $deps, true ); array_push( $queues, $deps ); } $done[ $queued ] = true; } } $queue = array_pop( $queues ); } $this->all_queued_deps = $all_deps; return isset( $this->all_queued_deps[ $handle ] ); } /** * Query the list for an item. * * @since 2.1.0 * @since 2.6.0 Moved from `WP_Scripts`. * * @param string $handle Name of the item. Should be unique. * @param string $status Optional. Status of the item to query. Default 'registered'. * @return bool|_WP_Dependency Found, or object Item data. */ public function query( $handle, $status = 'registered' ) { switch ( $status ) { case 'registered': case 'scripts': // Back compat. if ( isset( $this->registered[ $handle ] ) ) { return $this->registered[ $handle ]; } return false; case 'enqueued': case 'queue': // Back compat. if ( in_array( $handle, $this->queue, true ) ) { return true; } return $this->recurse_deps( $this->queue, $handle ); case 'to_do': case 'to_print': // Back compat. return in_array( $handle, $this->to_do, true ); case 'done': case 'printed': // Back compat. return in_array( $handle, $this->done, true ); } return false; } /** * Set item group, unless already in a lower group. * * @since 2.8.0 * * @param string $handle Name of the item. Should be unique. * @param bool $recursion Internal flag that calling function was called recursively. * @param int|false $group Group level: level (int), no group (false). * @return bool Not already in the group or a lower group. */ public function set_group( $handle, $recursion, $group ) { $group = (int) $group; if ( isset( $this->groups[ $handle ] ) && $this->groups[ $handle ] <= $group ) { return false; } $this->groups[ $handle ] = $group; return true; } }