<?php
/**
 * SSRF-safe wp_remote_* wrapper for any caller that fetches a URL
 * derived from user input (webhooks, oEmbed-style probes, link checkers,
 * etc.). Built as a shared helper so each plugin doesn't re-roll the
 * gate and forget the cloud-metadata IPs.
 *
 * NOT for hardcoded calls to api.avakode.com / hooks.slack.com — those
 * are already trusted and adding a DNS roundtrip is wasted latency.
 *
 * What it blocks:
 * - non-http(s) schemes (file://, gopher://, ftp://, …)
 * - obvious loopback hostnames (localhost, 127.0.0.1, ::1, 0.0.0.0)
 * - hostnames that fail DNS resolution
 * - IPv4 ranges flagged by FILTER_FLAG_NO_PRIV_RANGE (RFC 1918) and
 * FILTER_FLAG_NO_RES_RANGE (loopback, link-local, multicast,
 * broadcast, AWS-style 169.254.169.254 metadata)
 *
 * Independent security review 2026-05-26 (HIGH-Sec #5): SSRF surface in
 * plugins via wp_remote_* with user-controlled URL. RankForge already
 * has a DNS-pinned variant (`RANKFORGE_Core::fetch_safe_remote_head`)
 * that's stricter; this helper is the floor for everyone else.
 *
 * This file is a SHARED file (copy-on-edit). When editing, copy the
 * updated version into rankforge/, formforge/, fieldforge/ as well.
 */

defined('ABSPATH') || exit;

if (!class_exists('Forge_Net')) {

class Forge_Net {

    /**
 * Verify a URL is safe to fetch from the server side. Rejects:
 * non-http(s) schemes, loopback, RFC1918, link-local, reserved.
 *
 * @param string $url
 * @return bool
 */
    public static function is_safe_url($url) {
        if (!is_string($url) || $url === '') return false;
        if (!filter_var($url, FILTER_VALIDATE_URL)) return false;

        $parsed = wp_parse_url($url);
        if (!is_array($parsed)) return false;

        $scheme = strtolower($parsed['scheme'] ?? '');
        $host   = strtolower($parsed['host'] ?? '');
        if (!in_array($scheme, ['http', 'https'], true)) return false;
        if ($host === '') return false;

        $blocked_hosts = ['localhost', '127.0.0.1', '::1', '0.0.0.0', 'metadata.google.internal'];
        if (in_array($host, $blocked_hosts, true)) return false;

        // If the host is itself an IP literal, validate it directly. Otherwise
        // resolve via DNS — if resolution fails, refuse rather than guess.
        if (filter_var($host, FILTER_VALIDATE_IP)) {
            $ip = $host;
        } else {
            $ip = gethostbyname($host);
            if ($ip === $host) return false; // unresolved
        }

        $public = filter_var(
            $ip,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
        );
        return $public !== false;
    }

    /**
 * Safe wrapper around wp_remote_get. Returns WP_Error 'forge_net_ssrf'
 * if the URL fails the safety gate.
 */
    public static function safe_remote_get($url, $args = []) {
        if (!self::is_safe_url($url)) {
            return new WP_Error('forge_net_ssrf', __('Refused to fetch a non-public URL.', 'rankforge'));
        }
        return wp_remote_get($url, $args);
    }

    /**
 * Safe wrapper around wp_remote_post.
 */
    public static function safe_remote_post($url, $args = []) {
        if (!self::is_safe_url($url)) {
            return new WP_Error('forge_net_ssrf', __('Refused to POST to a non-public URL.', 'rankforge'));
        }
        return wp_remote_post($url, $args);
    }

    /**
 * Safe wrapper around wp_remote_head.
 */
    public static function safe_remote_head($url, $args = []) {
        if (!self::is_safe_url($url)) {
            return new WP_Error('forge_net_ssrf', __('Refused to HEAD a non-public URL.', 'rankforge'));
        }
        return wp_remote_head($url, $args);
    }
}

} // class_exists guard
