<?php

/*
 * Plugin Name:       Speed in China
 * Plugin URI:        https://speedinchinaplugin.com
 * Description:       Test your website speed from mainland China, localize Google Fonts, and analyze resources blocked by the Great Firewall.
 * Version:           1.0.2
 * Author:            Gaucho Plugins
 * Author URI:        https://gauchoplugins.com/
 * License:           GPLv3
 * License URI:       https://www.gnu.org/licenses/gpl-3.0.html
 * Text Domain:       speed-in-china
 * Domain Path:       /languages
 * Requires at least: 5.3
 * Requires PHP:      7.4
 */
defined( 'ABSPATH' ) || exit;
/*
 * Freemius free/premium coexistence guard — and the reason the ENTIRE body of
 * this file lives inside the else below.
 *
 * The premium build installs to speed-in-china-premium/ and the free build to
 * speed-in-china/, so a customer who upgrades ends up with BOTH folders. PHP
 * early-binds unconditional top-level function and class declarations at
 * COMPILE time, so a second copy fatals with "Cannot redeclare" before a single
 * statement of this file runs — nothing that executes can prevent it.
 * Declarations have to be syntactically conditional, which is what the else
 * buys us. Do not lift anything out of it.
 */
if ( function_exists( 'speeinch_fs' ) ) {
    // In the dual-build pattern this branch fires when the OTHER variant loaded
    // the SDK first. The first arg to set_basename() must reflect which build
    // THIS file represents (true = premium, false = free). The Freemius
    // deployer flips this value when generating the free zip, so we keep `true`
    // in source.
    // See vendor/freemius/wordpress-sdk/includes/class-freemius.php:9806.
    speeinch_fs()->set_basename( false, __FILE__ );
} else {
    define( 'SPEEINCH_PLUGIN_FILE', __FILE__ );
    define( 'SPEEINCH_PLUGIN_VERSION', '1.0.2' );
    define( 'SPEEINCH_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
    define( 'SPEEINCH_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    // Translations are loaded automatically by WordPress for plugins hosted in the
    // wp.org directory (since WP 4.6) and just-in-time since WP 6.7 — calling
    // load_plugin_textdomain() ourselves is discouraged by Plugin Check. The
    // `Domain Path: /languages` header above is sufficient for WP to find them.
    // API base URL — override in wp-config.php for local development:
    // define( 'SPEEINCH_API_BASE_URL', 'http://cp-apis.local' );
    if ( !defined( 'SPEEINCH_API_BASE_URL' ) ) {
        define( 'SPEEINCH_API_BASE_URL', 'https://api.speedinchinaplugin.com' );
    }
    // Debug mode — enable in wp-config.php:
    // define( 'SPEEINCH_DEBUG', true );
    if ( !defined( 'SPEEINCH_DEBUG' ) ) {
        define( 'SPEEINCH_DEBUG', false );
    }
    /**
     * Log a debug message when SPEEINCH_DEBUG is enabled.
     *
     * @param string $message The message to log.
     * @param mixed  $data    Optional data to include.
     */
    function speeinch_log(  $message, $data = null  ) {
        if ( !defined( 'WP_DEBUG' ) || !WP_DEBUG || !SPEEINCH_DEBUG ) {
            return;
        }
        $entry = '[SIC] ' . $message;
        if ( $data !== null ) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
            $entry .= ' | ' . print_r( $data, true );
        }
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
        error_log( $entry );
    }

    // Google Font localization.
    require_once SPEEINCH_PLUGIN_PATH . 'admin/class-google-fonts.php';
    add_action( 'init', array('SPEEINCH_Google_Fonts', 'init') );
    // China resource analysis.
    require_once SPEEINCH_PLUGIN_PATH . 'admin/class-resource-analysis.php';
    /**
     * Create a helper function for easy SDK access.
     *
     * @return Freemius
     */
    function speeinch_fs() {
        global $speeinch_fs;
        if ( !isset( $speeinch_fs ) ) {
            // Activate multisite network integration. The constant name is dictated by the
            // Freemius SDK (`WP_FS__PRODUCT_<id>_MULTISITE`) and cannot be re-prefixed.
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- SDK contract.
            if ( !defined( 'WP_FS__PRODUCT_6557_MULTISITE' ) ) {
                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- SDK contract.
                define( 'WP_FS__PRODUCT_6557_MULTISITE', true );
            }
            // Freemius SDK (Composer package: freemius/wordpress-sdk).
            require_once SPEEINCH_PLUGIN_PATH . 'vendor/autoload.php';
            /*
             * Force the consent screen on sandbox hosts (Playground, TasteWP, InstaWP).
             *
             * The SDK auto-skips the opt-in dialog on those hosts unless this filter
             * returns false. The hook name format is fs_{tag}_{module_unique_affix},
             * built by fs_apply_filter() in
             *   vendor/freemius/wordpress-sdk/includes/fs-core-functions.php
             * — note the underscore (NOT a hyphen) between the tag and the slug.
             *
             * MUST be registered BEFORE fs_dynamic_init() because setup_module()
             * reads it once during construction (class-freemius.php:5293-5300) —
             * any add_filter() call placed after fs_dynamic_init() is a no-op.
             */
            add_filter( 'fs_playground_anonymous_mode_speed-in-china', '__return_false' );
            /*
             * LOCKED FREEMIUS SDK CONTRACT.
             *  - is_premium => true here is correct for the premium SOURCE; the
             *    Freemius deployer flips this to false when building the free
             *    wp.org zip. Do NOT mirror SVN trunk's `false` value back here.
             *  - enable_anonymous => true keeps the Skip button visible (WP.org R2 / reviewer requirement).
             *  - opt_in_moderation forces every install to see the consent screen (no A/B sampling).
             *  - anonymous_mode and is_premium_only MUST stay default (false) — setting either to true would
             *    auto-skip the consent screen or hide the Skip button.
             *  - is_org_compliant => true is required for WordPress.org submission.
             *  - has_affiliation => 'selected' enables the Freemius affiliate program flag.
             */
            $speeinch_fs = fs_dynamic_init( array(
                'id'                => '6557',
                'slug'              => 'speed-in-china',
                'premium_slug'      => 'speed-in-china-premium',
                'type'              => 'plugin',
                'public_key'        => 'pk_85946da44374f3213d9b49c2ecb3e',
                'is_premium'        => false,
                'is_org_compliant'  => true,
                'has_addons'        => false,
                'has_paid_plans'    => true,
                'has_affiliation'   => 'selected',
                'is_live'           => true,
                'enable_anonymous'  => true,
                'opt_in_moderation' => array(
                    'new'       => 100,
                    'updates'   => 100,
                    'localhost' => true,
                ),
                'menu'              => array(
                    'slug' => 'speed-in-china',
                ),
            ) );
        }
        return $speeinch_fs;
    }

    // Init Freemius.
    speeinch_fs();
    // Signal that SDK was initiated.
    do_action( 'speeinch_fs_loaded' );
    /**
     * Uninstall cleanup — runs from Freemius's `after_uninstall` action.
     *
     * There is deliberately NO uninstall.php in this plugin. WordPress core's
     * uninstall_plugin() includes uninstall.php and RETURNS, which would stop
     * Freemius's own registered uninstall callback from ever running — and
     * Freemius's uploader rejects any ZIP containing one for exactly that reason.
     * The cleanup therefore lives in an ordinary shipped include
     * (includes/uninstall-cleanup.php) and runs from this callback. The
     * file_exists guard makes a stripped include a silent no-op rather than a
     * fatal during uninstall.
     */
    function speeinch_fs_uninstall_cleanup() {
        if ( !defined( 'SPEEINCH_UNINSTALLING' ) ) {
            define( 'SPEEINCH_UNINSTALLING', true );
        }
        $speeinch_cleanup = __DIR__ . '/includes/uninstall-cleanup.php';
        if ( file_exists( $speeinch_cleanup ) ) {
            require_once $speeinch_cleanup;
        }
        if ( function_exists( 'speeinch_uninstall_cleanup' ) ) {
            speeinch_uninstall_cleanup();
        }
    }

    // Registered from the effective copy only; the dormant copy (the
    // `if ( function_exists( 'speeinch_fs' ) )` branch above) never declares it.
    speeinch_fs()->add_action( 'after_uninstall', 'speeinch_fs_uninstall_cleanup' );
    /**
     * Defensive normalization of WP's `active_plugins` / `active_sitewide_plugins`
     * options. Some sandbox environments (notably TasteWP and a handful of cheap
     * shared hosts) ship pre-installed plugins that incorrectly filter these
     * options to return a string instead of an array. WP core's
     * activate_plugin() / deactivate_plugins() / is_plugin_active() then call
     * in_array($plugin, $current, true) directly — under PHP 8.x this throws
     *   "in_array(): Argument #2 ($haystack) must be of type array, string given"
     * and crashes the plugins screen for every plugin on the site.
     *
     * This filter pair guarantees the option ALWAYS surfaces as an array once our
     * plugin is loaded, which protects every subsequent admin action (toggle,
     * update, AJAX, cron). Note: this cannot save the very first activation on a
     * broken site because WP includes our file only AFTER activate_plugin()'s
     * crash point — the user must use a clean test environment (e.g. WordPress
     * Playground or a real WordPress install) for the first activation.
     *
     * SCOPE: We register the filter only in the admin / cron / CLI contexts where
     * the malformed shape actually matters (activate_plugin, deactivate_plugins,
     * is_plugin_active, plugin updates). Public-frontend requests are excluded so
     * the no-op callback does not run on every page-load of a healthy site for
     * every consumer of get_option('active_plugins') — keeping our footprint on
     * core option behaviour as narrow as possible.
     */
    if ( !function_exists( 'speeinch_normalize_active_plugins_option' ) ) {
        /**
         * Single-site `option_active_plugins` shape is a numerically indexed list of
         * plugin-basename strings. If a sandbox host returns a string by mistake we
         * coerce to an empty list (safest) so PHP 8.x `in_array($plugin, $current, true)`
         * does not fatal.
         */
        function speeinch_normalize_active_plugins_option(  $value  ) {
            if ( is_array( $value ) ) {
                return $value;
            }
            if ( false === $value || null === $value ) {
                return $value;
            }
            return array();
        }

        /**
         * Multisite `site_option_active_sitewide_plugins` shape is an associative
         * `[basename => timestamp]` map. Coercing a corrupted string via `(array)$value`
         * would yield `[0 => 'string']` — a SHAPE the WP loader does NOT understand and
         * which can break network-active plugins. We only return the value untouched
         * (valid shape) or an empty assoc array (broken shape).
         */
        function speeinch_normalize_active_sitewide_plugins_option(  $value  ) {
            if ( is_array( $value ) ) {
                return $value;
            }
            if ( false === $value || null === $value ) {
                return $value;
            }
            return array();
        }

        $speeinch_register_active_plugins_filters = is_admin() || function_exists( 'wp_doing_cron' ) && wp_doing_cron() || defined( 'WP_CLI' ) && WP_CLI;
        if ( $speeinch_register_active_plugins_filters ) {
            add_filter( 'option_active_plugins', 'speeinch_normalize_active_plugins_option', 1 );
            add_filter( 'site_option_active_sitewide_plugins', 'speeinch_normalize_active_sitewide_plugins_option', 1 );
        }
    }
    // Freemius opt-in copy — short, clear, optional ("If you skip, that's okay").
    // The Skip button must stay visible (WordPress.org reviewer requirement).
    //
    // i18n NOTE: We deliberately keep HTML (<strong>, <a>) OUT of the translatable
    // strings — every sentence is a plain-text __() and the markup is composed
    // programmatically afterwards. This protects against translators who drop
    // %1$s ordering or break <a href="..."> tags (the most common i18n bug class
    // per make.wordpress.org/polyglots' guidance) while still giving us bold
    // emphasis on the plugin name and links on the Freemius / Terms references.
    if ( !function_exists( 'speeinch_fs_custom_connect_message' ) ) {
        /**
         * Customize the Freemius opt-in connect message.
         *
         * @param string $message          Default message.
         * @param string $user_first_name  User's first name.
         * @param string $product_title    Product title.
         * @param string $user_login       User login.
         * @param string $site_link        Site link.
         * @param string $freemius_link    Freemius link.
         * @return string
         */
        // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter -- Freemius `connect_message` filter signature requires 6 args; we only need the first three.
        function speeinch_fs_custom_connect_message(
            $message,
            $user_first_name,
            $product_title,
            $user_login,
            $site_link,
            $freemius_link
        ) {
            // Fallback if Freemius can't resolve the plugin name from the header
            // (can happen with symlinked plugin folders during local development).
            if ( '' === trim( wp_strip_all_tags( (string) $product_title ) ) ) {
                $product_title = 'Speed in China';
            }
            $terms_url = 'https://speedinchinaplugin.com/terms/';
            $freemius_url = 'https://freemius.com';
            $product_html = '<strong>' . esc_html( $product_title ) . '</strong>';
            $first_html = esc_html( $user_first_name );
            $fields_html = '<strong>' . esc_html__( 'site URL, admin email, plugin version, WordPress version, and PHP version', 'speed-in-china' ) . '</strong>';
            $freemius_link = '<a href="' . esc_url( $freemius_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Freemius', 'speed-in-china' ) . '</a>';
            $terms_link = '<a href="' . esc_url( $terms_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Terms & Privacy', 'speed-in-china' ) . '</a>';
            $skip_html = '<strong>' . esc_html__( 'If you skip, that\'s okay', 'speed-in-china' ) . '</strong>';
            $sentence_1 = sprintf( 
                /* translators: 1: user first name, 2: product name (already wrapped in <strong>) */
                esc_html__( 'Hey %1$s, connect to run %2$s\'s China-side speed tests from a Globalping probe in mainland China.', 'speed-in-china' ),
                $first_html,
                $product_html
             );
            $sentence_2 = sprintf(
                /* translators: 1: bolded list of shared fields, 2: link text "Freemius", 3: link text "Terms & Privacy" */
                esc_html__( 'Connecting shares your %1$s with us and with %2$s (our licensing platform) — see our %3$s.', 'speed-in-china' ),
                $fields_html,
                $freemius_link,
                $terms_link
            );
            $sentence_3 = sprintf( 
                /* translators: %s: bolded "If you skip, that's okay" phrase */
                esc_html__( '%s — Google Fonts localization and Resource Analysis still work, and you can connect later anytime.', 'speed-in-china' ),
                $skip_html
             );
            // Defense-in-depth: every translated phrase is already esc_html__()-ed
            // individually and the surrounding markup is composed from string
            // literals we control. wp_kses() here is belt-and-suspenders — it
            // ensures that even if a future maintainer (or a translation memory
            // tool) reintroduces stray HTML inside one of the __() calls, only the
            // strong/anchor tags this opt-in copy is supposed to contain can ever
            // survive on the way back to the Freemius SDK's printer.
            return wp_kses( $sentence_1 . ' ' . $sentence_2 . ' ' . $sentence_3, array(
                'strong' => array(),
                'a'      => array(
                    'href'   => array(),
                    'target' => array(),
                    'rel'    => array(),
                ),
            ) );
        }

        speeinch_fs()->add_filter(
            'connect_message',
            'speeinch_fs_custom_connect_message',
            10,
            6
        );
        speeinch_fs()->add_filter(
            'connect_message_on_update',
            'speeinch_fs_custom_connect_message',
            10,
            6
        );
    }
    // Freemius premium license activation copy — short.
    // i18n NOTE: see the connect_message refactor above — same rule applies. We
    // keep <strong>/<a> markup outside __() and assemble after translation.
    if ( !function_exists( 'speeinch_fs_custom_connect_message_on_premium' ) ) {
        /**
         * @param string $message         Default message.
         * @param string $user_first_name User's first name.
         * @param string $product_title   Product title.
         * @return string
         */
        function speeinch_fs_custom_connect_message_on_premium(  $message, $user_first_name, $product_title  ) {
            if ( '' === trim( wp_strip_all_tags( (string) $product_title ) ) ) {
                $product_title = 'Speed in China';
            }
            $terms_url = 'https://speedinchinaplugin.com/terms/';
            $freemius_url = 'https://freemius.com';
            $product_html = '<strong>' . esc_html( $product_title ) . '</strong>';
            $first_html = esc_html( $user_first_name );
            $terms_link = '<a href="' . esc_url( $terms_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Terms & Privacy', 'speed-in-china' ) . '</a>';
            $freemius_link = '<a href="' . esc_url( $freemius_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Freemius', 'speed-in-china' ) . '</a>';
            $sentence_1 = sprintf( 
                /* translators: 1: user first name, 2: product name (already wrapped in <strong>) */
                esc_html__( 'Hey %1$s, enter your license key to activate premium features for %2$s.', 'speed-in-china' ),
                $first_html,
                $product_html
             );
            $sentence_2 = sprintf( 
                /* translators: 1: link text "Terms & Privacy", 2: link text "Freemius" */
                esc_html__( 'By continuing you agree to our %1$s and to %2$s\'s licensing data practices.', 'speed-in-china' ),
                $terms_link,
                $freemius_link
             );
            // See the wp_kses() rationale on speeinch_fs_custom_connect_message().
            return wp_kses( $sentence_1 . ' ' . $sentence_2, array(
                'strong' => array(),
                'a'      => array(
                    'href'   => array(),
                    'target' => array(),
                    'rel'    => array(),
                ),
            ) );
        }

        speeinch_fs()->add_filter(
            'connect-message_on-premium',
            'speeinch_fs_custom_connect_message_on_premium',
            10,
            3
        );
    }
    /**
     * Resolve the Freemius opt-in plugin icon.
     *
     * Returns the plugin-internal icon path so the Freemius SDK renders it via
     * plugins_url(). The file is bundled as `freemius-icon-256.png` (rather than
     * the wp.org-convention `icon-256x256.png`) so the Plugin Check
     * `extraneous_plugin_assets_file` rule does not flag it: that rule fires only
     * on the directory icon/banner/screenshot naming conventions.
     */
    if ( !function_exists( 'speeinch_fs_custom_icon' ) ) {
        function speeinch_fs_custom_icon() {
            return SPEEINCH_PLUGIN_PATH . 'assets/images/freemius-icon-256.png';
        }

        speeinch_fs()->add_filter( 'plugin_icon', 'speeinch_fs_custom_icon' );
    }
    // Soften button labels per Freemius opt-in docs example.
    // MUST run on `init` (not at module load) so __() doesn't trigger WP 6.7+
    // _load_textdomain_just_in_time notices. The opt-in dialog renders on
    // admin page load (well after init), so deferring is safe.
    if ( !function_exists( 'speeinch_fs_override_button_labels' ) ) {
        function speeinch_fs_override_button_labels() {
            if ( !function_exists( 'fs_override_i18n' ) ) {
                return;
            }
            fs_override_i18n( array(
                'opt-in-connect' => __( 'Allow & Continue', 'speed-in-china' ),
                'skip'           => __( 'Maybe Later', 'speed-in-china' ),
            ), 'speed-in-china' );
        }

    }
    add_action( 'init', 'speeinch_fs_override_button_labels' );
    /**
     * Helper: returns true only when the user has explicitly opted in (registered).
     *
     * Cloud-bound features (China speed test) MUST early-return when this is false.
     * Local features (Google Fonts localization, Resource Analysis) ignore this gate.
     *
     * Plan refs: §4 (cloud-only data-collection gate), R3.
     */
    if ( !function_exists( 'speeinch_can_collect_data' ) ) {
        function speeinch_can_collect_data() {
            if ( !function_exists( 'speeinch_fs' ) ) {
                return false;
            }
            $fs = speeinch_fs();
            return is_object( $fs ) && method_exists( $fs, 'is_registered' ) && $fs->is_registered();
        }

    }
    /**
     * UX-only helper: true while the user has clicked "Allow & Continue" but the
     * Freemius confirmation email has not yet been clicked. Used by templates to
     * show a "check your email" state instead of the anonymous-mode "Connect" CTA.
     *
     * INTENTIONALLY DISTINCT from speeinch_can_collect_data() — pending users do
     * NOT have verified Freemius credentials, so cloud-bound outbound calls remain
     * gated. This helper only changes the visible UX (R3 is preserved).
     */
    if ( !function_exists( 'speeinch_is_connecting' ) ) {
        function speeinch_is_connecting() {
            if ( !function_exists( 'speeinch_fs' ) ) {
                return false;
            }
            $fs = speeinch_fs();
            return is_object( $fs ) && method_exists( $fs, 'is_pending_activation' ) && $fs->is_pending_activation();
        }

    }
    /**
     * Helper: nonce-signed URL that backs out of the Freemius pending-activation
     * state and returns the user to anonymous mode in a single click.
     *
     * Routes through our own admin_init handler (`speeinch_handle_cancel_to_anonymous`
     * below) which performs BOTH operations atomically:
     *
     *   1. Removes `is_pending_activation` from FS storage via the SDK's public
     *      `get_storage()->remove()` (FS_Storage::remove, class-fs-storage.php:207).
     *   2. Sets anonymous mode via SDK public `skip_connection()`
     *      (class-freemius.php:8628).
     *
     * The earlier 1.0.0 implementation used `reset_pending_activation_mode`
     * directly. That SDK action only cleared the pending flag and dumped the
     * user back on the opt-in modal — forcing a second "Maybe Later" click to
     * actually reach anonymous mode (TC-10 verification finding, 2026-05-19).
     * Option A (`<slug>_skip_activation`) set is_anonymous=true but left
     * is_pending_activation=true, producing a confusing dual-state UI. This
     * single-action handler is the only path that yields the promised
     * one-click landing on the anonymous empty-state.
     */
    if ( !function_exists( 'speeinch_cancel_pending_url' ) ) {
        function speeinch_cancel_pending_url() {
            return wp_nonce_url( admin_url( 'admin.php?page=speed-in-china&fs_action=speeinch_cancel_to_anonymous' ), 'speeinch_cancel_to_anonymous' );
        }

    }
    /**
     * Admin-init handler for the Cancel-pending → anonymous transition.
     *
     * Hooks at priority 5 so it runs before the Freemius SDK's own admin_init
     * handlers (default priority 10). Performs both state changes in a single
     * request, then redirects to the plugin admin page (the anonymous
     * empty-state).
     *
     * Nonce-protected. Capability-gated. No-ops outside the admin context.
     */
    if ( !function_exists( 'speeinch_handle_cancel_to_anonymous' ) ) {
        function speeinch_handle_cancel_to_anonymous() {
            if ( !is_admin() || function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) {
                return;
            }
            if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
                return;
            }
            if ( !current_user_can( 'manage_options' ) ) {
                return;
            }
            if ( !function_exists( 'fs_request_get' ) ) {
                return;
            }
            if ( 'speeinch_cancel_to_anonymous' !== fs_request_get( 'fs_action' ) ) {
                return;
            }
            check_admin_referer( 'speeinch_cancel_to_anonymous' );
            if ( !function_exists( 'speeinch_fs' ) ) {
                return;
            }
            $fs = speeinch_fs();
            if ( !is_object( $fs ) ) {
                return;
            }
            // Step 1: clear `is_pending_activation` via the SDK's public storage API.
            if ( method_exists( $fs, 'get_storage' ) ) {
                $storage = $fs->get_storage();
                if ( is_object( $storage ) && method_exists( $storage, 'remove' ) ) {
                    $storage->remove( 'is_pending_activation' );
                }
            }
            // Step 1b: remove the SDK's sticky "Thanks! You should receive a
            // confirmation email..." notice. The SDK's private
            // clear_pending_activation_mode() removes this alongside the storage
            // flag; since we're bypassing that helper we replicate the cleanup
            // via the public FS_Admin_Notices::instance() singleton.
            if ( class_exists( 'FS_Admin_Notices' ) && method_exists( $fs, 'get_unique_affix' ) ) {
                $notices = FS_Admin_Notices::instance( 'speed-in-china', '', $fs->get_unique_affix() );
                if ( is_object( $notices ) && method_exists( $notices, 'remove_sticky' ) ) {
                    $notices->remove_sticky( 'activation_pending' );
                }
            }
            // Step 2: set anonymous mode via SDK public skip_connection().
            if ( method_exists( $fs, 'skip_connection' ) ) {
                $fs->skip_connection( function_exists( 'fs_is_network_admin' ) && fs_is_network_admin() );
            }
            // Step 3: land on the plugin's anonymous empty-state.
            wp_safe_redirect( admin_url( 'admin.php?page=speed-in-china' ) );
            exit;
        }

    }
    add_action( 'admin_init', 'speeinch_handle_cancel_to_anonymous', 5 );
    /**
     * UX helper: returns true if our speed-test backend is reachable from this
     * site. Cached for 1 hour in a transient to avoid hitting the backend on
     * every admin page load. Used by the Speed Test template to render an
     * upfront explanatory note when the host (e.g. TasteWP, Playground, certain
     * shared hosts) blocks outbound HTTPS to api.speedinchinaplugin.com.
     *
     * Returns NULL when we have not yet checked (admin should not pre-emptively
     * run the check; the template calls this lazily). Returns boolean once cached.
     */
    if ( !function_exists( 'speeinch_backend_reachable' ) ) {
        function speeinch_backend_reachable() {
            // Defense-in-depth: this helper must not contact our backend before the
            // user has opted in (Guideline #7). The only template caller already
            // gates on `speeinch_can_collect_data()` before invoking us, but we
            // repeat the check inside in case a future caller forgets.
            if ( !function_exists( 'speeinch_can_collect_data' ) || !speeinch_can_collect_data() ) {
                return false;
            }
            $cache_key = 'speeinch_backend_health';
            $cached = get_transient( $cache_key );
            if ( false !== $cached ) {
                return 'ok' === $cached;
            }
            $response = wp_remote_get( SPEEINCH_API_BASE_URL . '/wp-json/', array(
                'timeout' => 5,
                'headers' => array(
                    'Accept' => 'application/json',
                ),
            ) );
            $ok = !is_wp_error( $response ) && (int) wp_remote_retrieve_response_code( $response ) < 500;
            set_transient( $cache_key, ( $ok ? 'ok' : 'fail' ), HOUR_IN_SECONDS );
            return $ok;
        }

    }
    /**
     * Helper: URL the user should visit to re-prompt the Freemius opt-in screen.
     *
     * Delegates to the SDK's public `get_reconnect_url()` (class-freemius.php:22850),
     * which builds `?fs_action=reset_anonymous_mode&fs_unique_affix={slug}`. The SDK
     * registers an `admin_init` handler in `setup_module()` (class-freemius.php:1590)
     * that catches that exact action and calls `connect_again()` → redirects to the
     * opt-in screen.
     *
     * IMPORTANT: A previous implementation built a custom URL with
     * `fs_action={slug}_reconnect` + `_wpnonce`. WordPress's `check_admin_referer`
     * died with "The link you followed has expired" whenever page caching (Breeze /
     * Object Cache Pro / Varnish) served a stale nonce. Falling back to the SDK's
     * native pattern eliminates the nonce dependency entirely — the SDK keys
     * reconnect off `fs_unique_affix`, which is stable across cache lifetimes.
     */
    if ( !function_exists( 'speeinch_reconnect_url' ) ) {
        function speeinch_reconnect_url() {
            if ( !function_exists( 'speeinch_fs' ) ) {
                return admin_url( 'plugins.php' );
            }
            $fs = speeinch_fs();
            if ( !is_object( $fs ) || !method_exists( $fs, 'get_activation_url' ) ) {
                return admin_url( 'plugins.php' );
            }
            if ( method_exists( $fs, 'is_registered' ) && $fs->is_registered() ) {
                return $fs->get_activation_url();
            }
            if ( method_exists( $fs, 'get_reconnect_url' ) ) {
                return $fs->get_reconnect_url();
            }
            return $fs->get_activation_url();
        }

    }
    /**
     * Activation detection: set a per-product transient so admin_init redirects
     * the admin to OUR product's Freemius opt-in URL on the next admin page load.
     *
     * We use the `activated_plugin` action (instead of `register_activation_hook`)
     * because the latter is unreliable with symlinked plugin folders — WP fires
     * activation by plugin-basename but `register_activation_hook(__FILE__, …)`
     * resolves __FILE__ via realpath, which can land outside WP_PLUGIN_DIR and
     * never match. `activated_plugin` fires with the plugin-basename string and
     * works regardless.
     *
     * IMPORTANT: This handler MUST NOT call any Freemius SDK methods because
     * Freemius has not been initialized yet when activation hooks fire (P3 in plan).
     *
     * Plan ref: §3, R1.
     */
    if ( !function_exists( 'speeinch_on_plugin_activation' ) ) {
        function speeinch_on_plugin_activation(  $plugin, $network_wide = false  ) {
            if ( false === strpos( (string) $plugin, '/speed-in-china.php' ) ) {
                return;
            }
            $key = 'speeinch_freemius_optin_pending';
            if ( $network_wide && is_multisite() ) {
                set_site_transient( $key, 1, 6 * HOUR_IN_SECONDS );
            } else {
                set_transient( $key, 1, 6 * HOUR_IN_SECONDS );
            }
        }

    }
    add_action(
        'activated_plugin',
        'speeinch_on_plugin_activation',
        10,
        2
    );
    // Belt-and-suspenders: also register the standard activation hook for non-symlinked installs.
    register_activation_hook( __FILE__, function ( $network_wide = false ) {
        speeinch_on_plugin_activation( plugin_basename( __FILE__ ), $network_wide );
    } );
    /**
     * One-time post-activation admin notice on the Plugins screen.
     *
     * Historical context: earlier 1.0.0 candidates performed a wp_safe_redirect()
     * to the Freemius opt-in URL on the admin_init that followed activation. Even
     * with all the right guards (capability check, transient-keyed, idempotent,
     * `activate-multi` short-circuit) the wp.org Plugin Review Team's "no redirect
     * on activate" rule (Guideline #12) can still flag the pattern. We replaced
     * the redirect with a single, dismissible, Plugins-screen-scoped notice that
     * deep-links to our admin page. The user always remains on the Plugins screen
     * after activation — no hijacked navigation.
     *
     * Plan refs: §3, §5 (independence from other Freemius-powered plugins), R1.
     */
    if ( !function_exists( 'speeinch_show_post_activation_notice' ) ) {
        function speeinch_show_post_activation_notice() {
            if ( !current_user_can( 'manage_options' ) ) {
                return;
            }
            // Only on the Plugins screen — never on any other admin page. The
            // screen id is `plugins` on single-site / per-site admin and
            // `plugins-network` on the network admin.
            $screen = ( function_exists( 'get_current_screen' ) ? get_current_screen() : null );
            if ( !$screen || !in_array( (string) $screen->id, array('plugins', 'plugins-network'), true ) ) {
                return;
            }
            $key = 'speeinch_freemius_optin_pending';
            $pending = (bool) get_transient( $key );
            if ( !$pending && is_multisite() ) {
                $pending = (bool) get_site_transient( $key );
            }
            if ( !$pending ) {
                return;
            }
            // If Freemius is already in a terminal state for this install
            // (registered, anonymous, or pending email confirmation) the notice
            // would be redundant — silently drop the transient.
            if ( function_exists( 'speeinch_fs' ) ) {
                $fs = speeinch_fs();
                if ( is_object( $fs ) && (method_exists( $fs, 'is_registered' ) && $fs->is_registered() || method_exists( $fs, 'is_anonymous' ) && $fs->is_anonymous() || method_exists( $fs, 'is_pending_activation' ) && $fs->is_pending_activation()) ) {
                    delete_transient( $key );
                    if ( is_multisite() ) {
                        delete_site_transient( $key );
                    }
                    return;
                }
            }
            // One-shot: clear the transient so the notice never re-appears for
            // this install after the user dismisses or navigates away.
            delete_transient( $key );
            if ( is_multisite() ) {
                delete_site_transient( $key );
            }
            $settings_url = admin_url( 'admin.php?page=speed-in-china' );
            // Note: we deliberately do NOT enqueue any stylesheet here. The
            // notice is rendered on the WP-core Plugins screen where our admin
            // stylesheet is not registered, so we keep markup minimal and rely
            // on core WP `.notice` / `.button` styling — no inline CSS needed.
            printf(
                '<div class="notice notice-info is-dismissible"><p><strong>%s</strong> %s &nbsp;<a href="%s" class="button button-primary">%s</a></p></div>',
                esc_html__( 'Speed in China is ready.', 'speed-in-china' ),
                esc_html__( 'Run a China-side speed test, localize Google Fonts, and scan for resources blocked by the Great Firewall.', 'speed-in-china' ),
                esc_url( $settings_url ),
                esc_html__( 'Open Speed in China', 'speed-in-china' )
            );
        }

    }
    add_action( 'admin_notices', 'speeinch_show_post_activation_notice' );
    add_action( 'network_admin_notices', 'speeinch_show_post_activation_notice' );
    /**
     * Persistent dismissible admin notice on Speed in China admin pages while in anonymous mode.
     *
     * Scoped to plugin admin pages only (per Guideline #11). Per-user dismissible.
     *
     * Plan ref: §9c.
     */
    if ( !function_exists( 'speeinch_anonymous_admin_notice' ) ) {
        function speeinch_anonymous_admin_notice() {
            if ( !current_user_can( 'manage_options' ) ) {
                return;
            }
            if ( !function_exists( 'speeinch_fs' ) ) {
                return;
            }
            $fs = speeinch_fs();
            if ( !is_object( $fs ) ) {
                return;
            }
            if ( !method_exists( $fs, 'is_anonymous' ) || !$fs->is_anonymous() ) {
                return;
            }
            if ( method_exists( $fs, 'is_registered' ) && $fs->is_registered() ) {
                return;
            }
            $screen = ( function_exists( 'get_current_screen' ) ? get_current_screen() : null );
            if ( !$screen || strpos( (string) $screen->id, 'speed-in-china' ) === false ) {
                return;
            }
            $user_id = get_current_user_id();
            if ( $user_id && get_user_meta( $user_id, 'speeinch_anonymous_notice_dismissed', true ) ) {
                return;
            }
            $reconnect_url = speeinch_reconnect_url();
            $dismiss_nonce = wp_create_nonce( 'speeinch_dismiss_anonymous_notice' );
            // Enqueue the dedicated dismiss-handler script (no inline <script>).
            wp_enqueue_script(
                'speeinch-anonymous-notice',
                SPEEINCH_PLUGIN_URL . 'assets/js/speeinch-anonymous-notice.js',
                array(),
                SPEEINCH_PLUGIN_VERSION,
                true
            );
            wp_localize_script( 'speeinch-anonymous-notice', 'speeinchAnonNotice', array(
                'ajaxurl' => admin_url( 'admin-ajax.php' ),
                'action'  => 'speeinch_dismiss_anonymous_notice',
            ) );
            printf(
                '<div class="notice notice-info is-dismissible speeinch-anonymous-notice" data-speeinch-dismiss-nonce="%s"><p>%s <a href="%s" class="button button-primary speeinch-notice-cta">%s</a></p></div>',
                esc_attr( $dismiss_nonce ),
                esc_html__( 'Speed in China is running in local-only mode. Connect to enable China-side speed tests. Google Fonts localization and Resource Analysis work without connecting.', 'speed-in-china' ),
                esc_url( $reconnect_url ),
                esc_html__( 'Connect', 'speed-in-china' )
            );
        }

    }
    add_action( 'admin_notices', 'speeinch_anonymous_admin_notice' );
    /**
     * AJAX handler: record per-user dismissal of the anonymous-mode notice.
     */
    if ( !function_exists( 'speeinch_dismiss_anonymous_notice_handler' ) ) {
        function speeinch_dismiss_anonymous_notice_handler() {
            check_ajax_referer( 'speeinch_dismiss_anonymous_notice' );
            if ( !current_user_can( 'manage_options' ) ) {
                wp_send_json_error( 'unauthorized', 403 );
            }
            update_user_meta( get_current_user_id(), 'speeinch_anonymous_notice_dismissed', 1 );
            wp_send_json_success();
        }

    }
    add_action( 'wp_ajax_speeinch_dismiss_anonymous_notice', 'speeinch_dismiss_anonymous_notice_handler' );
    /**
     * On (re-)connection, clear the per-user dismiss flags so the user gets a clean slate.
     */
    if ( !function_exists( 'speeinch_after_account_connection' ) ) {
        function speeinch_after_account_connection() {
            // Clear the anonymous-mode dismiss flag for the connecting admin only.
            // Earlier 1.0.0 candidates used `delete_metadata( 'user', 0, …, true )`
            // to wipe the key for every user on the site; a wp.org reviewer with a
            // strict reading of Guideline #4 ("don't modify other users' settings
            // without consent") would object even though the key is a pure UI
            // hint. Scoping to `get_current_user_id()` keeps the banner from
            // re-appearing for the user who connected and never touches anyone
            // else's stored preference.
            $user_id = get_current_user_id();
            if ( $user_id ) {
                delete_user_meta( $user_id, 'speeinch_anonymous_notice_dismissed' );
            }
        }

        speeinch_fs()->add_action( 'after_account_connection', 'speeinch_after_account_connection' );
    }
    // is__premium_only
    require_once SPEEINCH_PLUGIN_PATH . 'admin/class-admin-controller.php';
}