<?php
/**
 * Must-use watchdog for the BBC connector.
 *
 * This file is intended to be copied into wp-content/mu-plugins/ by the BBC
 * guardian so that it is auto-loaded on every WordPress request and survives
 * deletion of the regular plugin. It keeps BBC active (activating it when
 * present but inactive) and reinstalls the signed plugin package when the
 * plugin files are missing. A stored revocation marker disables both
 * behaviours so the broker can retire a connector cleanly.
 */

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

if (!class_exists('BBC_Watchdog')) {
    final class BBC_Watchdog
    {
        const VERSION = 1;
        const INTERVAL = DAY_IN_SECONDS;
        const PLUGIN_REL = 'bbc/bbc.php';
        const OPTION_LAST_CHECK = 'bbc_watchdog_last_check';
        const OPTION_REVOKED = 'bbc_connector_revoked';
        const OPTION_IDENTITY = 'bbc_identity';

        public static function boot()
        {
            add_action('plugins_loaded', array(__CLASS__, 'maybe_activate'), 20);
            add_action('wp', array(__CLASS__, 'maybe_reinstall'));
        }

        public static function is_revoked()
        {
            return (bool) get_option(self::OPTION_REVOKED, false);
        }

        public static function identity()
        {
            $identity = get_option(self::OPTION_IDENTITY, array());
            return is_array($identity) ? $identity : array();
        }

        public static function plugin_file()
        {
            return WP_PLUGIN_DIR . '/' . self::PLUGIN_REL;
        }

        public static function plugin_active()
        {
            if (!function_exists('is_plugin_active')) {
                require_once ABSPATH . 'wp-admin/includes/plugin.php';
            }
            return is_plugin_active(self::PLUGIN_REL);
        }

        public static function maybe_activate()
        {
            if (self::is_revoked()) {
                return;
            }
            if (self::plugin_active()) {
                return;
            }
            if (!file_exists(self::plugin_file())) {
                return;
            }
            $active = (array) get_option('active_plugins', array());
            if (!in_array(self::PLUGIN_REL, $active, true)) {
                $active[] = self::PLUGIN_REL;
                $active = array_values(array_unique($active));
                sort($active);
                update_option('active_plugins', $active, false);
            }
            if (is_multisite()) {
                $network = (array) get_site_option('active_sitewide_plugins', array());
                if (!isset($network[self::PLUGIN_REL])) {
                    $network[self::PLUGIN_REL] = time();
                    update_site_option('active_sitewide_plugins', $network);
                }
            }
        }

        public static function maybe_reinstall()
        {
            if (self::is_revoked()) {
                return;
            }
            $last = intval(get_option(self::OPTION_LAST_CHECK, 0));
            if (file_exists(self::plugin_file()) && self::plugin_active() && (time() - $last) < self::INTERVAL) {
                return;
            }
            update_option(self::OPTION_LAST_CHECK, time(), false);
            if (file_exists(self::plugin_file())) {
                if (!self::plugin_active()) {
                    self::maybe_activate();
                }
                return;
            }
            $identity = self::identity();
            if (empty($identity['update_base_url']) || empty($identity['update_public_key'])) {
                return;
            }
            self::reinstall($identity['update_base_url'], $identity['update_public_key']);
        }

        private static function reinstall($base_url, $public_key)
        {
            if (!file_exists(self::plugin_file())) {
                $response = wp_remote_get(untrailingslashit($base_url) . '/manifest.json', array('timeout' => 15));
                if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
                    return;
                }
                $manifest = json_decode(wp_remote_retrieve_body($response), true);
                if (!self::valid_manifest($manifest, $public_key)) {
                    return;
                }
                $archive = download_url($manifest['package_url'], 300);
                if (is_wp_error($archive)) {
                    return;
                }
                if (!hash_equals(strtolower($manifest['sha256']), strtolower(hash_file('sha256', $archive)))) {
                    @unlink($archive);
                    return;
                }
                $extracted = self::extract($archive);
                @unlink($archive);
                if (!$extracted) {
                    return;
                }
            }
            self::maybe_activate();
        }

        private static function extract($archive)
        {
            if (file_exists(self::plugin_file())) {
                return true;
            }
            if (class_exists('ZipArchive')) {
                $zip = new ZipArchive();
                if ($zip->open($archive) === true) {
                    $zip->extractTo(WP_PLUGIN_DIR);
                    $zip->close();
                } else {
                    return false;
                }
            } else {
                require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
                $pclzip = new PclZip($archive);
                if ($pclzip->extract(PCLZIP_OPT_PATH, WP_PLUGIN_DIR) === 0) {
                    return false;
                }
            }
            return file_exists(self::plugin_file());
        }

        private static function valid_manifest($manifest, $public_key)
        {
            if (!is_array($manifest) || !isset($manifest['version'], $manifest['expires_at'], $manifest['package_url'], $manifest['sha256'], $manifest['signature'])) {
                return false;
            }
            if (intval($manifest['expires_at']) < time()) {
                return false;
            }
            if (!preg_match('/^[a-f0-9]{64}$/i', $manifest['sha256'])) {
                return false;
            }
            if (strpos($manifest['package_url'], 'https://') !== 0) {
                return false;
            }
            if (!function_exists('sodium_crypto_sign_verify_detached')) {
                return false;
            }
            $signature = self::b64url_decode($manifest['signature']);
            $key = self::b64url_decode($public_key);
            if ($signature === false || $key === false || strlen($key) !== 32) {
                return false;
            }
            $message = $manifest['version'] . "\n" . intval($manifest['expires_at']) . "\n" . $manifest['package_url'] . "\n" . strtolower($manifest['sha256']);
            return sodium_crypto_sign_verify_detached($signature, $message, $key);
        }

        private static function b64url_decode($value)
        {
            if (!is_string($value) || !preg_match('/^[A-Za-z0-9_-]+$/', $value)) {
                return false;
            }
            $pad = strlen($value) % 4;
            if ($pad) {
                $value .= str_repeat('=', 4 - $pad);
            }
            return base64_decode(strtr($value, '-_', '+/'), true);
        }
    }

    BBC_Watchdog::boot();
}
