bool, * 'body' => string (max MAX_UPSTREAM_BYTES, '' if has_external = false), * 'content_type' => string, * 'checked_at' => int (unix ts), * ) * * Returns null if not yet probed (caller should serve the default * behavior — replace — and let the cron pick it up). */ public static function get_result( $path ) { $key = self::TRANSIENT_PREFIX . md5( $path ); $val = get_transient( $key ); return is_array( $val ) ? $val : null; } /** * Force a fresh probe for the given path. Always writes a transient * (success or failure) so we don't hammer the merchant's own host. */ public static function probe( $path ) { $url = trailingslashit( home_url( '/' ) ) . ltrim( $path, '/' ); $response = wp_remote_get( $url, array( 'timeout' => self::PROBE_TIMEOUT, 'redirection' => 2, 'sslverify' => false, // Localhost dev / staging may not trust own cert; non-blocking. 'headers' => array( self::PROBE_HEADER => self::PROBE_HEADER_VALUE, 'User-Agent' => 'agentic-commerce-for-woocommerce/' . XPAY_WC_VERSION . ' (emitter-probe)', ), ) ); $key = self::TRANSIENT_PREFIX . md5( $path ); if ( is_wp_error( $response ) ) { set_transient( $key, array( 'has_external' => false, 'body' => '', 'content_type' => '', 'checked_at' => time(), 'error' => $response->get_error_code(), ), self::CACHE_FAIL_SECONDS ); return false; } $code = (int) wp_remote_retrieve_response_code( $response ); $body = (string) wp_remote_retrieve_body( $response ); $content_type = (string) wp_remote_retrieve_header( $response, 'content-type' ); $len = strlen( $body ); // HTML guard. Hybrid/headless storefronts and SPA catch-all routes answer // 200 with an HTML page for EVERY path — including /llms.txt and the // /.well-known/* files nothing actually serves. Without this, the probe // captures that HTML page as "external upstream content" and we prepend a // wall of markup above our llms.txt sections (and skip-if-external would // wrongly suppress our JSON emitters). None of our discovery files are // HTML, so an HTML response is never a real external emitter for them. $looks_html = ( false !== stripos( $content_type, 'text/html' ) || 1 === preg_match( '/^(?:\xEF\xBB\xBF)?\s*<(?:!doctype|html|head|body|\?xml)\b/i', $body ) ); $looks_external = ( $code >= 200 && $code < 300 && $len > 0 && $len <= self::MAX_UPSTREAM_BYTES && ! $looks_html && false === strpos( $body, self::SELF_FINGERPRINT ) ); $result = array( 'has_external' => $looks_external, 'body' => $looks_external ? $body : '', 'content_type' => $looks_external ? $content_type : '', 'http_code' => $code, 'bytes' => $len, 'checked_at' => time(), ); set_transient( $key, $result, self::CACHE_SECONDS ); return $looks_external; } /** * Register a daily cron to keep the probe cache warm. Triggered from * Xpay_Plugin::__construct(). Idempotent. */ public static function register_cron() { if ( ! wp_next_scheduled( self::CRON_HOOK ) ) { wp_schedule_event( time() + 5 * MINUTE_IN_SECONDS, 'daily', self::CRON_HOOK ); } add_action( self::CRON_HOOK, array( __CLASS__, 'cron_refresh_all' ) ); } public static function unregister_cron() { $ts = wp_next_scheduled( self::CRON_HOOK ); if ( $ts ) { wp_unschedule_event( $ts, self::CRON_HOOK ); } } /** * Cron callback. Re-probes every registered emitter path. Cheap — * 4 HTTP self-fetches with a 3s cap each, runs once per day. */ public static function cron_refresh_all() { $paths = self::known_paths(); foreach ( $paths as $path ) { self::probe( $path ); } } public static function known_paths() { return array( '/llms.txt', '/agents.md', '/.well-known/ucp', '/.well-known/oauth-protected-resource', '/.well-known/agent-card.json', ); } /** * Convenience: prime the cache for all paths on activation so the * first real request after install sees a non-null result. */ public static function prime_on_activation() { // Defer to a one-off cron a few seconds out so activation doesn't // block on the HTTP self-fetch (some hosts proxy WP through a // reverse proxy that's strict about same-request loops). wp_schedule_single_event( time() + 10, self::CRON_HOOK ); } }