<?php
// File: modules/ssl-audit-endpoint.php
// REST: GET /wp-json/myaio/v1/ssl-audit?url=https://example.com
// Auth: x-api-key header (preferred) OR ?key=...

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

add_action('rest_api_init', function () {
    register_rest_route('myaio/v1', '/ssl-audit', [
        'methods'  => 'GET',
        'callback' => 'myaio_sac_ssl_audit',
        'permission_callback' => 'myaio_sac_rest_permission_check', // we return our own 401 for consistency
    ]);
});

function myaio_sac_ssl_audit(WP_REST_Request $request) {

    // ✅ Consistent plugin-wide API key validation (uses sac-utils.php)
    if (function_exists('myaio_sac_require_api_key_or_401')) {
        $auth = myaio_sac_require_api_key_or_401($request);
        if ($auth !== true) return $auth;
    }

    $target = trim((string) $request->get_param('url')) ?: site_url('/');
    $parsed = wp_parse_url($target);
    if (!$parsed || empty($parsed['host'])) {
        return new WP_REST_Response(['error' => 'Invalid URL'], 400);
    }

    $host = $parsed['host'];
    $path = isset($parsed['path']) ? $parsed['path'] : '/';

    $https_url = 'https://' . $host . $path;
    $http_url  = 'http://'  . $host . $path;

    // 1) Certificate details & validity
    $cert_info = myaio_sac_ssl_get_certificate_info($host, 443);

    // 2) HEAD checks (no automatic redirects for redirect logic)
    $https_head = myaio_sac_ssl_fetch_headers($https_url, 0);
    $http_head  = myaio_sac_ssl_fetch_headers($http_url, 0);

    // 3) HSTS & cookies (fetch final HTTPS response allowing redirects)
    $https_final = myaio_sac_ssl_fetch_headers($https_url, 5);

    // 4) Mixed content + link counts (fetch HTML)
    $https_get   = myaio_sac_ssl_fetch_body($https_url);
    $link_count  = myaio_sac_ssl_count_links($https_get['body']);

    // Analyses
    $hsts      = myaio_sac_ssl_check_hsts($https_final['headers']);
    $redirects = myaio_sac_ssl_check_redirects($http_head, $https_head);
    $mixed     = myaio_sac_ssl_check_mixed_content($https_get['body']);
    $cookies   = myaio_sac_ssl_check_cookie_flags($https_final['headers']);

    $issues = [];
    if (!$cert_info['valid']) {
        $issues[] = 'Certificate invalid (date range, host mismatch, or retrieval failed).';
    }
    if ($cert_info['days_to_expiry'] !== null && $cert_info['days_to_expiry'] <= 30) {
        $issues[] = 'Certificate expires within 30 days.';
    }
    if (!$redirects['http_to_https']) {
        $issues[] = 'HTTP does not permanently redirect (301/308) to HTTPS.';
    }
    if (!$hsts['present']) {
        $issues[] = 'HSTS header missing.';
    }
    if ($mixed['insecure_references'] > 0) {
        $issues[] = 'Mixed content detected (HTTP resources on HTTPS pages).';
    }
    if ($cookies['insecure_cookies'] > 0) {
        $issues[] = 'Insecure cookies (missing Secure and/or SameSite).';
    }

    return rest_ensure_response([
        'summary' => [
            'target'    => $target,
            'host'      => $host,
            'compliant' => empty($issues),
            'issues'    => $issues,
        ],
        'certificate' => $cert_info,
        'redirects'   => $redirects,
        'hsts'        => $hsts,
        'cookies'     => $cookies,
        'mixed'       => $mixed,
        'http'        => [ 'head' => $http_head ],
        'https'       => [
            'head'  => $https_head,
            'final' => $https_final,
            'sample'=> [ 'status' => $https_get['status'], 'url' => $https_get['url'] ]
        ],
        'links' => [
            'http' => [
                'count' => $link_count['http_links'],
                'urls'  => $link_count['examples_http'],
            ],
            'https' => [
                'count' => $link_count['https_links'],
                'urls'  => $link_count['examples_https'],
            ],
        ]
    ]);
}

