wp = $wp; $this->route = Router::$route . '/?$'; $this->wp->matched_rule = $this->route; $this->context = $context; } /** * Given a Post object, validates it before returning it. * * @param \WP_Post $post * * @return \WP_Post|null */ public function validate_post( WP_Post $post ) { if ( isset( $this->wp->query_vars['post_type'] ) && ( $post->post_type !== $this->wp->query_vars['post_type'] ) ) { return null; } if ( ! $this->is_valid_node_type( 'ContentNode' ) ) { return null; } if ( empty( $this->wp->query_vars['uri'] ) ) { return $post; } // If the uri doesn't have the post's urlencoded name or ID in it, we must've found something we didn't expect // so we will return null. Check decoded form, sanitize_title form, and raw post_name so both decoded and // percent-encoded client input are accepted (issue #3582). $uri = $this->wp->query_vars['uri']; $name_in_uri = strpos( $uri, (string) $post->ID ) !== false || strpos( $uri, urldecode( sanitize_title( $post->post_name ) ) ) !== false || strpos( $uri, urldecode( $post->post_name ) ) !== false || strpos( $uri, $post->post_name ) !== false; if ( ! $name_in_uri ) { return null; } return $post; } /** * Given a Term object, validates it before returning it. * * @param \WP_Term $term * * @return \WP_Term|null */ public function validate_term( \WP_Term $term ) { if ( ! $this->is_valid_node_type( 'TermNode' ) ) { return null; } if ( isset( $this->wp->query_vars['taxonomy'] ) && $term->taxonomy !== $this->wp->query_vars['taxonomy'] ) { return null; } return $term; } /** * Given the URI of a resource, this method attempts to resolve it and return the * appropriate related object * * @param string $uri The path to be used as an identifier for the resource. * @param array|string $extra_query_vars Any extra query vars to consider * * @return mixed * @throws \GraphQL\Error\UserError If the query class does not exist. */ public function resolve_uri( string $uri, $extra_query_vars = '' ) { /** * When this filter return anything other than null, it will be used as a resolved node * and the execution will be skipped. * * This is to be used in extensions to resolve their own nodes which might not use * WordPress permalink structure. * * @param mixed|null $node The node, defaults to nothing. * @param string $uri The uri being searched. * @param \WPGraphQL\AppContext $content The app context. * @param \WP $wp WP object. * @param array|string $extra_query_vars Any extra query vars to consider. * * @hookGroup request-lifecycle * @since 0.0.5 */ $node = apply_filters( 'graphql_pre_resolve_uri', null, $uri, $this->context, $this->wp, $extra_query_vars ); if ( ! empty( $node ) ) { return $node; } /** * Comments are embedded as a #comment-{$id} in the post's content. * * If the URI is for a comment, we can resolve it now. */ $comment_id = $this->maybe_parse_comment_uri( $uri ); if ( null !== $comment_id ) { return $this->context->get_loader( 'comment' )->load_deferred( $comment_id ); } /** * Try to resolve the URI with WP_Query. * * This is the way WordPress native permalinks are resolved. * * @see \WP::main() */ // Parse the URI and sets the $wp->query_vars property. $uri = $this->parse_request( $uri, $extra_query_vars ); /** * If the URI is the home page, we can resolve it now. * * The home page doesn't get a rewrite rule, so a bare '/' is resolved directly. * We also detect the home page from the parsed request so the full home URL * resolves when WordPress is installed in a subdirectory (e.g. `/blog/`), where * parse_request() strips the home path and leaves an empty request (#3775). */ if ( '/' === $uri || $this->is_home_request() ) { return $this->resolve_home_page(); } /** * Filter the query class used to resolve the URI. By default this is WP_Query. * * This can be used by Extensions which use a different query class to resolve data. * * @param class-string $query_class The query class used to resolve the URI. Defaults to WP_Query. * @param ?string $uri The uri being searched. * @param \WPGraphQL\AppContext $content The app context. * @param \WP $wp WP object. * @param array|string $extra_query_vars Any extra query vars to consider. * * @hookGroup request-lifecycle * @since 0.0.5 */ $query_class = apply_filters( 'graphql_resolve_uri_query_class', 'WP_Query', $uri, $this->context, $this->wp, $extra_query_vars ); if ( ! class_exists( $query_class ) ) { throw new UserError( esc_html( sprintf( /* translators: %s: The query class used to resolve the URI */ __( 'The query class %s used to resolve the URI does not exist.', 'wp-graphql' ), $query_class ) ) ); } $query_vars = $this->wp->query_vars; /** @var \WP_Query $query */ $query = new $query_class( $query_vars ); // is the query is an archive if ( isset( $query->posts[0] ) && $query->posts[0] instanceof WP_Post && ! $query->is_archive() ) { $queried_object = $query->posts[0]; } else { $queried_object = $query->get_queried_object(); } // When no post was found but we have a slug, retry with alternate encoding so we can find // posts whose post_name is stored in percent-encoded form (non-ASCII slugs). See issue #3582. if ( ! $queried_object instanceof WP_Post && isset( $query_vars['name'] ) && is_string( $query_vars['name'] ) && '' !== $query_vars['name'] ) { $retry_name = strpos( $query_vars['name'], '%' ) !== false ? urldecode( $query_vars['name'] ) : rawurlencode( $query_vars['name'] ); $retry_query_vars = $query_vars; $retry_query_vars['name'] = $retry_name; /** @var \WP_Query $retry_query */ $retry_query = new $query_class( $retry_query_vars ); $retry_queried_object = null; if ( isset( $retry_query->posts[0] ) && $retry_query->posts[0] instanceof WP_Post && ! $retry_query->is_archive() ) { $retry_queried_object = $retry_query->posts[0]; } else { $retry_queried_object = $retry_query->get_queried_object(); } if ( $retry_queried_object instanceof WP_Post ) { $query = $retry_query; $queried_object = $retry_queried_object; } } /** * When this filter return anything other than null, it will be used as a resolved node * and the execution will be skipped. * * This is to be used in extensions to resolve their own nodes which might not use * WordPress permalink structure. * * It differs from 'graphql_pre_resolve_uri' in that it has been called after the query has been run using the query vars. * * @param mixed|null $node The node, defaults to nothing. * @param ?string $uri The uri being searched. * @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|null $queried_object The queried object, if WP_Query returns one. * @param \WP_Query $query The query object. * @param \WPGraphQL\AppContext $content The app context. * @param \WP $wp WP object. * @param array|string $extra_query_vars Any extra query vars to consider. * * @hookGroup request-lifecycle * @since 0.0.5 */ $node = apply_filters( 'graphql_resolve_uri', null, $uri, $queried_object, $query, $this->context, $this->wp, $extra_query_vars ); if ( ! empty( $node ) ) { return $node; } // Resolve Post Objects. if ( $queried_object instanceof WP_Post ) { // If Page for Posts is set, we need to return the Page archive, not the page. if ( $query->is_posts_page ) { // If were intentionally querying for a something other than a ContentType, we need to return null instead of the archive. if ( ! $this->is_valid_node_type( 'ContentType' ) ) { return null; } $post_type_object = get_post_type_object( 'post' ); if ( ! $post_type_object ) { return null; } return ! empty( $post_type_object->name ) ? $this->context->get_loader( 'post_type' )->load_deferred( $post_type_object->name ) : null; } // Validate the post before returning it. if ( ! $this->validate_post( $queried_object ) ) { return null; } // A 404 from parse_request() means the requested path did not match a // registered rewrite rule, so the queried object (if any) is an unrelated // post that WP_Query returned because a pre-seeded `post_type` query var // turned the request into an unbounded query. Bail in that case so typed // `idType: URI` fields stay consistent with nodeByUri, which returns null // for partial or wrong-hierarchy URIs (#3042). // // The exception is an explicit slug lookup (idType: SLUG), which passes a // `name` and intentionally resolves by post_name without requiring the // full path to match a rewrite rule. if ( isset( $this->wp->query_vars['error'] ) && '404' === $this->wp->query_vars['error'] ) { $is_slug_lookup = is_array( $extra_query_vars ) && ! empty( $extra_query_vars['name'] ); if ( ! $is_slug_lookup ) { return null; } } $post_id = $queried_object->ID; $as_preview = false; // if asPreview isn't passed explicitly as an argument on a node, // attempt to fill the value from the $query_vars passed on the URI as a query param if ( is_array( $extra_query_vars ) && array_key_exists( 'asPreview', $extra_query_vars ) && null === $extra_query_vars['asPreview'] && isset( $query_vars['preview'] ) ) { // note, the "preview" arg comes through as a string, not a boolean so we need to check 'true' as a string $as_preview = 'true' === $query_vars['preview']; } $as_preview = isset( $extra_query_vars['asPreview'] ) && true === $extra_query_vars['asPreview'] ? true : $as_preview; if ( true === $as_preview ) { $post_id = Utils::get_post_preview_id( $post_id ); } return ! empty( $post_id ) ? $this->context->get_loader( 'post' )->load_deferred( $post_id ) : null; } // Resolve Terms. if ( $queried_object instanceof \WP_Term ) { // Validate the term before returning it. if ( ! $this->validate_term( $queried_object ) ) { return null; } return ! empty( $queried_object->term_id ) ? $this->context->get_loader( 'term' )->load_deferred( $queried_object->term_id ) : null; } // Resolve Post Types. if ( $queried_object instanceof \WP_Post_Type ) { // Bail if we're explicitly requesting a different GraphQL type. if ( ! $this->is_valid_node_type( 'ContentType' ) ) { return null; } return ! empty( $queried_object->name ) ? $this->context->get_loader( 'post_type' )->load_deferred( $queried_object->name ) : null; } // Resolve Users if ( $queried_object instanceof \WP_User ) { // Bail if we're explicitly requesting a different GraphQL type. if ( ! $this->is_valid_node_type( 'User' ) ) { return null; } return ! empty( $queried_object->ID ) ? $this->context->get_loader( 'user' )->load_deferred( $queried_object->ID ) : null; } /** * This filter provides a fallback for resolving nodes that were unable to be resolved by NodeResolver::resolve_uri. * * This can be used by Extensions to resolve edge cases that are not handled by the core NodeResolver. * * @param mixed|null $node The node, defaults to nothing. * @param ?string $uri The uri being searched. * @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|null $queried_object The queried object, if WP_Query returns one. * @param \WP_Query $query The query object. * @param \WPGraphQL\AppContext $content The app context. * @param \WP $wp WP object. * @param array|string $extra_query_vars Any extra query vars to consider. * * @hookGroup request-lifecycle * @since 0.0.5 */ return apply_filters( 'graphql_post_resolve_uri', $node, $uri, $queried_object, $query, $this->context, $this->wp, $extra_query_vars ); } /** * Parses a URL to produce an array of query variables. * * Mimics WP::parse_request() * * @param string $uri * @param array|string $extra_query_vars * * @return string|null The parsed uri. */ public function parse_request( string $uri, $extra_query_vars = '' ) { // Attempt to parse the provided URI. $parsed_url = wp_parse_url( $uri ); if ( false === $parsed_url ) { graphql_debug( __( 'Cannot parse provided URI', 'wp-graphql' ), [ 'uri' => $uri, ] ); return null; } // Bail if external URI. if ( isset( $parsed_url['host'] ) ) { $site_parts = wp_parse_url( site_url() ); $home_parts = wp_parse_url( home_url() ); $default_allowed_hosts = []; if ( is_array( $site_parts ) && isset( $site_parts['host'] ) ) { $default_allowed_hosts[] = $site_parts['host']; } if ( is_array( $home_parts ) && isset( $home_parts['host'] ) ) { $default_allowed_hosts[] = $home_parts['host']; } /** * Filters hostnames treated as belonging to this WordPress install when resolving node URIs. * * By default includes the hosts from `site_url()` and `home_url()`. Extensions may append * hosts (for example language-specific domains mapped to the same site). * * @param string[] $allowed_hosts Hostnames permitted when comparing the parsed URI host. * * @hookGroup request-lifecycle * @since 2.12.0 */ $allowed_hosts = apply_filters( 'graphql_allowed_hosts', $default_allowed_hosts ); if ( ! in_array( $parsed_url['host'], $allowed_hosts, true ) ) { graphql_debug( __( 'Cannot return a resource for an external URI', 'wp-graphql' ), [ 'uri' => $uri, ] ); return null; } } if ( isset( $parsed_url['query'] ) && ( empty( $parsed_url['path'] ) || '/' === $parsed_url['path'] ) ) { $uri = $parsed_url['query']; } elseif ( isset( $parsed_url['path'] ) ) { $uri = $parsed_url['path']; } /** * Follows pattern from WP::parse_request() * * @see https://github.com/WordPress/wordpress-develop/blob/6.0.2/src/wp-includes/class-wp.php#L135 */ global $wp_rewrite; $this->wp->query_vars = []; $post_type_query_vars = []; // Save explicit slug when resolving by slug (idType: SLUG) so we can restore it after rewrite parsing. $saved_name = null; if ( is_array( $extra_query_vars ) ) { $this->wp->query_vars = &$extra_query_vars; if ( isset( $extra_query_vars['name'] ) ) { $saved_name = $extra_query_vars['name']; } } elseif ( ! empty( $extra_query_vars ) ) { parse_str( $extra_query_vars, $this->wp->extra_query_vars ); } // Set uri to Query vars. $this->wp->query_vars['uri'] = $uri; // Process PATH_INFO, REQUEST_URI, and 404 for permalinks. // Fetch the rewrite rules. $rewrite = $wp_rewrite->wp_rewrite_rules(); if ( ! empty( $rewrite ) ) { // If we match a rewrite rule, this will be cleared. $error = '404'; $this->wp->did_permalink = true; $pathinfo = ! empty( $uri ) ? $uri : ''; list( $pathinfo ) = explode( '?', $pathinfo ); $pathinfo = str_replace( '%', '%25', $pathinfo ); list( $req_uri ) = explode( '?', $pathinfo ); $home_path = parse_url( home_url(), PHP_URL_PATH ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url $home_path_regex = ''; if ( is_string( $home_path ) && '' !== $home_path ) { $home_path = trim( $home_path, '/' ); $home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) ); } /* * Trim path info from the end and the leading home path from the front. * For path info requests, this leaves us with the requesting filename, if any. * For 404 requests, this leaves us with the requested permalink. */ $query = ''; $matches = null; $req_uri = str_replace( $pathinfo, '', $req_uri ); $req_uri = trim( $req_uri, '/' ); $pathinfo = trim( $pathinfo, '/' ); if ( ! empty( $home_path_regex ) ) { $req_uri = preg_replace( $home_path_regex, '', $req_uri ); $req_uri = trim( $req_uri, '/' ); // @phpstan-ignore-line $pathinfo = preg_replace( $home_path_regex, '', $pathinfo ); $pathinfo = trim( $pathinfo, '/' ); // @phpstan-ignore-line } // The requested permalink is in $pathinfo for path info requests and // $req_uri for other requests. if ( ! empty( $pathinfo ) && ! preg_match( '|^.*' . $wp_rewrite->index . '$|', $pathinfo ) ) { $requested_path = $pathinfo; } else { // If the request uri is the index, blank it out so that we don't try to match it against a rule. if ( $req_uri === $wp_rewrite->index ) { $req_uri = ''; } $requested_path = $req_uri; } $requested_file = $req_uri; $this->wp->request = $requested_path; // Look for matches. $request_match = $requested_path; if ( empty( $request_match ) ) { // An empty request could only match against ^$ regex if ( isset( $rewrite['$'] ) ) { $this->wp->matched_rule = '$'; $query = $rewrite['$']; $matches = [ '' ]; } } else { foreach ( (array) $rewrite as $match => $query ) { // If the requested file is the anchor of the match, prepend it to the path info. if ( ! empty( $requested_file ) && strpos( $match, $requested_file ) === 0 && $requested_file !== $requested_path ) { $request_match = $requested_file . '/' . $requested_path; } if ( preg_match( "#^$match#", $request_match, $matches ) || preg_match( "#^$match#", urldecode( $request_match ), $matches ) ) { if ( $wp_rewrite->use_verbose_page_rules && preg_match( '/pagename=\$matches\[([0-9]+)\]/', $query, $varmatch ) ) { // This is a verbose page match, let's check to be sure about it. $page = get_page_by_path( $matches[ $varmatch[1] ] ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_page_by_path_get_page_by_path if ( ! $page ) { continue; } $post_status_obj = get_post_status_object( $page->post_status ); if ( ( ! isset( $post_status_obj->public ) || ! $post_status_obj->public ) && ( ! isset( $post_status_obj->protected ) || ! $post_status_obj->protected ) && ( ! isset( $post_status_obj->private ) || ! $post_status_obj->private ) && ( ! isset( $post_status_obj->exclude_from_search ) || $post_status_obj->exclude_from_search ) ) { continue; } } // Got a match. $this->wp->matched_rule = $match; break; } } } if ( ! empty( $this->wp->matched_rule ) && $this->wp->matched_rule !== $this->route ) { // Trim the query of everything up to the '?'. $query = preg_replace( '!^.+\?!', '', $query ); // Substitute the substring matches into the query. $query = addslashes( \WP_MatchesMapRegex::apply( $query, $matches ) ); // @phpstan-ignore-line $this->wp->matched_query = $query; // Parse the query. parse_str( $query, $perma_query_vars ); // If we're processing a 404 request, clear the error var since we found something. // @phpstan-ignore-next-line if ( '404' == $error ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual unset( $error ); } } } /** * Filters the query variables allowed before processing. * * Allows (publicly allowed) query vars to be added, removed, or changed prior * to executing the query. Needed to allow custom rewrite rules using your own arguments * to work, or any other custom query variables you want to be publicly available. * * @since 1.5.0 * * @param string[] $public_query_vars The array of allowed query variable names. */ $this->wp->public_query_vars = apply_filters( 'query_vars', $this->wp->public_query_vars ); foreach ( get_post_types( [ 'show_in_graphql' => true ], 'objects' ) as $post_type => $t ) { /** @var \WP_Post_Type $t */ if ( $t->query_var ) { $post_type_query_vars[ $t->query_var ] = $post_type; } } foreach ( $this->wp->public_query_vars as $wpvar ) { $parsed_query = []; if ( isset( $parsed_url['query'] ) ) { parse_str( $parsed_url['query'], $parsed_query ); } if ( isset( $this->wp->extra_query_vars[ $wpvar ] ) ) { $this->wp->query_vars[ $wpvar ] = $this->wp->extra_query_vars[ $wpvar ]; } elseif ( isset( $_GET[ $wpvar ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $this->wp->query_vars[ $wpvar ] = $_GET[ $wpvar ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended } elseif ( isset( $perma_query_vars[ $wpvar ] ) ) { $this->wp->query_vars[ $wpvar ] = $perma_query_vars[ $wpvar ]; } elseif ( isset( $parsed_query[ $wpvar ] ) ) { $this->wp->query_vars[ $wpvar ] = $parsed_query[ $wpvar ]; } if ( ! empty( $this->wp->query_vars[ $wpvar ] ) ) { if ( ! is_array( $this->wp->query_vars[ $wpvar ] ) ) { $this->wp->query_vars[ $wpvar ] = (string) $this->wp->query_vars[ $wpvar ]; } else { foreach ( $this->wp->query_vars[ $wpvar ] as $vkey => $v ) { if ( is_scalar( $v ) ) { $this->wp->query_vars[ $wpvar ][ $vkey ] = (string) $v; } } } if ( isset( $post_type_query_vars[ $wpvar ] ) ) { $this->wp->query_vars['post_type'] = $post_type_query_vars[ $wpvar ]; $this->wp->query_vars['name'] = $this->wp->query_vars[ $wpvar ]; } } } // Restore explicit slug when resolving by slug (e.g. idType: SLUG), so percent-encoded slugs // from the client are not overwritten by decoded values from rewrite rules (issue #3582). if ( null !== $saved_name ) { $this->wp->query_vars['name'] = $saved_name; } // Convert urldecoded spaces back into '+'. foreach ( get_taxonomies( [ 'show_in_graphql' => true ], 'objects' ) as $t ) { if ( $t->query_var && isset( $this->wp->query_vars[ $t->query_var ] ) ) { $this->wp->query_vars[ $t->query_var ] = str_replace( ' ', '+', $this->wp->query_vars[ $t->query_var ] ); } } // Limit publicly queried post_types to those that are publicly_queryable if ( isset( $this->wp->query_vars['post_type'] ) ) { $queryable_post_types = get_post_types( [ 'show_in_graphql' => true ] ); if ( ! is_array( $this->wp->query_vars['post_type'] ) ) { if ( ! in_array( $this->wp->query_vars['post_type'], $queryable_post_types, true ) ) { unset( $this->wp->query_vars['post_type'] ); } } else { $this->wp->query_vars['post_type'] = array_intersect( $this->wp->query_vars['post_type'], $queryable_post_types ); } } // Resolve conflicts between posts with numeric slugs and date archive queries. $this->wp->query_vars = wp_resolve_numeric_slug_conflicts( $this->wp->query_vars ); foreach ( (array) $this->wp->private_query_vars as $var ) { if ( isset( $this->wp->extra_query_vars[ $var ] ) ) { $this->wp->query_vars[ $var ] = $this->wp->extra_query_vars[ $var ]; } } if ( isset( $error ) ) { $this->wp->query_vars['error'] = $error; } // if the parsed url is ONLY a query, unset the pagename query var if ( isset( $this->wp->query_vars['pagename'], $parsed_url['query'] ) && ( $parsed_url['query'] === $this->wp->query_vars['pagename'] ) ) { unset( $this->wp->query_vars['pagename'] ); } /** * Filters the array of parsed query variables. * * @param array $query_vars The array of requested query variables. * * @since 2.1.0 */ $this->wp->query_vars = apply_filters( 'request', $this->wp->query_vars ); // We don't need the GraphQL args anymore. unset( $this->wp->query_vars['graphql'] ); // CRITICAL FIX: Prevent REST API from processing requests during GraphQL execution // // If we're processing a GraphQL request and WordPress has identified this URI as a // REST API route (rest_route is set in query_vars), we must prevent REST API from // processing it. REST API hooks into parse_request and will output JSON and exit, // breaking the GraphQL response. // // IMPORTANT: This fix is critical and was confirmed in production. Removing this // code will cause REST API JSON responses to be returned instead of GraphQL responses // when nodeByUri queries use REST API endpoint URIs. // // We use is_graphql_request() instead of Router::get_request() to ensure the fix // applies to all GraphQL requests, including internal calls via graphql() function, // not just HTTP-routed requests. // // Regression test: testRestRouteIsRemovedFromQueryVarsDuringGraphQLRequest() // See: https://github.com/wp-graphql/wp-graphql/issues/3513 if ( is_graphql_request() && isset( $this->wp->query_vars['rest_route'] ) ) { unset( $this->wp->query_vars['rest_route'] ); } do_action_ref_array( 'parse_request', [ &$this->wp ] ); return $uri; } /** * Checks if the node type is set in the query vars and, if so, whether it matches the node type. * * @param string $node_type The node type to check. */ protected function is_valid_node_type( string $node_type ): bool { return ! isset( $this->wp->query_vars['nodeType'] ) || $this->wp->query_vars['nodeType'] === $node_type; } /** * Determines whether the parsed request is for the site's home page. * * After parse_request() strips the home path, a request for the home page has an * empty `$wp->request`. This is how the home URL is recognized when WordPress is * installed in a subdirectory and the full home URL (e.g. `/blog/`) is requested, * rather than relying on a literal '/' uri (#3775). * * We require that permalink parsing actually ran (`did_permalink`) so plain * permalink installs keep relying on the literal '/' check, and we bail if any * query var that identifies a specific node or archive is present. */ protected function is_home_request(): bool { // Only infer the home page from the parsed request when permalink parsing ran. if ( empty( $this->wp->did_permalink ) ) { return false; } // A home request has no remaining path after the home path is stripped. if ( ! empty( $this->wp->request ) ) { return false; } // Bail if a query var that identifies specific content or an archive is set // (for example a query-string request like `/subdir/?p=5` or `/subdir/?cat=2`). $content_query_vars = [ 'p', 'page_id', 'name', 'pagename', 'attachment', 'attachment_id', 'cat', 'category_name', 'tag', 'tag_id', 'author', 'author_name', 'year', 'monthnum', 'day', 'hour', 'minute', 'second', 'm', 'w', 's', 'feed', ]; foreach ( $content_query_vars as $var ) { if ( ! empty( $this->wp->query_vars[ $var ] ) ) { return false; } } return true; } /** * Resolves the home page. * * If the homepage is a static page, return the page, otherwise we return the Posts `ContentType`. * * @todo Replace `ContentType` with an `Archive` type. */ protected function resolve_home_page(): ?Deferred { $page_id = get_option( 'page_on_front', 0 ); $show_on_front = get_option( 'show_on_front', 'posts' ); // If the homepage is a static page, return the page. if ( 'page' === $show_on_front && ! empty( $page_id ) ) { $page = get_post( $page_id ); if ( empty( $page ) ) { return null; } return $this->context->get_loader( 'post' )->load_deferred( $page->ID ); } // If the homepage is set to latest posts, we need to make sure not to resolve it when when for other types. if ( ! $this->is_valid_node_type( 'ContentType' ) ) { return null; } // We dont have an 'Archive' type, so we resolve to the ContentType. return $this->context->get_loader( 'post_type' )->load_deferred( 'post' ); } /** * Checks if the URI is a comment URI and, if so, returns the comment ID. * * @param string $uri The URI to check. */ protected function maybe_parse_comment_uri( string $uri ): ?int { $comment_match = []; // look for a #comment-{$id} anywhere in the uri. if ( preg_match( '/#comment-(\d+)/', $uri, $comment_match ) ) { $comment_id = absint( $comment_match[1] ); return ! empty( $comment_id ) ? $comment_id : null; } return null; } }