File: /var/www/html/wp-content/plugins/wp-graphql/src/Data/NodeResolver.php
<?php
namespace WPGraphQL\Data;
use GraphQL\Deferred;
use GraphQL\Error\UserError;
use WPGraphQL\AppContext;
use WPGraphQL\Router;
use WPGraphQL\Utils\Utils;
use WP_Post;
class NodeResolver {
/**
* @var \WP
*/
protected $wp;
/**
* @var \WPGraphQL\AppContext
*/
protected $context;
/**
* @var string
*/
protected $route;
/**
* NodeResolver constructor.
*
* @param \WPGraphQL\AppContext $context
*
* @return void
*/
public function __construct( AppContext $context ) {
global $wp;
$this->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,mixed>|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,mixed>|string $extra_query_vars Any extra query vars to consider.
*/
$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 '/', we can resolve it now.
*
* We don't rely on $this->parse_request(), since the home page doesn't get a rewrite rule.
*/
if ( '/' === $uri ) {
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,mixed>|string $extra_query_vars Any extra query vars to consider.
*/
$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,mixed>|string $extra_query_vars Any extra query vars to consider.
*/
$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;
}
if ( empty( $extra_query_vars ) && isset( $this->wp->query_vars['error'] ) && '404' === $this->wp->query_vars['error'] ) {
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,mixed>|string $extra_query_vars Any extra query vars to consider.
*/
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,mixed>|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_url = wp_parse_url( site_url() );
$home_url = wp_parse_url( home_url() );
/**
* @var array<string,mixed> $home_url
* @var array<string,mixed> $site_url
*/
if ( ! in_array(
$parsed_url['host'],
[
$site_url['host'],
$home_url['host'],
],
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<string,mixed> $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;
}
/**
* 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;
}
}