/* ===================== Helpers ===================== */

if (!function_exists('myaio_sac_ssl_fetch_headers')) {
function myaio_sac_ssl_fetch_headers($url, $redirection = 0) {
    $res = wp_remote_head($url, [
        'timeout'     => 15,
        'redirection' => $redirection,
        'sslverify'   => true,
    ]);
    if (is_wp_error($res)) {
        return [ 'ok' => false, 'status' => 0, 'headers' => [], 'error' => $res->get_error_message(), 'url' => $url ];
    }
    return [
        'ok'      => true,
        'status'  => (int) wp_remote_retrieve_response_code($res),
        'headers' => wp_remote_retrieve_headers($res),
        'url'     => $url,
    ];
}}
if (!function_exists('myaio_sac_ssl_fetch_body')) {
function myaio_sac_ssl_fetch_body($url) {
    $res = wp_remote_get($url, [
        'timeout'     => 20,
        'redirection' => 5,
        'sslverify'   => true,
    ]);
    if (is_wp_error($res)) {
        return [ 'ok' => false, 'status' => 0, 'url' => $url, 'body' => '', 'error' => $res->get_error_message() ];
    }
    return [
        'ok'     => true,
        'status' => (int) wp_remote_retrieve_response_code($res),
        'url'    => $url,
        'body'   => (string) wp_remote_retrieve_body($res),
    ];
}}
if (!function_exists('myaio_sac_ssl_check_redirects')) {
function myaio_sac_ssl_check_redirects($http_head, $https_head) {
    $http_status = (int) ($http_head['status'] ?? 0);
    $loc = myaio_sac_ssl_header_value($http_head['headers'] ?? [], 'location');

    $http_to_https = false;
    if (in_array($http_status, [301, 308], true) && is_string($loc) && stripos($loc, 'https://') === 0) {
        $http_to_https = true;
    }

    $https_status = (int) ($https_head['status'] ?? 0);
    $https_ok = ($https_status >= 200 && $https_status < 400);

    return [
        'http_status'   => $http_status,
        'http_location' => $loc,
        'http_to_https' => $http_to_https,
        'https_status'  => $https_status,
        'https_ok'      => $https_ok,
    ];
}}
if (!function_exists('myaio_sac_ssl_check_hsts')) {
function myaio_sac_ssl_check_hsts($headers) {
    $hsts = myaio_sac_ssl_header_value($headers, 'strict-transport-security');
    $present = is_string($hsts) && strlen($hsts) > 0;

    $maxage = null; $include_sub = false; $preload = false;
    if ($present) {
        if (preg_match('/max-age\s*=\s*(\d+)/i', $hsts, $m)) $maxage = (int) $m[1];
        $include_sub = (stripos($hsts, 'includesubdomains') !== false);
        $preload     = (stripos($hsts, 'preload') !== false);
    }

    return [
        'present' => $present,
        'value' => $hsts,
        'max_age' => $maxage,
        'include_subdomains' => $include_sub,
        'preload' => $preload
    ];
}}
if (!function_exists('myaio_sac_ssl_check_cookie_flags')) {
function myaio_sac_ssl_check_cookie_flags($headers) {
    $set_cookies = myaio_sac_ssl_header_values($headers, 'set-cookie');
    $total = count($set_cookies);

    $insecure = 0; $samples = [];
    foreach ($set_cookies as $c) {
        $has_secure   = (stripos($c, ' secure') !== false) || preg_match('/;\s*secure(=|;|$)/i', $c);
        $has_samesite = (stripos($c, ' samesite') !== false);

        if (!$has_secure || !$has_samesite) {
            $insecure++;
            if (count($samples) < 5) $samples[] = $c;
        }
    }

    return [
        'total_cookies' => $total,
        'insecure_cookies' => $insecure,
        'samples' => $samples
    ];
}}
if (!function_exists('myaio_sac_ssl_check_mixed_content')) {
function myaio_sac_ssl_check_mixed_content($html) {
    if (!is_string($html) || $html === '') return [ 'insecure_references' => 0, 'examples' => [] ];

    libxml_use_internal_errors(true);
    $dom = new DOMDocument();
    @$dom->loadHTML($html);
    $xpath = new DOMXPath($dom);

    $count = 0; $examples = [];

    foreach (['src','href','action','data','poster'] as $att) {
        $nodes = $xpath->query("//*[@$att]");
        foreach ($nodes as $node) {
            $val = trim($node->getAttribute($att));
            if (stripos($val, 'http://') === 0) {
                $count++;
                if (count($examples) < 10) $examples[] = $val;
            }
        }
    }

    // srcset
    $imgs = $dom->getElementsByTagName('img');
    foreach ($imgs as $img) {
        if ($img->hasAttribute('srcset')) {
            $srcset = $img->getAttribute('srcset');
            foreach (preg_split('/\s*,\s*/', $srcset) as $candidate) {
                $url = trim(preg_split('/\s+/', $candidate)[0]);
                if ($url && stripos($url, 'http://') === 0) {
                    $count++;
                    if (count($examples) < 10) $examples[] = $url;
                }
            }
        }
    }

    return [ 'insecure_references' => $count, 'examples' => $examples ];
}}
if ( ! function_exists( 'myaio_sac_ssl_get_certificate_info' ) ) {

    function myaio_sac_ssl_get_certificate_info( $host, $port = 443 ) {
        $result = [
            'valid'          => false,
            'subject_cn'     => null,
            'issuer_cn'      => null,
            'sans'           => [],
            'valid_from'     => null,
            'valid_to'       => null,
            'days_to_expiry' => null,
            'error'          => null,
        ];

        $host = strtolower( trim( (string) $host ) );
        $port = (int) $port;

        if ( empty( $host ) ) {
            $result['error'] = 'Empty host';
            return $result;
        }

        // Build URL. If port is 443, keep clean URL.
        $url = ( 443 === $port )
            ? "https://{$host}/"
            : "https://{$host}:{$port}/";

        /**
         * WordPress HTTP API call.
         * - sslverify true ensures invalid certs produce WP_Error.
         * - reject_unsafe_urls prevents weird wrappers.
         */
        $args = [
            'timeout'            => 10,
            'redirection'        => 0,
            'sslverify'          => true,
            'reject_unsafe_urls' => true,
            // Ask underlying transport for cert info (supported on many cURL setups).
            'sslcertificates'    => null, // keep default
        ];

        $response = wp_safe_remote_get( $url, $args );

        if ( is_wp_error( $response ) ) {
            // Connection failed OR SSL validation failed.
            $result['error'] = $response->get_error_message();
            return $result;
        }

        // If we got here, TLS verified successfully (host+date+chain were acceptable).
        $result['valid'] = true;

        /**
         * Try to extract certificate details if transport provides it.
         * Depending on WP/Requests/cURL, the certificate details may be available.
         */
        $certinfo = null;

        // Requests sometimes exposes cert info through response object internals;
        // wp_remote_get returns array. We can try to read underlying data if present.
        if ( isset( $response['http_response'] ) && is_object( $response['http_response'] ) ) {
            // WP_HTTP_Requests_Response
            $raw = $response['http_response'];

            if ( method_exists( $raw, 'get_response_object' ) ) {
                $obj = $raw->get_response_object();
                // Some environments expose cert info here.
                if ( is_object( $obj ) && isset( $obj->certinfo ) ) {
                    $certinfo = $obj->certinfo;
                }
            }
        }

        // If certinfo exists, map fields (best-effort).
        if ( is_array( $certinfo ) ) {
            // These keys vary by environment; keep it defensive.
            if ( ! empty( $certinfo['subject']['CN'] ) ) {
                $result['subject_cn'] = (string) $certinfo['subject']['CN'];
            }
            if ( ! empty( $certinfo['issuer']['CN'] ) ) {
                $result['issuer_cn'] = (string) $certinfo['issuer']['CN'];
            }

            // SANs may be available as a string or array.
            if ( ! empty( $certinfo['subjectAltName'] ) && is_string( $certinfo['subjectAltName'] ) ) {
                $sans = array_map( 'trim', explode( ',', $certinfo['subjectAltName'] ) );
                $sans = array_values( array_filter( array_map( static function ( $e ) {
                    return preg_replace( '/^DNS:\s*/i', '', $e );
                }, $sans ) ) );
                $result['sans'] = $sans;
            }

            // Validity times sometimes appear as timestamps or strings.
            if ( ! empty( $certinfo['validFrom_time_t'] ) ) {
                $from = (int) $certinfo['validFrom_time_t'];
                $result['valid_from'] = gmdate( 'c', $from );
            }
            if ( ! empty( $certinfo['validTo_time_t'] ) ) {
                $to = (int) $certinfo['validTo_time_t'];
                $result['valid_to'] = gmdate( 'c', $to );

                $now = time();
                $result['days_to_expiry'] = ( $to > $now ) ? (int) floor( ( $to - $now ) / 86400 ) : 0;
            }
        }

        return $result;
    }
}

