<?php
/**
 * IndexNow ownership proof — virtual `<key>.txt` in the site root.
 *
 * IndexNow verifies that a site may submit URLs for its host by fetching a text
 * file named after the key and checking that the file contains exactly that
 * key. The file is served from the site root, because a key file in a
 * subdirectory limits what may be submitted to that subdirectory and everything
 * under it (protocol, "Option 2").
 *
 * Nothing is written to disk. This uses the proven init + parse_request
 * priority-0 interception pattern (the same mechanism as
 * includes/well-known-verify.php and includes/sitemap.php), so it needs no
 * rewrite rule and no flush on activation.
 *
 * Only the one path that matches the stored key is answered. Every other .txt
 * in the root falls through to WordPress untouched and 404s as it always did —
 * this is not a generic text-file handler.
 *
 * @package TamRank
 */

if (!defined('ABSPATH')) {
    exit;
}

class TamRank_IndexNow_Key_File
{
    public function __construct()
    {
        // Priority 0 + both hooks so we intercept before WordPress routes the
        // request (mirrors well-known-verify.php / sitemap.php).
        add_action('init', array($this, 'handle'), 0);
        add_action('parse_request', array($this, 'handle'), 0);
    }

    /**
     * Serve the key as plaintext when the request is for this site's key file.
     *
     * @return void
     */
    public function handle()
    {
        static $checked = false;
        if ($checked) {
            return;
        }
        $checked = true;

        if (!class_exists('TamRank_IndexNow')) {
            return;
        }

        $key = TamRank_IndexNow::get_key();
        if ($key === '') {
            // No key minted (IndexNow was never switched on) — nothing to serve.
            return;
        }

        // Deliberately NOT gated on the enable_indexnow setting. A key is only
        // minted when the user switches IndexNow on, and once URLs have been
        // submitted the search engines fetch this file on their own schedule to
        // verify the key. Pulling it the moment someone toggles the feature off
        // would turn an in-flight verification into a 403 for URLs that were
        // already accepted. What stays reachable is a random token and nothing
        // else — it grants no access to this site.

        $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        $path        = wp_parse_url($request_uri, PHP_URL_PATH);
        if (!is_string($path) || $path === '') {
            return;
        }

        // Build the expected path from home_url() so a subdirectory install
        // serves the file at its own root, where keyLocation points.
        $home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH);
        $expected  = rtrim($home_path, '/') . '/' . $key . '.txt';

        // Constant-time compare: the key is a shared secret between this site
        // and the search engines, and this handler is reachable by anyone.
        if (!hash_equals($expected, $path)) {
            return;
        }

        // Never let a page cache / CDN serve this. nocache_headers() covers the
        // HTTP layer; DONOTCACHEPAGE asks page-cache drop-ins to skip it.
        if (!defined('DONOTCACHEPAGE')) {
            define('DONOTCACHEPAGE', true); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress-ecosystem standard constant read by caching plugins; must not be prefixed.
        }

        // Drain any output buffer a pre-init BOM / PHP notice / stray
        // early-loading plugin may have opened, so the body is EXACTLY the key
        // with nothing prepended — the search engine compares the file content
        // to the key, and a single leading byte fails that comparison.
        while (ob_get_level() > 0) {
            ob_end_clean();
        }

        if (!headers_sent()) {
            nocache_headers();
            // Strip headers WordPress may have queued so the fetcher sees a
            // clean plaintext body.
            header_remove('X-Pingback');
            header_remove('Link');
        }

        status_header(200);
        header('Content-Type: text/plain; charset=UTF-8');
        header('X-Robots-Tag: noindex');
        header('Content-Length: ' . strlen($key));
        // Raw, single value, no trailing newline. The file must contain the key
        // and nothing else; HTML-escaping or markup would corrupt the match.
        echo $key; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text/plain body, value is validated against /^[A-Za-z0-9-]{8,128}$/ at read time
        exit;
    }
}

// Initialize at file-load time (this file is required early, before 'init') so
// both the init@0 and parse_request@0 hooks register before those actions
// dispatch — same proven pattern as TamRank_WellKnown_Verify.
new TamRank_IndexNow_Key_File();
