*/ public static function routes(): array { $base = '/' . AgentAdapt_Urls::BASE; $home = AgentAdapt_Urls::home_path(); $routes = array( // RFC 8414 path-inserted location for the path issuer, AND the naive // "{issuer}/.well-known/..." location some clients build instead. // Identical bodies (PLAN.md §2). '/.well-known/oauth-authorization-server' . $home . $base => array( 'AgentAdapt_Discovery', 'serve_as_metadata' ), $home . $base . '/.well-known/oauth-authorization-server' => array( 'AgentAdapt_Discovery', 'serve_as_metadata' ), $home . $base . '/oauth/token' => array( 'AgentAdapt_Token_Endpoint', 'handle' ), $home . $base . '/oauth/revoke' => array( 'AgentAdapt_Revocation', 'handle' ), $home . $base . '/oauth/authorize' => array( 'AgentAdapt_Consent', 'handle' ), $home . $base . '/oauth/device' => array( 'AgentAdapt_Device', 'authorize' ), $home . $base . '/device' => array( 'AgentAdapt_Device', 'page' ), $home . $base . '/browser-session' => array( 'AgentAdapt_Browser_Session', 'handle' ), $home . $base . '/feedback' => array( 'AgentAdapt_Feedback', 'handle' ), ); $routes['/.well-known/poppy.json'] = array( 'AgentAdapt_Discovery', 'serve_poppy' ); /** * Protocol routes. Premium modules add theirs here. * * @param array $routes Path → handler. */ $routes = (array) apply_filters( 'agentadapt_routes', $routes ); // WordPress under a folder never sees the domain's own /.well-known/ // paths. The store's web server or CDN redirects each of them to the // same path under the folder, on the same host, so poppy.json still // speaks for that host (spec 3.1), and the store answers there too. // Site Health lists exactly these redirects (root_paths()). if ( '' !== $home ) { foreach ( self::root_paths( $routes ) as $path ) { $routes[ $home . $path ] = $routes[ $path ]; } } return $routes; } /** * Prefix routes: a path that ends in an identifier, such as a handoff * link's token. Each prefix ends in '/', and its handler receives the rest * of the path (never empty). Exact routes win over prefixes. * * @return array Prefix → handler( string $rest ). */ public static function prefixes(): array { $base = AgentAdapt_Urls::home_path() . '/' . AgentAdapt_Urls::BASE; $prefixes = array( $base . '/cart/' => array( 'AgentAdapt_Handoff', 'redeem' ), ); /** * Protocol routes that end in an identifier. Premium modules add * theirs here. Each key is a path ending in '/'; its handler gets the * rest of the path. * * @param array $prefixes Prefix → handler( string $rest ). */ $prefixes = (array) apply_filters( 'agentadapt_route_prefixes', $prefixes ); return array_filter( $prefixes, static fn( $call, $prefix ) => is_string( $prefix ) && str_ends_with( $prefix, '/' ) && is_callable( $call ), ARRAY_FILTER_USE_BOTH ); } /** * The routes that live at the domain's root, under /.well-known/: the * ones a store installed under a folder needs its web server to redirect. * * @param array|null $routes Routes (default: all of them). * @return string[] Paths from the domain root. */ public static function root_paths( ?array $routes = null ): array { $routes = $routes ?? self::routes(); return array_values( array_filter( array_keys( $routes ), static fn( $path ) => str_starts_with( (string) $path, '/.well-known/' ) ) ); } /** * Dispatch a matching request and stop; otherwise let WordPress continue. */ public static function dispatch(): void { $path = self::request_path(); if ( '' === $path ) { return; } $routes = self::routes(); $call = null; if ( isset( $routes[ $path ] ) ) { $call = static fn() => call_user_func( $routes[ $path ] ); } else { foreach ( self::prefixes() as $prefix => $handler ) { if ( str_starts_with( $path, $prefix ) && strlen( $path ) > strlen( $prefix ) ) { $rest = substr( $path, strlen( $prefix ) ); $call = static fn() => call_user_func( $handler, $rest ); break; } } } if ( null === $call ) { return; } // Protocol responses are never cached by page caches, whatever the // handler later says in its own Cache-Control. if ( ! defined( 'DONOTCACHEPAGE' ) ) { // The cache plugins' shared convention, not ours to prefix. define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound } $call(); } /** * The request path, without query string or a trailing slash. */ public static function request_path(): string { $uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- matched against a fixed table, never output. $path = (string) wp_parse_url( (string) $uri, PHP_URL_PATH ); return '/' === $path ? $path : rtrim( $path, '/' ); } }