<?php
/**
 * TamRank robots.txt capture
 *
 * Records what the site actually serves at /robots.txt while the old SEO plugin is
 * still active. That is the only way to get the truth: all three source plugins add
 * lines at render time that are nowhere in their options (Yoast appends its Sitemap
 * directive and its deny_* blocks, AIOSEO merges its search-appearance rules), and if a
 * physical robots.txt file exists the web server serves that and WordPress never runs
 * at all.
 *
 * Why loopback HTTP and not ob_start(); do_robots(); in the AJAX request: Rank Math
 * only hooks its robots_txt filter when ! is_admin() (class-robots-txt.php:37) and
 * Yoast's integration only loads on the front end or its file-editor page
 * (src/conditionals/robots-txt-conditional.php:31-33). In an admin-AJAX request
 * do_robots() therefore returns WordPress' bare default - which is a complete, valid
 * robots.txt, so the mistake is invisible in the result.
 */

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

class TamRank_Robots_Capture {

    /** @var int Request timeout in seconds. */
    const TIMEOUT = 10;

    /**
     * Fetch the live robots.txt over loopback HTTP.
     *
     * @return array {
     *     @type bool   $ok      Whether the capture is usable.
     *     @type string $body    Captured body (empty when !ok).
     *     @type string $reason  Machine-readable failure reason.
     *     @type string $message Human-readable failure reason.
     * }
     */
    public static function fetch() {
        $url = add_query_arg( 'tamrank_capture', '1', home_url( '/robots.txt' ) );

        $response = wp_remote_get(
            $url,
            array(
                'timeout'     => self::TIMEOUT,
                'redirection' => 2,
                'sslverify'   => false, // Loopback to our own host; a self-signed local cert must not break the capture.
            )
        );

        if ( is_wp_error( $response ) ) {
            return self::failure( 'request_failed', $response->get_error_message() );
        }

        $code = (int) wp_remote_retrieve_response_code( $response );
        if ( 200 !== $code ) {
            return self::failure( 'http_' . $code, sprintf( 'HTTP %d', $code ) );
        }

        $body = (string) wp_remote_retrieve_body( $response );

        // A valid robots.txt always names at least one user agent. An HTML error page,
        // a login wall or an empty body would otherwise pass as "captured".
        if ( false === stripos( $body, 'user-agent:' ) ) {
            return self::failure( 'not_robots_txt', 'Response contained no User-agent line' );
        }

        return array(
            'ok'      => true,
            'body'    => str_replace( array( "\r\n", "\r" ), "\n", $body ),
            'reason'  => '',
            'message' => '',
        );
    }

    /**
     * Whether the live capture may be used as the primary source.
     *
     * Two measured situations where it may not:
     *
     *  - blog_public = 0. Rank Math deliberately returns WordPress' own output on a
     *    non-public site (class-robots-txt.php:50-51), so the capture is a perfectly
     *    valid robots.txt that passes every check while missing exactly the content we
     *    came for. Staging sites are often non-public, which is precisely where people
     *    try a migration first.
     *  - Rank Math with an unfinished setup. While its registration is invalid,
     *    rank-math.php:292-297 returns before the module manager is built, so the
     *    robots-txt module never exists and its filter never runs - on the front end
     *    either. The option is filled, the live output is not.
     *
     * @param string $plugin Source plugin slug.
     * @return bool
     */
    public static function is_representative( $plugin ) {
        if ( ! get_option( 'blog_public' ) ) {
            return false;
        }

        if ( 'rankmath' === $plugin && ! self::rankmath_modules_active() ) {
            return false;
        }

        return true;
    }

    /**
     * Whether Rank Math has actually booted its modules.
     *
     * @return bool
     */
    private static function rankmath_modules_active() {
        if ( defined( 'RANK_MATH_REGISTRATION_SKIP' ) && RANK_MATH_REGISTRATION_SKIP ) {
            return true;
        }
        if ( get_option( 'rank_math_registration_skip' ) ) {
            return true;
        }
        if ( class_exists( '\RankMath\Helper' ) && method_exists( '\RankMath\Helper', 'is_site_connected' ) ) {
            return (bool) \RankMath\Helper::is_site_connected();
        }

        return false;
    }

    /**
     * Build a failure payload.
     *
     * @param string $reason  Machine-readable reason.
     * @param string $message Human-readable reason.
     * @return array
     */
    private static function failure( $reason, $message ) {
        return array(
            'ok'      => false,
            'body'    => '',
            'reason'  => $reason,
            'message' => $message,
        );
    }

}
