$tests Site Health tests. * @return array */ public static function register( array $tests ): array { foreach ( array( 'https', 'root', 'permalinks', 'discovery', 'authorization', 'launched' ) as $id ) { $tests['direct'][ 'agentadapt_' . $id ] = array( 'label' => 'Agent Adapt', 'test' => static fn() => self::result( $id ), ); } return $tests; } /** * A diagnostic route that only reports whether an Authorization header * arrived. It never reads or returns the value. */ public static function register_probe(): void { register_rest_route( AgentAdapt_Rest::NS, '/health/authorization', array( 'methods' => 'GET', 'callback' => static fn() => new WP_REST_Response( array( 'received' => self::ECHO_VALUE === AgentAdapt_Http::header( 'Authorization' ) ), 200, array( 'Cache-Control' => 'no-store' ) ), // Public on purpose: Site Health calls it over a loopback with a // fixed dummy header. It reveals only "did a header arrive". 'permission_callback' => '__return_true', ) ); } /** * For a store under a folder: do the root addresses reach it, through the * web server's redirects? poppy.json and the OAuth metadata, each naming * this store's issuer. */ private static function root_redirects_work(): bool { $issuer = AgentAdapt_Urls::issuer(); foreach ( array( AgentAdapt_Urls::poppy_url(), AgentAdapt_Urls::as_metadata_url() ) as $url ) { $res = wp_remote_get( $url, array( 'timeout' => 5, 'redirection' => 3 ) ); $doc = is_wp_error( $res ) || 200 !== wp_remote_retrieve_response_code( $res ) ? null : json_decode( (string) wp_remote_retrieve_body( $res ), true ); $got = is_array( $doc ) ? ( $doc['auth']['issuer'] ?? $doc['issuer'] ?? null ) : null; if ( $got !== $issuer ) { return false; } } return true; } /** * Run one check. * * @param string $id Check id. * @return array{status: string, ok: bool, label: string, description: string, fix: string} */ public static function check( string $id ): array { switch ( $id ) { case 'https': $ok = 'https' === wp_parse_url( home_url( '/' ), PHP_URL_SCHEME ); return self::row( $ok, __( 'Your store is served over HTTPS', 'agent-adapt' ), __( 'Personal agents only talk to stores over HTTPS.', 'agent-adapt' ), __( 'Install a TLS certificate and change the WordPress Address and Site Address to https://.', 'agent-adapt' ) ); case 'root': $ok = AgentAdapt_Urls::well_known_served() || self::root_redirects_work(); $paths = AgentAdapt_Router::root_paths(); $home = AgentAdapt_Urls::home_path(); $pairs = array_map( static fn( $path ) => AgentAdapt_Urls::origin() . $path . ' → ' . AgentAdapt_Urls::origin() . $home . $path, $paths ); /* translators: %s: list of "from → to" redirects, one per line */ $fix = sprintf( __( 'WordPress is installed under a folder, so these addresses at the root of your domain never reach it. On your web server or CDN, redirect each one to the same path under your WordPress folder, on the same host: %s', 'agent-adapt' ), implode( '; ', $pairs ) ); return self::row( $ok, __( 'Agents reach your discovery documents at the root of your domain', 'agent-adapt' ), __( 'Personal agents look for /.well-known/poppy.json and the OAuth server metadata at the root of your domain.', 'agent-adapt' ), $fix ); case 'permalinks': $ok = '' !== (string) get_option( 'permalink_structure' ); return self::row( $ok, __( 'Pretty permalinks are on', 'agent-adapt' ), __( 'The protocol endpoints rely on WordPress receiving requests for paths that are not files.', 'agent-adapt' ), __( 'Choose any setting other than "Plain" under Settings → Permalinks.', 'agent-adapt' ) ); case 'discovery': $res = wp_remote_get( AgentAdapt_Urls::poppy_url(), array( 'timeout' => 5, 'redirection' => 3 ) ); $doc = is_wp_error( $res ) ? null : json_decode( (string) wp_remote_retrieve_body( $res ), true ); $meta = wp_remote_get( AgentAdapt_Urls::as_metadata_url(), array( 'timeout' => 5 ) ); $md = is_wp_error( $meta ) ? null : json_decode( (string) wp_remote_retrieve_body( $meta ), true ); $ok = is_array( $doc ) && ( $doc['auth']['issuer'] ?? null ) === AgentAdapt_Urls::issuer() && is_array( $md ) && ( $md['issuer'] ?? null ) === AgentAdapt_Urls::issuer(); return self::row( $ok, __( 'Your discovery documents answer', 'agent-adapt' ), __( 'poppy.json and the OAuth server metadata are fetched by every agent before anything else.', 'agent-adapt' ), __( 'A page cache, CDN or security plugin may be intercepting /.well-known/ paths. Exclude /.well-known/ and /agent-adapt/ from caching and blocking rules.', 'agent-adapt' ) ); case 'authorization': $res = wp_remote_get( rest_url( AgentAdapt_Rest::NS . '/health/authorization' ), array( 'timeout' => 5, 'headers' => array( 'Authorization' => self::ECHO_VALUE ) ) ); $ok = ! is_wp_error( $res ) && true === ( json_decode( (string) wp_remote_retrieve_body( $res ), true )['received'] ?? null ); return self::row( $ok, __( 'The Authorization header reaches WordPress', 'agent-adapt' ), __( 'Agents send their access token in the Authorization header. Some hosts strip it before PHP sees it.', 'agent-adapt' ), __( 'Add this line to .htaccess above the WordPress rules: RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] — or ask your host to pass the Authorization header to PHP.', 'agent-adapt' ) ); case 'launched': $ok = 'yes' !== get_option( 'woocommerce_coming_soon' ); return self::row( $ok, __( 'Your store is open to personal agents\' browsers', 'agent-adapt' ), __( 'While WooCommerce\'s "Coming soon" mode is on, an agent browsing your website sees only the coming-soon page, like any other visitor. Search, product details and the cart still work through the agent API.', 'agent-adapt' ), __( 'Launch your store from WooCommerce → Settings → Site visibility when you are ready.', 'agent-adapt' ), 'recommended' ); } return self::row( false, $id, '', '' ); } /** * Site Health result shape. * * @param string $id Check id. * @return array */ private static function result( string $id ): array { $c = self::check( $id ); return array( 'label' => $c['label'], 'status' => $c['status'], 'badge' => array( 'label' => 'Agent Adapt', 'color' => $c['ok'] ? 'blue' : 'orange' ), 'description' => '

' . esc_html( $c['description'] ) . '

' . ( $c['ok'] ? '' : '

' . esc_html( $c['fix'] ) . '

' ), 'test' => 'agentadapt_' . $id, ); } /** * Build a row. * * @param bool $ok Passed. * @param string $label Label. * @param string $description Why it matters. * @param string $fix How to fix. * @param string $failing Site Health status when it fails: critical or recommended. * @return array{status: string, ok: bool, label: string, description: string, fix: string} */ private static function row( bool $ok, string $label, string $description, string $fix, string $failing = 'critical' ): array { return array( 'status' => $ok ? 'good' : $failing, 'ok' => $ok, 'label' => $label, 'description' => $description, 'fix' => $fix ); } }