e). */ $clauses = (array) apply_filters_ref_array( 'posts_clauses', array( compact( $pieces ), &$this ) ); $where = $clauses['where'] ?? ''; $groupby = $clauses['groupby'] ?? ''; $join = $clauses['join'] ?? ''; $orderby = $clauses['orderby'] ?? ''; $distinct = $clauses['distinct'] ?? ''; $fields = $clauses['fields'] ?? ''; $limits = $clauses['limits'] ?? ''; } /** * Fires to announce the query's current selection parameters. * * For use by caching plugins. * * @since 2.3.0 * * @param string $selection The assembled selection query. */ do_action( 'posts_selection', $where . $groupby . $orderby . $limits . $join ); /* * Filters again for the benefit of caching plugins. * Regular plugins should use the hooks above. */ if ( ! $query_vars['suppress_filters'] ) { /** * Filters the WHERE clause of the query. * * For use by caching plugins. * * @since 2.5.0 * * @param string $where The WHERE clause of the query. * @param WP_Query $query The WP_Query instance (passed by reference). */ $where = apply_filters_ref_array( 'posts_where_request', array( $where, &$this ) ); /** * Filters the GROUP BY clause of the query. * * For use by caching plugins. * * @since 2.5.0 * * @param string $groupby The GROUP BY clause of the query. * @param WP_Query $query The WP_Query instance (passed by reference). */ $groupby = apply_filters_ref_array( 'posts_groupby_request', array( $groupby, &$this ) ); /** * Filters the JOIN clause of the query. * * For use by caching plugins. * * @since 2.5.0 * * @param string $join The JOIN clause of the query. * @param WP_Query $query The WP_Query instance (passed by reference). */ $join = apply_filters_ref_array( 'posts_join_request', array( $join, &$this ) ); /** * Filters the ORDER BY clause of the query. * * For use by caching plugins. * * @since 2.5.0 * * @param string $orderby The ORDER BY clause of the query. * @param WP_Query $query The WP_Query instance (passed by reference). */ $orderby = apply_filters_ref_array( 'posts_orderby_request', array( $orderby, &$this ) ); /** * Filters the DISTINCT clause of the query. * * For use by caching plugins. * * @since 2.5.0 * * @param string $distinct The DISTINCT clause of the query. * @param WP_Query $query The WP_Query instance (passed by reference). */ $distinct = apply_filters_ref_array( 'posts_distinct_request', array( $distinct, &$this ) ); /** * Filters the SELECT clause of the query. * * For use by caching plugins. * * @since 2.5.0 * * @param string $fields The SELECT clause of the query. * @param WP_Query $query The WP_Query instance (passed by reference). */ $fields = apply_filters_ref_array( 'posts_fields_request', array( $fields, &$this ) ); /** * Filters the LIMIT clause of the query. * * For use by caching plugins. * * @since 2.5.0 * * @param string $limits The LIMIT clause of the query. * @param WP_Query $query The WP_Query instance (passed by reference). */ $limits = apply_filters_ref_array( 'post_limits_request', array( $limits, &$this ) ); /** * Filters all query clauses at once, for convenience. * * For use by caching plugins. * * Covers the WHERE, GROUP BY, JOIN, ORDER BY, DISTINCT, * fields (SELECT), and LIMIT clauses. * * @since 3.1.0 * * @param string[] $clauses { * Associative array of the clauses for the query. * * @type string $where The WHERE clause of the query. * @type string $groupby The GROUP BY clause of the query. * @type string $join The JOIN clause of the query. * @type string $orderby The ORDER BY clause of the query. * @type string $distinct The DISTINCT clause of the query. * @type string $fields The SELECT clause of the query. * @type string $limits The LIMIT clause of the query. * } * @param WP_Query $query The WP_Query instance (passed by reference). */ $clauses = (array) apply_filters_ref_array( 'posts_clauses_request', array( compact( $pieces ), &$this ) ); $where = $clauses['where'] ?? ''; $groupby = $clauses['groupby'] ?? ''; $join = $clauses['join'] ?? ''; $orderby = $clauses['orderby'] ?? ''; $distinct = $clauses['distinct'] ?? ''; $fields = $clauses['fields'] ?? ''; $limits = $clauses['limits'] ?? ''; } if ( ! empty( $groupby ) ) { $groupby = 'GROUP BY ' . $groupby; } if ( ! empty( $orderby ) ) { $orderby = 'ORDER BY ' . $orderby; } $found_rows = ''; if ( ! $query_vars['no_found_rows'] && ! empty( $limits ) ) { $found_rows = 'SQL_CALC_FOUND_ROWS'; } /* * Beginning of the string is on a new line to prevent leading whitespace. * * The additional indentation of subsequent lines is to ensure the SQL * queries are identical to those generated when splitting queries. This * improves caching of the query by ensuring the same cache key is * generated for the same database queries functionally. * * See https://core.trac.wordpress.org/ticket/56841. * See https://github.com/WordPress/wordpress-develop/pull/6393#issuecomment-2088217429 */ $old_request = "SELECT $found_rows $distinct $fields FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby $orderby $limits"; $this->request = $old_request; if ( ! $query_vars['suppress_filters'] ) { /** * Filters the completed SQL query before sending. * * @since 2.0.0 * * @param string $request The complete SQL query. * @param WP_Query $query The WP_Query instance (passed by reference). */ $this->request = apply_filters_ref_array( 'posts_request', array( $this->request, &$this ) ); } /** * Filters the posts array before the query takes place. * * Return a non-null value to bypass WordPress' default post queries. * * Filtering functions that require pagination information are encouraged to set * the `found_posts` and `max_num_pages` properties of the WP_Query object, * passed to the filter by reference. If WP_Query does not perform a database * query, it will not have enough information to generate these values itself. * * @since 4.6.0 * * @param WP_Post[]|int[]|null $posts Return an array of post data to short-circuit WP's query, * or null to allow WP to run its normal queries. * @param WP_Query $query The WP_Query instance (passed by reference). */ $this->posts = apply_filters_ref_array( 'posts_pre_query', array( null, &$this ) ); /* * Ensure the ID database query is able to be cached. * * Random queries are expected to have unpredictable results and * cannot be cached. Note the space before `RAND` in the string * search, that to ensure against a collision with another * function. * * If `$fields` has been modified by the `posts_fields`, * `posts_fields_request`, `post_clauses` or `posts_clauses_request` * filters, then caching is disabled to prevent caching collisions. */ $id_query_is_cacheable = ! str_contains( strtoupper( $orderby ), ' RAND(' ); $cacheable_field_values = array( "{$wpdb->posts}.*", "{$wpdb->posts}.ID, {$wpdb->posts}.post_parent", "{$wpdb->posts}.ID", ); if ( ! in_array( $fields, $cacheable_field_values, true ) ) { $id_query_is_cacheable = false; } $last_changed = (array) wp_cache_get_last_changed( 'posts' ); if ( ! empty( $this->tax_query->queries ) ) { $last_changed[] = wp_cache_get_last_changed( 'terms' ); } if ( $query_vars['cache_results'] && $id_query_is_cacheable ) { $new_request = str_replace( $fields, "{$wpdb->posts}.*", $this->request ); $cache_key = $this->generate_cache_key( $query_vars, $new_request ); $cache_found = false; if ( null === $this->posts ) { $cached_results = wp_cache_get_salted( $cache_key, 'post-queries', $last_changed ); if ( $cached_results ) { $cache_found = true; /** @var int[] */ $post_ids = array_map( 'intval', $cached_results['posts'] ); $this->post_count = count( $post_ids ); $this->found_posts = $cached_results['found_posts']; $this->max_num_pages = $cached_results['max_num_pages']; if ( 'ids' === $query_vars['fields'] ) { $this->posts = $post_ids; return $this->posts; } elseif ( 'id=>parent' === $query_vars['fields'] ) { _prime_post_parent_id_caches( $post_ids ); $post_parent_cache_keys = array(); foreach ( $post_ids as $post_id ) { $post_parent_cache_keys[] = 'post_parent:' . (string) $post_id; } /** @var int[] */ $post_parents = wp_cache_get_multiple( $post_parent_cache_keys, 'posts' ); foreach ( $post_parents as $cache_key => $post_parent ) { $obj = new stdClass(); $obj->ID = (int) str_replace( 'post_parent:', '', $cache_key ); $obj->post_parent = (int) $post_parent; $this->posts[] = $obj; } return $post_parents; } else { _prime_post_caches( $post_ids, $query_vars['update_post_term_cache'], $query_vars['update_post_meta_cache'] ); /** @var WP_Post[] */ $this->posts = array_map( 'get_post', $post_ids ); } } } } if ( 'ids' === $query_vars['fields'] ) { if ( null === $this->posts ) { $this->posts = $wpdb->get_col( $this->request ); } /** @var int[] */ $this->posts = array_map( 'intval', $this->posts ); $this->post_count = count( $this->posts ); $this->set_found_posts( $query_vars, $limits ); if ( $query_vars['cache_results'] && $id_query_is_cacheable ) { $cache_value = array( 'posts' => $this->posts, 'found_posts' => $this->found_posts, 'max_num_pages' => $this->max_num_pages, ); wp_cache_set_salted( $cache_key, $cache_value, 'post-queries', $last_changed ); } return $this->posts; } if ( 'id=>parent' === $query_vars['fields'] ) { if ( null === $this->posts ) { $this->posts = $wpdb->get_results( $this->request ); } $this->post_count = count( $this->posts ); $this->set_found_posts( $query_vars, $limits ); /** @var int[] */ $post_parents = array(); $post_ids = array(); $post_parents_cache = array(); foreach ( $this->posts as $key => $post ) { $this->posts[ $key ]->ID = (int) $post->ID; $this->posts[ $key ]->post_parent = (int) $post->post_parent; $post_parents[ (int) $post->ID ] = (int) $post->post_parent; $post_ids[] = (int) $post->ID; $post_parents_cache[ 'post_parent:' . (string) $post->ID ] = (int) $post->post_parent; } // Prime post parent caches, so that on second run, there is not another database query. wp_cache_add_multiple( $post_parents_cache, 'posts' ); if ( $query_vars['cache_results'] && $id_query_is_cacheable ) { $cache_value = array( 'posts' => $post_ids, 'found_posts' => $this->found_posts, 'max_num_pages' => $this->max_num_pages, ); wp_cache_set_salted( $cache_key, $cache_value, 'post-queries', $last_changed ); } return $post_parents; } $is_unfiltered_query = $old_request === $this->request && "{$wpdb->posts}.*" === $fields; if ( null === $this->posts ) { $split_the_query = ( $is_unfiltered_query && ( wp_using_ext_object_cache() || ( ! empty( $limits ) && $query_vars['posts_per_page'] < 500 ) ) ); /** * Filters whether to split the query. * * Splitting the query will cause it to fetch just the IDs of the found posts * (and then individually fetch each post by ID), rather than fetching every * complete row at once. One massive result vs. many small results. * * @since 3.4.0 * @since 6.6.0 Added the `$old_request` and `$clauses` parameters. * * @param bool $split_the_query Whether or not to split the query. * @param WP_Query $query The WP_Query instance. * @param string $old_request The complete SQL query before filtering. * @param string[] $clauses { * Associative array of the clauses for the query. * * @type string $where The WHERE clause of the query. * @type string $groupby The GROUP BY clause of the query. * @type string $join The JOIN clause of the query. * @type string $orderby The ORDER BY clause of the query. * @type string $distinct The DISTINCT clause of the query. * @type string $fields The SELECT clause of the query. * @type string $limits The LIMIT clause of the query. * } */ $split_the_query = apply_filters( 'split_the_query', $split_the_query, $this, $old_request, compact( $pieces ) ); if ( $split_the_query ) { // First get the IDs and then fill in the objects. // Beginning of the string is on a new line to prevent leading whitespace. See https://core.trac.wordpress.org/ticket/56841. $this->request = "SELECT $found_rows $distinct {$wpdb->posts}.ID FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby $orderby $limits"; /** * Filters the Post IDs SQL request before sending. * * @since 3.4.0 * * @param string $request The post ID request. * @param WP_Query $query The WP_Query instance. */ $this->request = apply_filters( 'posts_request_ids', $this->request, $this ); $post_ids = $wpdb->get_col( $this->request ); if ( $post_ids ) { $this->posts = $post_ids; $this->set_found_posts( $query_vars, $limits ); _prime_post_caches( $post_ids, $query_vars['update_post_term_cache'], $query_vars['update_post_meta_cache'] ); } else { $this->posts = array(); } } else { $this->posts = $wpdb->get_results( $this->request ); $this->set_found_posts( $query_vars, $limits ); } } // Convert to WP_Post objects. if ( $this->posts ) { /** @var WP_Post[] */ $this->posts = array_map( 'get_post', $this->posts ); } $unfiltered_posts = $this->posts; if ( $query_vars['cache_results'] && $id_query_is_cacheable && ! $cache_found ) { $post_ids = wp_list_pluck( $this->posts, 'ID' ); $cache_value = array( 'posts' => $post_ids, 'found_posts' => $this->found_posts, 'max_num_pages' => $this->max_num_pages, ); wp_cache_set_salted( $cache_key, $cache_value, 'post-queries', $last_changed ); } if ( ! $query_vars['suppress_filters'] ) { /** * Filters the raw post results array, prior to status checks. * * @since 2.3.0 * * @param WP_Post[] $posts Array of post objects. * @param WP_Query $query The WP_Query instance (passed by reference). */ $this->posts = apply_filters_ref_array( 'posts_results', array( $this->posts, &$this ) ); } // Check post status to determine if post should be displayed. if ( ! empty( $this->posts ) && ( $this->is_single || $this->is_page ) ) { $status = get_post_status( $this->posts[0] ); if ( 'attachment' === $this->posts[0]->post_type && 0 === (int) $this->posts[0]->post_parent ) { $this->is_page = false; $this->is_single = true; $this->is_attachment = true; } // If the post_status was specifically requested, let it pass through. if ( ! in_array( $status, $q_status, true ) ) { $post_status_obj = get_post_status_object( $status ); if ( $post_status_obj && ! $post_status_obj->public ) { if ( ! is_user_logged_in() ) { // User must be logged in to view unpublished posts. $this->posts = array(); } else { if ( $post_status_obj->protected ) { // User must have edit permissions on the draft to preview. if ( ! current_user_can( $edit_cap, $this->posts[0]->ID ) ) { $this->posts = array(); } else { $this->is_preview = true; if ( 'future' !== $status ) { $this->posts[0]->post_date = current_time( 'mysql' ); } } } elseif ( $post_status_obj->private ) { if ( ! current_user_can( $read_cap, $this->posts[0]->ID ) ) { $this->posts = array(); } } else { $this->posts = array(); } } } elseif ( ! $post_status_obj ) { // Post status is not registered, assume it's not public. if ( ! current_user_can( $edit_cap, $this->posts[0]->ID ) ) { $this->posts = array(); } } } if ( $this->is_preview && $this->posts && current_user_can( $edit_cap, $this->posts[0]->ID ) ) { /** * Filters the single post for preview mode. * * @since 2.7.0 * * @param WP_Post $post_preview The Post object. * @param WP_Query $query The WP_Query instance (passed by reference). */ $this->posts[0] = get_post( apply_filters_ref_array( 'the_preview', array( $this->posts[0], &$this ) ) ); } } if ( ! empty( $this->posts ) && $this->is_comment_feed && $this->is_singular ) { /** This filter is documented in wp-includes/class-wp-query.php */ $cjoin = apply_filters_ref_array( 'comment_feed_join', array( '', &$this ) ); /** This filter is documented in wp-includes/class-wp-query.php */ $cwhere = apply_filters_ref_array( 'comment_feed_where', array( "WHERE comment_post_ID = '{$this->posts[0]->ID}' AND comment_approved = '1' AND {$wpdb->comments}.comment_type != 'note'", &$this ) ); /** This filter is documented in wp-includes/class-wp-query.php */ $cgroupby = apply_filters_ref_array( 'comment_feed_groupby', array( '', &$this ) ); $cgroupby = ( ! empty( $cgroupby ) ) ? 'GROUP BY ' . $cgroupby : ''; /** This filter is documented in wp-includes/class-wp-query.php */ $corderby = apply_filters_ref_array( 'comment_feed_orderby', array( 'comment_date_gmt DESC', &$this ) ); $corderby = ( ! empty( $corderby ) ) ? 'ORDER BY ' . $corderby : ''; /** This filter is documented in wp-includes/class-wp-query.php */ $climits = apply_filters_ref_array( 'comment_feed_limits', array( 'LIMIT ' . get_option( 'posts_per_rss' ), &$this ) ); $comments_request = "SELECT {$wpdb->comments}.comment_ID FROM {$wpdb->comments} $cjoin $cwhere $cgroupby $corderby $climits"; $comment_key = md5( $comments_request ); $comment_last_changed = wp_cache_get_last_changed( 'comment' ); $comment_cache_key = "comment_feed:$comment_key"; $comment_ids = wp_cache_get_salted( $comment_cache_key, 'comment-queries', $comment_last_changed ); if ( false === $comment_ids ) { $comment_ids = $wpdb->get_col( $comments_request ); wp_cache_set_salted( $comment_cache_key, $comment_ids, 'comment-queries', $comment_last_changed ); } _prime_comment_caches( $comment_ids ); // Convert to WP_Comment. /** @var WP_Comment[] */ $this->comments = array_map( 'get_comment', $comment_ids ); $this->comment_count = count( $this->comments ); } // Put sticky posts at the top of the posts array. $sticky_posts = get_option( 'sticky_posts' ); if ( $this->is_home && $page <= 1 && is_array( $sticky_posts ) && ! empty( $sticky_posts ) && ! $query_vars['ignore_sticky_posts'] ) { $num_posts = count( $this->posts ); $sticky_offset = 0; // Loop over posts and relocate stickies to the front. for ( $i = 0; $i < $num_posts; $i++ ) { if ( in_array( $this->posts[ $i ]->ID, $sticky_posts, true ) ) { $sticky_post = $this->posts[ $i ]; // Remove sticky from current position. array_splice( $this->posts, $i, 1 ); // Move to front, after other stickies. array_splice( $this->posts, $sticky_offset, 0, array( $sticky_post ) ); // Increment the sticky offset. The next sticky will be placed at this offset. ++$sticky_offset; // Remove post from sticky posts array. $offset = array_search( $sticky_post->ID, $sticky_posts, true ); unset( $sticky_posts[ $offset ] ); } } // If any posts have been excluded specifically, Ignore those that are sticky. if ( ! empty( $sticky_posts ) && ! empty( $query_vars['post__not_in'] ) ) { $sticky_posts = array_diff( $sticky_posts, $query_vars['post__not_in'] ); } // Fetch sticky posts that weren't in the query results. if ( ! empty( $sticky_posts ) ) { $stickies = get_posts( array( 'post__in' => $sticky_posts, 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => count( $sticky_posts ), 'suppress_filters' => $query_vars['suppress_filters'], 'cache_results' => $query_vars['cache_results'], 'update_post_meta_cache' => $query_vars['update_post_meta_cache'], 'update_post_term_cache' => $query_vars['update_post_term_cache'], 'lazy_load_term_meta' => $query_vars['lazy_load_term_meta'], ) ); foreach ( $stickies as $sticky_post ) { array_splice( $this->posts, $sticky_offset, 0, array( $sticky_post ) ); ++$sticky_offset; } } } if ( ! $query_vars['suppress_filters'] ) { /** * Filters the array of retrieved posts after they've been fetched and * internally processed. * * @since 1.5.0 * * @param WP_Post[] $posts Array of post objects. * @param WP_Query $query The WP_Query instance (passed by reference). */ $this->posts = apply_filters_ref_array( 'the_posts', array( $this->posts, &$this ) ); } /* * Ensure that any posts added/modified via one of the filters above are * of the type WP_Post and are filtered. */ if ( $this->posts ) { $this->post_count = count( $this->posts ); /** @var WP_Post[] */ $this->posts = array_map( 'get_post', $this->posts ); if ( $query_vars['cache_results'] ) { if ( $is_unfiltered_query && $unfiltered_posts === $this->posts ) { update_post_caches( $this->posts, $post_type, $query_vars['update_post_term_cache'], $query_vars['update_post_meta_cache'] ); } else { $post_ids = wp_list_pluck( $this->posts, 'ID' ); _prime_post_caches( $post_ids, $query_vars['update_post_term_cache'], $query_vars['update_post_meta_cache'] ); } } /** @var WP_Post */ $this->post = reset( $this->posts ); } else { $this->post_count = 0; $this->posts = array(); } if ( ! empty( $this->posts ) && $query_vars['update_menu_item_cache'] ) { update_menu_item_cache( $this->posts ); } if ( $query_vars['lazy_load_term_meta'] ) { wp_queue_posts_for_term_meta_lazyload( $this->posts ); } return $this->posts; } /** * Sets up the amount of found posts and the number of pages (if limit clause was used) * for the current query. * * @since 3.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $query_vars Query variables. * @param string $limits LIMIT clauses of the query. */ private function set_found_posts( $query_vars, $limits ) { global $wpdb; /* * Bail if posts is an empty array. Continue if posts is an empty string, * null, or false to accommodate caching plugins that fill posts later. */ if ( $query_vars['no_found_rows'] || ( is_array( $this->posts ) && ! $this->posts ) ) { return; } if ( ! empty( $limits ) ) { /** * Filters the query to run for retrieving the found posts. * * @since 2.1.0 * * @param string $found_posts_query The query to run to find the found posts. * @param WP_Query $query The WP_Query instance (passed by reference). */ $found_posts_query = apply_filters_ref_array( 'found_posts_query', array( 'SELECT FOUND_ROWS()', &$this ) ); $this->found_posts = (int) $wpdb->get_var( $found_posts_query ); } else { if ( is_array( $this->posts ) ) { $this->found_posts = count( $this->posts ); } else { if ( null === $this->posts ) { $this->found_posts = 0; } else { $this->found_posts = 1; } } } /** * Filters the number of found posts for the query. * * @since 2.1.0 * * @param int $found_posts The number of posts found. * @param WP_Query $query The WP_Query instance (passed by reference). */ $this->found_posts = (int) apply_filters_ref_array( 'found_posts', array( $this->found_posts, &$this ) ); if ( ! empty( $limits ) ) { $this->max_num_pages = (int) ceil( $this->found_posts / $query_vars['posts_per_page'] ); } } /** * Sets up the next post and iterate current post index. * * @since 1.5.0 * * @return WP_Post Next post. */ public function next_post() { ++$this->current_post; /** @var WP_Post */ $this->post = $this->posts[ $this->current_post ]; return $this->post; } /** * Sets up the current post. * * Retrieves the next post, sets up the post, sets the 'in the loop' * property to true. * * @since 1.5.0 * * @global WP_Post $post Global post object. */ public function the_post() { global $post; if ( ! $this->in_the_loop ) { if ( 'all' === $this->query_vars['fields'] ) { // Full post objects queried. $post_objects = $this->posts; } else { if ( 'ids' === $this->query_vars['fields'] ) { // Post IDs queried. $post_ids = $this->posts; } else { // Only partial objects queried, need to prime the cache for the loop. $post_ids = array_reduce( $this->posts, function ( $carry, $post ) { if ( isset( $post->ID ) ) { $carry[] = $post->ID; } return $carry; }, array() ); } _prime_post_caches( $post_ids, $this->query_vars['update_post_term_cache'], $this->query_vars['update_post_meta_cache'] ); $post_objects = array_map( 'get_post', $post_ids ); } update_post_author_caches( $post_objects ); } $this->in_the_loop = true; $this->before_loop = false; if ( -1 === $this->current_post ) { // Loop has just started. /** * Fires once the loop is started. * * @since 2.0.0 * * @param WP_Query $query The WP_Query instance (passed by reference). */ do_action_ref_array( 'loop_start', array( &$this ) ); } $post = $this->next_post(); // Ensure a full post object is available. if ( 'all' !== $this->query_vars['fields'] ) { if ( 'ids' === $this->query_vars['fields'] ) { // Post IDs queried. $post = get_post( $post ); } elseif ( isset( $post->ID ) ) { /* * Partial object queried. * * The post object was queried with a partial set of * fields, populate the entire object for the loop. */ $post = get_post( $post->ID ); } } // Set up the global post object for the loop. $this->setup_postdata( $post ); } /** * Determines whether there are more posts available in the loop. * * Calls the {@see 'loop_end'} action when the loop is complete. * * @since 1.5.0 * * @return bool True if posts are available, false if end of the loop. */ public function have_posts() { if ( $this->current_post + 1 < $this->post_count ) { return true; } elseif ( $this->current_post + 1 === $this->post_count && $this->post_count > 0 ) { /** * Fires once the loop has ended. * * @since 2.0.0 * * @param WP_Query $query The WP_Query instance (passed by reference). */ do_action_ref_array( 'loop_end', array( &$this ) ); // Do some cleaning up after the loop. $this->rewind_posts(); } elseif ( 0 === $this->post_count ) { $this->before_loop = false; /** * Fires if no results are found in a post query. * * @since 4.9.0 * * @param WP_Query $query The WP_Query instance. */ do_action( 'loop_no_results', $this ); } $this->in_the_loop = false; return false; } /** * Rewinds the posts and resets post index. * * @since 1.5.0 */ public function rewind_posts() { $this->current_post = -1; if ( $this->post_count > 0 ) { $this->post = $this->posts[0]; } } /** * Iterates current comment index and returns WP_Comment object. * * @since 2.2.0 * * @return WP_Comment Comment object. */ public function next_comment() { ++$this->current_comment; /** @var WP_Comment */ $this->comment = $this->comments[ $this->current_comment ]; return $this->comment; } /** * Sets up the current comment. * * @since 2.2.0 * * @global WP_Comment $comment Global comment object. */ public function the_comment() { global $comment; $comment = $this->next_comment(); if ( 0 === $this->current_comment ) { /** * Fires once the comment loop is started. * * @since 2.2.0 */ do_action( 'comment_loop_start' ); } } /** * Determines whether there are more comments available. * * Automatically rewinds comments when finished. * * @since 2.2.0 * * @return bool True if comments are available, false if no more comments. */ public function have_comments() { if ( $this->current_comment + 1 < $this->comment_count ) { return true; } elseif ( $this->current_comment + 1 === $this->comment_count ) { $this->rewind_comments(); } return false; } /** * Rewinds the comments, resets the comment index and comment to first. * * @since 2.2.0 */ public function rewind_comments() { $this->current_comment = -1; if ( $this->comment_count > 0 ) { $this->comment = $this->comments[0]; } } /** * Sets up the WordPress query by parsing query string. * * @since 1.5.0 * * @see WP_Query::parse_query() for all available arguments. * * @param string|array $query URL query string or array of query arguments. * @return WP_Post[]|int[] Array of post objects or post IDs. * * @phpstan-return ( * $query is array{ fields: 'ids', ... } ? int[] : WP_Post[] * ) */ public function query( $query ) { $this->init(); $this->query = wp_parse_args( $query ); $this->query_vars = $this->query; return $this->get_posts(); } /** * Retrieves the currently queried object. * * If queried object is not set, then the queried object will be set from * the category, tag, taxonomy, posts page, single post, page, or author * query variable. After it is set up, it will be returned. * * @since 1.5.0 * * @return WP_Term|WP_Post_Type|WP_Post|WP_User|null The queried object. */ public function get_queried_object() { if ( isset( $this->queried_object ) ) { return $this->queried_object; } $this->queried_object = null; $this->queried_object_id = null; if ( $this->is_category || $this->is_tag || $this->is_tax ) { if ( $this->is_category ) { $cat = $this->get( 'cat' ); $category_name = $this->get( 'category_name' ); if ( $cat ) { $term = get_term( $cat, 'category' ); } elseif ( $category_name ) { $term = get_term_by( 'slug', $category_name, 'category' ); } } elseif ( $this->is_tag ) { $tag_id = $this->get( 'tag_id' ); $tag = $this->get( 'tag' ); if ( $tag_id ) { $term = get_term( $tag_id, 'post_tag' ); } elseif ( $tag ) { $term = get_term_by( 'slug', $tag, 'post_tag' ); } } else { // For other tax queries, grab the first term from the first clause. if ( ! empty( $this->tax_query->queried_terms ) ) { $queried_taxonomies = array_keys( $this->tax_query->queried_terms ); $matched_taxonomy = reset( $queried_taxonomies ); $query = $this->tax_query->queried_terms[ $matched_taxonomy ]; if ( ! empty( $query['terms'] ) ) { if ( 'term_id' === $query['field'] ) { $term = get_term( reset( $query['terms'] ), $matched_taxonomy ); } else { $term = get_term_by( $query['field'], reset( $query['terms'] ), $matched_taxonomy ); } } } } if ( ! empty( $term ) && ! is_wp_error( $term ) ) { $this->queried_object = $term; $this->queried_object_id = (int) $term->term_id; if ( $this->is_category && 'category' === $this->queried_object->taxonomy ) { _make_cat_compat( $this->queried_object ); } } } elseif ( $this->is_post_type_archive ) { $post_type = $this->get( 'post_type' ); if ( is_array( $post_type ) ) { $post_type = reset( $post_type ); } $this->queried_object = get_post_type_object( $post_type ); } elseif ( $this->is_posts_page ) { $posts_page = get_post( get_option( 'page_for_posts' ) ); if ( $posts_page ) { $this->queried_object = $posts_page; $this->queried_object_id = (int) $posts_page->ID; } } elseif ( $this->is_singular && ! empty( $this->post ) ) { $this->queried_object = $this->post; $this->queried_object_id = (int) $this->post->ID; } elseif ( $this->is_author ) { $author = (int) $this->get( 'author' ); $author_name = $this->get( 'author_name' ); if ( $author ) { $this->queried_object_id = $author; } elseif ( $author_name ) { $user = get_user_by( 'slug', $author_name ); if ( $user ) { $this->queried_object_id = $user->ID; } } if ( $this->queried_object_id ) { $user = get_userdata( $this->queried_object_id ); if ( $user ) { $this->queried_object = $user; } } } return $this->queried_object; } /** * Retrieves the ID of the currently queried object. * * @since 1.5.0 * * @return int */ public function get_queried_object_id() { $this->get_queried_object(); return $this->queried_object_id ?? 0; } /** * Constructor. * * Sets up the WordPress query, if parameter is not empty. * * @since 1.5.0 * * @see WP_Query::parse_query() for all available arguments. * * @param string|array $query URL query string or array of vars. */ public function __construct( $query = '' ) { if ( ! empty( $query ) ) { $this->query( $query ); } } /** * 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 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 ); } return false; } /** * 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|false Return value of the callback, false otherwise. */ public function __call( $name, $arguments ) { if ( in_array( $name, $this->compat_methods, true ) ) { return $this->$name( ...$arguments ); } return false; } /** * Determines whether the query is for an existing archive page. * * Archive pages include category, tag, author, date, custom post type, * and custom taxonomy based archives. * * @since 3.1.0 * * @see WP_Query::is_category() * @see WP_Query::is_tag() * @see WP_Query::is_author() * @see WP_Query::is_date() * @see WP_Query::is_post_type_archive() * @see WP_Query::is_tax() * * @return bool Whether the query is for an existing archive page. */ public function is_archive() { return (bool) $this->is_archive; } /** * Determines whether the query is for an existing post type archive page. * * @since 3.1.0 * * @param string|string[] $post_types Optional. Post type or array of posts types * to check against. Default empty. * @return bool Whether the query is for an existing post type archive page. */ public function is_post_type_archive( $post_types = '' ) { if ( empty( $post_types ) || ! $this->is_post_type_archive ) { return (bool) $this->is_post_type_archive; } $post_type = $this->get( 'post_type' ); if ( is_array( $post_type ) ) { $post_type = reset( $post_type ); } $post_type_object = get_post_type_object( $post_type ); if ( ! $post_type_object ) { return false; } return in_array( $post_type_object->name, (array) $post_types, true ); } /** * Determines whether the query is for an existing attachment page. * * @since 3.1.0 * * @param int|string|int[]|string[] $attachment Optional. Attachment ID, title, slug, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing attachment page. */ public function is_attachment( $attachment = '' ) { if ( ! $this->is_attachment ) { return false; } if ( empty( $attachment ) ) { return true; } $attachment = array_map( 'strval', (array) $attachment ); $post_obj = $this->get_queried_object(); if ( ! $post_obj ) { return false; } if ( in_array( (string) $post_obj->ID, $attachment, true ) ) { return true; } elseif ( in_array( $post_obj->post_title, $attachment, true ) ) { return true; } elseif ( in_array( $post_obj->post_name, $attachment, true ) ) { return true; } return false; } /** * Determines whether the query is for an existing author archive page. * * If the $author parameter is specified, this function will additionally * check if the query is for one of the authors specified. * * @since 3.1.0 * * @param int|string|int[]|string[] $author Optional. User ID, nickname, nicename, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing author archive page. */ public function is_author( $author = '' ) { if ( ! $this->is_author ) { return false; } if ( empty( $author ) ) { return true; } $author_obj = $this->get_queried_object(); if ( ! $author_obj ) { return false; } $author = array_map( 'strval', (array) $author ); if ( in_array( (string) $author_obj->ID, $author, true ) ) { return true; } elseif ( in_array( $author_obj->nickname, $author, true ) ) { return true; } elseif ( in_array( $author_obj->user_nicename, $author, true ) ) { return true; } return false; } /** * Determines whether the query is for an existing category archive page. * * If the $category parameter is specified, this function will additionally * check if the query is for one of the categories specified. * * @since 3.1.0 * * @param int|string|int[]|string[] $category Optional. Category ID, name, slug, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing category archive page. */ public function is_category( $category = '' ) { if ( ! $this->is_category ) { return false; } if ( empty( $category ) ) { return true; } $cat_obj = $this->get_queried_object(); if ( ! $cat_obj ) { return false; } $category = array_map( 'strval', (array) $category ); if ( in_array( (string) $cat_obj->term_id, $category, true ) ) { return true; } elseif ( in_array( $cat_obj->name, $category, true ) ) { return true; } elseif ( in_array( $cat_obj->slug, $category, true ) ) { return true; } return false; } /** * Determines whether the query is for an existing tag archive page. * * If the $tag parameter is specified, this function will additionally * check if the query is for one of the tags specified. * * @since 3.1.0 * * @param int|string|int[]|string[] $tag Optional. Tag ID, name, slug, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing tag archive page. */ public function is_tag( $tag = '' ) { if ( ! $this->is_tag ) { return false; } if ( empty( $tag ) ) { return true; } $tag_obj = $this->get_queried_object(); if ( ! $tag_obj ) { return false; } $tag = array_map( 'strval', (array) $tag ); if ( in_array( (string) $tag_obj->term_id, $tag, true ) ) { return true; } elseif ( in_array( $tag_obj->name, $tag, true ) ) { return true; } elseif ( in_array( $tag_obj->slug, $tag, true ) ) { return true; } return false; } /** * Determines whether the query is for an existing custom taxonomy archive page. * * If the $taxonomy parameter is specified, this function will additionally * check if the query is for that specific $taxonomy. * * If the $term parameter is specified in addition to the $taxonomy parameter, * this function will additionally check if the query is for one of the terms * specified. * * @since 3.1.0 * * @global WP_Taxonomy[] $wp_taxonomies Registered taxonomies. * * @param string|string[] $taxonomy Optional. Taxonomy slug or slugs to check against. * Default empty. * @param int|string|int[]|string[] $term Optional. Term ID, name, slug, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing custom taxonomy archive page. * True for custom taxonomy archive pages, false for built-in taxonomies * (category and tag archives). */ public function is_tax( $taxonomy = '', $term = '' ) { global $wp_taxonomies; if ( ! $this->is_tax ) { return false; } if ( empty( $taxonomy ) ) { return true; } $queried_object = $this->get_queried_object(); $tax_array = array_intersect( array_keys( $wp_taxonomies ), (array) $taxonomy ); $term_array = (array) $term; // Check that the taxonomy matches. if ( ! ( isset( $queried_object->taxonomy ) && count( $tax_array ) && in_array( $queried_object->taxonomy, $tax_array, true ) ) ) { return false; } // Only a taxonomy provided. if ( empty( $term ) ) { return true; } return isset( $queried_object->term_id ) && count( array_intersect( array( $queried_object->term_id, $queried_object->name, $queried_object->slug ), $term_array ) ); } /** * Determines whether the current URL is within the comments popup window. * * @since 3.1.0 * @deprecated 4.5.0 * * @return false Always returns false. */ public function is_comments_popup() { _deprecated_function( __FUNCTION__, '4.5.0' ); return false; } /** * Determines whether the query is for an existing date archive. * * @since 3.1.0 * * @return bool Whether the query is for an existing date archive. */ public function is_date() { return (bool) $this->is_date; } /** * Determines whether the query is for an existing day archive. * * @since 3.1.0 * * @return bool Whether the query is for an existing day archive. */ public function is_day() { return (bool) $this->is_day; } /** * Determines whether the query is for a feed. * * @since 3.1.0 * * @param string|string[] $feeds Optional. Feed type or array of feed types * to check against. Default empty. * @return bool Whether the query is for a feed. */ public function is_feed( $feeds = '' ) { if ( empty( $feeds ) || ! $this->is_feed ) { return (bool) $this->is_feed; } $query_var = $this->get( 'feed' ); if ( 'feed' === $query_var ) { $query_var = get_default_feed(); } return in_array( $query_var, (array) $feeds, true ); } /** * Determines whether the query is for a comments feed. * * @since 3.1.0 * * @return bool Whether the query is for a comments feed. */ public function is_comment_feed() { return (bool) $this->is_comment_feed; } /** * Determines whether the query is for the front page of the site. * * This is for what is displayed at your site's main URL. * * Depends on the site's "Front page displays" Reading Settings 'show_on_front' and 'page_on_front'. * * If you set a static page for the front page of your site, this function will return * true when viewing that page. * * Otherwise the same as {@see WP_Query::is_home()}. * * @since 3.1.0 * * @return bool Whether the query is for the front page of the site. */ public function is_front_page() { // Most likely case. if ( 'posts' === get_option( 'show_on_front' ) && $this->is_home() ) { return true; } elseif ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) && $this->is_page( get_option( 'page_on_front' ) ) ) { return true; } else { return false; } } /** * Determines whether the query is for the blog homepage. * * This is the page which shows the time based blog content of your site. * * Depends on the site's "Front page displays" Reading Settings 'show_on_front' and 'page_for_posts'. * * If you set a static page for the front page of your site, this function will return * true only on the page you set as the "Posts page". * * @since 3.1.0 * * @see WP_Query::is_front_page() * * @return bool Whether the query is for the blog homepage. */ public function is_home() { return (bool) $this->is_home; } /** * Determines whether the query is for the Privacy Policy page. * * This is the page which shows the Privacy Policy content of your site. * * Depends on the site's "Change your Privacy Policy page" Privacy Settings 'wp_page_for_privacy_policy'. * * This function will return true only on the page you set as the "Privacy Policy page". * * @since 5.2.0 * * @return bool Whether the query is for the Privacy Policy page. */ public function is_privacy_policy() { if ( get_option( 'wp_page_for_privacy_policy' ) && $this->is_page( get_option( 'wp_page_for_privacy_policy' ) ) ) { return true; } else { return false; } } /** * Determines whether the query is for an existing month archive. * * @since 3.1.0 * * @return bool Whether the query is for an existing month archive. */ public function is_month() { return (bool) $this->is_month; } /** * Determines whether the query is for an existing single page. * * If the $page parameter is specified, this function will additionally * check if the query is for one of the pages specified. * * @since 3.1.0 * * @see WP_Query::is_single() * @see WP_Query::is_singular() * * @param int|string|int[]|string[] $page Optional. Page ID, title, slug, path, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing single page. */ public function is_page( $page = '' ) { if ( ! $this->is_page ) { return false; } if ( empty( $page ) ) { return true; } $page_obj = $this->get_queried_object(); if ( ! $page_obj ) { return false; } $page = array_map( 'strval', (array) $page ); if ( in_array( (string) $page_obj->ID, $page, true ) ) { return true; } elseif ( in_array( $page_obj->post_title, $page, true ) ) { return true; } elseif ( in_array( $page_obj->post_name, $page, true ) ) { return true; } else { foreach ( $page as $pagepath ) { if ( ! strpos( $pagepath, '/' ) ) { continue; } $pagepath_obj = get_page_by_path( $pagepath ); if ( $pagepath_obj && ( $pagepath_obj->ID === $page_obj->ID ) ) { return true; } } } return false; } /** * Determines whether the query is for a paged result and not for the first page. * * @since 3.1.0 * * @return bool Whether the query is for a paged result. */ public function is_paged() { return (bool) $this->is_paged; } /** * Determines whether the query is for a post or page preview. * * @since 3.1.0 * * @return bool Whether the query is for a post or page preview. */ public function is_preview() { return (bool) $this->is_preview; } /** * Determines whether the query is for the robots.txt file. * * @since 3.1.0 * * @return bool Whether the query is for the robots.txt file. */ public function is_robots() { return (bool) $this->is_robots; } /** * Determines whether the query is for the favicon.ico file. * * @since 5.4.0 * * @return bool Whether the query is for the favicon.ico file. */ public function is_favicon() { return (bool) $this->is_favicon; } /** * Determines whether the query is for a sitemap. * * @since 7.1.0 * * @return bool Whether the query is for a sitemap. */ public function is_sitemap(): bool { return $this->is_sitemap; } /** * Determines whether the query is for a search. * * @since 3.1.0 * * @return bool Whether the query is for a search. */ public function is_search() { return (bool) $this->is_search; } /** * Determines whether the query is for an existing single post. * * Works for any post type excluding pages. * * If the $post parameter is specified, this function will additionally * check if the query is for one of the Posts specified. * * @since 3.1.0 * * @see WP_Query::is_page() * @see WP_Query::is_singular() * * @param int|string|int[]|string[] $post Optional. Post ID, title, slug, path, or array of such * to check against. Default empty. * @return bool Whether the query is for an existing single post. */ public function is_single( $post = '' ) { if ( ! $this->is_single ) { return false; } if ( empty( $post ) ) { return true; } $post_obj = $this->get_queried_object(); if ( ! $post_obj ) { return false; } $post = array_map( 'strval', (array) $post ); if ( in_array( (string) $post_obj->ID, $post, true ) ) { return true; } elseif ( in_array( $post_obj->post_title, $post, true ) ) { return true; } elseif ( in_array( $post_obj->post_name, $post, true ) ) { return true; } else { foreach ( $post as $postpath ) { if ( ! strpos( $postpath, '/' ) ) { continue; } $postpath_obj = get_page_by_path( $postpath, OBJECT, $post_obj->post_type ); if ( $postpath_obj && ( $postpath_obj->ID === $post_obj->ID ) ) { return true; } } } return false; } /** * Determines whether the query is for an existing single post of any post type * (post, attachment, page, custom post types). * * If the $post_types parameter is specified, this function will additionally * check if the query is for one of the Posts Types specified. * * @since 3.1.0 * * @see WP_Query::is_page() * @see WP_Query::is_single() * * @param string|string[] $post_types Optional. Post type or array of post types * to check against. Default empty. * @return bool Whether the query is for an existing single post * or any of the given post types. */ public function is_singular( $post_types = '' ) { if ( empty( $post_types ) || ! $this->is_singular ) { return (bool) $this->is_singular; } $post_obj = $this->get_queried_object(); if ( ! $post_obj ) { return false; } return in_array( $post_obj->post_type, (array) $post_types, true ); } /** * Determines whether the query is for a specific time. * * @since 3.1.0 * * @return bool Whether the query is for a specific time. */ public function is_time() { return (bool) $this->is_time; } /** * Determines whether the query is for a trackback endpoint call. * * @since 3.1.0 * * @return bool Whether the query is for a trackback endpoint call. */ public function is_trackback() { return (bool) $this->is_trackback; } /** * Determines whether the query is for an existing year archive. * * @since 3.1.0 * * @return bool Whether the query is for an existing year archive. */ public function is_year() { return (bool) $this->is_year; } /** * Determines whether the query is a 404 (returns no results). * * @since 3.1.0 * * @return bool Whether the query is a 404 error. */ public function is_404() { return (bool) $this->is_404; } /** * Determines whether the query is for an embedded post. * * @since 4.4.0 * * @return bool Whether the query is for an embedded post. */ public function is_embed() { return (bool) $this->is_embed; } /** * Determines whether the query is the main query. * * @since 3.3.0 * * @global WP_Query $wp_the_query WordPress Query object. * * @return bool Whether the query is the main query. */ public function is_main_query() { global $wp_the_query; return $wp_the_query === $this; } /** * Sets up global post data. * * @since 4.1.0 * @since 4.4.0 Added the ability to pass a post ID to `$post`. * * @global int $id * @global WP_User $authordata * @global string $currentday * @global string $currentmonth * @global int $page * @global array $pages * @global int $multipage * @global int $more * @global int $numpages * * @param WP_Post|object|int $post WP_Post instance or Post ID/object. * @return bool True on success, false on failure. */ public function setup_postdata( $post ) { global $id, $authordata, $currentday, $currentmonth, $page, $pages, $multipage, $more, $numpages; if ( ! ( $post instanceof WP_Post ) ) { $post = get_post( $post ); } if ( ! $post ) { return false; } $elements = $this->generate_postdata( $post ); if ( false === $elements ) { return false; } $id = $elements['id']; $authordata = $elements['authordata']; $currentday = $elements['currentday']; $currentmonth = $elements['currentmonth']; $page = $elements['page']; $pages = $elements['pages']; $multipage = $elements['multipage']; $more = $elements['more']; $numpages = $elements['numpages']; /** * Fires once the post data has been set up. * * @since 2.8.0 * @since 4.1.0 Introduced `$query` parameter. * * @param WP_Post $post The Post object (passed by reference). * @param WP_Query $query The current Query object (passed by reference). */ do_action_ref_array( 'the_post', array( &$post, &$this ) ); return true; } /** * Generates post data. * * @since 5.2.0 * * @param WP_Post|object|int $post WP_Post instance or Post ID/object. * @return array|false Elements of post or false on failure. */ public function generate_postdata( $post ) { if ( ! ( $post instanceof WP_Post ) ) { $post = get_post( $post ); } if ( ! $post ) { return false; } $id = (int) $post->ID; $authordata = get_userdata( $post->post_author ); $currentday = false; $currentmonth = false; $post_date = $post->post_date; if ( ! empty( $post_date ) && '0000-00-00 00:00:00' !== $post_date ) { // Avoid using mysql2date for performance reasons. $currentmonth = substr( $post_date, 5, 2 ); $day = substr( $post_date, 8, 2 ); $year = substr( $post_date, 2, 2 ); $currentday = sprintf( '%s.%s.%s', $day, $currentmonth, $year ); } $numpages = 1; $multipage = 0; $page = $this->get( 'page' ); if ( ! $page ) { $page = 1; } /* * Force full post content when viewing the permalink for the $post, * or when on an RSS feed. Otherwise respect the 'more' tag. */ if ( get_queried_object_id() === $post->ID && ( $this->is_page() || $this->is_single() ) ) { $more = 1; } elseif ( $this->is_feed() ) { $more = 1; } else { $more = 0; } $content = $post->post_content; if ( str_contains( $content, '' ) ) { $content = str_replace( "\n\n", '', $content ); $content = str_replace( "\n", '', $content ); $content = str_replace( "\n", '', $content ); // Remove the nextpage block delimiters, to avoid invalid block structures in the split content. $content = str_replace( '', '', $content ); $content = str_replace( '', '', $content ); // Ignore nextpage at the beginning of the content. if ( str_starts_with( $content, '' ) ) { $content = substr( $content, 15 ); } $pages = explode( '', $content ); } else { $pages = array( $post->post_content ); } /** * Filters the "pages" derived from splitting the post content. * * "Pages" are determined by splitting the post content based on the presence * of `` tags. * * @since 4.4.0 * * @param string[] $pages Array of "pages" from the post content split by `` tags. * @param WP_Post $post Current post object. */ $pages = apply_filters( 'content_pagination', $pages, $post ); $numpages = count( $pages ); if ( $numpages > 1 ) { if ( $page > 1 ) { $more = 1; } $multipage = 1; } else { $multipage = 0; } $elements = compact( 'id', 'authordata', 'currentday', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages' ); return $elements; } /** * Generates cache key. * * @since 6.1.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; unset( $args['cache_results'], $args['fields'], $args['lazy_load_term_meta'], $args['update_post_meta_cache'], $args['update_post_term_cache'], $args['update_menu_item_cache'], $args['suppress_filters'] ); if ( empty( $args['post_type'] ) ) { if ( $this->is_attachment ) { $args['post_type'] = 'attachment'; } elseif ( $this->is_page ) { $args['post_type'] = 'page'; } else { $args['post_type'] = 'post'; } } elseif ( 'any' === $args['post_type'] ) { $args['post_type'] = array_values( get_post_types( array( 'exclude_from_search' => false ) ) ); } $args['post_type'] = (array) $args['post_type']; // Sort post types to ensure same cache key generation. sort( $args['post_type'] ); /* * Sort arrays that can be used for ordering prior to cache key generation. * * These arrays are sorted in the query generator for the purposes of the * WHERE clause but the arguments are not modified as they can be used for * the orderby clause. * * Their use in the orderby clause will generate a different SQL query so * they can be sorted for the cache key generation. */ $sortable_arrays_with_int_values = array( 'post__in', 'post_parent__in', ); foreach ( $sortable_arrays_with_int_values as $key ) { if ( isset( $args[ $key ] ) && is_array( $args[ $key ] ) ) { $args[ $key ] = array_unique( array_map( 'absint', $args[ $key ] ) ); sort( $args[ $key ] ); } } // Sort and unique the 'post_name__in' for cache key generation. if ( isset( $args['post_name__in'] ) && is_array( $args['post_name__in'] ) ) { $args['post_name__in'] = array_unique( $args['post_name__in'] ); sort( $args['post_name__in'] ); } if ( isset( $args['post_status'] ) ) { $args['post_status'] = (array) $args['post_status']; // Sort post status to ensure same cache key generation. sort( $args['post_status'] ); } // Add a default orderby value of date to ensure same cache key generation. if ( ! isset( $args['orderby'] ) ) { $args['orderby'] = 'date'; } $placeholder = $wpdb->placeholder_escape(); array_walk_recursive( $args, /* * Replace wpdb placeholders with the string used in the database * query to avoid unreachable cache keys. This is necessary because * the placeholder is randomly generated in each request. * * $value is passed by reference to allow it to be modified. * array_walk_recursive() does not return an array. */ static function ( &$value ) use ( $wpdb, $placeholder ) { if ( is_string( $value ) && str_contains( $value, $placeholder ) ) { $value = $wpdb->remove_placeholder_escape( $value ); } } ); ksort( $args ); // Replace wpdb placeholder in the SQL statement used by the cache key. $sql = $wpdb->remove_placeholder_escape( $sql ); $key = md5( serialize( $args ) . $sql ); $this->query_cache_key = "wp_query:$key"; return $this->query_cache_key; } /** * After looping through a nested query, this function * restores the $post global to the current post in this query. * * @since 3.7.0 * * @global WP_Post $post Global post object. */ public function reset_postdata() { if ( ! empty( $this->post ) ) { $GLOBALS['post'] = $this->post; $this->setup_postdata( $this->post ); } } /** * Lazyloads term meta for posts in the loop. * * @since 4.4.0 * @deprecated 4.5.0 See wp_queue_posts_for_term_meta_lazyload(). * * @param mixed $check * @param int $term_id * @return mixed */ public function lazyload_term_meta( $check, $term_id ) { _deprecated_function( __METHOD__, '4.5.0' ); return $check; } /** * Lazyloads comment meta for comments in the loop. * * @since 4.4.0 * @deprecated 4.5.0 See wp_lazyload_comment_meta(). * * @param mixed $check * @param int $comment_id * @return mixed */ public function lazyload_comment_meta( $check, $comment_id ) { _deprecated_function( __METHOD__, '4.5.0' ); return $check; } } ional. Default column to query against. See WP_Date_Query::validate_column() * and the {@see 'date_query_valid_columns'} filter for the list of accepted values. * Default 'post_date'. */ public function __construct( $date_query, $default_column = 'post_date' ) { if ( empty( $date_query ) || ! is_array( $date_query ) ) { return; } if ( isset( $date_query['relation'] ) ) { $this->relation = $this->sanitize_relation( $date_query['relation'] ); } else { $this->relation = 'AND'; } // Support for passing time-based keys in the top level of the $date_query array. if ( ! isset( $date_query[0] ) ) { $date_query = array( $date_query ); } if ( ! empty( $date_query['column'] ) ) { $date_query['column'] = esc_sql( $date_query['column'] ); } else { $date_query['column'] = esc_sql( $default_column ); } $this->column = $this->validate_column( $this->column ); $this->compare = $this->get_compare( $date_query ); $this->queries = $this->sanitize_query( $date_query ); } /** * Recursive-friendly query sanitizer. * * Ensures that each query-level clause has a 'relation' key, and that * each first-order clause contains all the necessary keys from `$defaults`. * * @since 4.1.0 * * @param array $queries * @param array $parent_query * @return array Sanitized queries. */ public function sanitize_query( $queries, $parent_query = null ) { $cleaned_query = array(); $defaults = array( 'column' => 'post_date', 'compare' => '=', 'relation' => 'AND', ); // Numeric keys should always have array values. foreach ( $queries as $qkey => $qvalue ) { if ( is_numeric( $qkey ) && ! is_array( $qvalue ) ) { unset( $queries[ $qkey ] ); } } // Each query should have a value for each default key. Inherit from the parent when possible. foreach ( $defaults as $dkey => $dvalue ) { if ( isset( $queries[ $dkey ] ) ) { continue; } $queries[ $dkey ] = $parent_query[ $dkey ] ?? $dvalue; } // Validate the dates passed in the query. if ( $this->is_first_order_clause( $queries ) ) { $this->validate_date_values( $queries ); } // Sanitize the relation parameter. $queries['relation'] = $this->sanitize_relation( $queries['relation'] ); foreach ( $queries as $key => $q ) { if ( ! is_array( $q ) || in_array( $key, $this->time_keys, true ) ) { // This is a first-order query. Trust the values and sanitize when building SQL. $cleaned_query[ $key ] = $q; } else { // Any array without a time key is another query, so we recurse. $cleaned_query[] = $this->sanitize_query( $q, $queries ); } } return $cleaned_query; } /** * Determines whether this is a first-order clause. * * Checks to see if the current clause has any time-related keys. * If so, it's first-order. * * @since 4.1.0 * * @param array $query Query clause. * @return bool True if this is a first-order clause. */ protected function is_first_order_clause( $query ) { $time_keys = array_intersect( $this->time_keys, array_keys( $query ) ); return ! empty( $time_keys ); } /** * Determines and validates what comparison operator to use. * * @since 3.7.0 * * @param array $query A date query or a date subquery. * @return string The comparison operator. */ public function get_compare( $query ) { if ( ! empty( $query['compare'] ) && in_array( $query['compare'], array( '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) { return strtoupper( $query['compare'] ); } return $this->compare; } /** * Validates the given date_query values and triggers errors if something is not valid. * * Note that date queries with invalid date ranges are allowed to * continue (though of course no items will be found for impossible dates). * This method only generates debug notices for these cases. * * @since 4.1.0 * * @param array $date_query The date_query array. * @return bool True if all values in the query are valid, false if one or more fail. */ public function validate_date_values( $date_query = array() ) { if ( empty( $date_query ) ) { return false; } $valid = true; /* * Validate 'before' and 'after' up front, then let the * validation routine continue to be sure that all invalid * values generate errors too. */ if ( array_key_exists( 'before', $date_query ) && is_array( $date_query['before'] ) ) { $valid = $this->validate_date_values( $date_query['before'] ); } if ( array_key_exists( 'after', $date_query ) && is_array( $date_query['after'] ) ) { $valid = $this->validate_date_values( $date_query['after'] ); } // Array containing all min-max checks. $min_max_checks = array(); // Days per year. if ( array_key_exists( 'year', $date_query ) ) { /* * If a year exists in the date query, we can use it to get the days. * If multiple years are provided (as in a BETWEEN), use the first one. */ if ( is_array( $date_query['year'] ) ) { $_year = reset( $date_query['year'] ); } else { $_year = $date_query['year']; } $max_days_of_year = (int) gmdate( 'z', mktime( 0, 0, 0, 12, 31, $_year ) ) + 1; } else { // Otherwise we use the max of 366 (leap-year). $max_days_of_year = 366; } $min_max_checks['dayofyear'] = array( 'min' => 1, 'max' => $max_days_of_year, ); // Days per week. $min_max_checks['dayofweek'] = array( 'min' => 1, 'max' => 7, ); // Days per week. $min_max_checks['dayofweek_iso'] = array( 'min' => 1, 'max' => 7, ); // Months per year. $min_max_checks['month'] = array( 'min' => 1, 'max' => 12, ); // Weeks per year. if ( isset( $_year ) ) { /* * If we have a specific year, use it to calculate number of weeks. * Note: the number of weeks in a year is the date in which Dec 28 appears. */ $week_count = gmdate( 'W', mktime( 0, 0, 0, 12, 28, $_year ) ); } else { // Otherwise set the week-count to a maximum of 53. $week_count = 53; } $min_max_checks['week'] = array( 'min' => 1, 'max' => $week_count, ); // Days per month. $min_max_checks['day'] = array( 'min' => 1, 'max' => 31, ); // Hours per day. $min_max_checks['hour'] = array( 'min' => 0, 'max' => 23, ); // Minutes per hour. $min_max_checks['minute'] = array( 'min' => 0, 'max' => 59, ); // Seconds per minute. $min_max_checks['second'] = array( 'min' => 0, 'max' => 59, ); // Concatenate and throw a notice for each invalid value. foreach ( $min_max_checks as $key => $check ) { if ( ! array_key_exists( $key, $date_query ) ) { continue; } // Throw a notice for each failing value. foreach ( (array) $date_query[ $key ] as $_value ) { $is_between = $_value >= $check['min'] && $_value <= $check['max']; if ( ! is_numeric( $_value ) || ! $is_between ) { $error = sprintf( /* translators: Date query invalid date message. 1: Invalid value, 2: Type of value, 3: Minimum valid value, 4: Maximum valid value. */ __( 'Invalid value %1$s for %2$s. Expected value should be between %3$s and %4$s.' ), '' . esc_html( $_value ) . '', '' . esc_html( $key ) . '', '' . esc_html( $check['min'] ) . '', '' . esc_html( $check['max'] ) . '' ); _doing_it_wrong( __CLASS__, $error, '4.1.0' ); $valid = false; } } } // If we already have invalid date messages, don't bother running through checkdate(). if ( ! $valid ) { return $valid; } $day_month_year_error_msg = ''; $day_exists = array_key_exists( 'day', $date_query ) && is_numeric( $date_query['day'] ); $month_exists = array_key_exists( 'month', $date_query ) && is_numeric( $date_query['month'] ); $year_exists = array_key_exists( 'year', $date_query ) && is_numeric( $date_query['year'] ); if ( $day_exists && $month_exists && $year_exists ) { // 1. Checking day, month, year combination. if ( ! wp_checkdate( $date_query['month'], $date_query['day'], $date_query['year'], sprintf( '%s-%s-%s', $date_query['year'], $date_query['month'], $date_query['day'] ) ) ) { $day_month_year_error_msg = sprintf( /* translators: 1: Year, 2: Month, 3: Day of month. */ __( 'The following values do not describe a valid date: year %1$s, month %2$s, day %3$s.' ), '' . esc_html( $date_query['year'] ) . '', '' . esc_html( $date_query['month'] ) . '', '' . esc_html( $date_query['day'] ) . '' ); $valid = false; } } elseif ( $day_exists && $month_exists ) { /* * 2. checking day, month combination * We use 2012 because, as a leap year, it's the most permissive. */ if ( ! wp_checkdate( $date_query['month'], $date_query['day'], 2012, sprintf( '2012-%s-%s', $date_query['month'], $date_query['day'] ) ) ) { $day_month_year_error_msg = sprintf( /* translators: 1: Month, 2: Day of month. */ __( 'The following values do not describe a valid date: month %1$s, day %2$s.' ), '' . esc_html( $date_query['month'] ) . '', '' . esc_html( $date_query['day'] ) . '' ); $valid = false; } } if ( ! empty( $day_month_year_error_msg ) ) { _doing_it_wrong( __CLASS__, $day_month_year_error_msg, '4.1.0' ); } return $valid; } /** * Validates a column name parameter. * * Column names without a table prefix (like 'post_date') are checked against a list of * allowed and known tables, and then, if found, have a table prefix (such as 'wp_posts.') * prepended. Prefixed column names (such as 'wp_posts.post_date') bypass this allowed * check, and are only sanitized to remove illegal characters. * * @since 3.7.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $column The user-supplied column name. * @return string A validated column name value. */ public function validate_column( $column ) { global $wpdb; $valid_columns = array( 'post_date', // Part of $wpdb->posts. 'post_date_gmt', // Part of $wpdb->posts. 'post_modified', // Part of $wpdb->posts. 'post_modified_gmt', // Part of $wpdb->posts. 'comment_date', // Part of $wpdb->comments. 'comment_date_gmt', // Part of $wpdb->comments. 'user_registered', // Part of $wpdb->users. ); if ( is_multisite() ) { $valid_columns = array_merge( $valid_columns, array( 'registered', // Part of $wpdb->blogs. 'last_updated', // Part of $wpdb->blogs. ) ); } // Attempt to detect a table prefix. if ( ! str_contains( $column, '.' ) ) { /** * Filters the list of valid date query columns. * * @since 3.7.0 * @since 4.1.0 Added 'user_registered' to the default recognized columns. * @since 4.6.0 Added 'registered' and 'last_updated' to the default recognized columns. * * @param string[] $valid_columns An array of valid date query columns. Defaults * are 'post_date', 'post_date_gmt', 'post_modified', * 'post_modified_gmt', 'comment_date', 'comment_date_gmt', * 'user_registered', 'registered', 'last_updated'. */ if ( ! in_array( $column, apply_filters( 'date_query_valid_columns', $valid_columns ), true ) ) { $column = 'post_date'; } $known_columns = array( $wpdb->posts => array( 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt', ), $wpdb->comments => array( 'comment_date', 'comment_date_gmt', ), $wpdb->users => array( 'user_registered', ), ); if ( is_multisite() ) { $known_columns[ $wpdb->blogs ] = array( 'registered', 'last_updated', ); } // If it's a known column name, add the appropriate table prefix. foreach ( $known_columns as $table_name => $table_columns ) { if ( in_array( $column, $table_columns, true ) ) { $column = $table_name . '.' . $column; break; } } } // Remove unsafe characters. return preg_replace( '/[^a-zA-Z0-9_$\.]/', '', $column ); } /** * Generates WHERE clause to be appended to a main query. * * @since 3.7.0 * * @return string MySQL WHERE clause. */ public function get_sql() { $sql = $this->get_sql_clauses(); $where = $sql['where']; /** * Filters the date query WHERE clause. * * @since 3.7.0 * * @param string $where WHERE clause of the date query. * @param WP_Date_Query $query The WP_Date_Query instance. */ return apply_filters( 'get_date_sql', $where, $this ); } /** * Generates SQL clauses to be appended to a main query. * * Called by the public WP_Date_Query::get_sql(), this method is abstracted * out to maintain parity with the other Query classes. * * @since 4.1.0 * * @return string[] { * Array containing JOIN and WHERE SQL clauses to append to the main query. * * @type string $join SQL fragment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ protected function get_sql_clauses() { $sql = $this->get_sql_for_query( $this->queries ); if ( ! empty( $sql['where'] ) ) { $sql['where'] = ' AND ' . $sql['where']; } return $sql; } /** * Generates SQL clauses for a single query array. * * If nested subqueries are found, this method recurses the tree to * produce the properly nested SQL. * * @since 4.1.0 * * @param array $query Query to parse. * @param int $depth Optional. Number of tree levels deep we currently are. * Used to calculate indentation. Default 0. * @return array { * Array containing JOIN and WHERE SQL clauses to append to a single query array. * * @type string $join SQL fragment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ protected function get_sql_for_query( $query, $depth = 0 ) { $sql_chunks = array( 'join' => array(), 'where' => array(), ); $sql = array( 'join' => '', 'where' => '', ); $indent = ''; for ( $i = 0; $i < $depth; $i++ ) { $indent .= ' '; } foreach ( $query as $key => $clause ) { if ( 'relation' === $key ) { $relation = $query['relation']; } elseif ( is_array( $clause ) ) { // This is a first-order clause. if ( $this->is_first_order_clause( $clause ) ) { $clause_sql = $this->get_sql_for_clause( $clause, $query ); $where_count = count( $clause_sql['where'] ); if ( ! $where_count ) { $sql_chunks['where'][] = ''; } elseif ( 1 === $where_count ) { $sql_chunks['where'][] = $clause_sql['where'][0]; } else { $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; } $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); // This is a subquery, so we recurse. } else { $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); $sql_chunks['where'][] = $clause_sql['where']; $sql_chunks['join'][] = $clause_sql['join']; } } } // Filter to remove empties. $sql_chunks['join'] = array_filter( $sql_chunks['join'] ); $sql_chunks['where'] = array_filter( $sql_chunks['where'] ); if ( empty( $relation ) ) { $relation = 'AND'; } // Filter duplicate JOIN clauses and combine into a single string. if ( ! empty( $sql_chunks['join'] ) ) { $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) ); } // Generate a single WHERE clause with proper brackets and indentation. if ( ! empty( $sql_chunks['where'] ) ) { $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')'; } return $sql; } /** * Turns a single date clause into pieces for a WHERE clause. * * A wrapper for get_sql_for_clause(), included here for backward * compatibility while retaining the naming convention across Query classes. * * @since 3.7.0 * * @param array $query Date query arguments. * @return array { * Array containing JOIN and WHERE SQL clauses to append to the main query. * * @type string[] $join Array of SQL fragments to append to the main JOIN clause. * @type string[] $where Array of SQL fragments to append to the main WHERE clause. * } */ protected function get_sql_for_subquery( $query ) { return $this->get_sql_for_clause( $query, '' ); } /** * Turns a first-order date query into SQL for a WHERE clause. * * @since 4.1.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $query Date query clause. * @param array $parent_query Parent query of the current date query. * @return array { * Array containing JOIN and WHERE SQL clauses to append to the main query. * * @type string[] $join Array of SQL fragments to append to the main JOIN clause. * @type string[] $where Array of SQL fragments to append to the main WHERE clause. * } */ protected function get_sql_for_clause( $query, $parent_query ) { global $wpdb; // The sub-parts of a $where part. $where_parts = array(); $column = ( ! empty( $query['column'] ) ) ? esc_sql( $query['column'] ) : $this->column; $column = $this->validate_column( $column ); $compare = $this->get_compare( $query ); $inclusive = ! empty( $query['inclusive'] ); // Assign greater- and less-than values. $lt = '<'; $gt = '>'; if ( $inclusive ) { $lt .= '='; $gt .= '='; } // Range queries. if ( ! empty( $query['after'] ) ) { $where_parts[] = $wpdb->prepare( "$column $gt %s", $this->build_mysql_datetime( $query['after'], ! $inclusive ) ); } if ( ! empty( $query['before'] ) ) { $where_parts[] = $wpdb->prepare( "$column $lt %s", $this->build_mysql_datetime( $query['before'], $inclusive ) ); } // Specific value queries. $date_units = array( 'YEAR' => array( 'year' ), 'MONTH' => array( 'month', 'monthnum' ), '_wp_mysql_week' => array( 'week', 'w' ), 'DAYOFYEAR' => array( 'dayofyear' ), 'DAYOFMONTH' => array( 'day' ), 'DAYOFWEEK' => array( 'dayofweek' ), 'WEEKDAY' => array( 'dayofweek_iso' ), ); // Check of the possible date units and add them to the query. foreach ( $date_units as $sql_part => $query_parts ) { foreach ( $query_parts as $query_part ) { if ( isset( $query[ $query_part ] ) ) { $value = $this->build_value( $compare, $query[ $query_part ] ); if ( $value ) { switch ( $sql_part ) { case '_wp_mysql_week': $where_parts[] = _wp_mysql_week( $column ) . " $compare $value"; break; case 'WEEKDAY': $where_parts[] = "$sql_part( $column ) + 1 $compare $value"; break; default: $where_parts[] = "$sql_part( $column ) $compare $value"; } break; } } } } if ( isset( $query['hour'] ) || isset( $query['minute'] ) || isset( $query['second'] ) ) { // Avoid notices. foreach ( array( 'hour', 'minute', 'second' ) as $unit ) { if ( ! isset( $query[ $unit ] ) ) { $query[ $unit ] = null; } } $time_query = $this->build_time_query( $column, $compare, $query['hour'], $query['minute'], $query['second'] ); if ( $time_query ) { $where_parts[] = $time_query; } } /* * Return an array of 'join' and 'where' for compatibility * with other query classes. */ return array( 'where' => $where_parts, 'join' => array(), ); } /** * Builds and validates a value string based on the comparison operator. * * @since 3.7.0 * * @param string $compare The compare operator to use. * @param string|array $value The value. * @return string|false|int The value to be used in SQL or false on error. */ public function build_value( $compare, $value ) { if ( ! isset( $value ) ) { return false; } switch ( $compare ) { case 'IN': case 'NOT IN': $value = (array) $value; // Remove non-numeric values. $value = array_filter( $value, 'is_numeric' ); if ( empty( $value ) ) { return false; } return '(' . implode( ',', array_map( 'intval', $value ) ) . ')'; case 'BETWEEN': case 'NOT BETWEEN': if ( ! is_array( $value ) || 2 !== count( $value ) ) { $value = array( $value, $value ); } else { $value = array_values( $value ); } // If either value is non-numeric, bail. foreach ( $value as $v ) { if ( ! is_numeric( $v ) ) { return false; } } $value = array_map( 'intval', $value ); return $value[0] . ' AND ' . $value[1]; default: if ( ! is_numeric( $value ) ) { return false; } return (int) $value; } } /** * Builds a MySQL format date/time based on some query parameters. * * You can pass an array of values (year, month, etc.) with missing parameter values being defaulted to * either the maximum or minimum values (controlled by the $default_to parameter). Alternatively you can * pass a string that will be passed to date_create(). * * @since 3.7.0 * * @param string|array $datetime An array of parameters or a strtotime() string. * @param bool $default_to_max Whether to round up incomplete dates. Supported by values * of $datetime that are arrays, or string values that are a * subset of MySQL date format ('Y', 'Y-m', 'Y-m-d', 'Y-m-d H:i'). * Default: false. * @return string|false A MySQL format date/time or false on failure. */ public function build_mysql_datetime( $datetime, $default_to_max = false ) { if ( ! is_array( $datetime ) ) { /* * Try to parse some common date formats, so we can detect * the level of precision and support the 'inclusive' parameter. */ if ( preg_match( '/^(\d{4})$/', $datetime, $matches ) ) { // Y $datetime = array( 'year' => (int) $matches[1], ); } elseif ( preg_match( '/^(\d{4})\-(\d{2})$/', $datetime, $matches ) ) { // Y-m $datetime = array( 'year' => (int) $matches[1], 'month' => (int) $matches[2], ); } elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2})$/', $datetime, $matches ) ) { // Y-m-d $datetime = array( 'year' => (int) $matches[1], 'month' => (int) $matches[2], 'day' => (int) $matches[3], ); } elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2}) (\d{2}):(\d{2})$/', $datetime, $matches ) ) { // Y-m-d H:i $datetime = array( 'year' => (int) $matches[1], 'month' => (int) $matches[2], 'day' => (int) $matches[3], 'hour' => (int) $matches[4], 'minute' => (int) $matches[5], ); } // If no match is found, we don't support default_to_max. if ( ! is_array( $datetime ) ) { $wp_timezone = wp_timezone(); // Assume local timezone if not provided. $dt = date_create( $datetime, $wp_timezone ); if ( false === $dt ) { return gmdate( 'Y-m-d H:i:s', false ); } return $dt->setTimezone( $wp_timezone )->format( 'Y-m-d H:i:s' ); } } $datetime = array_map( 'absint', $datetime ); if ( ! isset( $datetime['year'] ) ) { $datetime['year'] = current_time( 'Y' ); } if ( ! isset( $datetime['month'] ) ) { $datetime['month'] = ( $default_to_max ) ? 12 : 1; } if ( ! isset( $datetime['day'] ) ) { $datetime['day'] = ( $default_to_max ) ? (int) gmdate( 't', mktime( 0, 0, 0, $datetime['month'], 1, $datetime['year'] ) ) : 1; } if ( ! isset( $datetime['hour'] ) ) { $datetime['hour'] = ( $default_to_max ) ? 23 : 0; } if ( ! isset( $datetime['minute'] ) ) { $datetime['minute'] = ( $default_to_max ) ? 59 : 0; } if ( ! isset( $datetime['second'] ) ) { $datetime['second'] = ( $default_to_max ) ? 59 : 0; } return sprintf( '%04d-%02d-%02d %02d:%02d:%02d', $datetime['year'], $datetime['month'], $datetime['day'], $datetime['hour'], $datetime['minute'], $datetime['second'] ); } /** * Builds a query string for comparing time values (hour, minute, second). * * If just hour, minute, or second is set than a normal comparison will be done. * However if multiple values are passed, a pseudo-decimal time will be created * in order to be able to accurately compare against. * * @since 3.7.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $column The column to query against. Needs to be pre-validated! * @param string $compare The comparison operator. Needs to be pre-validated! * @param int|null $hour Optional. An hour value (0-23). * @param int|null $minute Optional. A minute value (0-59). * @param int|null $second Optional. A second value (0-59). * @return string|false A query part or false on failure. */ public function build_time_query( $column, $compare, $hour = null, $minute = null, $second = null ) { global $wpdb; // Have to have at least one. if ( ! isset( $hour ) && ! isset( $minute ) && ! isset( $second ) ) { return false; } // Complex combined queries aren't supported for multi-value queries. if ( in_array( $compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) { $return = array(); $value = $this->build_value( $compare, $hour ); if ( false !== $value ) { $return[] = "HOUR( $column ) $compare $value"; } $value = $this->build_value( $compare, $minute ); if ( false !== $value ) { $return[] = "MINUTE( $column ) $compare $value"; } $value = $this->build_value( $compare, $second ); if ( false !== $value ) { $return[] = "SECOND( $column ) $compare $value"; } return implode( ' AND ', $return ); } // Cases where just one unit is set. if ( isset( $hour ) && ! isset( $minute ) && ! isset( $second ) ) { $value = $this->build_value( $compare, $hour ); if ( false !== $value ) { return "HOUR( $column ) $compare $value"; } } elseif ( ! isset( $hour ) && isset( $minute ) && ! isset( $second ) ) { $value = $this->build_value( $compare, $minute ); if ( false !== $value ) { return "MINUTE( $column ) $compare $value"; } } elseif ( ! isset( $hour ) && ! isset( $minute ) && isset( $second ) ) { $value = $this->build_value( $compare, $second ); if ( false !== $value ) { return "SECOND( $column ) $compare $value"; } } // Single units were already handled. Since hour & second isn't allowed, minute must to be set. if ( ! isset( $minute ) ) { return false; } $format = ''; $time = ''; // Hour. if ( null !== $hour ) { $format .= '%H.'; $time .= sprintf( '%02d', $hour ) . '.'; } else { $format .= '0.'; $time .= '0.'; } // Minute. $format .= '%i'; $time .= sprintf( '%02d', $minute ); if ( isset( $second ) ) { $format .= '%s'; $time .= sprintf( '%02d', $second ); } return $wpdb->prepare( "DATE_FORMAT( $column, %s ) $compare %f", $format, $time ); } /** * Sanitizes a 'relation' operator. * * @since 6.0.3 * * @param string $relation Raw relation key from the query argument. * @return string Sanitized relation. Either 'AND' or 'OR'. */ public function sanitize_relation( $relation ) { if ( 'OR' === strtoupper( $relation ) ) { return 'OR'; } else { return 'AND'; } } }