if (!function_exists('myaio_sac_ssl_host_matches_cert')) {
function myaio_sac_ssl_host_matches_cert($host, $names) {
    $host = strtolower($host);
    foreach ($names as $name) {
        $name = strtolower($name);
        if ($name === $host) return true;

        if (strpos($name, '*.') === 0) {
            $suffix = substr($name, 1); // .example.com
            if (substr($host, -strlen($suffix)) === $suffix) return true;
        }
    }
    return false;
}}
if (!function_exists('myaio_sac_ssl_header_value')) {
function myaio_sac_ssl_header_value($headers, $key) {
    if (empty($headers)) return null;

    if (is_object($headers) && method_exists($headers, 'offsetGet')) {
        $val = $headers->offsetGet($key);
        if ($val !== null) return is_array($val) ? (string) reset($val) : $val;
    }

    foreach ((array) $headers as $k => $v) {
        if (strtolower($k) === strtolower($key)) {
            return is_array($v) ? (string) reset($v) : $v;
        }
    }

    return null;
}}
if (!function_exists('myaio_sac_ssl_header_values')) {
function myaio_sac_ssl_header_values($headers, $key) {
    $values = [];
    foreach ((array) $headers as $k => $v) {
        if (strtolower($k) === strtolower($key)) {
            if (is_array($v)) $values = array_merge($values, $v);
            else $values[] = $v;
        }
    }
    return $values;
}}
if (!function_exists('myaio_sac_ssl_count_links')) {
function myaio_sac_ssl_count_links($html) {
    $result = [
        'http_links'  => 0,
        'https_links' => 0,
        'examples_http'  => [],
        'examples_https' => []
    ];
    if (!is_string($html) || trim($html) === '') return $result;

    libxml_use_internal_errors(true);
    $dom = new DOMDocument();
    @$dom->loadHTML($html);
    $xpath = new DOMXPath($dom);

    foreach (['src','href','action','data','poster'] as $att) {
        $nodes = $xpath->query("//*[@$att]");
        foreach ($nodes as $node) {
            $val = trim($node->getAttribute($att));

            if (stripos($val, 'https://') === 0) {
                $result['https_links']++;
                if (count($result['examples_https']) < 10) $result['examples_https'][] = $val;
            } elseif (stripos($val, 'http://') === 0) {
                $result['http_links']++;
                if (count($result['examples_http']) < 10) $result['examples_http'][] = $val;
            }
        }
    }

    return $result;
}}
