[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f2cl7tHvFOvv-Uqnp09U_35nNJ6o6KnZZuIPaBoDTn3U":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":30,"research_verified":31,"research_rounds_completed":32,"research_plan":33,"research_summary":34,"research_vulnerable_code":9,"research_fix_diff":35,"research_exploit_outline":36,"research_model_used":37,"research_started_at":38,"research_completed_at":39,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":31,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":31,"source_links":40},"CVE-2026-14245","miniorange-otp-login-verification-and-sms-notifications-authentication-bypass-to-administrator-account-takeover-via-user","miniOrange OTP Login, Verification and SMS Notifications \u003C= 5.5.1 - Authentication Bypass to Administrator Account Takeover via 'username_b' Parameter","The miniOrange OTP Login, Verification and SMS Notifications plugin for WordPress is vulnerable to Authentication Bypass leading to Administrator Account Takeover in all versions up to, and including, 5.5.1. This is due to the `um_reset_password_process_hook()` function performing no server-side verification that the OTP validation step was completed, and relying solely on a public `form_nonce` nonce that the plugin itself emits to unauthenticated visitors via the `moumprvar` JavaScript object on the Ultimate Member password reset page, while still accepting the attacker-controlled `username_b` parameter to target any WordPress user without role restriction or any binding to a previously validated OTP session. This makes it possible for unauthenticated attackers to obtain a freshly generated password-reset URL for an arbitrary Administrator account — returned in a 302 `Location` header — and use it to take full control of that account. Exploitation requires the Ultimate Member Password Reset Form integration to be active and the plugin to not be configured for phone-only reset.","miniorange-otp-verification",null,"\u003C=5.5.1","5.5.2","critical",9.8,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:N\u002FUI:N\u002FS:U\u002FC:H\u002FI:H\u002FA:H","Missing Authorization","2026-07-08 19:20:36","2026-07-09 07:55:14",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002Fd34f4e77-f384-4d84-be32-0d349962b614?source=api-prod",1,[22,23,24,25,26,27,28,29],"addons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamajax.php","addons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamintegration.php","addons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspampreventeraddonhandler.php","addons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspampreventerhandler.php","addons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamstorage.php","addons\u002Fotpspampreventer\u002Fhelper\u002Fclass-mopuzzlehelper.php","addons\u002Fotpspampreventer\u002Fhelper\u002Fclass-mosecurityhelper.php","addons\u002Fotpspampreventer\u002Fincludes\u002Fcss\u002Fmo-admin.css","researched",false,3,"# Exploitation Research Plan - CVE-2026-14245\n\n## 1. Vulnerability Summary\nThe **miniOrange OTP Login, Verification and SMS Notifications** plugin (\u003C= 5.5.1) contains a critical authentication bypass vulnerability within its integration with the **Ultimate Member (UM)** plugin. Specifically, the function `um_reset_password_process_hook()` fails to verify that a one-time password (OTP) has been successfully validated before proceeding with the password reset logic. \n\nThe plugin relies on a publicly accessible nonce (`form_nonce`) exposed in the frontend JavaScript object `moumprvar`. An attacker can provide an arbitrary target username via the `username_b` parameter. Because there is no server-side check linking the session to a validated OTP, the plugin generates a password reset link for the targeted user (including Administrators) and returns it in a `302 Redirect`'s `Location` header.\n\n## 2. Attack Vector Analysis\n*   **Vulnerable Endpoint:** The Ultimate Member Password Reset page (typically `\u002Fpassword-reset\u002F` or any page containing the `[ultimatemember_password_reset]` shortcode).\n*   **Vulnerable Function:** `um_reset_password_process_hook()` (inferred from description).\n*   **Payload Parameter:** `username_b` (the target username) and `form_nonce` (the public nonce).\n*   **Authentication Level:** Unauthenticated (PR:N).\n*   **Preconditions:** \n    *   Ultimate Member plugin must be active.\n    *   The \"Ultimate Member Password Reset Form\" integration must be enabled in miniOrange settings.\n    *   The plugin must NOT be in \"phone-only\" reset mode.\n\n## 3. Code Flow\n1.  **Entry Point:** An unauthenticated visitor accesses the Ultimate Member password reset page.\n2.  **Nonce Exposure:** The plugin enqueues a script that localizes the `moumprvar` object, containing a `form_nonce`.\n3.  **Request Submission:** The attacker sends a POST request to the password reset page.\n4.  **Vulnerable Hook:** The `um_reset_password_process_hook()` is triggered by Ultimate Member's form processing logic.\n5.  **Bypass:** The function checks if `form_nonce` is present and valid (which it is, as it's public). It then looks for the `username_b` parameter.\n6.  **SINK:** The function generates a password reset key for the user specified in `username_b` via `retrieve_password()` or UM's equivalent and initiates a redirect to the reset URL.\n7.  **Exfiltration:** The reset URL, containing the secret key, is sent back to the attacker in the `Location` header.\n\n## 4. Nonce Acquisition Strategy\nThe nonce is required for the `um_reset_password_process_hook()` to execute its logic.\n1.  **Identify Page:** Find the page with the Ultimate Member reset shortcode.\n2.  **Navigation:** Use `browser_navigate` to load that page.\n3.  **Extraction:** Use `browser_eval` to extract the nonce from the `moumprvar` global object.\n\n**Javascript to execute:**\n```javascript\nwindow.moumprvar?.form_nonce\n```\n\n## 5. Exploitation Strategy\nThe goal is to trigger the bypass and capture the `Location` header containing the reset link.\n\n### Step-by-Step Execution\n1.  **Identify Target:** Choose an administrator username (e.g., `admin`).\n2.  **Get Nonce:** Navigate to the UM Password Reset page and extract `moumprvar.form_nonce`.\n3.  **Craft Request:** Perform a POST request to the reset page URL.\n4.  **Capture Redirect:** Ensure the `http_request` tool does **not** automatically follow redirects (or inspect the first response in the chain) to grab the `Location` header.\n\n**Exploit Request:**\n*   **Method:** `POST`\n*   **URL:** `http:\u002F\u002F\u003Ctarget-site>\u002Fpassword-reset\u002F` (or the identified UM reset slug)\n*   **Headers:** `Content-Type: application\u002Fx-www-form-urlencoded`\n*   **Body:**\n    ```text\n    username_b=admin&form_nonce=\u003CEXTRACTED_NONCE>&user_login=admin&um_for_reset_password=1&_wpnonce=\u003CUM_NONCE_IF_REQUIRED>\n    ```\n    *Note: `user_login` is often required by Ultimate Member itself to initiate the flow, while `username_b` is used by the miniOrange bypass.*\n\n## 6. Test Data Setup\n1.  **Users:**\n    *   Create an admin user: `wp user create victim_admin victim@example.com --role=administrator --user_pass=password123`\n2.  **Ultimate Member Setup:**\n    *   Install and activate Ultimate Member.\n    *   Create a Password Reset page using the UM shortcode: `wp post create --post_type=page --post_title=\"Password Reset\" --post_status=publish --post_content='[ultimatemember_password_reset]'`\n3.  **miniOrange Configuration:**\n    *   Activate `miniorange-otp-verification`.\n    *   (Via `wp-cli` or `browser_eval` on the settings page): Enable the \"Ultimate Member Password Reset Form\" integration.\n\n## 7. Expected Results\n*   The server responds with a `302 Found`.\n*   The `Location` header contains a URL similar to:\n    `http:\u002F\u002F\u003Ctarget-site>\u002Fpassword-reset\u002F?rp_key=REDACTED_SECRET_KEY&rp_login=victim_admin`\n*   Navigating to this URL allows the attacker to set a new password without knowing the old one or providing an OTP.\n\n## 8. Verification Steps\n1.  **Check Redirection:** Verify the `Location` header contains `rp_key`.\n2.  **Verify Password Change:** \n    *   Use the captured URL to change the password via the browser.\n    *   Verify the admin can no longer login with the old password: `wp user check-password victim_admin password123` (Should return non-zero\u002Ffail).\n    *   Verify the new password works.\n\n## 9. Alternative Approaches\n*   **Parameter variations:** If `username_b` is ignored, check if the plugin accepts `user_login` directly within the `um_reset_password_process_hook` logic if `form_nonce` is present.\n*   **Direct Hook Trigger:** If the frontend page is complex, attempt to trigger the hook via `admin-ajax.php` if UM allows password reset actions via AJAX, providing the same `username_b` and `form_nonce` parameters.\n*   **Check for Phone-Only:** If the exploit fails, ensure the plugin setting `mo_otp_um_password_reset_phone_only` is not set to `1`, as this might change the expected parameters to a phone number.","The miniOrange OTP Login, Verification and SMS Notifications plugin is vulnerable to authentication bypass via its Ultimate Member integration. Unauthenticated attackers can exploit a logic flaw in the password reset process where the plugin fails to verify that an OTP was successfully validated, allowing them to obtain a password reset link for any user, including administrators.","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamajax.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamajax.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamajax.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamajax.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,1008 +1,1008 @@\n-\u003C?php\n-\u002F**\n- * OTP Spam AJAX Handler\n- *\n- * @package otpspampreventer\u002Fhandler\n- *\u002F\n-\n-namespace OSP\\Handler;\n-\n-use OSP\\Handler\\MoOtpSpamStorage;\n-use OSP\\Handler\\MoOtpSpamPreventerHandler;\n-use OSP\\Helper\\MoPuzzleHelper;\n-use OSP\\Helper\\MoSecurityHelper;\n-use OSP\\Helper\\MoSessionHelper;\n-use OSP\\Traits\\Instance;\n-use OTP\\Helper\\MoMessages;\n-use OTP\\Helper\\MoPHPSessions;\n-use OTP\\Helper\\MoUtility;\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-if ( ! class_exists( 'MoOtpSpamAjax' ) ) {\n-\t\u002F**\n-\t * Handles AJAX requests for spam prevention functionality.\n-\t *\u002F\n-\tclass MoOtpSpamAjax {\n-\n-\t\tuse Instance;\n-\n-\t\t\u002F**\n-\t\t * Storage instance\n-\t\t *\n-\t\t * @var MoOtpSpamStorage\n-\t\t *\u002F\n-\t\tprivate $storage;\n-\n-\t\t\u002F**\n-\t\t * Handler instance\n-\t\t *\n-\t\t * @var MoOtpSpamPreventerHandler\n-\t\t *\u002F\n-\t\tprivate $handler;\n-\n-\t\t\u002F**\n-\t\t * Constructor\n-\t\t *\u002F\n-\t\tpublic function __construct() {\n-\t\t\t$this->storage = MoOtpSpamStorage::instance();\n-\t\t\t$this->handler = MoOtpSpamPreventerHandler::instance();\n-\n-\t\t\tadd_action( 'wp_ajax_mo_osp_check_spam', array( $this, 'mosp_check_spam_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_check_spam', array( $this, 'mosp_check_spam_ajax' ) );\n-\n-\t\t\tadd_action( 'wp_ajax_mo_osp_log_attempt', array( $this, 'mosp_log_attempt_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_log_attempt', array( $this, 'mosp_log_attempt_ajax' ) );\n-\n-\t\t\tadd_action( 'wp_ajax_mo_osp_save_settings', array( $this, 'mosp_save_settings_ajax' ) );\n-\n-\t\t\tadd_action( 'wp_ajax_mo_osp_check_puzzle', array( $this, 'mosp_check_puzzle_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_check_puzzle', array( $this, 'mosp_check_puzzle_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_mo_osp_generate_puzzle', array( $this, 'mosp_generate_puzzle_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_generate_puzzle', array( $this, 'mosp_generate_puzzle_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_mo_osp_verify_puzzle', array( $this, 'mosp_verify_puzzle_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_verify_puzzle', array( $this, 'mosp_verify_puzzle_ajax' ) );\n-\n-\t\t\tadd_action( 'wp_ajax_mo_osp_check_timer_status', array( $this, 'mosp_check_timer_status_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_check_timer_status', array( $this, 'mosp_check_timer_status_ajax' ) );\n-\n-\t\t\tadd_action( 'wp_ajax_mo_osp_check_puzzle_requirement', array( $this, 'mosp_check_puzzle_requirement_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_check_puzzle_requirement', array( $this, 'mosp_check_puzzle_requirement_ajax' ) );\n-\n-\t\t\tadd_action( 'wp_ajax_mo_osp_check_blocked', array( $this, 'mosp_check_blocked_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_check_blocked', array( $this, 'mosp_check_blocked_ajax' ) );\n-\n-\t\t\tadd_action( 'wp_ajax_mo_osp_unblock_user', array( $this, 'mosp_unblock_user_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_unblock_user', array( $this, 'mosp_unblock_user_ajax' ) );\n-\n-\t\t\tadd_action( 'wp_ajax_mo_osp_get_blocked_users', array( $this, 'mosp_get_blocked_users_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_mo_osp_unblock_user_by_hash', array( $this, 'mosp_unblock_user_by_hash_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_mo_osp_clear_all_blocked_users', array( $this, 'mosp_clear_all_blocked_users_ajax' ) );\n-\t\t\tadd_action( 'wp_ajax_mo_osp_toggle_addon', array( $this, 'mosp_toggle_addon_ajax' ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for checking spam before OTP send.\n-\t\t *\n-\t\t * @return void Sends JSON response.\n-\t\t *\u002F\n-\t\tpublic function mosp_check_spam_ajax() {\n-\t\t\tcheck_ajax_referer( 'mo_osp_nonce', 'security' );\n-\n-\t\t\t$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';\n-\t\t\t$phone      = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';\n-\t\t\t$browser_id = isset( $_POST['browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['browser_id'] ) ) : '';\n-\n-\t\t\tif ( empty( $email ) && empty( $phone ) ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray( 'message' => __( 'Phone or email is required', 'miniorange-otp-verification' ) ),\n-\t\t\t\t\t400\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\tif ( $browser_id ) {\n-\t\t\t\t$_POST['mo_osp_browser_id'] = $browser_id;\n-\t\t\t}\n-\n-\t\t\t$result = $this->handler->mosp_check_spam_before_otp_send( true, '', $email, $phone );\n-\n-\t\t\tif ( is_wp_error( $result ) ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray(\n-\t\t\t\t\t\t'message' => $result->get_error_message(),\n-\t\t\t\t\t\t'code'    => $result->get_error_code(),\n-\t\t\t\t\t),\n-\t\t\t\t\t429\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\twp_send_json_success(\n-\t\t\t\tarray( 'message' => __( 'Request allowed', 'miniorange-otp-verification' ) )\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for logging OTP attempts (for checkout mode)\n-\t\t *\n-\t\t * @return void Sends JSON response.\n-\t\t *\u002F\n-\t\tpublic function mosp_log_attempt_ajax() {\n-\t\t\tcheck_ajax_referer( 'mo_osp_nonce', 'security' );\n-\n-\t\t\t$browser_id = isset( $_POST['browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['browser_id'] ) ) : '';\n-\t\t\t$mode       = isset( $_POST['mode'] ) ? sanitize_text_field( wp_unslash( $_POST['mode'] ) ) : '';\n-\n-\t\t\tif ( empty( $browser_id ) ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray( 'message' => __( 'Browser ID is required', 'miniorange-otp-verification' ) ),\n-\t\t\t\t\t400\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$_POST['mo_osp_browser_id'] = $browser_id;\n-\n-\t\t\t$this->handler->mosp_record_otp_attempt( '', '', '' );\n-\n-\t\t\twp_send_json_success(\n-\t\t\t\tarray( 'message' => __( 'Attempt logged', 'miniorange-otp-verification' ) )\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for saving settings (admin only).\n-\t\t *\n-\t\t * @return void Sends JSON response.\n-\t\t *\u002F\n-\t\tpublic function mosp_save_settings_ajax() {\n-\t\t\tcheck_ajax_referer( 'mo_osp_admin_nonce', 'security' );\n-\n-\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray( 'message' => __( 'Insufficient permissions', 'miniorange-otp-verification' ) ),\n-\t\t\t\t\t403\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$settings = $this->storage->mosp_get_settings();\n-\n-\t\t\tif ( isset( $_POST['enabled'] ) ) {\n-\t\t\t\t$settings['enabled'] = ( 1 === absint( $_POST['enabled'] ) );\n-\t\t\t}\n-\n-\t\t\t$settings['cooldown_time'] = isset( $_POST['cooldown_time'] ) ? absint( $_POST['cooldown_time'] ) : 60;\n-\n-\t\t\t$max_attempts             = isset( $_POST['max_attempts'] ) ? absint( $_POST['max_attempts'] ) : 3;\n-\t\t\t$settings['max_attempts'] = max( 1, min( 10, $max_attempts ) );\n-\n-\t\t\t$settings['block_time'] = isset( $_POST['block_time'] ) ? absint( $_POST['block_time'] ) : 900;\n-\n-\t\t\t$settings['daily_limit']  = isset( $_POST['daily_limit'] ) ? absint( $_POST['daily_limit'] ) : 10;\n-\t\t\t$settings['hourly_limit'] = isset( $_POST['hourly_limit'] ) ? absint( $_POST['hourly_limit'] ) : 5;\n-\n-\t\t\t$settings['track_phone']   = true;\n-\t\t\t$settings['track_email']   = true;\n-\t\t\t$settings['track_ip']      = true;\n-\t\t\t$settings['track_browser'] = true;\n-\n-\t\t\t$whitelist_ips             = isset( $_POST['whitelist_ips'] ) ? sanitize_textarea_field( wp_unslash( $_POST['whitelist_ips'] ) ) : '';\n-\t\t\t$settings['whitelist_ips'] = array_filter( array_map( 'trim', explode( \"\\n\", $whitelist_ips ) ) );\n-\t\t\t$settings['whitelist_ips'] = array_values( $settings['whitelist_ips'] );\n-\n-\t\t\t$validation_errors = $this->mosp_validate_settings( $settings );\n-\t\t\tif ( ! empty( $validation_errors ) ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray(\n-\t\t\t\t\t\t'message' => __( 'Invalid settings', 'miniorange-otp-verification' ),\n-\t\t\t\t\t\t'errors'  => $validation_errors,\n-\t\t\t\t\t),\n-\t\t\t\t\t400\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$result = $this->storage->mosp_update_settings( $settings );\n-\n-\t\t\tif ( $result ) {\n-\t\t\t\twp_send_json_success(\n-\t\t\t\t\tarray( 'message' => __( 'Settings saved successfully', 'miniorange-otp-verification' ) )\n-\t\t\t\t);\n-\t\t\t} else {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray( 'message' => __( 'Failed to save settings', 'miniorange-otp-verification' ) ),\n-\t\t\t\t\t500\n-\t\t\t\t);\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Validate settings array\n-\t\t *\n-\t\t * @param array $settings Settings to validate.\n-\t\t * @return array Validation errors\n-\t\t *\u002F\n-\t\tprivate function mosp_validate_settings( $settings ) {\n-\t\t\t$errors = array();\n-\n-\t\t\tif ( $settings['cooldown_time'] \u003C 0 || $settings['cooldown_time'] > 86400 ) {\n-\t\t\t\t$errors['cooldown_time'] = __( 'Cooldown time must be between 0 and 86400 seconds', 'miniorange-otp-verification' );\n-\t\t\t}\n-\n-\t\t\tif ( $settings['max_attempts'] \u003C 1 || $settings['max_attempts'] > 10 ) {\n-\t\t\t\t$errors['max_attempts'] = __( 'Max attempts must be between 1 and 10', 'miniorange-otp-verification' );\n-\t\t\t}\n-\n-\t\t\tif ( $settings['block_time'] \u003C 60 || $settings['block_time'] > 604800 ) {\n-\t\t\t\t$errors['block_time'] = __( 'Block time must be between 60 and 604800 seconds', 'miniorange-otp-verification' );\n-\t\t\t}\n-\t\t\tif ( $settings['daily_limit'] \u003C 1 || $settings['daily_limit'] > 1000 ) {\n-\t\t\t\t$errors['daily_limit'] = __( 'Daily limit must be between 1 and 1000', 'miniorange-otp-verification' );\n-\t\t\t}\n-\n-\t\t\tif ( $settings['hourly_limit'] \u003C 1 || $settings['hourly_limit'] > 100 ) {\n-\t\t\t\t$errors['hourly_limit'] = __( 'Hourly limit must be between 1 and 100', 'miniorange-otp-verification' );\n-\t\t\t}\n-\n-\t\t\tif ( $settings['hourly_limit'] \u003C= $settings['max_attempts'] ) {\n-\t\t\t\t$errors['hourly_limit'] = sprintf(\n-\t\t\t\t\t\u002F* translators: %d: max attempts value *\u002F\n-\t\t\t\t\t__( 'Hourly limit must be greater than max attempts per window (%d)', 'miniorange-otp-verification' ),\n-\t\t\t\t\t$settings['max_attempts']\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\tif ( $settings['daily_limit'] \u003C= $settings['hourly_limit'] ) {\n-\t\t\t\t$errors['daily_limit'] = sprintf(\n-\t\t\t\t\t\u002F* translators: %d: hourly limit value *\u002F\n-\t\t\t\t\t__( 'Daily limit must be greater than hourly limit (%d)', 'miniorange-otp-verification' ),\n-\t\t\t\t\t$settings['hourly_limit']\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\tif ( ! $settings['track_phone'] && ! $settings['track_email'] && ! $settings['track_ip'] && ! $settings['track_browser'] ) {\n-\t\t\t\t$errors['tracking'] = __( 'At least one tracking method must be enabled', 'miniorange-otp-verification' );\n-\t\t\t}\n-\n-\t\t\tforeach ( $settings['whitelist_ips'] as $ip ) {\n-\t\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {\n-\t\t\t\t\t$errors['whitelist_ips'] = sprintf(\n-\t\t\t\t\t\t\u002F* translators: %s: invalid IP address *\u002F\n-\t\t\t\t\t\t__( 'Invalid IP address: %s', 'miniorange-otp-verification' ),\n-\t\t\t\t\t\t$ip\n-\t\t\t\t\t);\n-\t\t\t\t\tbreak;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn $errors;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * SECURITY ENHANCEMENT: AJAX handler for generating secure puzzles\n-\t\t *\n-\t\t * This method generates a new puzzle and stores it securely in the session,\n-\t\t * preventing client-side manipulation of puzzle data.\n-\t\t *\n-\t\t * @return void Sends JSON response.\n-\t\t *\u002F\n-\t\tpublic function mosp_generate_puzzle_ajax() {\n-\t\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'mo_osp_nonce' ) ) {\n-\t\t\t\twp_send_json_error( array( 'message' => __( 'Security check failed', 'miniorange-otp-verification' ) ) );\n-\t\t\t}\n-\n-\t\t\t$puzzle = MoPuzzleHelper::mosp_generate_secure_puzzle();\n-\n-\t\t\tif ( ! $puzzle ) {\n-\t\t\t\twp_send_json_error( array( 'message' => __( 'Failed to generate puzzle', 'miniorange-otp-verification' ) ) );\n-\t\t\t}\n-\n-\t\t\tMoPuzzleHelper::mosp_store_puzzle_in_session( $puzzle['question'], $puzzle['answer'] );\n-\n-\t\t\t$puzzle_image = MoPuzzleHelper::mosp_generate_puzzle_image( $puzzle['question'] );\n-\n-\t\t\t$response = array(\n-\t\t\t\t'question' => $puzzle['question'],\n-\t\t\t\t'message'  => __( 'Puzzle generated successfully', 'miniorange-otp-verification' ),\n-\t\t\t);\n-\n-\t\t\tif ( $puzzle_image ) {\n-\t\t\t\t$response['image'] = $puzzle_image;\n-\t\t\t}\n-\n-\t\t\twp_send_json_success( $response );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for checking if puzzle verification is required\n-\t\t *\n-\t\t * @return void Sends JSON response.\n-\t\t *\u002F\n-\t\tpublic function mosp_check_puzzle_ajax() {\n-\t\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'mo_osp_nonce' ) ) {\n-\t\t\t\twp_send_json_error( array( 'message' => __( 'Security check failed', 'miniorange-otp-verification' ) ) );\n-\t\t\t}\n-\n-\t\t\t$email      = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );\n-\t\t\t$phone      = sanitize_text_field( wp_unslash( $_POST['phone'] ?? '' ) );\n-\t\t\t$browser_id = sanitize_text_field( wp_unslash( $_POST['browser_id'] ?? '' ) );\n-\t\t\t$ip         = $this->handler->mosp_get_client_ip();\n-\n-\t\t\tif ( ! empty( $phone ) ) {\n-\t\t\t\t$phone       = MoUtility::process_phone_number( $phone );\n-\t\t\t\t$digit_count = strlen( preg_replace( '\u002F\\D\u002F', '', $phone ) );\n-\t\t\t\tif ( $digit_count \u003C 6 ) {\n-\t\t\t\t\t$phone = '';\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( ! empty( $email ) ) {\n-\t\t\t\tMoPHPSessions::add_session_var( 'user_email', $email );\n-\t\t\t}\n-\t\t\tif ( ! empty( $phone ) ) {\n-\t\t\t\tMoPHPSessions::add_session_var( 'phone_number_mo', $phone );\n-\t\t\t}\n-\t\t\t$requires_puzzle = $this->handler->mosp_requires_puzzle_verification( $email, $phone, $ip, $browser_id );\n-\n-\t\t\twp_send_json_success(\n-\t\t\t\tarray(\n-\t\t\t\t\t'requires_puzzle' => $requires_puzzle,\n-\t\t\t\t\t'message'         => $requires_puzzle ? __( 'Puzzle verification required', 'miniorange-otp-verification' ) : __( 'No puzzle required', 'miniorange-otp-verification' ),\n-\t\t\t\t)\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for verifying puzzle completion\n-\t\t * SECURITY ENHANCEMENT: Uses session-stored puzzle data for validation\n-\t\t *\n-\t\t * @return void Sends JSON response.\n-\t\t *\u002F\n-\t\tpublic function mosp_verify_puzzle_ajax() {\n-\t\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'mo_osp_nonce' ) ) {\n-\t\t\t\twp_send_json_error( array( 'message' => __( 'Security check failed', 'miniorange-otp-verification' ) ) );\n-\t\t\t}\n-\n-\t\t\t$email      = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );\n-\t\t\t$phone      = sanitize_text_field( wp_unslash( $_POST['phone'] ?? '' ) );\n-\t\t\t$browser_id = sanitize_text_field( wp_unslash( $_POST['browser_id'] ?? '' ) );\n-\t\t\t$ip         = $this->handler->mosp_get_client_ip();\n-\n-\t\t\tif ( ! empty( $phone ) ) {\n-\t\t\t\t$phone       = MoUtility::process_phone_number( $phone );\n-\t\t\t\t$digit_count = strlen( preg_replace( '\u002F\\D\u002F', '', $phone ) );\n-\t\t\t\tif ( $digit_count \u003C 6 ) {\n-\t\t\t\t\t$phone = '';\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\tif ( empty( $email ) ) {\n-\t\t\t\t$email = MoPHPSessions::get_session_var( 'user_email' );\n-\t\t\t}\n-\t\t\t$session_phone = MoPHPSessions::get_session_var( 'phone_number_mo' );\n-\t\t\tif ( empty( $phone ) && ! empty( $session_phone ) ) {\n-\t\t\t\t$phone = $session_phone;\n-\t\t\t}\n-\t\t\tif ( ! empty( $phone ) ) {\n-\t\t\t\t$phone       = MoUtility::process_phone_number( $phone );\n-\t\t\t\t$digit_count = strlen( preg_replace( '\u002F\\D\u002F', '', $phone ) );\n-\t\t\t\tif ( $digit_count \u003C 6 ) {\n-\t\t\t\t\t$phone = '';\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\tif ( ! empty( $session_phone ) ) {\n-\t\t\t\t$normalized_session_phone = MoUtility::process_phone_number( $session_phone );\n-\t\t\t\t$session_digits           = strlen( preg_replace( '\u002F\\D\u002F', '', $normalized_session_phone ) );\n-\t\t\t\tif ( $session_digits >= 6 ) {\n-\t\t\t\t\t$phone = $normalized_session_phone;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$has_answer    = array_key_exists( 'puzzle_answer', $_POST );\n-\t\t\t$puzzle_answer = $has_answer ? intval( wp_unslash( $_POST['puzzle_answer'] ) ) : null;\n-\n-\t\t\tif ( ! $has_answer ) {\n-\t\t\t\twp_send_json_error( array( 'message' => __( 'Puzzle answer is required', 'miniorange-otp-verification' ) ) );\n-\t\t\t}\n-\n-\t\t\tif ( ! MoPuzzleHelper::mosp_has_puzzle_in_session() ) {\n-\t\t\t\twp_send_json_error( array( 'message' => __( 'Puzzle session expired. Please refresh and try again.', 'miniorange-otp-verification' ) ) );\n-\t\t\t}\n-\n-\t\t\t$is_valid = MoPuzzleHelper::mosp_validate_puzzle_answer_from_session( $puzzle_answer );\n-\n-\t\t\tif ( ! $is_valid ) {\n-\t\t\t\t$puzzle_attempts_key = 'mo_osp_puzzle_attempts_' . md5( $ip . MoSecurityHelper::mosp_get_user_agent() );\n-\t\t\t\t$incorrect_attempts  = MoPHPSessions::get_session_var( $puzzle_attempts_key );\n-\n-\t\t\t\tif ( false === $incorrect_attempts ) {\n-\t\t\t\t\t$incorrect_attempts = 0;\n-\t\t\t\t}\n-\n-\t\t\t\t++$incorrect_attempts;\n-\n-\t\t\t\t$max_attempts_per_puzzle = 2;\n-\n-\t\t\t\tif ( $incorrect_attempts >= $max_attempts_per_puzzle ) {\n-\t\t\t\t\tMoPHPSessions::unset_session( $puzzle_attempts_key );\n-\n-\t\t\t\t\t$new_puzzle = MoPuzzleHelper::mosp_generate_secure_puzzle();\n-\t\t\t\t\tMoPuzzleHelper::mosp_store_puzzle_in_session(\n-\t\t\t\t\t\t$new_puzzle['question'],\n-\t\t\t\t\t\t$new_puzzle['answer'],\n-\t\t\t\t\t\t$ip,\n-\t\t\t\t\t\tMoSecurityHelper::mosp_get_user_agent()\n-\t\t\t\t\t);\n-\n-\t\t\t\t\t$puzzle_image = MoPuzzleHelper::mosp_generate_puzzle_image( $new_puzzle['question'] );\n-\n-\t\t\t\t\t$response_data = array(\n-\t\t\t\t\t\t'message'      => __( 'Incorrect puzzle answer. A new puzzle has been generated. Please solve it.', 'miniorange-otp-verification' ),\n-\t\t\t\t\t\t'puzzle_reset' => true,\n-\t\t\t\t\t);\n-\n-\t\t\t\t\tif ( $puzzle_image ) {\n-\t\t\t\t\t\t$response_data['puzzle_image'] = $puzzle_image;\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\t$response_data['puzzle_question'] = $new_puzzle['question'];\n-\t\t\t\t\t}\n-\n-\t\t\t\t\twp_send_json_error( $response_data );\n-\t\t\t\t} else {\n-\t\t\t\t\tMoPHPSessions::add_session_var( $puzzle_attempts_key, $incorrect_attempts );\n-\t\t\t\t\t$remaining_attempts = $max_attempts_per_puzzle - $incorrect_attempts;\n-\n-\t\t\t\t\twp_send_json_error(\n-\t\t\t\t\t\tarray(\n-\t\t\t\t\t\t\t'message'            => sprintf(\n-\t\t\t\t\t\t\t\t\u002F* translators: %d: number of remaining attempts *\u002F\n-\t\t\t\t\t\t\t\t__( 'Incorrect answer. Please try again. (%d attempt(s) remaining)', 'miniorange-otp-verification' ),\n-\t\t\t\t\t\t\t\t$remaining_attempts\n-\t\t\t\t\t\t\t),\n-\t\t\t\t\t\t\t'puzzle_reset'       => false,\n-\t\t\t\t\t\t\t'remaining_attempts' => $remaining_attempts,\n-\t\t\t\t\t\t)\n-\t\t\t\t\t);\n-\t\t\t\t}\n-\t\t\t} else {\n-\t\t\t\t$puzzle_attempts_key = 'mo_osp_puzzle_attempts_' . md5( $ip . MoSecurityHelper::mosp_get_user_agent() );\n-\t\t\t\tMoPHPSessions::unset_session( $puzzle_attempts_key );\n-\t\t\t}\n-\n-\t\t\t$this->handler->mosp_reset_immediate_spam_protection( $email, $phone );\n-\t\t\t$requires_limit_puzzle = $this->handler->mosp_requires_limit_puzzle_verification( $email, $phone );\n-\t\t\tif ( $requires_limit_puzzle ) {\n-\t\t\t\t$this->handler->mosp_mark_limit_puzzle_completed( $email, $phone );\n-\t\t\t} else {\n-\t\t\t\tMoSecurityHelper::mosp_mark_puzzle_verification_complete( $email, $phone );\n-\t\t\t}\n-\n-\t\t\t$verification_token = MoSecurityHelper::mosp_generate_puzzle_verification_token( $email, $phone );\n-\n-\t\t\twp_send_json_success(\n-\t\t\t\tarray(\n-\t\t\t\t\t'message'            => __( 'Puzzle verified successfully', 'miniorange-otp-verification' ),\n-\t\t\t\t\t'puzzle_cleared'     => true,\n-\t\t\t\t\t'verification_token' => $verification_token,\n-\t\t\t\t\t'puzzle_nonce'       => wp_create_nonce( 'mo_osp_puzzle_verify' ),\n-\t\t\t\t)\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * SECURITY FIX: Calculate the correct answer for a puzzle question\n-\t\t *\n-\t\t * @param string $question The puzzle question.\n-\t\t * @return int|false The correct answer or false if invalid.\n-\t\t *\u002F\n-\t\tprivate function calculate_puzzle_answer( $question ) {\n-\t\t\tif ( preg_match( '\u002F(\\d+)\\s*([+\\-×÷*\\\u002F])\\s*(\\d+)\u002F', $question, $matches ) ) {\n-\t\t\t\t$a        = intval( $matches[1] );\n-\t\t\t\t$operator = $matches[2];\n-\t\t\t\t$b        = intval( $matches[3] );\n-\n-\t\t\t\tswitch ( $operator ) {\n-\t\t\t\t\tcase '+':\n-\t\t\t\t\t\treturn $a + $b;\n-\t\t\t\t\tcase '-':\n-\t\t\t\t\t\treturn $a - $b;\n-\t\t\t\t\tcase '×':\n-\t\t\t\t\tcase '*':\n-\t\t\t\t\t\treturn $a * $b;\n-\t\t\t\t\tcase '÷':\n-\t\t\t\t\tcase '\u002F':\n-\t\t\t\t\t\treturn 0 !== $b ? intval( $a \u002F $b ) : false;\n-\t\t\t\t\tdefault:\n-\t\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( preg_match( '\u002F(\\d+)\\s*([+\\-])\\s*(\\d+)\\s*([+\\-])\\s*(\\d+)\u002F', $question, $matches ) ) {\n-\t\t\t\t$a   = intval( $matches[1] );\n-\t\t\t\t$op1 = $matches[2];\n-\t\t\t\t$b   = intval( $matches[3] );\n-\t\t\t\t$op2 = $matches[4];\n-\t\t\t\t$c   = intval( $matches[5] );\n-\n-\t\t\t\t$result = $a;\n-\t\t\t\t$result = ( '+' === $op1 ) ? $result + $b : $result - $b;\n-\t\t\t\t$result = ( '+' === $op2 ) ? $result + $c : $result - $c;\n-\n-\t\t\t\treturn $result;\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for checking timer status with persistent state management.\n-\t\t *\n-\t\t * @return void Sends JSON response.\n-\t\t *\u002F\n-\t\tpublic function mosp_check_timer_status_ajax() {\n-\t\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'mo_osp_nonce' ) ) {\n-\t\t\t\twp_send_json_error( array( 'message' => __( 'Security check failed', 'miniorange-otp-verification' ) ) );\n-\t\t\t}\n-\n-\t\t\t$browser_id = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\n-\t\t\t$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';\n-\t\t\t$phone      = isset( $_POST['phone'] ) ? MoUtility::process_phone_number( sanitize_text_field( wp_unslash( $_POST['phone'] ) ) ) : '';\n-\n-\t\t\t$state = $this->mosp_get_current_user_state( $email, $phone, $browser_id );\n-\t\t\twp_send_json_success( $state );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get current user state with accurate timer information.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $browser_id Browser ID.\n-\t\t * @return array Current user state.\n-\t\t *\u002F\n-\t\tprivate function mosp_get_current_user_state( $email, $phone, $browser_id ) {\n-\t\t\t$settings     = $this->storage->mosp_get_settings();\n-\t\t\t$current_time = time();\n-\n-\t\t\t$is_blocked = $this->handler->mosp_is_blocked( $email, $phone, $browser_id, 'timer_status' );\n-\n-\t\t\tif ( $is_blocked ) {\n-\t\t\t\t$block_data      = $this->handler->mosp_get_block_data( $email, $phone, $browser_id, 'timer_status' );\n-\t\t\t\t$block_remaining = $block_data['remaining_time'];\n-\t\t\t\t$block_reason    = $block_data['reason'];\n-\n-\t\t\t\tif ( $block_remaining > 0 ) {\n-\t\t\t\t\treturn array(\n-\t\t\t\t\t\t'blocked'        => true,\n-\t\t\t\t\t\t'remaining_time' => $block_remaining,\n-\t\t\t\t\t\t'blocked_type'   => $block_reason,\n-\t\t\t\t\t\t'message'        => MoMessages::showMessage( MoMessages::USER_IS_BLOCKED_AJAX ),\n-\t\t\t\t\t);\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$requires_regular_puzzle = $this->handler->mosp_requires_puzzle_verification( $email, $phone, $this->handler->mosp_get_client_ip(), $browser_id );\n-\t\t\t$requires_limit_puzzle   = $this->handler->mosp_requires_limit_puzzle_verification( $email, $phone );\n-\t\t\t$requires_puzzle         = $requires_regular_puzzle || $requires_limit_puzzle;\n-\n-\t\t\tif ( $requires_puzzle ) {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'puzzle_required' => true,\n-\t\t\t\t\t'message'         => 'Please complete the security verification to continue.',\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$ip                = $this->handler->mosp_get_client_ip();\n-\t\t\t$is_ip_whitelisted = false;\n-\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t$is_ip_whitelisted = $this->handler->mosp_is_whitelisted( $ip, 'ip' );\n-\t\t\t}\n-\n-\t\t\t$cooldown_remaining = $is_ip_whitelisted ? 0 : $this->mosp_get_cooldown_remaining_time( $email, $phone, $browser_id );\n-\n-\t\t\tif ( $cooldown_remaining > 0 ) {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'status'         => 'cooldown',\n-\t\t\t\t\t'cooldown'       => true,\n-\t\t\t\t\t'timer_active'   => true,\n-\t\t\t\t\t'remaining_time' => $cooldown_remaining,\n-\t\t\t\t\t'message'        => MoMessages::showMessage( MoMessages::LIMIT_OTP_SENT ),\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\treturn array(\n-\t\t\t\t'status'  => 'ready',\n-\t\t\t\t'message' => 'Ready to send OTP',\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get remaining cooldown time for user.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $browser_id Browser ID.\n-\t\t * @return int Remaining cooldown time.\n-\t\t *\u002F\n-\t\tprivate function mosp_get_cooldown_remaining_time( $email, $phone, $browser_id ) {\n-\t\t\ttry {\n-\t\t\t\t$identifiers   = $this->handler->mosp_get_all_identifiers( $email, $phone, $this->handler->mosp_get_client_ip(), $browser_id );\n-\t\t\t\t$current_time  = time();\n-\t\t\t\t$settings      = $this->storage->mosp_get_settings();\n-\t\t\t\t$cooldown_time = isset( $settings['cooldown_time'] ) ? (int) $settings['cooldown_time'] : 60;\n-\n-\t\t\t\tif ( empty( $identifiers ) || ! is_array( $identifiers ) ) {\n-\t\t\t\t\treturn 0;\n-\t\t\t\t}\n-\n-\t\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t\tif ( empty( $identifier ) ) {\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\n-\t\t\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\n-\n-\t\t\t\t\tif ( false === $data || ! isset( $data['attempts'] ) || ! is_array( $data['attempts'] ) ) {\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$attempts      = $data['attempts'];\n-\t\t\t\t\t$attempt_count = count( $attempts );\n-\n-\t\t\t\t\tif ( $attempt_count \u003C 2 ) {\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$sorted_attempts = $attempts;\n-\t\t\t\t\trsort( $sorted_attempts );\n-\n-\t\t\t\t\tif ( ! isset( $sorted_attempts[0] ) || ! isset( $sorted_attempts[1] ) ) {\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$most_recent_attempt    = (int) $sorted_attempts[0];\n-\t\t\t\t\t$second_to_last_attempt = (int) $sorted_attempts[1];\n-\n-\t\t\t\t\t$time_between_attempts = $most_recent_attempt - $second_to_last_attempt;\n-\n-\t\t\t\t\tif ( $time_between_attempts \u003C $cooldown_time ) {\n-\t\t\t\t\t\t$cooldown_expires_at = $second_to_last_attempt + $cooldown_time;\n-\t\t\t\t\t\t$remaining           = $cooldown_expires_at - $current_time;\n-\n-\t\t\t\t\t\tif ( $remaining > 0 ) {\n-\t\t\t\t\t\t\treturn $remaining;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t} catch ( Exception $e ) {\n-\t\t\t\treturn 0;\n-\t\t\t} catch ( Error $e ) {\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\n-\t\t\treturn 0;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for checking puzzle requirement (separate from timer status)\n-\t\t *\n-\t\t * @return void Sends JSON response.\n-\t\t *\u002F\n-\t\tpublic function mosp_check_puzzle_requirement_ajax() {\n-\t\t\tif ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'mo_osp_nonce' ) ) {\n-\t\t\t\twp_send_json_error( array( 'message' => __( 'Security check failed', 'miniorange-otp-verification' ) ) );\n-\t\t\t}\n-\n-\t\t\t$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';\n-\t\t\t$phone      = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';\n-\t\t\t$browser_id = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\n-\n-\t\t\t$requires_regular_puzzle = $this->handler->mosp_requires_puzzle_verification( $email, $phone, $this->handler->mosp_get_client_ip(), $browser_id );\n-\t\t\t$requires_limit_puzzle   = $this->handler->mosp_requires_limit_puzzle_verification( $email, $phone );\n-\t\t\t$requires_puzzle         = $requires_regular_puzzle || $requires_limit_puzzle;\n-\n-\t\t\twp_send_json_success(\n-\t\t\t\tarray(\n-\t\t\t\t\t'puzzle_required' => $requires_puzzle,\n-\t\t\t\t\t'regular_puzzle'  => $requires_regular_puzzle,\n-\t\t\t\t\t'limit_puzzle'    => $requires_limit_puzzle,\n-\t\t\t\t\t'message'         => $requires_puzzle ? MoMessages::showMessage( MoMessages::PLEASE_VALIDATE ) : __( 'No puzzle required', 'miniorange-otp-verification' ),\n-\t\t\t\t)\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for checking if user is blocked in popup (similar to resendcontrol)\n-\t\t *\n-\t\t * Checks if the user is blocked and sends a JSON response.\n-\t\t * This function calculates the remaining block time and cooldown for a user.\n-\t\t * If the user is still blocked or on cooldown, it returns the remaining time.\n-\t\t *\n-\t\t * @return void Sends a JSON response with the blocked status and remaining time.\n-\t\t *\u002F\n-\t\tpublic function mosp_check_blocked_ajax() {\n-\t\t\t\u002F\u002F phpcs:disable WordPress.Security.NonceVerification.Missing -- Public AJAX endpoint, nonce not required for read operations.\n-\t\t\ttry {\n-\t\t\t\t$browser_id = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\n-\t\t\t\t$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';\n-\t\t\t\t$phone      = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';\n-\n-\t\t\t\t$is_blocked = $this->handler->mosp_is_blocked( $email, $phone, $browser_id, 'timer_status' );\n-\n-\t\t\t\tif ( $is_blocked ) {\n-\t\t\t\t\t$block_data      = $this->handler->mosp_get_block_data( $email, $phone, $browser_id, 'timer_status' );\n-\t\t\t\t\t$block_remaining = isset( $block_data['remaining_time'] ) ? $block_data['remaining_time'] : 0;\n-\t\t\t\t\t$block_reason    = isset( $block_data['reason'] ) ? $block_data['reason'] : '';\n-\n-\t\t\t\t\tif ( $block_remaining > 0 ) {\n-\t\t\t\t\t\twp_send_json(\n-\t\t\t\t\t\t\tarray(\n-\t\t\t\t\t\t\t\t'blocked'        => true,\n-\t\t\t\t\t\t\t\t'remaining_time' => $block_remaining,\n-\t\t\t\t\t\t\t\t'blocked_type'   => $block_reason,\n-\t\t\t\t\t\t\t\t'message'        => MoMessages::showMessage( MoMessages::USER_IS_BLOCKED_AJAX ),\n-\t\t\t\t\t\t\t)\n-\t\t\t\t\t\t);\n-\t\t\t\t\t\treturn;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\n-\t\t\t\t$ip                = $this->handler->mosp_get_client_ip();\n-\t\t\t\t$is_ip_whitelisted = false;\n-\t\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t\t$is_ip_whitelisted = $this->handler->mosp_is_whitelisted( $ip, 'ip' );\n-\t\t\t\t}\n-\n-\t\t\t\t$cooldown_remaining = $is_ip_whitelisted ? 0 : $this->mosp_get_cooldown_remaining_time( $email, $phone, $browser_id );\n-\n-\t\t\t\tif ( $cooldown_remaining > 0 ) {\n-\t\t\t\t\twp_send_json(\n-\t\t\t\t\t\tarray(\n-\t\t\t\t\t\t\t'cooldown'       => true,\n-\t\t\t\t\t\t\t'remaining_time' => $cooldown_remaining,\n-\t\t\t\t\t\t\t'message'        => MoMessages::showMessage( MoMessages::LIMIT_OTP_SENT ),\n-\t\t\t\t\t\t)\n-\t\t\t\t\t);\n-\t\t\t\t\treturn;\n-\t\t\t\t}\n-\n-\t\t\t\twp_send_json(\n-\t\t\t\t\tarray(\n-\t\t\t\t\t\t'blocked'  => false,\n-\t\t\t\t\t\t'cooldown' => false,\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t} catch ( Exception $e ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray(\n-\t\t\t\t\t\t'message' => __( 'An error occurred while checking status.', 'miniorange-otp-verification' ),\n-\t\t\t\t\t\t'error'   => defined( 'WP_DEBUG' ) && WP_DEBUG ? $e->getMessage() : '',\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t} catch ( Error $e ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray(\n-\t\t\t\t\t\t'message' => __( 'An error occurred while checking status.', 'miniorange-otp-verification' ),\n-\t\t\t\t\t\t'error'   => defined( 'WP_DEBUG' ) && WP_DEBUG ? $e->getMessage() : '',\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for unblocking user in popup (similar to resendcontrol)\n-\t\t *\n-\t\t * Unblocks a user if the block duration has expired and sends a JSON response.\n-\t\t * This function checks whether the user's block time has expired. If expired,\n-\t\t * it removes the block and sends a response indicating the user is unblocked.\n-\t\t *\n-\t\t * @return void Sends a JSON response with the blocked\u002Funblocked status.\n-\t\t *\u002F\n-\t\tpublic function mosp_unblock_user_ajax() {\n-\t\t\t\u002F\u002F phpcs:disable WordPress.Security.NonceVerification.Missing -- Public AJAX endpoint, nonce not required for read operations.\n-\t\t\t$browser_id = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\n-\t\t\t$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';\n-\t\t\t$phone      = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';\n-\n-\t\t\t$is_blocked = $this->handler->mosp_is_blocked( $email, $phone, $browser_id, 'timer_status' );\n-\n-\t\t\tif ( ! $is_blocked ) {\n-\t\t\t\twp_send_json( array( 'unblocked' => true ) );\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$block_data      = $this->handler->mosp_get_block_data( $email, $phone, $browser_id, 'timer_status' );\n-\t\t\t$block_remaining = $block_data['remaining_time'];\n-\n-\t\t\twp_send_json(\n-\t\t\t\tarray(\n-\t\t\t\t\t'blocked'        => true,\n-\t\t\t\t\t'remaining_time' => $block_remaining,\n-\t\t\t\t)\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for getting list of blocked users (admin only).\n-\t\t *\n-\t\t * @return void Sends JSON response with blocked users list.\n-\t\t *\u002F\n-\t\tpublic function mosp_get_blocked_users_ajax() {\n-\t\t\tcheck_ajax_referer( 'mo_osp_admin_nonce', 'security' );\n-\n-\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray( 'message' => __( 'Insufficient permissions', 'miniorange-otp-verification' ) ),\n-\t\t\t\t\t403\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$limit  = isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 100;\n-\t\t\t$offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0;\n-\n-\t\t\t$result = $this->storage->mosp_get_all_blocked_users( $limit, $offset );\n-\n-\t\t\tforeach ( $result['users'] as &$user ) {\n-\t\t\t\t$user['remaining_time_formatted'] = $this->mosp_format_time( $user['remaining_time'] );\n-\t\t\t\t$user['block_reason_label']       = $this->mosp_get_block_reason_label( $user['block_reason'] );\n-\t\t\t}\n-\n-\t\t\twp_send_json_success( $result );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for unblocking user by hash (admin only).\n-\t\t *\n-\t\t * @return void Sends JSON response.\n-\t\t *\u002F\n-\t\tpublic function mosp_unblock_user_by_hash_ajax() {\n-\t\t\tcheck_ajax_referer( 'mo_osp_admin_nonce', 'security' );\n-\n-\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray( 'message' => __( 'Insufficient permissions', 'miniorange-otp-verification' ) ),\n-\t\t\t\t\t403\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$identifier_hash = isset( $_POST['identifier_hash'] ) ? sanitize_text_field( wp_unslash( $_POST['identifier_hash'] ) ) : '';\n-\n-\t\t\tif ( empty( $identifier_hash ) ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray( 'message' => __( 'Invalid identifier hash', 'miniorange-otp-verification' ) ),\n-\t\t\t\t\t400\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$result = $this->handler->mosp_unblock_user_by_hash( $identifier_hash );\n-\n-\t\t\tif ( $result['success'] ) {\n-\t\t\t\twp_send_json_success( array( 'message' => $result['message'] ) );\n-\t\t\t} else {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray( 'message' => $result['message'] ),\n-\t\t\t\t\t400\n-\t\t\t\t);\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler: clear all blocked users \u002F limits \u002F puzzle flags (admin only).\n-\t\t *\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic function mosp_clear_all_blocked_users_ajax() {\n-\t\t\tcheck_ajax_referer( 'mo_osp_admin_nonce', 'security' );\n-\n-\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray( 'message' => __( 'Insufficient permissions', 'miniorange-otp-verification' ) ),\n-\t\t\t\t\t403\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$result = $this->handler->mosp_clear_all_blocked_data();\n-\n-\t\t\tif ( ! $result['success'] ) {\n-\t\t\t\twp_send_json_error( array( 'message' => $result['message'] ) );\n-\t\t\t}\n-\n-\t\t\twp_send_json_success(\n-\t\t\t\tarray(\n-\t\t\t\t\t'message' => $result['message'],\n-\t\t\t\t\t'deleted' => $result['deleted'],\n-\t\t\t\t)\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * AJAX handler for enabling\u002Fdisabling addon (admin only).\n-\t\t *\n-\t\t * @return void Sends JSON response.\n-\t\t *\u002F\n-\t\tpublic function mosp_toggle_addon_ajax() {\n-\t\t\tcheck_ajax_referer( 'mo_osp_admin_nonce', 'security' );\n-\n-\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\n-\t\t\t\twp_send_json_error(\n-\t\t\t\t\tarray( 'message' => __( 'Insufficient permissions', 'miniorange-otp-verification' ) ),\n-\t\t\t\t\t403\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$enabled             = isset( $_POST['enabled'] ) ? absint( $_POST['enabled'] ) : 0;\n-\t\t\t$settings            = $this->storage->mosp_get_settings();\n-\t\t\t$settings['enabled'] = ( 1 === $enabled );\n-\n-\t\t\t$result = $this->storage->mosp_update_settings( $settings );\n-\n-\t\t\tif ( $result ) {\n-\t\t\t\t$message = $settings['enabled']\n-\t\t\t\t\t? __( 'Addon enabled.', 'miniorange-otp-verification' )\n-\t\t\t\t\t: __( 'Addon disabled.', 'miniorange-otp-verification' );\n-\t\t\t\twp_send_json_success( array( 'message' => $message ) );\n-\t\t\t}\n-\n-\t\t\twp_send_json_error(\n-\t\t\t\tarray( 'message' => __( 'Failed to update addon status.', 'miniorange-otp-verification' ) ),\n-\t\t\t\t500\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Format time in seconds to human-readable format.\n-\t\t *\n-\t\t * @param int $seconds Time in seconds.\n-\t\t * @return string Formatted time string.\n-\t\t *\u002F\n-\t\tprivate function mosp_format_time( $seconds ) {\n-\t\t\tif ( $seconds \u003C 60 ) {\n-\t\t\t\t\u002F\u002F translators: %d: Number of seconds.\n-\t\t\t\treturn sprintf( _n( '%d second', '%d seconds', $seconds, 'miniorange-otp-verification' ), $seconds );\n-\t\t\t} elseif ( $seconds \u003C 3600 ) {\n-\t\t\t\t$minutes = floor( $seconds \u002F 60 );\n-\t\t\t\t$secs    = $seconds % 60;\n-\t\t\t\tif ( $secs > 0 ) {\n-\t\t\t\t\t\u002F\u002F translators: %d: Number of minutes.\n-\t\t\t\t\t$minutes_str = sprintf( _n( '%d minute', '%d minutes', $minutes, 'miniorange-otp-verification' ), $minutes );\n-\t\t\t\t\t\u002F\u002F translators: %d: Number of seconds.\n-\t\t\t\t\t$seconds_str = sprintf( _n( '%d second', '%d seconds', $secs, 'miniorange-otp-verification' ), $secs );\n-\t\t\t\t\treturn $minutes_str . ' ' . $seconds_str;\n-\t\t\t\t}\n-\t\t\t\t\u002F\u002F translators: %d: Number of minutes.\n-\t\t\t\treturn sprintf( _n( '%d minute', '%d minutes', $minutes, 'miniorange-otp-verification' ), $minutes );\n-\t\t\t} else {\n-\t\t\t\t$hours   = floor( $seconds \u002F 3600 );\n-\t\t\t\t$minutes = floor( ( $seconds % 3600 ) \u002F 60 );\n-\t\t\t\tif ( $minutes > 0 ) {\n-\t\t\t\t\t\u002F\u002F translators: %d: Number of hours.\n-\t\t\t\t\t$hours_str = sprintf( _n( '%d hour', '%d hours', $hours, 'miniorange-otp-verification' ), $hours );\n-\t\t\t\t\t\u002F\u002F translators: %d: Number of minutes.\n-\t\t\t\t\t$minutes_str = sprintf( _n( '%d minute', '%d minutes', $minutes, 'miniorange-otp-verification' ), $minutes );\n-\t\t\t\t\treturn $hours_str . ' ' . $minutes_str;\n-\t\t\t\t}\n-\t\t\t\t\u002F\u002F translators: %d: Number of hours.\n-\t\t\t\treturn sprintf( _n( '%d hour', '%d hours', $hours, 'miniorange-otp-verification' ), $hours );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get human-readable label for block reason.\n-\t\t *\n-\t\t * @param string $reason Block reason code.\n-\t\t * @return string Human-readable label.\n-\t\t *\u002F\n-\t\tprivate function mosp_get_block_reason_label( $reason ) {\n-\t\t\tswitch ( $reason ) {\n-\t\t\t\tcase 'hourly_limit_exceeded':\n-\t\t\t\t\treturn __( 'Hourly Limit Exceeded', 'miniorange-otp-verification' );\n-\t\t\t\tcase 'daily_limit_exceeded':\n-\t\t\t\t\treturn __( 'Daily Limit Exceeded', 'miniorange-otp-verification' );\n-\t\t\t\tcase 'max_attempts_exceeded':\n-\t\t\t\t\treturn __( 'Max Attempts Exceeded', 'miniorange-otp-verification' );\n-\t\t\t\tcase 'cooldown':\n-\t\t\t\t\treturn __( 'Cooldown Period', 'miniorange-otp-verification' );\n-\t\t\t\tdefault:\n-\t\t\t\t\treturn __( 'Blocked', 'miniorange-otp-verification' );\n-\t\t\t}\n-\t\t}\n-\t}\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * OTP Spam AJAX Handler\r\n+ *\r\n+ * @package otpspampreventer\u002Fhandler\r\n+ *\u002F\r\n+\r\n+namespace OSP\\Handler;\r\n+\r\n+use OSP\\Handler\\MoOtpSpamStorage;\r\n+use OSP\\Handler\\MoOtpSpamPreventerHandler;\r\n+use OSP\\Helper\\MoPuzzleHelper;\r\n+use OSP\\Helper\\MoSecurityHelper;\r\n+use OSP\\Helper\\MoSessionHelper;\r\n+use OSP\\Traits\\Instance;\r\n+use OTP\\Helper\\MoMessages;\r\n+use OTP\\Helper\\MoPHPSessions;\r\n+use OTP\\Helper\\MoUtility;\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+if ( ! class_exists( 'MoOtpSpamAjax' ) ) {\r\n+\t\u002F**\r\n+\t * Handles AJAX requests for spam prevention functionality.\r\n+\t *\u002F\r\n+\tclass MoOtpSpamAjax {\r\n+\r\n+\t\tuse Instance;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Storage instance\r\n+\t\t *\r\n+\t\t * @var MoOtpSpamStorage\r\n+\t\t *\u002F\r\n+\t\tprivate $storage;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Handler instance\r\n+\t\t *\r\n+\t\t * @var MoOtpSpamPreventerHandler\r\n+\t\t *\u002F\r\n+\t\tprivate $handler;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Constructor\r\n+\t\t *\u002F\r\n+\t\tpublic function __construct() {\r\n+\t\t\t$this->storage = MoOtpSpamStorage::instance();\r\n+\t\t\t$this->handler = MoOtpSpamPreventerHandler::instance();\r\n+\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_check_spam', array( $this, 'mosp_check_spam_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_check_spam', array( $this, 'mosp_check_spam_ajax' ) );\r\n+\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_log_attempt', array( $this, 'mosp_log_attempt_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_log_attempt', array( $this, 'mosp_log_attempt_ajax' ) );\r\n+\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_save_settings', array( $this, 'mosp_save_settings_ajax' ) );\r\n+\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_check_puzzle', array( $this, 'mosp_check_puzzle_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_check_puzzle', array( $this, 'mosp_check_puzzle_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_generate_puzzle', array( $this, 'mosp_generate_puzzle_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_generate_puzzle', array( $this, 'mosp_generate_puzzle_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_verify_puzzle', array( $this, 'mosp_verify_puzzle_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_verify_puzzle', array( $this, 'mosp_verify_puzzle_ajax' ) );\r\n+\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_check_timer_status', array( $this, 'mosp_check_timer_status_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_check_timer_status', array( $this, 'mosp_check_timer_status_ajax' ) );\r\n+\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_check_puzzle_requirement', array( $this, 'mosp_check_puzzle_requirement_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_check_puzzle_requirement', array( $this, 'mosp_check_puzzle_requirement_ajax' ) );\r\n+\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_check_blocked', array( $this, 'mosp_check_blocked_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_check_blocked', array( $this, 'mosp_check_blocked_ajax' ) );\r\n+\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_unblock_user', array( $this, 'mosp_unblock_user_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_nopriv_mo_osp_unblock_user', array( $this, 'mosp_unblock_user_ajax' ) );\r\n+\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_get_blocked_users', array( $this, 'mosp_get_blocked_users_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_unblock_user_by_hash', array( $this, 'mosp_unblock_user_by_hash_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_clear_all_blocked_users', array( $this, 'mosp_clear_all_blocked_users_ajax' ) );\r\n+\t\t\tadd_action( 'wp_ajax_mo_osp_toggle_addon', array( $this, 'mosp_toggle_addon_ajax' ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for checking spam before OTP send.\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_check_spam_ajax() {\r\n+\t\t\tcheck_ajax_referer( 'mo_osp_nonce', 'security' );\r\n+\r\n+\t\t\t$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';\r\n+\t\t\t$phone      = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';\r\n+\t\t\t$browser_id = isset( $_POST['browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['browser_id'] ) ) : '';\r\n+\r\n+\t\t\tif ( empty( $email ) && empty( $phone ) ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray( 'message' => __( 'Phone or email is required', 'miniorange-otp-verification' ) ),\r\n+\t\t\t\t\t400\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $browser_id ) {\r\n+\t\t\t\t$_POST['mo_osp_browser_id'] = $browser_id;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$result = $this->handler->mosp_check_spam_before_otp_send( true, '', $email, $phone );\r\n+\r\n+\t\t\tif ( is_wp_error( $result ) ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t'message' => $result->get_error_message(),\r\n+\t\t\t\t\t\t'code'    => $result->get_error_code(),\r\n+\t\t\t\t\t),\r\n+\t\t\t\t\t429\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\twp_send_json_success(\r\n+\t\t\t\tarray( 'message' => __( 'Request allowed', 'miniorange-otp-verification' ) )\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for logging OTP attempts (for checkout mode)\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_log_attempt_ajax() {\r\n+\t\t\tcheck_ajax_referer( 'mo_osp_nonce', 'security' );\r\n+\r\n+\t\t\t$browser_id = isset( $_POST['browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['browser_id'] ) ) : '';\r\n+\t\t\t$mode       = isset( $_POST['mode'] ) ? sanitize_text_field( wp_unslash( $_POST['mode'] ) ) : '';\r\n+\r\n+\t\t\tif ( empty( $browser_id ) ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray( 'message' => __( 'Browser ID is required', 'miniorange-otp-verification' ) ),\r\n+\t\t\t\t\t400\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$_POST['mo_osp_browser_id'] = $browser_id;\r\n+\r\n+\t\t\t$this->handler->mosp_record_otp_attempt( '', '', '' );\r\n+\r\n+\t\t\twp_send_json_success(\r\n+\t\t\t\tarray( 'message' => __( 'Attempt logged', 'miniorange-otp-verification' ) )\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for saving settings (admin only).\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_save_settings_ajax() {\r\n+\t\t\tcheck_ajax_referer( 'mo_osp_admin_nonce', 'security' );\r\n+\r\n+\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray( 'message' => __( 'Insufficient permissions', 'miniorange-otp-verification' ) ),\r\n+\t\t\t\t\t403\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings = $this->storage->mosp_get_settings();\r\n+\r\n+\t\t\tif ( isset( $_POST['enabled'] ) ) {\r\n+\t\t\t\t$settings['enabled'] = ( 1 === absint( $_POST['enabled'] ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings['cooldown_time'] = isset( $_POST['cooldown_time'] ) ? absint( $_POST['cooldown_time'] ) : 60;\r\n+\r\n+\t\t\t$max_attempts             = isset( $_POST['max_attempts'] ) ? absint( $_POST['max_attempts'] ) : 3;\r\n+\t\t\t$settings['max_attempts'] = max( 1, min( 10, $max_attempts ) );\r\n+\r\n+\t\t\t$settings['block_time'] = isset( $_POST['block_time'] ) ? absint( $_POST['block_time'] ) : 900;\r\n+\r\n+\t\t\t$settings['daily_limit']  = isset( $_POST['daily_limit'] ) ? absint( $_POST['daily_limit'] ) : 10;\r\n+\t\t\t$settings['hourly_limit'] = isset( $_POST['hourly_limit'] ) ? absint( $_POST['hourly_limit'] ) : 5;\r\n+\r\n+\t\t\t$settings['track_phone']   = true;\r\n+\t\t\t$settings['track_email']   = true;\r\n+\t\t\t$settings['track_ip']      = true;\r\n+\t\t\t$settings['track_browser'] = true;\r\n+\r\n+\t\t\t$whitelist_ips             = isset( $_POST['whitelist_ips'] ) ? sanitize_textarea_field( wp_unslash( $_POST['whitelist_ips'] ) ) : '';\r\n+\t\t\t$settings['whitelist_ips'] = array_filter( array_map( 'trim', explode( \"\\n\", $whitelist_ips ) ) );\r\n+\t\t\t$settings['whitelist_ips'] = array_values( $settings['whitelist_ips'] );\r\n+\r\n+\t\t\t$validation_errors = $this->mosp_validate_settings( $settings );\r\n+\t\t\tif ( ! empty( $validation_errors ) ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t'message' => __( 'Invalid settings', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t\t'errors'  => $validation_errors,\r\n+\t\t\t\t\t),\r\n+\t\t\t\t\t400\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$result = $this->storage->mosp_update_settings( $settings );\r\n+\r\n+\t\t\tif ( $result ) {\r\n+\t\t\t\twp_send_json_success(\r\n+\t\t\t\t\tarray( 'message' => __( 'Settings saved successfully', 'miniorange-otp-verification' ) )\r\n+\t\t\t\t);\r\n+\t\t\t} else {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray( 'message' => __( 'Failed to save settings', 'miniorange-otp-verification' ) ),\r\n+\t\t\t\t\t500\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Validate settings array\r\n+\t\t *\r\n+\t\t * @param array $settings Settings to validate.\r\n+\t\t * @return array Validation errors\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_validate_settings( $settings ) {\r\n+\t\t\t$errors = array();\r\n+\r\n+\t\t\tif ( $settings['cooldown_time'] \u003C 0 || $settings['cooldown_time'] > 86400 ) {\r\n+\t\t\t\t$errors['cooldown_time'] = __( 'Cooldown time must be between 0 and 86400 seconds', 'miniorange-otp-verification' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $settings['max_attempts'] \u003C 1 || $settings['max_attempts'] > 10 ) {\r\n+\t\t\t\t$errors['max_attempts'] = __( 'Max attempts must be between 1 and 10', 'miniorange-otp-verification' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $settings['block_time'] \u003C 60 || $settings['block_time'] > 604800 ) {\r\n+\t\t\t\t$errors['block_time'] = __( 'Block time must be between 60 and 604800 seconds', 'miniorange-otp-verification' );\r\n+\t\t\t}\r\n+\t\t\tif ( $settings['daily_limit'] \u003C 1 || $settings['daily_limit'] > 1000 ) {\r\n+\t\t\t\t$errors['daily_limit'] = __( 'Daily limit must be between 1 and 1000', 'miniorange-otp-verification' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $settings['hourly_limit'] \u003C 1 || $settings['hourly_limit'] > 100 ) {\r\n+\t\t\t\t$errors['hourly_limit'] = __( 'Hourly limit must be between 1 and 100', 'miniorange-otp-verification' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $settings['hourly_limit'] \u003C= $settings['max_attempts'] ) {\r\n+\t\t\t\t$errors['hourly_limit'] = sprintf(\r\n+\t\t\t\t\t\u002F* translators: %d: max attempts value *\u002F\r\n+\t\t\t\t\t__( 'Hourly limit must be greater than max attempts per window (%d)', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t$settings['max_attempts']\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $settings['daily_limit'] \u003C= $settings['hourly_limit'] ) {\r\n+\t\t\t\t$errors['daily_limit'] = sprintf(\r\n+\t\t\t\t\t\u002F* translators: %d: hourly limit value *\u002F\r\n+\t\t\t\t\t__( 'Daily limit must be greater than hourly limit (%d)', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t$settings['hourly_limit']\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! $settings['track_phone'] && ! $settings['track_email'] && ! $settings['track_ip'] && ! $settings['track_browser'] ) {\r\n+\t\t\t\t$errors['tracking'] = __( 'At least one tracking method must be enabled', 'miniorange-otp-verification' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $settings['whitelist_ips'] as $ip ) {\r\n+\t\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {\r\n+\t\t\t\t\t$errors['whitelist_ips'] = sprintf(\r\n+\t\t\t\t\t\t\u002F* translators: %s: invalid IP address *\u002F\r\n+\t\t\t\t\t\t__( 'Invalid IP address: %s', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t\t$ip\r\n+\t\t\t\t\t);\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn $errors;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * SECURITY ENHANCEMENT: AJAX handler for generating secure puzzles\r\n+\t\t *\r\n+\t\t * This method generates a new puzzle and stores it securely in the session,\r\n+\t\t * preventing client-side manipulation of puzzle data.\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_generate_puzzle_ajax() {\r\n+\t\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'mo_osp_nonce' ) ) {\r\n+\t\t\t\twp_send_json_error( array( 'message' => __( 'Security check failed', 'miniorange-otp-verification' ) ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$puzzle = MoPuzzleHelper::mosp_generate_secure_puzzle();\r\n+\r\n+\t\t\tif ( ! $puzzle ) {\r\n+\t\t\t\twp_send_json_error( array( 'message' => __( 'Failed to generate puzzle', 'miniorange-otp-verification' ) ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\tMoPuzzleHelper::mosp_store_puzzle_in_session( $puzzle['question'], $puzzle['answer'] );\r\n+\r\n+\t\t\t$puzzle_image = MoPuzzleHelper::mosp_generate_puzzle_image( $puzzle['question'] );\r\n+\r\n+\t\t\t$response = array(\r\n+\t\t\t\t'question' => $puzzle['question'],\r\n+\t\t\t\t'message'  => __( 'Puzzle generated successfully', 'miniorange-otp-verification' ),\r\n+\t\t\t);\r\n+\r\n+\t\t\tif ( $puzzle_image ) {\r\n+\t\t\t\t$response['image'] = $puzzle_image;\r\n+\t\t\t}\r\n+\r\n+\t\t\twp_send_json_success( $response );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for checking if puzzle verification is required\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_check_puzzle_ajax() {\r\n+\t\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'mo_osp_nonce' ) ) {\r\n+\t\t\t\twp_send_json_error( array( 'message' => __( 'Security check failed', 'miniorange-otp-verification' ) ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$email      = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );\r\n+\t\t\t$phone      = sanitize_text_field( wp_unslash( $_POST['phone'] ?? '' ) );\r\n+\t\t\t$browser_id = sanitize_text_field( wp_unslash( $_POST['browser_id'] ?? '' ) );\r\n+\t\t\t$ip         = $this->handler->mosp_get_client_ip();\r\n+\r\n+\t\t\tif ( ! empty( $phone ) ) {\r\n+\t\t\t\t$phone       = MoUtility::process_phone_number( $phone );\r\n+\t\t\t\t$digit_count = strlen( preg_replace( '\u002F\\D\u002F', '', $phone ) );\r\n+\t\t\t\tif ( $digit_count \u003C 6 ) {\r\n+\t\t\t\t\t$phone = '';\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! empty( $email ) ) {\r\n+\t\t\t\tMoPHPSessions::add_session_var( 'user_email', $email );\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $phone ) ) {\r\n+\t\t\t\tMoPHPSessions::add_session_var( 'phone_number_mo', $phone );\r\n+\t\t\t}\r\n+\t\t\t$requires_puzzle = $this->handler->mosp_requires_puzzle_verification( $email, $phone, $ip, $browser_id );\r\n+\r\n+\t\t\twp_send_json_success(\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'requires_puzzle' => $requires_puzzle,\r\n+\t\t\t\t\t'message'         => $requires_puzzle ? __( 'Puzzle verification required', 'miniorange-otp-verification' ) : __( 'No puzzle required', 'miniorange-otp-verification' ),\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for verifying puzzle completion\r\n+\t\t * SECURITY ENHANCEMENT: Uses session-stored puzzle data for validation\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_verify_puzzle_ajax() {\r\n+\t\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'mo_osp_nonce' ) ) {\r\n+\t\t\t\twp_send_json_error( array( 'message' => __( 'Security check failed', 'miniorange-otp-verification' ) ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$email      = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );\r\n+\t\t\t$phone      = sanitize_text_field( wp_unslash( $_POST['phone'] ?? '' ) );\r\n+\t\t\t$browser_id = sanitize_text_field( wp_unslash( $_POST['browser_id'] ?? '' ) );\r\n+\t\t\t$ip         = $this->handler->mosp_get_client_ip();\r\n+\r\n+\t\t\tif ( ! empty( $phone ) ) {\r\n+\t\t\t\t$phone       = MoUtility::process_phone_number( $phone );\r\n+\t\t\t\t$digit_count = strlen( preg_replace( '\u002F\\D\u002F', '', $phone ) );\r\n+\t\t\t\tif ( $digit_count \u003C 6 ) {\r\n+\t\t\t\t\t$phone = '';\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\tif ( empty( $email ) ) {\r\n+\t\t\t\t$email = MoPHPSessions::get_session_var( 'user_email' );\r\n+\t\t\t}\r\n+\t\t\t$session_phone = MoPHPSessions::get_session_var( 'phone_number_mo' );\r\n+\t\t\tif ( empty( $phone ) && ! empty( $session_phone ) ) {\r\n+\t\t\t\t$phone = $session_phone;\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $phone ) ) {\r\n+\t\t\t\t$phone       = MoUtility::process_phone_number( $phone );\r\n+\t\t\t\t$digit_count = strlen( preg_replace( '\u002F\\D\u002F', '', $phone ) );\r\n+\t\t\t\tif ( $digit_count \u003C 6 ) {\r\n+\t\t\t\t\t$phone = '';\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $session_phone ) ) {\r\n+\t\t\t\t$normalized_session_phone = MoUtility::process_phone_number( $session_phone );\r\n+\t\t\t\t$session_digits           = strlen( preg_replace( '\u002F\\D\u002F', '', $normalized_session_phone ) );\r\n+\t\t\t\tif ( $session_digits >= 6 ) {\r\n+\t\t\t\t\t$phone = $normalized_session_phone;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$has_answer    = array_key_exists( 'puzzle_answer', $_POST );\r\n+\t\t\t$puzzle_answer = $has_answer ? intval( wp_unslash( $_POST['puzzle_answer'] ) ) : null;\r\n+\r\n+\t\t\tif ( ! $has_answer ) {\r\n+\t\t\t\twp_send_json_error( array( 'message' => __( 'Puzzle answer is required', 'miniorange-otp-verification' ) ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! MoPuzzleHelper::mosp_has_puzzle_in_session() ) {\r\n+\t\t\t\twp_send_json_error( array( 'message' => __( 'Puzzle session expired. Please refresh and try again.', 'miniorange-otp-verification' ) ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$is_valid = MoPuzzleHelper::mosp_validate_puzzle_answer_from_session( $puzzle_answer );\r\n+\r\n+\t\t\tif ( ! $is_valid ) {\r\n+\t\t\t\t$puzzle_attempts_key = 'mo_osp_puzzle_attempts_' . md5( $ip . MoSecurityHelper::mosp_get_user_agent() );\r\n+\t\t\t\t$incorrect_attempts  = MoPHPSessions::get_session_var( $puzzle_attempts_key );\r\n+\r\n+\t\t\t\tif ( false === $incorrect_attempts ) {\r\n+\t\t\t\t\t$incorrect_attempts = 0;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t++$incorrect_attempts;\r\n+\r\n+\t\t\t\t$max_attempts_per_puzzle = 2;\r\n+\r\n+\t\t\t\tif ( $incorrect_attempts >= $max_attempts_per_puzzle ) {\r\n+\t\t\t\t\tMoPHPSessions::unset_session( $puzzle_attempts_key );\r\n+\r\n+\t\t\t\t\t$new_puzzle = MoPuzzleHelper::mosp_generate_secure_puzzle();\r\n+\t\t\t\t\tMoPuzzleHelper::mosp_store_puzzle_in_session(\r\n+\t\t\t\t\t\t$new_puzzle['question'],\r\n+\t\t\t\t\t\t$new_puzzle['answer'],\r\n+\t\t\t\t\t\t$ip,\r\n+\t\t\t\t\t\tMoSecurityHelper::mosp_get_user_agent()\r\n+\t\t\t\t\t);\r\n+\r\n+\t\t\t\t\t$puzzle_image = MoPuzzleHelper::mosp_generate_puzzle_image( $new_puzzle['question'] );\r\n+\r\n+\t\t\t\t\t$response_data = array(\r\n+\t\t\t\t\t\t'message'      => __( 'Incorrect puzzle answer. A new puzzle has been generated. Please solve it.', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t\t'puzzle_reset' => true,\r\n+\t\t\t\t\t);\r\n+\r\n+\t\t\t\t\tif ( $puzzle_image ) {\r\n+\t\t\t\t\t\t$response_data['puzzle_image'] = $puzzle_image;\r\n+\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t$response_data['puzzle_question'] = $new_puzzle['question'];\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\twp_send_json_error( $response_data );\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\tMoPHPSessions::add_session_var( $puzzle_attempts_key, $incorrect_attempts );\r\n+\t\t\t\t\t$remaining_attempts = $max_attempts_per_puzzle - $incorrect_attempts;\r\n+\r\n+\t\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t\t'message'            => sprintf(\r\n+\t\t\t\t\t\t\t\t\u002F* translators: %d: number of remaining attempts *\u002F\r\n+\t\t\t\t\t\t\t\t__( 'Incorrect answer. Please try again. (%d attempt(s) remaining)', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t\t\t\t$remaining_attempts\r\n+\t\t\t\t\t\t\t),\r\n+\t\t\t\t\t\t\t'puzzle_reset'       => false,\r\n+\t\t\t\t\t\t\t'remaining_attempts' => $remaining_attempts,\r\n+\t\t\t\t\t\t)\r\n+\t\t\t\t\t);\r\n+\t\t\t\t}\r\n+\t\t\t} else {\r\n+\t\t\t\t$puzzle_attempts_key = 'mo_osp_puzzle_attempts_' . md5( $ip . MoSecurityHelper::mosp_get_user_agent() );\r\n+\t\t\t\tMoPHPSessions::unset_session( $puzzle_attempts_key );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$this->handler->mosp_reset_immediate_spam_protection( $email, $phone );\r\n+\t\t\t$requires_limit_puzzle = $this->handler->mosp_requires_limit_puzzle_verification( $email, $phone );\r\n+\t\t\tif ( $requires_limit_puzzle ) {\r\n+\t\t\t\t$this->handler->mosp_mark_limit_puzzle_completed( $email, $phone );\r\n+\t\t\t} else {\r\n+\t\t\t\tMoSecurityHelper::mosp_mark_puzzle_verification_complete( $email, $phone );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$verification_token = MoSecurityHelper::mosp_generate_puzzle_verification_token( $email, $phone );\r\n+\r\n+\t\t\twp_send_json_success(\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'message'            => __( 'Puzzle verified successfully', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t'puzzle_cleared'     => true,\r\n+\t\t\t\t\t'verification_token' => $verification_token,\r\n+\t\t\t\t\t'puzzle_nonce'       => wp_create_nonce( 'mo_osp_puzzle_verify' ),\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * SECURITY FIX: Calculate the correct answer for a puzzle question\r\n+\t\t *\r\n+\t\t * @param string $question The puzzle question.\r\n+\t\t * @return int|false The correct answer or false if invalid.\r\n+\t\t *\u002F\r\n+\t\tprivate function calculate_puzzle_answer( $question ) {\r\n+\t\t\tif ( preg_match( '\u002F(\\d+)\\s*([+\\-×÷*\\\u002F])\\s*(\\d+)\u002F', $question, $matches ) ) {\r\n+\t\t\t\t$a        = intval( $matches[1] );\r\n+\t\t\t\t$operator = $matches[2];\r\n+\t\t\t\t$b        = intval( $matches[3] );\r\n+\r\n+\t\t\t\tswitch ( $operator ) {\r\n+\t\t\t\t\tcase '+':\r\n+\t\t\t\t\t\treturn $a + $b;\r\n+\t\t\t\t\tcase '-':\r\n+\t\t\t\t\t\treturn $a - $b;\r\n+\t\t\t\t\tcase '×':\r\n+\t\t\t\t\tcase '*':\r\n+\t\t\t\t\t\treturn $a * $b;\r\n+\t\t\t\t\tcase '÷':\r\n+\t\t\t\t\tcase '\u002F':\r\n+\t\t\t\t\t\treturn 0 !== $b ? intval( $a \u002F $b ) : false;\r\n+\t\t\t\t\tdefault:\r\n+\t\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( preg_match( '\u002F(\\d+)\\s*([+\\-])\\s*(\\d+)\\s*([+\\-])\\s*(\\d+)\u002F', $question, $matches ) ) {\r\n+\t\t\t\t$a   = intval( $matches[1] );\r\n+\t\t\t\t$op1 = $matches[2];\r\n+\t\t\t\t$b   = intval( $matches[3] );\r\n+\t\t\t\t$op2 = $matches[4];\r\n+\t\t\t\t$c   = intval( $matches[5] );\r\n+\r\n+\t\t\t\t$result = $a;\r\n+\t\t\t\t$result = ( '+' === $op1 ) ? $result + $b : $result - $b;\r\n+\t\t\t\t$result = ( '+' === $op2 ) ? $result + $c : $result - $c;\r\n+\r\n+\t\t\t\treturn $result;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for checking timer status with persistent state management.\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_check_timer_status_ajax() {\r\n+\t\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'mo_osp_nonce' ) ) {\r\n+\t\t\t\twp_send_json_error( array( 'message' => __( 'Security check failed', 'miniorange-otp-verification' ) ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$browser_id = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\r\n+\t\t\t$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';\r\n+\t\t\t$phone      = isset( $_POST['phone'] ) ? MoUtility::process_phone_number( sanitize_text_field( wp_unslash( $_POST['phone'] ) ) ) : '';\r\n+\r\n+\t\t\t$state = $this->mosp_get_current_user_state( $email, $phone, $browser_id );\r\n+\t\t\twp_send_json_success( $state );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get current user state with accurate timer information.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $browser_id Browser ID.\r\n+\t\t * @return array Current user state.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_get_current_user_state( $email, $phone, $browser_id ) {\r\n+\t\t\t$settings     = $this->storage->mosp_get_settings();\r\n+\t\t\t$current_time = time();\r\n+\r\n+\t\t\t$is_blocked = $this->handler->mosp_is_blocked( $email, $phone, $browser_id, 'timer_status' );\r\n+\r\n+\t\t\tif ( $is_blocked ) {\r\n+\t\t\t\t$block_data      = $this->handler->mosp_get_block_data( $email, $phone, $browser_id, 'timer_status' );\r\n+\t\t\t\t$block_remaining = $block_data['remaining_time'];\r\n+\t\t\t\t$block_reason    = $block_data['reason'];\r\n+\r\n+\t\t\t\tif ( $block_remaining > 0 ) {\r\n+\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t'blocked'        => true,\r\n+\t\t\t\t\t\t'remaining_time' => $block_remaining,\r\n+\t\t\t\t\t\t'blocked_type'   => $block_reason,\r\n+\t\t\t\t\t\t'message'        => MoMessages::showMessage( MoMessages::USER_IS_BLOCKED_AJAX ),\r\n+\t\t\t\t\t);\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$requires_regular_puzzle = $this->handler->mosp_requires_puzzle_verification( $email, $phone, $this->handler->mosp_get_client_ip(), $browser_id );\r\n+\t\t\t$requires_limit_puzzle   = $this->handler->mosp_requires_limit_puzzle_verification( $email, $phone );\r\n+\t\t\t$requires_puzzle         = $requires_regular_puzzle || $requires_limit_puzzle;\r\n+\r\n+\t\t\tif ( $requires_puzzle ) {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'puzzle_required' => true,\r\n+\t\t\t\t\t'message'         => 'Please complete the security verification to continue.',\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$ip                = $this->handler->mosp_get_client_ip();\r\n+\t\t\t$is_ip_whitelisted = false;\r\n+\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t$is_ip_whitelisted = $this->handler->mosp_is_whitelisted( $ip, 'ip' );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$cooldown_remaining = $is_ip_whitelisted ? 0 : $this->mosp_get_cooldown_remaining_time( $email, $phone, $browser_id );\r\n+\r\n+\t\t\tif ( $cooldown_remaining > 0 ) {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'status'         => 'cooldown',\r\n+\t\t\t\t\t'cooldown'       => true,\r\n+\t\t\t\t\t'timer_active'   => true,\r\n+\t\t\t\t\t'remaining_time' => $cooldown_remaining,\r\n+\t\t\t\t\t'message'        => MoMessages::showMessage( MoMessages::LIMIT_OTP_SENT ),\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn array(\r\n+\t\t\t\t'status'  => 'ready',\r\n+\t\t\t\t'message' => 'Ready to send OTP',\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get remaining cooldown time for user.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $browser_id Browser ID.\r\n+\t\t * @return int Remaining cooldown time.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_get_cooldown_remaining_time( $email, $phone, $browser_id ) {\r\n+\t\t\ttry {\r\n+\t\t\t\t$identifiers   = $this->handler->mosp_get_all_identifiers( $email, $phone, $this->handler->mosp_get_client_ip(), $browser_id );\r\n+\t\t\t\t$current_time  = time();\r\n+\t\t\t\t$settings      = $this->storage->mosp_get_settings();\r\n+\t\t\t\t$cooldown_time = isset( $settings['cooldown_time'] ) ? (int) $settings['cooldown_time'] : 60;\r\n+\r\n+\t\t\t\tif ( empty( $identifiers ) || ! is_array( $identifiers ) ) {\r\n+\t\t\t\t\treturn 0;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t\tif ( empty( $identifier ) ) {\r\n+\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\r\n+\t\t\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\r\n+\r\n+\t\t\t\t\tif ( false === $data || ! isset( $data['attempts'] ) || ! is_array( $data['attempts'] ) ) {\r\n+\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$attempts      = $data['attempts'];\r\n+\t\t\t\t\t$attempt_count = count( $attempts );\r\n+\r\n+\t\t\t\t\tif ( $attempt_count \u003C 2 ) {\r\n+\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$sorted_attempts = $attempts;\r\n+\t\t\t\t\trsort( $sorted_attempts );\r\n+\r\n+\t\t\t\t\tif ( ! isset( $sorted_attempts[0] ) || ! isset( $sorted_attempts[1] ) ) {\r\n+\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$most_recent_attempt    = (int) $sorted_attempts[0];\r\n+\t\t\t\t\t$second_to_last_attempt = (int) $sorted_attempts[1];\r\n+\r\n+\t\t\t\t\t$time_between_attempts = $most_recent_attempt - $second_to_last_attempt;\r\n+\r\n+\t\t\t\t\tif ( $time_between_attempts \u003C $cooldown_time ) {\r\n+\t\t\t\t\t\t$cooldown_expires_at = $second_to_last_attempt + $cooldown_time;\r\n+\t\t\t\t\t\t$remaining           = $cooldown_expires_at - $current_time;\r\n+\r\n+\t\t\t\t\t\tif ( $remaining > 0 ) {\r\n+\t\t\t\t\t\t\treturn $remaining;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t} catch ( Exception $e ) {\r\n+\t\t\t\treturn 0;\r\n+\t\t\t} catch ( Error $e ) {\r\n+\t\t\t\treturn 0;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn 0;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for checking puzzle requirement (separate from timer status)\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_check_puzzle_requirement_ajax() {\r\n+\t\t\tif ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'mo_osp_nonce' ) ) {\r\n+\t\t\t\twp_send_json_error( array( 'message' => __( 'Security check failed', 'miniorange-otp-verification' ) ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';\r\n+\t\t\t$phone      = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';\r\n+\t\t\t$browser_id = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\r\n+\r\n+\t\t\t$requires_regular_puzzle = $this->handler->mosp_requires_puzzle_verification( $email, $phone, $this->handler->mosp_get_client_ip(), $browser_id );\r\n+\t\t\t$requires_limit_puzzle   = $this->handler->mosp_requires_limit_puzzle_verification( $email, $phone );\r\n+\t\t\t$requires_puzzle         = $requires_regular_puzzle || $requires_limit_puzzle;\r\n+\r\n+\t\t\twp_send_json_success(\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'puzzle_required' => $requires_puzzle,\r\n+\t\t\t\t\t'regular_puzzle'  => $requires_regular_puzzle,\r\n+\t\t\t\t\t'limit_puzzle'    => $requires_limit_puzzle,\r\n+\t\t\t\t\t'message'         => $requires_puzzle ? MoMessages::showMessage( MoMessages::PLEASE_VALIDATE ) : __( 'No puzzle required', 'miniorange-otp-verification' ),\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for checking if user is blocked in popup (similar to resendcontrol)\r\n+\t\t *\r\n+\t\t * Checks if the user is blocked and sends a JSON response.\r\n+\t\t * This function calculates the remaining block time and cooldown for a user.\r\n+\t\t * If the user is still blocked or on cooldown, it returns the remaining time.\r\n+\t\t *\r\n+\t\t * @return void Sends a JSON response with the blocked status and remaining time.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_check_blocked_ajax() {\r\n+\t\t\t\u002F\u002F phpcs:disable WordPress.Security.NonceVerification.Missing -- Public AJAX endpoint, nonce not required for read operations.\r\n+\t\t\ttry {\r\n+\t\t\t\t$browser_id = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\r\n+\t\t\t\t$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';\r\n+\t\t\t\t$phone      = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';\r\n+\r\n+\t\t\t\t$is_blocked = $this->handler->mosp_is_blocked( $email, $phone, $browser_id, 'timer_status' );\r\n+\r\n+\t\t\t\tif ( $is_blocked ) {\r\n+\t\t\t\t\t$block_data      = $this->handler->mosp_get_block_data( $email, $phone, $browser_id, 'timer_status' );\r\n+\t\t\t\t\t$block_remaining = isset( $block_data['remaining_time'] ) ? $block_data['remaining_time'] : 0;\r\n+\t\t\t\t\t$block_reason    = isset( $block_data['reason'] ) ? $block_data['reason'] : '';\r\n+\r\n+\t\t\t\t\tif ( $block_remaining > 0 ) {\r\n+\t\t\t\t\t\twp_send_json(\r\n+\t\t\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t\t\t'blocked'        => true,\r\n+\t\t\t\t\t\t\t\t'remaining_time' => $block_remaining,\r\n+\t\t\t\t\t\t\t\t'blocked_type'   => $block_reason,\r\n+\t\t\t\t\t\t\t\t'message'        => MoMessages::showMessage( MoMessages::USER_IS_BLOCKED_AJAX ),\r\n+\t\t\t\t\t\t\t)\r\n+\t\t\t\t\t\t);\r\n+\t\t\t\t\t\treturn;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$ip                = $this->handler->mosp_get_client_ip();\r\n+\t\t\t\t$is_ip_whitelisted = false;\r\n+\t\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t\t$is_ip_whitelisted = $this->handler->mosp_is_whitelisted( $ip, 'ip' );\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$cooldown_remaining = $is_ip_whitelisted ? 0 : $this->mosp_get_cooldown_remaining_time( $email, $phone, $browser_id );\r\n+\r\n+\t\t\t\tif ( $cooldown_remaining > 0 ) {\r\n+\t\t\t\t\twp_send_json(\r\n+\t\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t\t'cooldown'       => true,\r\n+\t\t\t\t\t\t\t'remaining_time' => $cooldown_remaining,\r\n+\t\t\t\t\t\t\t'message'        => MoMessages::showMessage( MoMessages::LIMIT_OTP_SENT ),\r\n+\t\t\t\t\t\t)\r\n+\t\t\t\t\t);\r\n+\t\t\t\t\treturn;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\twp_send_json(\r\n+\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t'blocked'  => false,\r\n+\t\t\t\t\t\t'cooldown' => false,\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t} catch ( Exception $e ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t'message' => __( 'An error occurred while checking status.', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t\t'error'   => defined( 'WP_DEBUG' ) && WP_DEBUG ? $e->getMessage() : '',\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t} catch ( Error $e ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t'message' => __( 'An error occurred while checking status.', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t\t'error'   => defined( 'WP_DEBUG' ) && WP_DEBUG ? $e->getMessage() : '',\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for unblocking user in popup (similar to resendcontrol)\r\n+\t\t *\r\n+\t\t * Unblocks a user if the block duration has expired and sends a JSON response.\r\n+\t\t * This function checks whether the user's block time has expired. If expired,\r\n+\t\t * it removes the block and sends a response indicating the user is unblocked.\r\n+\t\t *\r\n+\t\t * @return void Sends a JSON response with the blocked\u002Funblocked status.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_unblock_user_ajax() {\r\n+\t\t\t\u002F\u002F phpcs:disable WordPress.Security.NonceVerification.Missing -- Public AJAX endpoint, nonce not required for read operations.\r\n+\t\t\t$browser_id = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\r\n+\t\t\t$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';\r\n+\t\t\t$phone      = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';\r\n+\r\n+\t\t\t$is_blocked = $this->handler->mosp_is_blocked( $email, $phone, $browser_id, 'timer_status' );\r\n+\r\n+\t\t\tif ( ! $is_blocked ) {\r\n+\t\t\t\twp_send_json( array( 'unblocked' => true ) );\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$block_data      = $this->handler->mosp_get_block_data( $email, $phone, $browser_id, 'timer_status' );\r\n+\t\t\t$block_remaining = $block_data['remaining_time'];\r\n+\r\n+\t\t\twp_send_json(\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'blocked'        => true,\r\n+\t\t\t\t\t'remaining_time' => $block_remaining,\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for getting list of blocked users (admin only).\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response with blocked users list.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_get_blocked_users_ajax() {\r\n+\t\t\tcheck_ajax_referer( 'mo_osp_admin_nonce', 'security' );\r\n+\r\n+\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray( 'message' => __( 'Insufficient permissions', 'miniorange-otp-verification' ) ),\r\n+\t\t\t\t\t403\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$limit  = isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 100;\r\n+\t\t\t$offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0;\r\n+\r\n+\t\t\t$result = $this->storage->mosp_get_all_blocked_users( $limit, $offset );\r\n+\r\n+\t\t\tforeach ( $result['users'] as &$user ) {\r\n+\t\t\t\t$user['remaining_time_formatted'] = $this->mosp_format_time( $user['remaining_time'] );\r\n+\t\t\t\t$user['block_reason_label']       = $this->mosp_get_block_reason_label( $user['block_reason'] );\r\n+\t\t\t}\r\n+\r\n+\t\t\twp_send_json_success( $result );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for unblocking user by hash (admin only).\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_unblock_user_by_hash_ajax() {\r\n+\t\t\tcheck_ajax_referer( 'mo_osp_admin_nonce', 'security' );\r\n+\r\n+\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray( 'message' => __( 'Insufficient permissions', 'miniorange-otp-verification' ) ),\r\n+\t\t\t\t\t403\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$identifier_hash = isset( $_POST['identifier_hash'] ) ? sanitize_text_field( wp_unslash( $_POST['identifier_hash'] ) ) : '';\r\n+\r\n+\t\t\tif ( empty( $identifier_hash ) ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray( 'message' => __( 'Invalid identifier hash', 'miniorange-otp-verification' ) ),\r\n+\t\t\t\t\t400\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$result = $this->handler->mosp_unblock_user_by_hash( $identifier_hash );\r\n+\r\n+\t\t\tif ( $result['success'] ) {\r\n+\t\t\t\twp_send_json_success( array( 'message' => $result['message'] ) );\r\n+\t\t\t} else {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray( 'message' => $result['message'] ),\r\n+\t\t\t\t\t400\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler: clear all blocked users \u002F limits \u002F puzzle flags (admin only).\r\n+\t\t *\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_clear_all_blocked_users_ajax() {\r\n+\t\t\tcheck_ajax_referer( 'mo_osp_admin_nonce', 'security' );\r\n+\r\n+\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray( 'message' => __( 'Insufficient permissions', 'miniorange-otp-verification' ) ),\r\n+\t\t\t\t\t403\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$result = $this->handler->mosp_clear_all_blocked_data();\r\n+\r\n+\t\t\tif ( ! $result['success'] ) {\r\n+\t\t\t\twp_send_json_error( array( 'message' => $result['message'] ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\twp_send_json_success(\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'message' => $result['message'],\r\n+\t\t\t\t\t'deleted' => $result['deleted'],\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * AJAX handler for enabling\u002Fdisabling addon (admin only).\r\n+\t\t *\r\n+\t\t * @return void Sends JSON response.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_toggle_addon_ajax() {\r\n+\t\t\tcheck_ajax_referer( 'mo_osp_admin_nonce', 'security' );\r\n+\r\n+\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\r\n+\t\t\t\twp_send_json_error(\r\n+\t\t\t\t\tarray( 'message' => __( 'Insufficient permissions', 'miniorange-otp-verification' ) ),\r\n+\t\t\t\t\t403\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$enabled             = isset( $_POST['enabled'] ) ? absint( $_POST['enabled'] ) : 0;\r\n+\t\t\t$settings            = $this->storage->mosp_get_settings();\r\n+\t\t\t$settings['enabled'] = ( 1 === $enabled );\r\n+\r\n+\t\t\t$result = $this->storage->mosp_update_settings( $settings );\r\n+\r\n+\t\t\tif ( $result ) {\r\n+\t\t\t\t$message = $settings['enabled']\r\n+\t\t\t\t\t? __( 'Addon enabled.', 'miniorange-otp-verification' )\r\n+\t\t\t\t\t: __( 'Addon disabled.', 'miniorange-otp-verification' );\r\n+\t\t\t\twp_send_json_success( array( 'message' => $message ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\twp_send_json_error(\r\n+\t\t\t\tarray( 'message' => __( 'Failed to update addon status.', 'miniorange-otp-verification' ) ),\r\n+\t\t\t\t500\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Format time in seconds to human-readable format.\r\n+\t\t *\r\n+\t\t * @param int $seconds Time in seconds.\r\n+\t\t * @return string Formatted time string.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_format_time( $seconds ) {\r\n+\t\t\tif ( $seconds \u003C 60 ) {\r\n+\t\t\t\t\u002F\u002F translators: %d: Number of seconds.\r\n+\t\t\t\treturn sprintf( _n( '%d second', '%d seconds', $seconds, 'miniorange-otp-verification' ), $seconds );\r\n+\t\t\t} elseif ( $seconds \u003C 3600 ) {\r\n+\t\t\t\t$minutes = floor( $seconds \u002F 60 );\r\n+\t\t\t\t$secs    = $seconds % 60;\r\n+\t\t\t\tif ( $secs > 0 ) {\r\n+\t\t\t\t\t\u002F\u002F translators: %d: Number of minutes.\r\n+\t\t\t\t\t$minutes_str = sprintf( _n( '%d minute', '%d minutes', $minutes, 'miniorange-otp-verification' ), $minutes );\r\n+\t\t\t\t\t\u002F\u002F translators: %d: Number of seconds.\r\n+\t\t\t\t\t$seconds_str = sprintf( _n( '%d second', '%d seconds', $secs, 'miniorange-otp-verification' ), $secs );\r\n+\t\t\t\t\treturn $minutes_str . ' ' . $seconds_str;\r\n+\t\t\t\t}\r\n+\t\t\t\t\u002F\u002F translators: %d: Number of minutes.\r\n+\t\t\t\treturn sprintf( _n( '%d minute', '%d minutes', $minutes, 'miniorange-otp-verification' ), $minutes );\r\n+\t\t\t} else {\r\n+\t\t\t\t$hours   = floor( $seconds \u002F 3600 );\r\n+\t\t\t\t$minutes = floor( ( $seconds % 3600 ) \u002F 60 );\r\n+\t\t\t\tif ( $minutes > 0 ) {\r\n+\t\t\t\t\t\u002F\u002F translators: %d: Number of hours.\r\n+\t\t\t\t\t$hours_str = sprintf( _n( '%d hour', '%d hours', $hours, 'miniorange-otp-verification' ), $hours );\r\n+\t\t\t\t\t\u002F\u002F translators: %d: Number of minutes.\r\n+\t\t\t\t\t$minutes_str = sprintf( _n( '%d minute', '%d minutes', $minutes, 'miniorange-otp-verification' ), $minutes );\r\n+\t\t\t\t\treturn $hours_str . ' ' . $minutes_str;\r\n+\t\t\t\t}\r\n+\t\t\t\t\u002F\u002F translators: %d: Number of hours.\r\n+\t\t\t\treturn sprintf( _n( '%d hour', '%d hours', $hours, 'miniorange-otp-verification' ), $hours );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get human-readable label for block reason.\r\n+\t\t *\r\n+\t\t * @param string $reason Block reason code.\r\n+\t\t * @return string Human-readable label.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_get_block_reason_label( $reason ) {\r\n+\t\t\tswitch ( $reason ) {\r\n+\t\t\t\tcase 'hourly_limit_exceeded':\r\n+\t\t\t\t\treturn __( 'Hourly Limit Exceeded', 'miniorange-otp-verification' );\r\n+\t\t\t\tcase 'daily_limit_exceeded':\r\n+\t\t\t\t\treturn __( 'Daily Limit Exceeded', 'miniorange-otp-verification' );\r\n+\t\t\t\tcase 'max_attempts_exceeded':\r\n+\t\t\t\t\treturn __( 'Max Attempts Exceeded', 'miniorange-otp-verification' );\r\n+\t\t\t\tcase 'cooldown':\r\n+\t\t\t\t\treturn __( 'Cooldown Period', 'miniorange-otp-verification' );\r\n+\t\t\t\tdefault:\r\n+\t\t\t\t\treturn __( 'Blocked', 'miniorange-otp-verification' );\r\n+\t\t\t}\r\n+\t\t}\r\n+\t}\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamintegration.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamintegration.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamintegration.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamintegration.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,712 +1,712 @@\n-\u003C?php\n-\u002F**\n- * OTP Spam Integration Handler.\n- *\n- * @package otpspampreventer\u002Fhandler\n- *\u002F\n-\n-namespace OSP\\Handler;\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-use OSP\\Traits\\Instance;\n-use OSP\\Handler\\MoOtpSpamStorage;\n-use OSP\\Handler\\MoOtpSpamPreventerHandler;\n-use OSP\\Helper\\MoSecurityHelper;\n-use OSP\\Helper\\MoPuzzleHelper;\n-use OTP\\Helper\\FormList;\n-use OTP\\Helper\\MoPHPSessions;\n-use OTP\\Helper\\MoMessages;\n-\n-if ( ! class_exists( 'MoOtpSpamIntegration' ) ) {\n-\t\u002F**\n-\t * Integrates spam prevention with the main OTP plugin\n-\t *\u002F\n-\tclass MoOtpSpamIntegration {\n-\n-\t\tuse Instance;\n-\n-\t\t\u002F**\n-\t\t * Storage handler\n-\t\t *\n-\t\t * @var MoOtpSpamStorage\n-\t\t *\u002F\n-\t\tprivate $storage;\n-\n-\t\t\u002F**\n-\t\t * Spam prevention handler\n-\t\t *\n-\t\t * @var MoOtpSpamPreventerHandler\n-\t\t *\u002F\n-\t\tprivate $handler;\n-\n-\t\t\u002F**\n-\t\t * Flag to track if hooks are initialized\n-\t\t *\n-\t\t * @var bool\n-\t\t *\u002F\n-\t\tprivate $hooks_initialized = false;\n-\n-\t\t\u002F**\n-\t\t * Initialize the integration\n-\t\t *\u002F\n-\t\tprotected function __construct() {\n-\t\t\t$this->storage = MoOtpSpamStorage::instance();\n-\t\t\t$this->handler = MoOtpSpamPreventerHandler::instance();\n-\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$this->init_essential_hooks();\n-\n-\t\t\tadd_action( 'wp_enqueue_scripts', array( $this, 'mosp_enqueue_frontend_scripts' ) );\n-\n-\t\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'mosp_enqueue_admin_scripts' ) );\n-\n-\t\t\tadd_action( 'wp_footer', array( $this, 'mosp_add_puzzle_popup_to_frontend' ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Initialize essential hooks that don't require database access\n-\t\t *\u002F\n-\t\tprivate function init_essential_hooks() {\n-\t\t\tadd_filter( 'mo_osp_get_cooldown_time', array( $this, 'mosp_filter_get_cooldown_time' ), 10, 1 );\n-\n-\t\t\tadd_action( 'mo_osp_mosp_check_spam_before_otp_send', array( $this, 'mosp_check_spam_before_otp_send' ), 1, 5 );\n-\n-\t\t\tadd_action( 'mo_generate_or_resend_otp', array( $this, 'mosp_check_spam_before_otp_send' ), 1, 5 );\n-\n-\t\t\tadd_action( 'mo_include_js', array( $this, 'mosp_include_timer_js' ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check for spam before OTP is sent (CENTRALIZED RATE LIMITING).\n-\t\t *\n-\t\t * This method is called by FormActionHandler::handleOTPAction via mo_osp_mosp_check_spam_before_otp_send action.\n-\t\t * It's called BEFORE OTP is sent, alongside ResendControl checks, in ONE central location.\n-\t\t * This ensures ALL spam prevention (rate limits, puzzles, cooldowns) is enforced consistently.\n-\t\t *\n-\t\t * @param string $user_login Username or identifier.\n-\t\t * @param string $user_email Email address.\n-\t\t * @param string $phone_number Phone number.\n-\t\t * @param string $otp_type Type of OTP (email\u002Fsms).\n-\t\t * @param string $from_both Whether both email and phone are enabled.\n-\t\t * @return void Exits early if blocked or puzzle required.\n-\t\t *\n-\t\t * @phpcs:disable WordPress.Security.NonceVerification.Missing -- Called from OTP generation hook, no nonce available\n-\t\t *\u002F\n-\t\tpublic function mosp_check_spam_before_otp_send( $user_login, $user_email, $phone_number, $otp_type, $from_both ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$otp_type = strtolower( trim( (string) $otp_type ) );\n-\t\t\tswitch ( $otp_type ) {\n-\t\t\t\tcase \\OTP\\Objects\\VerificationType::EMAIL:\n-\t\t\t\t\t$phone_number = '';\n-\t\t\t\t\tbreak;\n-\t\t\t\tcase \\OTP\\Objects\\VerificationType::PHONE:\n-\t\t\t\t\t$user_email = '';\n-\t\t\t\t\tbreak;\n-\t\t\t\tcase \\OTP\\Objects\\VerificationType::BOTH:\n-\t\t\t\t\t\u002F\u002F Keep both identifiers.\n-\t\t\t\t\tbreak;\n-\t\t\t}\n-\n-\t\t\tif ( ! empty( $phone_number ) && strpos( $phone_number, '@' ) !== false ) {\n-\t\t\t\t$phone_number = '';\n-\t\t\t}\n-\n-\t\t\tif ( ! empty( $user_email ) ) {\n-\t\t\t\tMoPHPSessions::add_session_var( 'user_email', $user_email );\n-\t\t\t}\n-\t\t\tif ( ! empty( $phone_number ) ) {\n-\t\t\t\tMoPHPSessions::add_session_var( 'phone_number_mo', $phone_number );\n-\t\t\t}\n-\n-\t\t\tstatic $attempt_recorded = false;\n-\t\t\t$request_key             = $user_email . '|' . $phone_number . '|' . time();\n-\t\t\tstatic $last_request_key = '';\n-\n-\t\t\tif ( $last_request_key === $request_key && $attempt_recorded ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$last_request_key = $request_key;\n-\n-\t\t\t$browser_id = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\n-\n-\t\t\t$puzzle_already_verified = MoSecurityHelper::mosp_is_puzzle_verification_valid( $user_email, $phone_number );\n-\n-\t\t\tif ( $puzzle_already_verified ) {\n-\t\t\t\t$this->handler->mosp_record_attempt_for_identifiers( $user_email, $phone_number, $browser_id );\n-\t\t\t\t$attempt_recorded = true;\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$is_blocked = $this->handler->mosp_is_blocked( $user_email, $phone_number, $browser_id, 'otp_send' );\n-\n-\t\t\t$current_block_data           = $this->handler->mosp_get_block_data( $user_email, $phone_number, $browser_id, 'otp_send' );\n-\t\t\t$current_block_remaining_time = $current_block_data['remaining_time'];\n-\t\t\t$current_block_reason         = $current_block_data['reason'];\n-\n-\t\t\t$ip                = $this->handler->mosp_get_client_ip();\n-\t\t\t$is_ip_whitelisted = false;\n-\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t$is_ip_whitelisted = $this->handler->mosp_is_whitelisted( $ip, 'ip' );\n-\n-\t\t\t}\n-\n-\t\t\t$would_be_blocked_reason         = '';\n-\t\t\t$would_be_blocked_remaining_time = 0;\n-\n-\t\t\tif ( ! $is_blocked && $current_block_remaining_time \u003C= 0 && ! $is_ip_whitelisted ) {\n-\t\t\t\t$would_be_blocked_result = $this->handler->mosp_would_be_blocked_after_attempt_with_details( $user_email, $phone_number, $browser_id );\n-\t\t\t\tif ( $would_be_blocked_result['would_be_blocked'] ) {\n-\t\t\t\t\t$is_blocked                      = true;\n-\t\t\t\t\t$would_be_blocked_reason         = $would_be_blocked_result['reason'];\n-\t\t\t\t\t$would_be_blocked_remaining_time = $would_be_blocked_result['remaining_time'];\n-\n-\t\t\t\t\t$this->handler->mosp_store_block_for_identifiers( $user_email, $phone_number, $browser_id, $would_be_blocked_reason, $would_be_blocked_remaining_time );\n-\t\t\t\t}\n-\t\t\t} elseif ( ( $is_blocked || $current_block_remaining_time > 0 ) && ! $is_ip_whitelisted ) {\n-\t\t\t\t$is_blocked                      = true;\n-\t\t\t\t$would_be_blocked_reason         = $current_block_reason;\n-\t\t\t\t$would_be_blocked_remaining_time = $current_block_remaining_time;\n-\t\t\t} elseif ( $is_ip_whitelisted ) {\n-\t\t\t\t$is_blocked                      = false;\n-\t\t\t\t$would_be_blocked_reason         = '';\n-\t\t\t\t$would_be_blocked_remaining_time = 0;\n-\t\t\t}\n-\t\t\t$requires_puzzle = false;\n-\n-\t\t\tif ( ! $is_blocked ) {\n-\n-\t\t\t\t$requires_puzzle = $this->handler->mosp_requires_puzzle_verification( $user_email, $phone_number, $this->handler->mosp_get_client_ip(), $browser_id );\n-\n-\t\t\t\t$requires_limit_puzzle = $this->handler->mosp_requires_limit_puzzle_verification( $user_email, $phone_number );\n-\n-\t\t\t\t$requires_puzzle = $requires_puzzle || $requires_limit_puzzle;\n-\t\t\t}\n-\n-\t\t\tif ( $requires_puzzle ) {\n-\t\t\t\t$is_ajax_form = apply_filters( 'is_ajax_form', false );\n-\n-\t\t\t\tif ( $is_ajax_form || 'ajax_phone' === $user_login ) {\n-\t\t\t\t\twp_send_json(\n-\t\t\t\t\t\tarray(\n-\t\t\t\t\t\t\t'result'          => 'puzzle_required',\n-\t\t\t\t\t\t\t'message'         => __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' ),\n-\t\t\t\t\t\t\t'puzzle_required' => true,\n-\t\t\t\t\t\t\t'authType'        => 'PUZZLE_REQUIRED',\n-\t\t\t\t\t\t)\n-\t\t\t\t\t);\n-\t\t\t\t} else {\n-\n-\t\t\t\t\t$puzzle_email = ! empty( $user_email ) ? $user_email : 'puzzle@temp.local';\n-\t\t\t\t\t$puzzle_phone = ! empty( $phone_number ) ? $phone_number : null;\n-\n-\t\t\t\t\tminiorange_site_otp_validation_form( $user_login, $puzzle_email, $puzzle_phone, __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' ), $otp_type, $from_both );\n-\t\t\t\t\texit;\n-\t\t\t\t}\n-\t\t\t} elseif ( ! $is_blocked ) {\n-\t\t\t\t$this->handler->mosp_record_attempt_for_identifiers( $user_email, $phone_number, $browser_id );\n-\t\t\t\t$attempt_recorded = true;\n-\n-\t\t\t\t$is_blocked_after_attempt = $this->handler->mosp_is_blocked( $user_email, $phone_number, $browser_id, 'otp_send' );\n-\t\t\t\tif ( $is_blocked_after_attempt ) {\n-\t\t\t\t\t$is_blocked = true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( $is_blocked ) {\n-\t\t\t\tif ( $would_be_blocked_remaining_time > 0 ) {\n-\t\t\t\t\t$remaining_time = $would_be_blocked_remaining_time;\n-\t\t\t\t\t$block_reason   = $would_be_blocked_reason;\n-\t\t\t\t} elseif ( $current_block_remaining_time > 0 ) {\n-\t\t\t\t\t$remaining_time = $current_block_remaining_time;\n-\t\t\t\t\t$block_reason   = $current_block_reason;\n-\t\t\t\t} else {\n-\t\t\t\t\t\u002F\u002F Fallback: re-check block status right before displaying error (block may have expired since initial check).\n-\t\t\t\t\t$block_data     = $this->handler->mosp_get_block_data( $user_email, $phone_number, $browser_id, 'otp_send' );\n-\t\t\t\t\t$remaining_time = $block_data['remaining_time'];\n-\t\t\t\t\t$block_reason   = $block_data['reason'];\n-\t\t\t\t}\n-\n-\t\t\t\tif ( $would_be_blocked_remaining_time > 0 ) {\n-\t\t\t\t\t$remaining_time = $would_be_blocked_remaining_time;\n-\t\t\t\t\t$block_reason   = $would_be_blocked_reason;\n-\t\t\t\t} elseif ( $remaining_time \u003C= 0 ) {\n-\t\t\t\t\t$requires_puzzle       = $this->handler->mosp_requires_puzzle_verification( $user_email, $phone_number, $this->handler->mosp_get_client_ip(), $browser_id );\n-\t\t\t\t\t$requires_limit_puzzle = $this->handler->mosp_requires_limit_puzzle_verification( $user_email, $phone_number );\n-\t\t\t\t\t$requires_puzzle       = $requires_puzzle || $requires_limit_puzzle;\n-\n-\t\t\t\t\tif ( ! $requires_puzzle && ! $attempt_recorded ) {\n-\t\t\t\t\t\t$this->handler->mosp_record_attempt_for_identifiers( $user_email, $phone_number, $browser_id );\n-\t\t\t\t\t\t$attempt_recorded = true;\n-\t\t\t\t\t\treturn;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tif ( $requires_puzzle ) {\n-\t\t\t\t\t\t$is_ajax_form = apply_filters( 'is_ajax_form', false );\n-\t\t\t\t\t\tif ( $is_ajax_form || 'ajax_phone' === $user_login ) {\n-\t\t\t\t\t\t\twp_send_json(\n-\t\t\t\t\t\t\t\tarray(\n-\t\t\t\t\t\t\t\t\t'result'          => 'puzzle_required',\n-\t\t\t\t\t\t\t\t\t'message'         => __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' ),\n-\t\t\t\t\t\t\t\t\t'puzzle_required' => true,\n-\t\t\t\t\t\t\t\t\t'authType'        => 'PUZZLE_REQUIRED',\n-\t\t\t\t\t\t\t\t)\n-\t\t\t\t\t\t\t);\n-\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t$puzzle_email = ! empty( $user_email ) ? $user_email : 'puzzle@temp.local';\n-\t\t\t\t\t\t\t$puzzle_phone = ! empty( $phone_number ) ? $phone_number : null;\n-\t\t\t\t\t\t\tminiorange_site_otp_validation_form( $user_login, $puzzle_email, $puzzle_phone, __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' ), $otp_type, $from_both );\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\texit;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t\u002F\u002F Block window is over (0s left) but stale $is_blocked — allow OTP send instead of a bogus 00:00 error.\n-\t\t\t\t\treturn;\n-\t\t\t\t}\n-\n-\t\t\t\t$message      = $this->handler->mosp_get_block_message_with_timer( $remaining_time );\n-\t\t\t\t$is_ajax_form = apply_filters( 'is_ajax_form', false );\n-\n-\t\t\t\tif ( $is_ajax_form || 'ajax_phone' === $user_login ) {\n-\t\t\t\t\t\twp_send_json(\n-\t\t\t\t\t\t\tarray(\n-\t\t\t\t\t\t\t\t'result'         => 'error',\n-\t\t\t\t\t\t\t\t'message'        => $message,\n-\t\t\t\t\t\t\t\t'blocked'        => true,\n-\t\t\t\t\t\t\t\t'remaining_time' => $remaining_time,\n-\t\t\t\t\t\t\t\t'blocked_type'   => $block_reason,\n-\t\t\t\t\t\t\t\t'authType'       => 'BLOCKED',\n-\t\t\t\t\t\t\t\t'status'         => 'BLOCKED',\n-\t\t\t\t\t\t\t)\n-\t\t\t\t\t\t);\n-\t\t\t\t\texit;\n-\t\t\t\t} else {\n-\t\t\t\t\tminiorange_site_otp_validation_form( null, null, null, $message, $otp_type, $from_both );\n-\t\t\t\t\texit;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Enqueue frontend scripts.\n-\t\t *\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic function mosp_enqueue_frontend_scripts() {\n-\t\t\tif ( is_admin() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tif ( ! $this->is_otp_verification_active_on_page() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$settings = $this->storage->mosp_get_settings();\n-\n-\t\t\twp_enqueue_script(\n-\t\t\t\t'mo-osp-frontend',\n-\t\t\t\tMO_OSP_URL . 'includes\u002Fjs\u002Fspam-preventer.js',\n-\t\t\t\tarray( 'jquery' ),\n-\t\t\t\t'1.1.0',\n-\t\t\t\ttrue\n-\t\t\t);\n-\n-\t\t\twp_enqueue_script(\n-\t\t\t\t'mo-osp-puzzle-system',\n-\t\t\t\tMO_OSP_URL . 'includes\u002Fjs\u002Fpuzzle-system.js',\n-\t\t\t\tarray( 'jquery', 'mo-osp-frontend' ),\n-\t\t\t\t'1.0.0',\n-\t\t\t\ttrue\n-\t\t\t);\n-\n-\t\t\twp_enqueue_style(\n-\t\t\t\t'mo-osp-puzzle-css',\n-\t\t\t\tMO_OSP_URL . 'includes\u002Fcss\u002Fmo-admin.css',\n-\t\t\t\tarray(),\n-\t\t\t\t'1.0.5'\n-\t\t\t);\n-\n-\t\t\twp_localize_script(\n-\t\t\t\t'mo-osp-frontend',\n-\t\t\t\t'mo_osp_ajax',\n-\t\t\t\tarray(\n-\t\t\t\t\t'ajax_url'     => admin_url( 'admin-ajax.php' ),\n-\t\t\t\t\t'nonce'        => wp_create_nonce( 'mo_osp_nonce' ),\n-\t\t\t\t\t'loading_text' => __( 'Checking...', 'miniorange-otp-verification' ),\n-\t\t\t\t\t'timer_time'   => $settings['cooldown_time'],\n-\t\t\t\t)\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Enqueue admin scripts.\n-\t\t *\n-\t\t * @param string $hook_suffix Current admin page.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic function mosp_enqueue_admin_scripts( $hook_suffix ) {\n-\t\t\tif ( strpos( $hook_suffix, 'mo_otp_verification' ) === false ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$mo_osp_admin_js = MO_OSP_DIR . 'includes\u002Fjs\u002Fspam-preventer-admin.js';\n-\t\t\twp_enqueue_script(\n-\t\t\t\t'mo-osp-admin',\n-\t\t\t\tMO_OSP_URL . 'includes\u002Fjs\u002Fspam-preventer-admin.js',\n-\t\t\t\tarray( 'jquery' ),\n-\t\t\t\tfile_exists( $mo_osp_admin_js ) ? (string) filemtime( $mo_osp_admin_js ) : '1.0.1',\n-\t\t\t\ttrue\n-\t\t\t);\n-\n-\t\t\twp_localize_script(\n-\t\t\t\t'mo-osp-admin',\n-\t\t\t\t'mo_osp_admin_ajax',\n-\t\t\t\tarray(\n-\t\t\t\t\t'ajax_url' => admin_url( 'admin-ajax.php' ),\n-\t\t\t\t\t'nonce'    => wp_create_nonce( 'mo_osp_admin_nonce' ),\n-\t\t\t\t)\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Include timer JavaScript for pop-up forms.\n-\t\t * This is called via mo_include_js action when popup is rendered.\n-\t\t *\u002F\n-\t\tpublic function mosp_include_timer_js() {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$settings = $this->storage->mosp_get_settings();\n-\n-\t\t\t$email = MoPHPSessions::get_session_var( 'user_email' );\n-\t\t\t$phone = MoPHPSessions::get_session_var( 'phone_number_mo' );\n-\t\t\t\u002F\u002F phpcs:disable WordPress.Security.NonceVerification.Missing\n-\t\t\t$browser_id        = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\n-\t\t\t$ip                = $this->handler->mosp_get_client_ip();\n-\t\t\t$is_ip_whitelisted = false;\n-\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t$is_ip_whitelisted = $this->handler->mosp_is_whitelisted( $ip, 'ip' );\n-\t\t\t}\n-\n-\t\t\t$cooldown_remaining = $is_ip_whitelisted ? 0 : $this->mosp_get_cooldown_remaining_time( $email, $phone, $browser_id );\n-\t\t\t$is_blocked         = $this->handler->mosp_is_blocked( $email, $phone, $browser_id, 'popup_render' );\n-\n-\t\t\t\u002F*\n-\t\t\t * mo_include_js runs mid-document (during popup HTML). Bundled scripts end with IIFEs like })(jQuery);\n-\t\t\t * Inline code below also invokes jQuery immediately. Force core jQuery to print first so it is defined.\n-\t\t\t *\u002F\n-\t\t\twp_enqueue_script( 'jquery' );\n-\t\t\twp_print_scripts( 'jquery' );\n-\n-\t\t\twp_register_style( 'mo-osp-puzzle-css-popup', MO_OSP_URL . 'includes\u002Fcss\u002Fmo-admin.css', array(), '1.0.5', 'all' );\n-\t\t\twp_print_styles( 'mo-osp-puzzle-css-popup' );\n-\n-\t\t\twp_register_script( 'mo-osp-timer', MO_OSP_URL . 'includes\u002Fjs\u002Fspam-preventer.js', array( 'jquery' ), '1.0.0', false );\n-\t\t\twp_localize_script(\n-\t\t\t\t'mo-osp-timer',\n-\t\t\t\t'mo_osp_timer',\n-\t\t\t\tarray(\n-\t\t\t\t\t'timer_time' => $settings['cooldown_time'],\n-\t\t\t\t)\n-\t\t\t);\n-\t\t\twp_print_scripts( 'mo-osp-timer' );\n-\n-\t\t\twp_register_script( 'mo-osp-puzzle-system', MO_OSP_URL . 'includes\u002Fjs\u002Fpuzzle-system.js', array( 'jquery', 'mo-osp-timer' ), '1.0.0', false );\n-\t\t\twp_localize_script(\n-\t\t\t\t'mo-osp-puzzle-system',\n-\t\t\t\t'mo_osp_ajax',\n-\t\t\t\tarray(\n-\t\t\t\t\t'ajax_url'    => admin_url( 'admin-ajax.php' ),\n-\t\t\t\t\t'nonce'       => wp_create_nonce( 'mo_osp_nonce' ),\n-\t\t\t\t\t'timer_time'  => $settings['cooldown_time'],\n-\t\t\t\t\t'block_time'  => $settings['block_time'],\n-\t\t\t\t\t'enable_logs' => defined( 'WP_DEBUG' ) && WP_DEBUG,\n-\t\t\t\t)\n-\t\t\t);\n-\t\t\twp_print_scripts( 'mo-osp-puzzle-system' );\n-\n-\t\t\twp_register_script( 'mo-osp-popup-timer', MO_OSP_URL . 'includes\u002Fjs\u002Fpopup-timer.js', array( 'jquery', 'mo-osp-timer', 'mo-osp-puzzle-system' ), '1.0.0', false );\n-\t\t\twp_localize_script(\n-\t\t\t\t'mo-osp-popup-timer',\n-\t\t\t\t'mo_osp_popup_timer',\n-\t\t\t\tarray(\n-\t\t\t\t\t'ajax_url'              => admin_url( 'admin-ajax.php' ),\n-\t\t\t\t\t'nonce'                 => wp_create_nonce( 'mo_osp_nonce' ),\n-\t\t\t\t\t'timer_time'            => $settings['cooldown_time'],\n-\t\t\t\t\t'block_time'            => $settings['block_time'],\n-\t\t\t\t\t'limit_otp_sent'        => MoMessages::showMessage( MoMessages::LIMIT_OTP_SENT ),\n-\t\t\t\t\t'user_blocked'          => MoMessages::showMessage( MoMessages::USER_IS_BLOCKED_AJAX ),\n-\t\t\t\t\t'error_otp_verify'      => MoMessages::showMessage( MoMessages::ERROR_OTP_VERIFY ),\n-\t\t\t\t\t'initial_cooldown_time' => $cooldown_remaining,\n-\t\t\t\t\t'initial_blocked'       => $is_blocked,\n-\t\t\t\t\t'puzzle_required_text'  => __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' ),\n-\t\t\t\t)\n-\t\t\t);\n-\t\t\twp_print_scripts( 'mo-osp-popup-timer' );\n-\n-\t\t\techo '\u003Cdiv id=\"mo-osp-puzzle-popup-outer-div\" style=\"display:none;\">';\n-\t\t\tMoPuzzleHelper::mosp_render_puzzle_popup();\n-\t\t\techo '\u003C\u002Fdiv>';\n-\t\t\t$puzzle_message = __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' );\n-\t\t\t?>\n-\t\t\t\u003Cscript type=\"text\u002Fjavascript\">\n-\t\t\t(function() {\n-\t\t\t\tfunction moOspRunInlinePuzzle($) {\n-\t\t\t\t'use strict';\n-\t\t\t\t\n-\t\t\t\tif (window.mo_osp_inline_puzzle_check_executed) {\n-\t\t\t\t\treturn;\n-\t\t\t\t}\n-\t\t\t\twindow.mo_osp_inline_puzzle_check_executed = true;\n-\t\t\t\t\n-\t\t\t\tfunction checkAndShowPuzzle() {\n-\t\t\t\t\tif (window.mo_osp_puzzle_shown) {\n-\t\t\t\t\t\treturn;\n-\t\t\t\t\t}\n-\t\t\t\t\t\n-\t\t\t\t\tvar $popupBody = $('.mo_customer_validation-modal-body');\n-\t\t\t\t\tvar $firstDiv = $popupBody.children('div').first();\n-\t\t\t\t\tvar messageText = '';\n-\t\t\t\t\t\n-\t\t\t\t\tif ($firstDiv.length > 0) {\n-\t\t\t\t\t\tmessageText = $firstDiv.text().trim();\n-\t\t\t\t\t}\n-\t\t\t\t\tif (!messageText && $popupBody.length > 0) {\n-\t\t\t\t\t\tmessageText = $popupBody.text().trim();\n-\t\t\t\t\t}\n-\t\t\t\t\t\n-\t\t\t\t\tvar puzzleText = '\u003C?php echo esc_js( $puzzle_message ); ?>';\n-\t\t\t\t\t\n-\t\t\t\t\tif (messageText && messageText.toLowerCase().includes(puzzleText.toLowerCase())) {\n-\t\t\t\t\t\t\n-\t\t\t\t\t\twindow.mo_osp_puzzle_shown = true;\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t$('#mo_site_otp_form').hide();\n-\t\t\t\t\t\t$('.mo_customer_validation-modal').hide();\n-\t\t\t\t\t\t$('.mo-modal-backdrop').hide();\n-\t\t\t\t\t\t\n-\t\t\t\t\t\tif ($('#mo-osp-puzzle-overlay').length === 0) {\n-\t\t\t\t\t\t\treturn;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\t\n-\t\t\t\t\t\tvar $puzzleOverlay = $('#mo-osp-puzzle-overlay');\n-\t\t\t\t\t\t$puzzleOverlay.css('z-index', '100001');\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t$('#mo-osp-puzzle-popup-outer-div').show().css('z-index', '100002');\n-\t\t\t\t\t\t$puzzleOverlay.removeClass('mo-osp-hidden');\n-\t\t\t\t\t\t\n-\t\t\t\t\t\twindow.MO_OSP_Puzzle_onPopupSuccess = function() {\n-\t\t\t\t\t\t\tif (typeof window.MO_OSP_Puzzle !== 'undefined' && window.MO_OSP_Puzzle.closePuzzle) {\n-\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.closePuzzle();\n-\t\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t\tjQuery('#mo-osp-puzzle-overlay').addClass('mo-osp-hidden');\n-\t\t\t\t\t\t\t\tjQuery('#mo-osp-puzzle-popup-outer-div').hide();\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\n-\t\t\t\t\t\t\tvar resendForm = document.getElementById('verification_resend_otp_form');\n-\t\t\t\t\t\t\tif (resendForm) {\n-\t\t\t\t\t\t\t\tif (!resendForm.querySelector('input[name=\"puzzle_verified\"]')) {\n-\t\t\t\t\t\t\t\t\tvar puzzleVerifiedInput = document.createElement('input');\n-\t\t\t\t\t\t\t\t\tpuzzleVerifiedInput.type = 'hidden';\n-\t\t\t\t\t\t\t\t\tpuzzleVerifiedInput.name = 'puzzle_verified';\n-\t\t\t\t\t\t\t\t\tpuzzleVerifiedInput.value = 'true';\n-\t\t\t\t\t\t\t\t\tresendForm.appendChild(puzzleVerifiedInput);\n-\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\tresendForm.submit();\n-\t\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t\tsessionStorage.setItem('mo_osp_puzzle_completed', 'true');\n-\t\t\t\t\t\t\t\twindow.location.reload();\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t};\n-\t\t\t\t\t\t\n-\t\t\t\t\t\tif (typeof window.MO_OSP_Puzzle !== 'undefined') {\n-\t\t\t\t\t\t\tif (typeof window.MO_OSP_Puzzle.init === 'function' && !window.MO_OSP_Puzzle.initialized) {\n-\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.init();\n-\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.initialized = true;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.showPuzzle({});\n-\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\tsetTimeout(function() {\n-\t\t\t\t\t\t\t\tif (typeof window.MO_OSP_Puzzle !== 'undefined') {\n-\t\t\t\t\t\t\t\t\tif (typeof window.MO_OSP_Puzzle.init === 'function' && !window.MO_OSP_Puzzle.initialized) {\n-\t\t\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.init();\n-\t\t\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.initialized = true;\n-\t\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.showPuzzle({});\n-\t\t\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t\t\tconsole.error('MO_OSP_Puzzle still not available after wait');\n-\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t}, 500);\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t\t\n-\t\t\t\tvar checkExecuted = false;\n-\t\t\t\tfunction runCheckOnce() {\n-\t\t\t\t\tif (checkExecuted) {\n-\t\t\t\t\t\treturn;\n-\t\t\t\t\t}\n-\t\t\t\t\tcheckExecuted = true;\n-\t\t\t\t\tcheckAndShowPuzzle();\n-\t\t\t\t}\n-\t\t\t\t\n-\t\t\t\tif (document.readyState === 'loading') {\n-\t\t\t\t\t$(document).ready(function() {\n-\t\t\t\t\t\tsetTimeout(runCheckOnce, 300);\n-\t\t\t\t\t});\n-\t\t\t\t} else {\n-\t\t\t\t\tsetTimeout(runCheckOnce, 300);\n-\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t\tfunction moOspTryInlinePuzzle() {\n-\t\t\t\t\tvar jq = window.jQuery;\n-\t\t\t\t\tif (typeof jq === 'undefined') {\n-\t\t\t\t\t\treturn false;\n-\t\t\t\t\t}\n-\t\t\t\t\tmoOspRunInlinePuzzle(jq);\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t\tif (!moOspTryInlinePuzzle()) {\n-\t\t\t\t\tvar moOspInlineIv = setInterval(function () {\n-\t\t\t\t\t\tif (moOspTryInlinePuzzle()) {\n-\t\t\t\t\t\t\tclearInterval(moOspInlineIv);\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}, 30);\n-\t\t\t\t\tsetTimeout(function () {\n-\t\t\t\t\t\tclearInterval(moOspInlineIv);\n-\t\t\t\t\t}, 15000);\n-\t\t\t\t}\n-\t\t\t})();\n-\t\t\t\u003C\u002Fscript>\n-\t\t\t\u003C?php\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Add puzzle popup HTML to frontend.\n-\t\t *\u002F\n-\t\tpublic function mosp_add_puzzle_popup_to_frontend() {\n-\t\t\tif ( is_admin() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tif ( ! $this->is_otp_verification_active_on_page() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\techo '\u003Cdiv id=\"mo-osp-puzzle-popup-outer-div\" style=\"display:none;\">';\n-\t\t\tMoPuzzleHelper::mosp_render_puzzle_popup();\n-\t\t\techo '\u003C\u002Fdiv>';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if OTP verification is active on the current page.\n-\t\t *\n-\t\t * @return bool True if any form has OTP verification enabled\n-\t\t *\u002F\n-\t\tprivate function is_otp_verification_active_on_page() {\n-\t\t\t$form_list = FormList::instance();\n-\t\t\t$all_forms = $form_list->get_list();\n-\n-\t\t\tforeach ( $all_forms as $form_handler ) {\n-\t\t\t\tif ( $form_handler && method_exists( $form_handler, 'is_form_enabled' ) ) {\n-\t\t\t\t\tif ( $form_handler->is_form_enabled() ) {\n-\t\t\t\t\t\treturn true;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$otp_verification_options = array(\n-\t\t\t\t'cf_submit_id',\n-\t\t\t\t'wc_default_enable',\n-\t\t\t\t'wp_default_enable',\n-\t\t\t\t'wp_login_enable',\n-\t\t\t\t'wc_checkout_enable',\n-\t\t\t\t'bp_registration_enable',\n-\t\t\t\t'um_default_enable',\n-\t\t\t\t'pmpro_default_enable',\n-\t\t\t);\n-\n-\t\t\tforeach ( $otp_verification_options as $option ) {\n-\t\t\t\tif ( get_mo_option( $option ) ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Provide addon cooldown time to host plugin for server-side formatting.\n-\t\t *\n-\t\t * @param int $default_value Default fallback value.\n-\t\t * @return int seconds\n-\t\t *\u002F\n-\t\tpublic function mosp_filter_get_cooldown_time( $default_value = 60 ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn (int) $default_value;\n-\t\t\t}\n-\n-\t\t\t$settings = $this->storage->mosp_get_settings();\n-\t\t\treturn isset( $settings['cooldown_time'] ) ? (int) $settings['cooldown_time'] : (int) $default_value;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if spam preventer addon is enabled in settings.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tprivate function mosp_is_addon_enabled() {\n-\t\t\t$settings = $this->storage->mosp_get_settings();\n-\t\t\treturn ! empty( $settings['enabled'] );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get remaining cooldown time for user.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $browser_id Browser ID.\n-\t\t * @return int Remaining cooldown time in seconds.\n-\t\t *\u002F\n-\t\tprivate function mosp_get_cooldown_remaining_time( $email, $phone, $browser_id ) {\n-\t\t\t$identifiers   = $this->handler->mosp_get_all_identifiers( $email, $phone, $this->handler->mosp_get_client_ip(), $browser_id );\n-\t\t\t$current_time  = time();\n-\t\t\t$settings      = $this->storage->mosp_get_settings();\n-\t\t\t$cooldown_time = $settings['cooldown_time'];\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\n-\t\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\n-\n-\t\t\t\tif ( false !== $data && isset( $data['last_attempt'] ) && $data['last_attempt'] > 0 ) {\n-\t\t\t\t\t$time_since_last = $current_time - $data['last_attempt'];\n-\t\t\t\t\t$remaining       = $cooldown_time - $time_since_last;\n-\n-\t\t\t\t\tif ( $remaining > 0 ) {\n-\t\t\t\t\t\treturn $remaining;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn 0;\n-\t\t}\n-\t}\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * OTP Spam Integration Handler.\r\n+ *\r\n+ * @package otpspampreventer\u002Fhandler\r\n+ *\u002F\r\n+\r\n+namespace OSP\\Handler;\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+use OSP\\Traits\\Instance;\r\n+use OSP\\Handler\\MoOtpSpamStorage;\r\n+use OSP\\Handler\\MoOtpSpamPreventerHandler;\r\n+use OSP\\Helper\\MoSecurityHelper;\r\n+use OSP\\Helper\\MoPuzzleHelper;\r\n+use OTP\\Helper\\FormList;\r\n+use OTP\\Helper\\MoPHPSessions;\r\n+use OTP\\Helper\\MoMessages;\r\n+\r\n+if ( ! class_exists( 'MoOtpSpamIntegration' ) ) {\r\n+\t\u002F**\r\n+\t * Integrates spam prevention with the main OTP plugin\r\n+\t *\u002F\r\n+\tclass MoOtpSpamIntegration {\r\n+\r\n+\t\tuse Instance;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Storage handler\r\n+\t\t *\r\n+\t\t * @var MoOtpSpamStorage\r\n+\t\t *\u002F\r\n+\t\tprivate $storage;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Spam prevention handler\r\n+\t\t *\r\n+\t\t * @var MoOtpSpamPreventerHandler\r\n+\t\t *\u002F\r\n+\t\tprivate $handler;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Flag to track if hooks are initialized\r\n+\t\t *\r\n+\t\t * @var bool\r\n+\t\t *\u002F\r\n+\t\tprivate $hooks_initialized = false;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Initialize the integration\r\n+\t\t *\u002F\r\n+\t\tprotected function __construct() {\r\n+\t\t\t$this->storage = MoOtpSpamStorage::instance();\r\n+\t\t\t$this->handler = MoOtpSpamPreventerHandler::instance();\r\n+\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$this->init_essential_hooks();\r\n+\r\n+\t\t\tadd_action( 'wp_enqueue_scripts', array( $this, 'mosp_enqueue_frontend_scripts' ) );\r\n+\r\n+\t\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'mosp_enqueue_admin_scripts' ) );\r\n+\r\n+\t\t\tadd_action( 'wp_footer', array( $this, 'mosp_add_puzzle_popup_to_frontend' ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Initialize essential hooks that don't require database access\r\n+\t\t *\u002F\r\n+\t\tprivate function init_essential_hooks() {\r\n+\t\t\tadd_filter( 'mo_osp_get_cooldown_time', array( $this, 'mosp_filter_get_cooldown_time' ), 10, 1 );\r\n+\r\n+\t\t\tadd_action( 'mo_osp_mosp_check_spam_before_otp_send', array( $this, 'mosp_check_spam_before_otp_send' ), 1, 5 );\r\n+\r\n+\t\t\tadd_action( 'mo_generate_or_resend_otp', array( $this, 'mosp_check_spam_before_otp_send' ), 1, 5 );\r\n+\r\n+\t\t\tadd_action( 'mo_include_js', array( $this, 'mosp_include_timer_js' ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check for spam before OTP is sent (CENTRALIZED RATE LIMITING).\r\n+\t\t *\r\n+\t\t * This method is called by FormActionHandler::handleOTPAction via mo_osp_mosp_check_spam_before_otp_send action.\r\n+\t\t * It's called BEFORE OTP is sent, alongside ResendControl checks, in ONE central location.\r\n+\t\t * This ensures ALL spam prevention (rate limits, puzzles, cooldowns) is enforced consistently.\r\n+\t\t *\r\n+\t\t * @param string $user_login Username or identifier.\r\n+\t\t * @param string $user_email Email address.\r\n+\t\t * @param string $phone_number Phone number.\r\n+\t\t * @param string $otp_type Type of OTP (email\u002Fsms).\r\n+\t\t * @param string $from_both Whether both email and phone are enabled.\r\n+\t\t * @return void Exits early if blocked or puzzle required.\r\n+\t\t *\r\n+\t\t * @phpcs:disable WordPress.Security.NonceVerification.Missing -- Called from OTP generation hook, no nonce available\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_check_spam_before_otp_send( $user_login, $user_email, $phone_number, $otp_type, $from_both ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$otp_type = strtolower( trim( (string) $otp_type ) );\r\n+\t\t\tswitch ( $otp_type ) {\r\n+\t\t\t\tcase \\OTP\\Objects\\VerificationType::EMAIL:\r\n+\t\t\t\t\t$phone_number = '';\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\tcase \\OTP\\Objects\\VerificationType::PHONE:\r\n+\t\t\t\t\t$user_email = '';\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\tcase \\OTP\\Objects\\VerificationType::BOTH:\r\n+\t\t\t\t\t\u002F\u002F Keep both identifiers.\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! empty( $phone_number ) && strpos( $phone_number, '@' ) !== false ) {\r\n+\t\t\t\t$phone_number = '';\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! empty( $user_email ) ) {\r\n+\t\t\t\tMoPHPSessions::add_session_var( 'user_email', $user_email );\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $phone_number ) ) {\r\n+\t\t\t\tMoPHPSessions::add_session_var( 'phone_number_mo', $phone_number );\r\n+\t\t\t}\r\n+\r\n+\t\t\tstatic $attempt_recorded = false;\r\n+\t\t\t$request_key             = $user_email . '|' . $phone_number . '|' . time();\r\n+\t\t\tstatic $last_request_key = '';\r\n+\r\n+\t\t\tif ( $last_request_key === $request_key && $attempt_recorded ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$last_request_key = $request_key;\r\n+\r\n+\t\t\t$browser_id = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\r\n+\r\n+\t\t\t$puzzle_already_verified = MoSecurityHelper::mosp_is_puzzle_verification_valid( $user_email, $phone_number );\r\n+\r\n+\t\t\tif ( $puzzle_already_verified ) {\r\n+\t\t\t\t$this->handler->mosp_record_attempt_for_identifiers( $user_email, $phone_number, $browser_id );\r\n+\t\t\t\t$attempt_recorded = true;\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$is_blocked = $this->handler->mosp_is_blocked( $user_email, $phone_number, $browser_id, 'otp_send' );\r\n+\r\n+\t\t\t$current_block_data           = $this->handler->mosp_get_block_data( $user_email, $phone_number, $browser_id, 'otp_send' );\r\n+\t\t\t$current_block_remaining_time = $current_block_data['remaining_time'];\r\n+\t\t\t$current_block_reason         = $current_block_data['reason'];\r\n+\r\n+\t\t\t$ip                = $this->handler->mosp_get_client_ip();\r\n+\t\t\t$is_ip_whitelisted = false;\r\n+\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t$is_ip_whitelisted = $this->handler->mosp_is_whitelisted( $ip, 'ip' );\r\n+\r\n+\t\t\t}\r\n+\r\n+\t\t\t$would_be_blocked_reason         = '';\r\n+\t\t\t$would_be_blocked_remaining_time = 0;\r\n+\r\n+\t\t\tif ( ! $is_blocked && $current_block_remaining_time \u003C= 0 && ! $is_ip_whitelisted ) {\r\n+\t\t\t\t$would_be_blocked_result = $this->handler->mosp_would_be_blocked_after_attempt_with_details( $user_email, $phone_number, $browser_id );\r\n+\t\t\t\tif ( $would_be_blocked_result['would_be_blocked'] ) {\r\n+\t\t\t\t\t$is_blocked                      = true;\r\n+\t\t\t\t\t$would_be_blocked_reason         = $would_be_blocked_result['reason'];\r\n+\t\t\t\t\t$would_be_blocked_remaining_time = $would_be_blocked_result['remaining_time'];\r\n+\r\n+\t\t\t\t\t$this->handler->mosp_store_block_for_identifiers( $user_email, $phone_number, $browser_id, $would_be_blocked_reason, $would_be_blocked_remaining_time );\r\n+\t\t\t\t}\r\n+\t\t\t} elseif ( ( $is_blocked || $current_block_remaining_time > 0 ) && ! $is_ip_whitelisted ) {\r\n+\t\t\t\t$is_blocked                      = true;\r\n+\t\t\t\t$would_be_blocked_reason         = $current_block_reason;\r\n+\t\t\t\t$would_be_blocked_remaining_time = $current_block_remaining_time;\r\n+\t\t\t} elseif ( $is_ip_whitelisted ) {\r\n+\t\t\t\t$is_blocked                      = false;\r\n+\t\t\t\t$would_be_blocked_reason         = '';\r\n+\t\t\t\t$would_be_blocked_remaining_time = 0;\r\n+\t\t\t}\r\n+\t\t\t$requires_puzzle = false;\r\n+\r\n+\t\t\tif ( ! $is_blocked ) {\r\n+\r\n+\t\t\t\t$requires_puzzle = $this->handler->mosp_requires_puzzle_verification( $user_email, $phone_number, $this->handler->mosp_get_client_ip(), $browser_id );\r\n+\r\n+\t\t\t\t$requires_limit_puzzle = $this->handler->mosp_requires_limit_puzzle_verification( $user_email, $phone_number );\r\n+\r\n+\t\t\t\t$requires_puzzle = $requires_puzzle || $requires_limit_puzzle;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $requires_puzzle ) {\r\n+\t\t\t\t$is_ajax_form = apply_filters( 'is_ajax_form', false );\r\n+\r\n+\t\t\t\tif ( $is_ajax_form || 'ajax_phone' === $user_login ) {\r\n+\t\t\t\t\twp_send_json(\r\n+\t\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t\t'result'          => 'puzzle_required',\r\n+\t\t\t\t\t\t\t'message'         => __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t\t\t'puzzle_required' => true,\r\n+\t\t\t\t\t\t\t'authType'        => 'PUZZLE_REQUIRED',\r\n+\t\t\t\t\t\t)\r\n+\t\t\t\t\t);\r\n+\t\t\t\t} else {\r\n+\r\n+\t\t\t\t\t$puzzle_email = ! empty( $user_email ) ? $user_email : 'puzzle@temp.local';\r\n+\t\t\t\t\t$puzzle_phone = ! empty( $phone_number ) ? $phone_number : null;\r\n+\r\n+\t\t\t\t\tminiorange_site_otp_validation_form( $user_login, $puzzle_email, $puzzle_phone, __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' ), $otp_type, $from_both );\r\n+\t\t\t\t\texit;\r\n+\t\t\t\t}\r\n+\t\t\t} elseif ( ! $is_blocked ) {\r\n+\t\t\t\t$this->handler->mosp_record_attempt_for_identifiers( $user_email, $phone_number, $browser_id );\r\n+\t\t\t\t$attempt_recorded = true;\r\n+\r\n+\t\t\t\t$is_blocked_after_attempt = $this->handler->mosp_is_blocked( $user_email, $phone_number, $browser_id, 'otp_send' );\r\n+\t\t\t\tif ( $is_blocked_after_attempt ) {\r\n+\t\t\t\t\t$is_blocked = true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $is_blocked ) {\r\n+\t\t\t\tif ( $would_be_blocked_remaining_time > 0 ) {\r\n+\t\t\t\t\t$remaining_time = $would_be_blocked_remaining_time;\r\n+\t\t\t\t\t$block_reason   = $would_be_blocked_reason;\r\n+\t\t\t\t} elseif ( $current_block_remaining_time > 0 ) {\r\n+\t\t\t\t\t$remaining_time = $current_block_remaining_time;\r\n+\t\t\t\t\t$block_reason   = $current_block_reason;\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\t\u002F\u002F Fallback: re-check block status right before displaying error (block may have expired since initial check).\r\n+\t\t\t\t\t$block_data     = $this->handler->mosp_get_block_data( $user_email, $phone_number, $browser_id, 'otp_send' );\r\n+\t\t\t\t\t$remaining_time = $block_data['remaining_time'];\r\n+\t\t\t\t\t$block_reason   = $block_data['reason'];\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\tif ( $would_be_blocked_remaining_time > 0 ) {\r\n+\t\t\t\t\t$remaining_time = $would_be_blocked_remaining_time;\r\n+\t\t\t\t\t$block_reason   = $would_be_blocked_reason;\r\n+\t\t\t\t} elseif ( $remaining_time \u003C= 0 ) {\r\n+\t\t\t\t\t$requires_puzzle       = $this->handler->mosp_requires_puzzle_verification( $user_email, $phone_number, $this->handler->mosp_get_client_ip(), $browser_id );\r\n+\t\t\t\t\t$requires_limit_puzzle = $this->handler->mosp_requires_limit_puzzle_verification( $user_email, $phone_number );\r\n+\t\t\t\t\t$requires_puzzle       = $requires_puzzle || $requires_limit_puzzle;\r\n+\r\n+\t\t\t\t\tif ( ! $requires_puzzle && ! $attempt_recorded ) {\r\n+\t\t\t\t\t\t$this->handler->mosp_record_attempt_for_identifiers( $user_email, $phone_number, $browser_id );\r\n+\t\t\t\t\t\t$attempt_recorded = true;\r\n+\t\t\t\t\t\treturn;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\tif ( $requires_puzzle ) {\r\n+\t\t\t\t\t\t$is_ajax_form = apply_filters( 'is_ajax_form', false );\r\n+\t\t\t\t\t\tif ( $is_ajax_form || 'ajax_phone' === $user_login ) {\r\n+\t\t\t\t\t\t\twp_send_json(\r\n+\t\t\t\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t\t\t\t'result'          => 'puzzle_required',\r\n+\t\t\t\t\t\t\t\t\t'message'         => __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t\t\t\t\t'puzzle_required' => true,\r\n+\t\t\t\t\t\t\t\t\t'authType'        => 'PUZZLE_REQUIRED',\r\n+\t\t\t\t\t\t\t\t)\r\n+\t\t\t\t\t\t\t);\r\n+\t\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t\t$puzzle_email = ! empty( $user_email ) ? $user_email : 'puzzle@temp.local';\r\n+\t\t\t\t\t\t\t$puzzle_phone = ! empty( $phone_number ) ? $phone_number : null;\r\n+\t\t\t\t\t\t\tminiorange_site_otp_validation_form( $user_login, $puzzle_email, $puzzle_phone, __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' ), $otp_type, $from_both );\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t\texit;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\u002F\u002F Block window is over (0s left) but stale $is_blocked — allow OTP send instead of a bogus 00:00 error.\r\n+\t\t\t\t\treturn;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$message      = $this->handler->mosp_get_block_message_with_timer( $remaining_time );\r\n+\t\t\t\t$is_ajax_form = apply_filters( 'is_ajax_form', false );\r\n+\r\n+\t\t\t\tif ( $is_ajax_form || 'ajax_phone' === $user_login ) {\r\n+\t\t\t\t\t\twp_send_json(\r\n+\t\t\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t\t\t'result'         => 'error',\r\n+\t\t\t\t\t\t\t\t'message'        => $message,\r\n+\t\t\t\t\t\t\t\t'blocked'        => true,\r\n+\t\t\t\t\t\t\t\t'remaining_time' => $remaining_time,\r\n+\t\t\t\t\t\t\t\t'blocked_type'   => $block_reason,\r\n+\t\t\t\t\t\t\t\t'authType'       => 'BLOCKED',\r\n+\t\t\t\t\t\t\t\t'status'         => 'BLOCKED',\r\n+\t\t\t\t\t\t\t)\r\n+\t\t\t\t\t\t);\r\n+\t\t\t\t\texit;\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\tminiorange_site_otp_validation_form( null, null, null, $message, $otp_type, $from_both );\r\n+\t\t\t\t\texit;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Enqueue frontend scripts.\r\n+\t\t *\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_enqueue_frontend_scripts() {\r\n+\t\t\tif ( is_admin() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! $this->is_otp_verification_active_on_page() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings = $this->storage->mosp_get_settings();\r\n+\r\n+\t\t\twp_enqueue_script(\r\n+\t\t\t\t'mo-osp-frontend',\r\n+\t\t\t\tMO_OSP_URL . 'includes\u002Fjs\u002Fspam-preventer.js',\r\n+\t\t\t\tarray( 'jquery' ),\r\n+\t\t\t\t'1.1.0',\r\n+\t\t\t\ttrue\r\n+\t\t\t);\r\n+\r\n+\t\t\twp_enqueue_script(\r\n+\t\t\t\t'mo-osp-puzzle-system',\r\n+\t\t\t\tMO_OSP_URL . 'includes\u002Fjs\u002Fpuzzle-system.js',\r\n+\t\t\t\tarray( 'jquery', 'mo-osp-frontend' ),\r\n+\t\t\t\t'1.0.0',\r\n+\t\t\t\ttrue\r\n+\t\t\t);\r\n+\r\n+\t\t\twp_enqueue_style(\r\n+\t\t\t\t'mo-osp-puzzle-css',\r\n+\t\t\t\tMO_OSP_URL . 'includes\u002Fcss\u002Fmo-admin.css',\r\n+\t\t\t\tarray(),\r\n+\t\t\t\t'1.0.5'\r\n+\t\t\t);\r\n+\r\n+\t\t\twp_localize_script(\r\n+\t\t\t\t'mo-osp-frontend',\r\n+\t\t\t\t'mo_osp_ajax',\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'ajax_url'     => admin_url( 'admin-ajax.php' ),\r\n+\t\t\t\t\t'nonce'        => wp_create_nonce( 'mo_osp_nonce' ),\r\n+\t\t\t\t\t'loading_text' => __( 'Checking...', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t'timer_time'   => $settings['cooldown_time'],\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Enqueue admin scripts.\r\n+\t\t *\r\n+\t\t * @param string $hook_suffix Current admin page.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_enqueue_admin_scripts( $hook_suffix ) {\r\n+\t\t\tif ( strpos( $hook_suffix, 'mo_otp_verification' ) === false ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$mo_osp_admin_js = MO_OSP_DIR . 'includes\u002Fjs\u002Fspam-preventer-admin.js';\r\n+\t\t\twp_enqueue_script(\r\n+\t\t\t\t'mo-osp-admin',\r\n+\t\t\t\tMO_OSP_URL . 'includes\u002Fjs\u002Fspam-preventer-admin.js',\r\n+\t\t\t\tarray( 'jquery' ),\r\n+\t\t\t\tfile_exists( $mo_osp_admin_js ) ? (string) filemtime( $mo_osp_admin_js ) : '1.0.1',\r\n+\t\t\t\ttrue\r\n+\t\t\t);\r\n+\r\n+\t\t\twp_localize_script(\r\n+\t\t\t\t'mo-osp-admin',\r\n+\t\t\t\t'mo_osp_admin_ajax',\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'ajax_url' => admin_url( 'admin-ajax.php' ),\r\n+\t\t\t\t\t'nonce'    => wp_create_nonce( 'mo_osp_admin_nonce' ),\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Include timer JavaScript for pop-up forms.\r\n+\t\t * This is called via mo_include_js action when popup is rendered.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_include_timer_js() {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings = $this->storage->mosp_get_settings();\r\n+\r\n+\t\t\t$email = MoPHPSessions::get_session_var( 'user_email' );\r\n+\t\t\t$phone = MoPHPSessions::get_session_var( 'phone_number_mo' );\r\n+\t\t\t\u002F\u002F phpcs:disable WordPress.Security.NonceVerification.Missing\r\n+\t\t\t$browser_id        = isset( $_POST['mo_osp_browser_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ) : '';\r\n+\t\t\t$ip                = $this->handler->mosp_get_client_ip();\r\n+\t\t\t$is_ip_whitelisted = false;\r\n+\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t$is_ip_whitelisted = $this->handler->mosp_is_whitelisted( $ip, 'ip' );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$cooldown_remaining = $is_ip_whitelisted ? 0 : $this->mosp_get_cooldown_remaining_time( $email, $phone, $browser_id );\r\n+\t\t\t$is_blocked         = $this->handler->mosp_is_blocked( $email, $phone, $browser_id, 'popup_render' );\r\n+\r\n+\t\t\t\u002F*\r\n+\t\t\t * mo_include_js runs mid-document (during popup HTML). Bundled scripts end with IIFEs like })(jQuery);\r\n+\t\t\t * Inline code below also invokes jQuery immediately. Force core jQuery to print first so it is defined.\r\n+\t\t\t *\u002F\r\n+\t\t\twp_enqueue_script( 'jquery' );\r\n+\t\t\twp_print_scripts( 'jquery' );\r\n+\r\n+\t\t\twp_register_style( 'mo-osp-puzzle-css-popup', MO_OSP_URL . 'includes\u002Fcss\u002Fmo-admin.css', array(), '1.0.5', 'all' );\r\n+\t\t\twp_print_styles( 'mo-osp-puzzle-css-popup' );\r\n+\r\n+\t\t\twp_register_script( 'mo-osp-timer', MO_OSP_URL . 'includes\u002Fjs\u002Fspam-preventer.js', array( 'jquery' ), '1.0.0', false );\r\n+\t\t\twp_localize_script(\r\n+\t\t\t\t'mo-osp-timer',\r\n+\t\t\t\t'mo_osp_timer',\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'timer_time' => $settings['cooldown_time'],\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t\twp_print_scripts( 'mo-osp-timer' );\r\n+\r\n+\t\t\twp_register_script( 'mo-osp-puzzle-system', MO_OSP_URL . 'includes\u002Fjs\u002Fpuzzle-system.js', array( 'jquery', 'mo-osp-timer' ), '1.0.0', false );\r\n+\t\t\twp_localize_script(\r\n+\t\t\t\t'mo-osp-puzzle-system',\r\n+\t\t\t\t'mo_osp_ajax',\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'ajax_url'    => admin_url( 'admin-ajax.php' ),\r\n+\t\t\t\t\t'nonce'       => wp_create_nonce( 'mo_osp_nonce' ),\r\n+\t\t\t\t\t'timer_time'  => $settings['cooldown_time'],\r\n+\t\t\t\t\t'block_time'  => $settings['block_time'],\r\n+\t\t\t\t\t'enable_logs' => defined( 'WP_DEBUG' ) && WP_DEBUG,\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t\twp_print_scripts( 'mo-osp-puzzle-system' );\r\n+\r\n+\t\t\twp_register_script( 'mo-osp-popup-timer', MO_OSP_URL . 'includes\u002Fjs\u002Fpopup-timer.js', array( 'jquery', 'mo-osp-timer', 'mo-osp-puzzle-system' ), '1.0.0', false );\r\n+\t\t\twp_localize_script(\r\n+\t\t\t\t'mo-osp-popup-timer',\r\n+\t\t\t\t'mo_osp_popup_timer',\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'ajax_url'              => admin_url( 'admin-ajax.php' ),\r\n+\t\t\t\t\t'nonce'                 => wp_create_nonce( 'mo_osp_nonce' ),\r\n+\t\t\t\t\t'timer_time'            => $settings['cooldown_time'],\r\n+\t\t\t\t\t'block_time'            => $settings['block_time'],\r\n+\t\t\t\t\t'limit_otp_sent'        => MoMessages::showMessage( MoMessages::LIMIT_OTP_SENT ),\r\n+\t\t\t\t\t'user_blocked'          => MoMessages::showMessage( MoMessages::USER_IS_BLOCKED_AJAX ),\r\n+\t\t\t\t\t'error_otp_verify'      => MoMessages::showMessage( MoMessages::ERROR_OTP_VERIFY ),\r\n+\t\t\t\t\t'initial_cooldown_time' => $cooldown_remaining,\r\n+\t\t\t\t\t'initial_blocked'       => $is_blocked,\r\n+\t\t\t\t\t'puzzle_required_text'  => __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' ),\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t\twp_print_scripts( 'mo-osp-popup-timer' );\r\n+\r\n+\t\t\techo '\u003Cdiv id=\"mo-osp-puzzle-popup-outer-div\" style=\"display:none;\">';\r\n+\t\t\tMoPuzzleHelper::mosp_render_puzzle_popup();\r\n+\t\t\techo '\u003C\u002Fdiv>';\r\n+\t\t\t$puzzle_message = __( 'Please complete the security verification to continue.', 'miniorange-otp-verification' );\r\n+\t\t\t?>\r\n+\t\t\t\u003Cscript type=\"text\u002Fjavascript\">\r\n+\t\t\t(function() {\r\n+\t\t\t\tfunction moOspRunInlinePuzzle($) {\r\n+\t\t\t\t'use strict';\r\n+\t\t\t\t\r\n+\t\t\t\tif (window.mo_osp_inline_puzzle_check_executed) {\r\n+\t\t\t\t\treturn;\r\n+\t\t\t\t}\r\n+\t\t\t\twindow.mo_osp_inline_puzzle_check_executed = true;\r\n+\t\t\t\t\r\n+\t\t\t\tfunction checkAndShowPuzzle() {\r\n+\t\t\t\t\tif (window.mo_osp_puzzle_shown) {\r\n+\t\t\t\t\t\treturn;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t\r\n+\t\t\t\t\tvar $popupBody = $('.mo_customer_validation-modal-body');\r\n+\t\t\t\t\tvar $firstDiv = $popupBody.children('div').first();\r\n+\t\t\t\t\tvar messageText = '';\r\n+\t\t\t\t\t\r\n+\t\t\t\t\tif ($firstDiv.length > 0) {\r\n+\t\t\t\t\t\tmessageText = $firstDiv.text().trim();\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tif (!messageText && $popupBody.length > 0) {\r\n+\t\t\t\t\t\tmessageText = $popupBody.text().trim();\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t\r\n+\t\t\t\t\tvar puzzleText = '\u003C?php echo esc_js( $puzzle_message ); ?>';\r\n+\t\t\t\t\t\r\n+\t\t\t\t\tif (messageText && messageText.toLowerCase().includes(puzzleText.toLowerCase())) {\r\n+\t\t\t\t\t\t\r\n+\t\t\t\t\t\twindow.mo_osp_puzzle_shown = true;\r\n+\t\t\t\t\t\t\r\n+\t\t\t\t\t\t$('#mo_site_otp_form').hide();\r\n+\t\t\t\t\t\t$('.mo_customer_validation-modal').hide();\r\n+\t\t\t\t\t\t$('.mo-modal-backdrop').hide();\r\n+\t\t\t\t\t\t\r\n+\t\t\t\t\t\tif ($('#mo-osp-puzzle-overlay').length === 0) {\r\n+\t\t\t\t\t\t\treturn;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\r\n+\t\t\t\t\t\tvar $puzzleOverlay = $('#mo-osp-puzzle-overlay');\r\n+\t\t\t\t\t\t$puzzleOverlay.css('z-index', '100001');\r\n+\t\t\t\t\t\t\r\n+\t\t\t\t\t\t$('#mo-osp-puzzle-popup-outer-div').show().css('z-index', '100002');\r\n+\t\t\t\t\t\t$puzzleOverlay.removeClass('mo-osp-hidden');\r\n+\t\t\t\t\t\t\r\n+\t\t\t\t\t\twindow.MO_OSP_Puzzle_onPopupSuccess = function() {\r\n+\t\t\t\t\t\t\tif (typeof window.MO_OSP_Puzzle !== 'undefined' && window.MO_OSP_Puzzle.closePuzzle) {\r\n+\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.closePuzzle();\r\n+\t\t\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t\t\tjQuery('#mo-osp-puzzle-overlay').addClass('mo-osp-hidden');\r\n+\t\t\t\t\t\t\t\tjQuery('#mo-osp-puzzle-popup-outer-div').hide();\r\n+\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\r\n+\t\t\t\t\t\t\tvar resendForm = document.getElementById('verification_resend_otp_form');\r\n+\t\t\t\t\t\t\tif (resendForm) {\r\n+\t\t\t\t\t\t\t\tif (!resendForm.querySelector('input[name=\"puzzle_verified\"]')) {\r\n+\t\t\t\t\t\t\t\t\tvar puzzleVerifiedInput = document.createElement('input');\r\n+\t\t\t\t\t\t\t\t\tpuzzleVerifiedInput.type = 'hidden';\r\n+\t\t\t\t\t\t\t\t\tpuzzleVerifiedInput.name = 'puzzle_verified';\r\n+\t\t\t\t\t\t\t\t\tpuzzleVerifiedInput.value = 'true';\r\n+\t\t\t\t\t\t\t\t\tresendForm.appendChild(puzzleVerifiedInput);\r\n+\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\tresendForm.submit();\r\n+\t\t\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t\t\tsessionStorage.setItem('mo_osp_puzzle_completed', 'true');\r\n+\t\t\t\t\t\t\t\twindow.location.reload();\r\n+\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t};\r\n+\t\t\t\t\t\t\r\n+\t\t\t\t\t\tif (typeof window.MO_OSP_Puzzle !== 'undefined') {\r\n+\t\t\t\t\t\t\tif (typeof window.MO_OSP_Puzzle.init === 'function' && !window.MO_OSP_Puzzle.initialized) {\r\n+\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.init();\r\n+\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.initialized = true;\r\n+\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.showPuzzle({});\r\n+\t\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t\tsetTimeout(function() {\r\n+\t\t\t\t\t\t\t\tif (typeof window.MO_OSP_Puzzle !== 'undefined') {\r\n+\t\t\t\t\t\t\t\t\tif (typeof window.MO_OSP_Puzzle.init === 'function' && !window.MO_OSP_Puzzle.initialized) {\r\n+\t\t\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.init();\r\n+\t\t\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.initialized = true;\r\n+\t\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\t\twindow.MO_OSP_Puzzle.showPuzzle({});\r\n+\t\t\t\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t\t\t\tconsole.error('MO_OSP_Puzzle still not available after wait');\r\n+\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t}, 500);\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t\t\r\n+\t\t\t\tvar checkExecuted = false;\r\n+\t\t\t\tfunction runCheckOnce() {\r\n+\t\t\t\t\tif (checkExecuted) {\r\n+\t\t\t\t\t\treturn;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tcheckExecuted = true;\r\n+\t\t\t\t\tcheckAndShowPuzzle();\r\n+\t\t\t\t}\r\n+\t\t\t\t\r\n+\t\t\t\tif (document.readyState === 'loading') {\r\n+\t\t\t\t\t$(document).ready(function() {\r\n+\t\t\t\t\t\tsetTimeout(runCheckOnce, 300);\r\n+\t\t\t\t\t});\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\tsetTimeout(runCheckOnce, 300);\r\n+\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t\tfunction moOspTryInlinePuzzle() {\r\n+\t\t\t\t\tvar jq = window.jQuery;\r\n+\t\t\t\t\tif (typeof jq === 'undefined') {\r\n+\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tmoOspRunInlinePuzzle(jq);\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t\tif (!moOspTryInlinePuzzle()) {\r\n+\t\t\t\t\tvar moOspInlineIv = setInterval(function () {\r\n+\t\t\t\t\t\tif (moOspTryInlinePuzzle()) {\r\n+\t\t\t\t\t\t\tclearInterval(moOspInlineIv);\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}, 30);\r\n+\t\t\t\t\tsetTimeout(function () {\r\n+\t\t\t\t\t\tclearInterval(moOspInlineIv);\r\n+\t\t\t\t\t}, 15000);\r\n+\t\t\t\t}\r\n+\t\t\t})();\r\n+\t\t\t\u003C\u002Fscript>\r\n+\t\t\t\u003C?php\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Add puzzle popup HTML to frontend.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_add_puzzle_popup_to_frontend() {\r\n+\t\t\tif ( is_admin() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! $this->is_otp_verification_active_on_page() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\techo '\u003Cdiv id=\"mo-osp-puzzle-popup-outer-div\" style=\"display:none;\">';\r\n+\t\t\tMoPuzzleHelper::mosp_render_puzzle_popup();\r\n+\t\t\techo '\u003C\u002Fdiv>';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if OTP verification is active on the current page.\r\n+\t\t *\r\n+\t\t * @return bool True if any form has OTP verification enabled\r\n+\t\t *\u002F\r\n+\t\tprivate function is_otp_verification_active_on_page() {\r\n+\t\t\t$form_list = FormList::instance();\r\n+\t\t\t$all_forms = $form_list->get_list();\r\n+\r\n+\t\t\tforeach ( $all_forms as $form_handler ) {\r\n+\t\t\t\tif ( $form_handler && method_exists( $form_handler, 'is_form_enabled' ) ) {\r\n+\t\t\t\t\tif ( $form_handler->is_form_enabled() ) {\r\n+\t\t\t\t\t\treturn true;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$otp_verification_options = array(\r\n+\t\t\t\t'cf_submit_id',\r\n+\t\t\t\t'wc_default_enable',\r\n+\t\t\t\t'wp_default_enable',\r\n+\t\t\t\t'wp_login_enable',\r\n+\t\t\t\t'wc_checkout_enable',\r\n+\t\t\t\t'bp_registration_enable',\r\n+\t\t\t\t'um_default_enable',\r\n+\t\t\t\t'pmpro_default_enable',\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $otp_verification_options as $option ) {\r\n+\t\t\t\tif ( get_mo_option( $option ) ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Provide addon cooldown time to host plugin for server-side formatting.\r\n+\t\t *\r\n+\t\t * @param int $default_value Default fallback value.\r\n+\t\t * @return int seconds\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_filter_get_cooldown_time( $default_value = 60 ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn (int) $default_value;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings = $this->storage->mosp_get_settings();\r\n+\t\t\treturn isset( $settings['cooldown_time'] ) ? (int) $settings['cooldown_time'] : (int) $default_value;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if spam preventer addon is enabled in settings.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_is_addon_enabled() {\r\n+\t\t\t$settings = $this->storage->mosp_get_settings();\r\n+\t\t\treturn ! empty( $settings['enabled'] );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get remaining cooldown time for user.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $browser_id Browser ID.\r\n+\t\t * @return int Remaining cooldown time in seconds.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_get_cooldown_remaining_time( $email, $phone, $browser_id ) {\r\n+\t\t\t$identifiers   = $this->handler->mosp_get_all_identifiers( $email, $phone, $this->handler->mosp_get_client_ip(), $browser_id );\r\n+\t\t\t$current_time  = time();\r\n+\t\t\t$settings      = $this->storage->mosp_get_settings();\r\n+\t\t\t$cooldown_time = $settings['cooldown_time'];\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\r\n+\t\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\r\n+\r\n+\t\t\t\tif ( false !== $data && isset( $data['last_attempt'] ) && $data['last_attempt'] > 0 ) {\r\n+\t\t\t\t\t$time_since_last = $current_time - $data['last_attempt'];\r\n+\t\t\t\t\t$remaining       = $cooldown_time - $time_since_last;\r\n+\r\n+\t\t\t\t\tif ( $remaining > 0 ) {\r\n+\t\t\t\t\t\treturn $remaining;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn 0;\r\n+\t\t}\r\n+\t}\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspampreventeraddonhandler.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspampreventeraddonhandler.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspampreventeraddonhandler.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspampreventeraddonhandler.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,181 +1,181 @@\n-\u003C?php\n-\u002F**\n- * OTP Spam Preventer Handler\n- *\n- * @package otpspampreventer\u002Fhandler\n- *\u002F\n-\n-namespace OSP\\Handler;\n-\n-use OSP\\Handler\\MoOtpSpamPreventerHandler;\n-use OSP\\Handler\\MoOtpSpamStorage;\n-use OSP\\Handler\\MoOtpSpamAjax;\n-use OSP\\Traits\\Instance;\n-use OTP\\Objects\\BaseAddOnHandler;\n-use OTP\\Helper\\AddOnList;\n-use OTP\\Helper\\MoUtility;\n-use OTP\\Helper\\MoMessages;\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-if ( ! class_exists( 'MoOtpSpamPreventerAddonHandler' ) ) {\n-\t\u002F**\n-\t * The class is used to handle all OTP Spam Preventer related functionality.\n-\t *\u002F\n-\tclass MoOtpSpamPreventerAddonHandler extends BaseAddOnHandler {\n-\n-\t\tuse Instance;\n-\n-\t\t\u002F**\n-\t\t * Constructor checks if add-on has been enabled by the admin and initializes\n-\t\t * all the class variables. This function also defines all the hooks to\n-\t\t * hook into to make the add-on functionality work.\n-\t\t *\u002F\n-\t\tpublic function __construct() {\n-\t\t\tparent::__construct();\n-\t\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'mo_enqueue_admin_assets' ) );\n-\t\t\tif ( ! $this->moAddOnV() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\tMoOtpSpamPreventerHandler::instance();\n-\t\t\tMoOtpSpamStorage::instance();\n-\t\t\tMoOtpSpamAjax::instance();\n-\n-\t\t\tadd_action( 'admin_init', array( $this, 'mo_handle_settings_save' ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Set a unique key for the AddOn\n-\t\t *\u002F\n-\t\tpublic function set_addon_key() {\n-\t\t\t$this->add_on_key = 'otp_spam_preventer';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Set a AddOn Description\n-\t\t * Store raw string to avoid early translation loading warning.\n-\t\t *\u002F\n-\t\tpublic function set_add_on_desc() {\n-\t\t\t$this->add_on_desc = 'Prevents OTP request spamming based on phone number, email, IP address, and browser fingerprint. '\n-\t\t\t\t. 'Click on the settings button to the right to configure settings for the same.';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Set an AddOnName\n-\t\t * Store raw string to avoid early translation loading warning.\n-\t\t *\u002F\n-\t\tpublic function set_add_on_name() {\n-\t\t\t$this->addon_name = 'OTP Spam Preventer';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Return the Addon Description (with lazy translation)\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tpublic function getAddOnDesc() {\n-\t\t\tif ( did_action( 'plugins_loaded' ) ) {\n-\t\t\t\t\u002F\u002F phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- Dynamic translation needed for lazy loading.\n-\t\t\t\treturn __( $this->add_on_desc, 'miniorange-otp-verification' );\n-\t\t\t}\n-\t\t\treturn $this->add_on_desc;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Return AddOn Name (with lazy translation)\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tpublic function get_add_on_name() {\n-\t\t\tif ( did_action( 'plugins_loaded' ) ) {\n-\t\t\t\t\u002F\u002F phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- Dynamic translation needed for lazy loading.\n-\t\t\t\treturn __( $this->addon_name, 'miniorange-otp-verification' );\n-\t\t\t}\n-\t\t\treturn $this->addon_name;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Set Settings Page URL\n-\t\t *\u002F\n-\t\tpublic function set_settings_url() {\n-\t\t\t$req_url            = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; \u002F\u002F phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- esc_url_raw() handles sanitization.\n-\t\t\t$this->settings_url = add_query_arg( array( 'addon' => 'otp_spam_preventer' ), $req_url );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Set an Addon Docs link\n-\t\t *\u002F\n-\t\tpublic function set_add_on_docs() {}\n-\n-\t\t\u002F**\n-\t\t * Set an Addon Video link\n-\t\t *\u002F\n-\t\tpublic function set_add_on_video() {}\n-\n-\t\t\u002F**\n-\t\t * Handle settings save POST request.\n-\t\t *\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic function mo_handle_settings_save() {\n-\t\t\tif ( ! isset( $_POST['option'] ) ) { \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Missing -- false positive.\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$option = sanitize_text_field( wp_unslash( $_POST['option'] ) ); \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Missing -- false positive.\n-\t\t\tif ( 'mo_osp_settings_save' !== $option ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tcheck_admin_referer( 'mo_osp_settings_save' );\n-\n-\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\n-\t\t\t\twp_die( esc_html( MoMessages::showMessage( MoMessages::INSUFFICIENT_PERMISSIONS ) ) );\n-\t\t\t}\n-\n-\t\t\t$handler = MoOtpSpamPreventerHandler::instance();\n-\t\t\t$posted  = MoUtility::mo_sanitize_array( $_POST ); \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Missing -- sanitized within the function.\n-\t\t\t$result  = $handler->mosp_save_settings( $posted );\n-\n-\t\t\t$message_type = $result['success'] ? 'SUCCESS' : 'ERROR';\n-\t\t\tdo_action( 'mo_registration_show_message', $result['message'], $message_type );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Enqueue admin assets.\n-\t\t *\n-\t\t * @param string $hook_suffix Current admin page hook suffix. Not used but required by hook signature.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic function mo_enqueue_admin_assets( $hook_suffix ) { \u002F\u002F phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter -- Required by admin_enqueue_scripts hook signature.\n-\t\t\tif ( ! isset( $_GET['addon'] ) ) { \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for checking the addon name, doesn't require nonce verification.\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$addon = sanitize_text_field( wp_unslash( $_GET['addon'] ) ); \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for checking the addon name, doesn't require nonce verification.\n-\t\t\tif ( 'otp_spam_preventer' !== $addon ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\twp_enqueue_style( 'mo-osp-admin', MO_OSP_URL . 'includes\u002Fcss\u002Fmo-admin.css', array(), '1.6.0' );\n-\t\t\t$mo_osp_admin_js = MO_OSP_DIR . 'includes\u002Fjs\u002Fspam-preventer-admin.js';\n-\t\t\twp_enqueue_script(\n-\t\t\t\t'mo-osp-admin',\n-\t\t\t\tMO_OSP_URL . 'includes\u002Fjs\u002Fspam-preventer-admin.js',\n-\t\t\t\tarray( 'jquery' ),\n-\t\t\t\tfile_exists( $mo_osp_admin_js ) ? (string) filemtime( $mo_osp_admin_js ) : '1.0.1',\n-\t\t\t\ttrue\n-\t\t\t);\n-\t\t\twp_localize_script(\n-\t\t\t\t'mo-osp-admin',\n-\t\t\t\t'mo_osp_admin_ajax',\n-\t\t\t\tarray(\n-\t\t\t\t\t'ajax_url' => admin_url( 'admin-ajax.php' ),\n-\t\t\t\t\t'nonce'    => wp_create_nonce( 'mo_osp_admin_nonce' ),\n-\t\t\t\t)\n-\t\t\t);\n-\t\t}\n-\t}\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * OTP Spam Preventer Handler\r\n+ *\r\n+ * @package otpspampreventer\u002Fhandler\r\n+ *\u002F\r\n+\r\n+namespace OSP\\Handler;\r\n+\r\n+use OSP\\Handler\\MoOtpSpamPreventerHandler;\r\n+use OSP\\Handler\\MoOtpSpamStorage;\r\n+use OSP\\Handler\\MoOtpSpamAjax;\r\n+use OSP\\Traits\\Instance;\r\n+use OTP\\Objects\\BaseAddOnHandler;\r\n+use OTP\\Helper\\AddOnList;\r\n+use OTP\\Helper\\MoUtility;\r\n+use OTP\\Helper\\MoMessages;\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+if ( ! class_exists( 'MoOtpSpamPreventerAddonHandler' ) ) {\r\n+\t\u002F**\r\n+\t * The class is used to handle all OTP Spam Preventer related functionality.\r\n+\t *\u002F\r\n+\tclass MoOtpSpamPreventerAddonHandler extends BaseAddOnHandler {\r\n+\r\n+\t\tuse Instance;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Constructor checks if add-on has been enabled by the admin and initializes\r\n+\t\t * all the class variables. This function also defines all the hooks to\r\n+\t\t * hook into to make the add-on functionality work.\r\n+\t\t *\u002F\r\n+\t\tpublic function __construct() {\r\n+\t\t\tparent::__construct();\r\n+\t\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'mo_enqueue_admin_assets' ) );\r\n+\t\t\tif ( ! $this->moAddOnV() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t\tMoOtpSpamPreventerHandler::instance();\r\n+\t\t\tMoOtpSpamStorage::instance();\r\n+\t\t\tMoOtpSpamAjax::instance();\r\n+\r\n+\t\t\tadd_action( 'admin_init', array( $this, 'mo_handle_settings_save' ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Set a unique key for the AddOn\r\n+\t\t *\u002F\r\n+\t\tpublic function set_addon_key() {\r\n+\t\t\t$this->add_on_key = 'otp_spam_preventer';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Set a AddOn Description\r\n+\t\t * Store raw string to avoid early translation loading warning.\r\n+\t\t *\u002F\r\n+\t\tpublic function set_add_on_desc() {\r\n+\t\t\t$this->add_on_desc = 'Prevents OTP request spamming based on phone number, email, IP address, and browser fingerprint. '\r\n+\t\t\t\t. 'Click on the settings button to the right to configure settings for the same.';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Set an AddOnName\r\n+\t\t * Store raw string to avoid early translation loading warning.\r\n+\t\t *\u002F\r\n+\t\tpublic function set_add_on_name() {\r\n+\t\t\t$this->addon_name = 'OTP Spam Preventer';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Return the Addon Description (with lazy translation)\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tpublic function getAddOnDesc() {\r\n+\t\t\tif ( did_action( 'plugins_loaded' ) ) {\r\n+\t\t\t\t\u002F\u002F phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- Dynamic translation needed for lazy loading.\r\n+\t\t\t\treturn __( $this->add_on_desc, 'miniorange-otp-verification' );\r\n+\t\t\t}\r\n+\t\t\treturn $this->add_on_desc;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Return AddOn Name (with lazy translation)\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tpublic function get_add_on_name() {\r\n+\t\t\tif ( did_action( 'plugins_loaded' ) ) {\r\n+\t\t\t\t\u002F\u002F phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- Dynamic translation needed for lazy loading.\r\n+\t\t\t\treturn __( $this->addon_name, 'miniorange-otp-verification' );\r\n+\t\t\t}\r\n+\t\t\treturn $this->addon_name;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Set Settings Page URL\r\n+\t\t *\u002F\r\n+\t\tpublic function set_settings_url() {\r\n+\t\t\t$req_url            = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; \u002F\u002F phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- esc_url_raw() handles sanitization.\r\n+\t\t\t$this->settings_url = add_query_arg( array( 'addon' => 'otp_spam_preventer' ), $req_url );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Set an Addon Docs link\r\n+\t\t *\u002F\r\n+\t\tpublic function set_add_on_docs() {}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Set an Addon Video link\r\n+\t\t *\u002F\r\n+\t\tpublic function set_add_on_video() {}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Handle settings save POST request.\r\n+\t\t *\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic function mo_handle_settings_save() {\r\n+\t\t\tif ( ! isset( $_POST['option'] ) ) { \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Missing -- false positive.\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$option = sanitize_text_field( wp_unslash( $_POST['option'] ) ); \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Missing -- false positive.\r\n+\t\t\tif ( 'mo_osp_settings_save' !== $option ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tcheck_admin_referer( 'mo_osp_settings_save' );\r\n+\r\n+\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\r\n+\t\t\t\twp_die( esc_html( MoMessages::showMessage( MoMessages::INSUFFICIENT_PERMISSIONS ) ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$handler = MoOtpSpamPreventerHandler::instance();\r\n+\t\t\t$posted  = MoUtility::mo_sanitize_array( $_POST ); \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Missing -- sanitized within the function.\r\n+\t\t\t$result  = $handler->mosp_save_settings( $posted );\r\n+\r\n+\t\t\t$message_type = $result['success'] ? 'SUCCESS' : 'ERROR';\r\n+\t\t\tdo_action( 'mo_registration_show_message', $result['message'], $message_type );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Enqueue admin assets.\r\n+\t\t *\r\n+\t\t * @param string $hook_suffix Current admin page hook suffix. Not used but required by hook signature.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic function mo_enqueue_admin_assets( $hook_suffix ) { \u002F\u002F phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter -- Required by admin_enqueue_scripts hook signature.\r\n+\t\t\tif ( ! isset( $_GET['addon'] ) ) { \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for checking the addon name, doesn't require nonce verification.\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$addon = sanitize_text_field( wp_unslash( $_GET['addon'] ) ); \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for checking the addon name, doesn't require nonce verification.\r\n+\t\t\tif ( 'otp_spam_preventer' !== $addon ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\twp_enqueue_style( 'mo-osp-admin', MO_OSP_URL . 'includes\u002Fcss\u002Fmo-admin.css', array(), '1.6.0' );\r\n+\t\t\t$mo_osp_admin_js = MO_OSP_DIR . 'includes\u002Fjs\u002Fspam-preventer-admin.js';\r\n+\t\t\twp_enqueue_script(\r\n+\t\t\t\t'mo-osp-admin',\r\n+\t\t\t\tMO_OSP_URL . 'includes\u002Fjs\u002Fspam-preventer-admin.js',\r\n+\t\t\t\tarray( 'jquery' ),\r\n+\t\t\t\tfile_exists( $mo_osp_admin_js ) ? (string) filemtime( $mo_osp_admin_js ) : '1.0.1',\r\n+\t\t\t\ttrue\r\n+\t\t\t);\r\n+\t\t\twp_localize_script(\r\n+\t\t\t\t'mo-osp-admin',\r\n+\t\t\t\t'mo_osp_admin_ajax',\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'ajax_url' => admin_url( 'admin-ajax.php' ),\r\n+\t\t\t\t\t'nonce'    => wp_create_nonce( 'mo_osp_admin_nonce' ),\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t}\r\n+\t}\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspampreventerhandler.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspampreventerhandler.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspampreventerhandler.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspampreventerhandler.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,2231 +1,2231 @@\n-\u003C?php\n-\u002F**\n- * OTP Spam Preventer Main Handler\n- *\n- * @package otpspampreventer\u002Fhandler\n- *\u002F\n-\n-namespace OSP\\Handler;\n-\n-use OSP\\Handler\\MoOtpSpamStorage;\n-use OSP\\Helper\\MoRateLimitHelper;\n-use OSP\\Helper\\MoSecurityHelper;\n-use OSP\\Traits\\Instance;\n-use OTP\\Helper\\MoMessages;\n-use OTP\\Helper\\MoPHPSessions;\n-use OTP\\Helper\\MoUtility;\n-\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-if ( ! class_exists( 'MoOtpSpamPreventerHandler' ) ) {\n-\t\u002F**\n-\t * The main handler class for OTP Spam Prevention functionality.\n-\t * Integrates with the existing OTP verification flow.\n-\t * Updated: Fixed block expiration logic\n-\t *\u002F\n-\tclass MoOtpSpamPreventerHandler {\n-\n-\t\tuse Instance;\n-\n-\t\t\u002F**\n-\t\t * Storage instance\n-\t\t *\n-\t\t * @var MoOtpSpamStorage\n-\t\t *\u002F\n-\t\tprivate $storage;\n-\n-\t\t\u002F**\n-\t\t * Constructor\n-\t\t *\u002F\n-\t\tpublic function __construct() {\n-\t\t\t$this->storage = MoOtpSpamStorage::instance();\n-\t\t\tMoRateLimitHelper::init( $this->storage );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Save settings from POST data.\n-\t\t *\n-\t\t * @param array $posted POST data array.\n-\t\t * @return array Result array with 'success' and 'message' keys.\n-\t\t *\u002F\n-\t\tpublic function mosp_save_settings( $posted ) {\n-\t\t\t$settings                  = array();\n-\t\t\t$settings['enabled']       = isset( $posted['mo_osp_enabled'] );\n-\t\t\t$settings['cooldown_time'] = isset( $posted['mo_osp_cooldown_time'] ) ? absint( $posted['mo_osp_cooldown_time'] ) : 60;\n-\n-\t\t\t$max_attempts             = isset( $posted['mo_osp_max_attempts'] ) ? absint( $posted['mo_osp_max_attempts'] ) : 3;\n-\t\t\t$settings['max_attempts'] = max( 1, min( 10, $max_attempts ) );\n-\n-\t\t\t$settings['block_time'] = isset( $posted['mo_osp_block_time'] ) ? absint( $posted['mo_osp_block_time'] ) : 900;\n-\n-\t\t\t$settings['daily_limit']  = isset( $posted['mo_osp_daily_limit'] ) ? absint( $posted['mo_osp_daily_limit'] ) : 10;\n-\t\t\t$settings['hourly_limit'] = isset( $posted['mo_osp_hourly_limit'] ) ? absint( $posted['mo_osp_hourly_limit'] ) : 5;\n-\n-\t\t\t$validation_errors = array();\n-\n-\t\t\tif ( $settings['hourly_limit'] \u003C= $settings['max_attempts'] ) {\n-\t\t\t\t$validation_errors[] = 'Hourly limit (' . $settings['hourly_limit'] . ') must be greater than max attempts per window (' . $settings['max_attempts'] . ')';\n-\t\t\t}\n-\n-\t\t\tif ( $settings['daily_limit'] \u003C= $settings['hourly_limit'] ) {\n-\t\t\t\t$validation_errors[] = 'Daily limit (' . $settings['daily_limit'] . ') must be greater than hourly limit (' . $settings['hourly_limit'] . ')';\n-\t\t\t}\n-\n-\t\t\tif ( ! empty( $validation_errors ) ) {\n-\t\t\t\t$error_message = 'Settings validation failed: ' . implode( '; ', $validation_errors );\n-\t\t\t\tdo_action( 'mo_otp_verification_show_message', $error_message, 'ERROR' );\n-\t\t\t\treturn array(\n-\t\t\t\t\t'success' => false,\n-\t\t\t\t\t'message' => $error_message,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$settings['track_phone']   = true;\n-\t\t\t$settings['track_email']   = true;\n-\t\t\t$settings['track_ip']      = true;\n-\t\t\t$settings['track_browser'] = true;\n-\n-\t\t\t\u002F\u002F Process whitelists.\n-\t\t\t$whitelist_ips             = isset( $posted['mo_osp_whitelist_ips'] ) ? sanitize_textarea_field( wp_unslash( $posted['mo_osp_whitelist_ips'] ) ) : '';\n-\t\t\t$settings['whitelist_ips'] = array_filter( array_map( 'trim', explode( \"\\n\", $whitelist_ips ) ) );\n-\t\t\t$settings['whitelist_ips'] = array_values( $settings['whitelist_ips'] );\n-\n-\t\t\t$result = $this->storage->mosp_update_settings( $settings );\n-\n-\t\t\tif ( $result ) {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'success' => true,\n-\t\t\t\t\t'message' => __( 'Settings saved successfully!', 'miniorange-otp-verification' ),\n-\t\t\t\t);\n-\t\t\t} else {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'success' => false,\n-\t\t\t\t\t'message' => __( 'Failed to save settings!', 'miniorange-otp-verification' ),\n-\t\t\t\t);\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if identifier is whitelisted.\n-\t\t *\n-\t\t * @param string $identifier The identifier to check (IP, email, phone, etc.).\n-\t\t * @param string $type The type of identifier (ip, email, phone).\n-\t\t * @return bool True if whitelisted, false otherwise.\n-\t\t *\u002F\n-\t\tpublic function mosp_is_whitelisted( $identifier, $type ) {\n-\t\t\treturn $this->storage->mosp_is_whitelisted( $identifier, $type );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if a request should be blocked due to spam prevention rules\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $browser_id Browser fingerprint ID.\n-\t\t * @param string $context Context of the check ('otp_send' or 'timer_status').\n-\t\t * @return bool True if blocked, false if allowed\n-\t\t *\u002F\n-\t\tpublic function mosp_is_blocked( $email, $phone, $browser_id = '', $context = 'otp_send' ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$settings = $this->storage->mosp_get_settings();\n-\n-\t\t\t$ip = $this->mosp_get_client_ip();\n-\n-\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t$is_whitelisted = $this->storage->mosp_is_whitelisted( $ip, 'ip' );\n-\t\t\t\tif ( $is_whitelisted ) {\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$this->mosp_log_security_event( $email, $phone, $ip, $browser_id );\n-\n-\t\t\t$ip_switching_detected = $this->detect_ip_switching_attack( $email, $phone, $browser_id, $ip );\n-\t\t\tif ( $ip_switching_detected ) {\n-\t\t\t\t$this->mosp_log_security_event( $email, $phone, $ip, $browser_id, 'IP_SWITCHING_DETECTED' );\n-\t\t\t\treturn true;\n-\t\t\t}\n-\n-\t\t\t$daily_limit_exceeded = $this->mosp_is_daily_limit_exceeded( $email, $phone, $settings, $context );\n-\t\t\tif ( $daily_limit_exceeded ) {\n-\t\t\t\treturn true;\n-\t\t\t}\n-\n-\t\t\t$hourly_limit_exceeded = $this->mosp_is_hourly_limit_exceeded( $email, $phone, $settings, $context );\n-\t\t\tif ( $hourly_limit_exceeded ) {\n-\t\t\t\treturn true;\n-\t\t\t}\n-\n-\t\t\t$identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id );\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$identifier_blocked = $this->is_identifier_blocked( $identifier );\n-\t\t\t\tif ( $identifier_blocked ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$cross_identifier_blocked = $this->mosp_is_cross_identifier_blocked( $email, $phone, $ip, $browser_id );\n-\t\t\tif ( $cross_identifier_blocked ) {\n-\t\t\t\treturn true;\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Record an OTP attempt for rate limiting (new method for integration)\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $browser_id Browser fingerprint ID.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic function mosp_record_attempt_for_identifiers( $email, $phone, $browser_id = '' ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$settings = $this->storage->mosp_get_settings();\n-\n-\t\t\t$ip = $this->mosp_get_client_ip();\n-\n-\t\t\t$identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id );\n-\n-\t\t\t$current_time = time();\n-\t\t\t$context      = array(\n-\t\t\t\t'ip'         => $ip,\n-\t\t\t\t'browser_id' => $browser_id,\n-\t\t\t\t'email'      => $email,\n-\t\t\t\t'phone'      => $phone,\n-\t\t\t);\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$this->mosp_record_identifier_attempt( $identifier, $current_time, $context );\n-\t\t\t}\n-\n-\t\t\t$this->mosp_record_cross_identifier_attempt( $email, $phone, $ip, $browser_id, $current_time, $context );\n-\n-\t\t\t$this->mosp_record_daily_hourly_attempts( $email, $phone );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Store a block for all identifiers when a block is detected via \"would be blocked\" check.\n-\t\t * This ensures the block persists in the database so the timer doesn't reset on each click.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $browser_id Browser fingerprint ID.\n-\t\t * @param string $block_reason Reason for the block (e.g., 'max_attempts_exceeded', 'cooldown').\n-\t\t * @param int    $remaining_time Remaining time in seconds until block expires.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic function mosp_store_block_for_identifiers( $email, $phone, $browser_id, $block_reason, $remaining_time ) {\n-\t\t\t$ip            = $this->mosp_get_client_ip();\n-\t\t\t$identifiers   = $this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id );\n-\t\t\t$now           = time();\n-\t\t\t$blocked_until = $now + $remaining_time;\n-\n-\t\t\t\u002F\u002F Extract identifier type and value for storage.\n-\t\t\t$identifier_type_map = array();\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\tif ( strpos( $identifier, 'email:' ) === 0 ) {\n-\t\t\t\t\t$identifier_type_map[ $identifier ] = array(\n-\t\t\t\t\t\t'type'  => 'email',\n-\t\t\t\t\t\t'value' => substr( $identifier, 6 ),\n-\t\t\t\t\t);\n-\t\t\t\t} elseif ( strpos( $identifier, 'phone:' ) === 0 ) {\n-\t\t\t\t\t$identifier_type_map[ $identifier ] = array(\n-\t\t\t\t\t\t'type'  => 'phone',\n-\t\t\t\t\t\t'value' => substr( $identifier, 6 ),\n-\t\t\t\t\t);\n-\t\t\t\t} elseif ( strpos( $identifier, 'ip:' ) === 0 ) {\n-\t\t\t\t\t$identifier_type_map[ $identifier ] = array(\n-\t\t\t\t\t\t'type'  => 'ip',\n-\t\t\t\t\t\t'value' => substr( $identifier, 3 ),\n-\t\t\t\t\t);\n-\t\t\t\t} elseif ( strpos( $identifier, 'browser:' ) === 0 ) {\n-\t\t\t\t\t$identifier_type_map[ $identifier ] = array(\n-\t\t\t\t\t\t'type'  => 'browser',\n-\t\t\t\t\t\t'value' => substr( $identifier, 8 ),\n-\t\t\t\t\t);\n-\t\t\t\t} else {\n-\t\t\t\t\t$identifier_type_map[ $identifier ] = array(\n-\t\t\t\t\t\t'type'  => 'unknown',\n-\t\t\t\t\t\t'value' => $identifier,\n-\t\t\t\t\t);\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\n-\t\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\n-\n-\t\t\t\t$id_info = isset( $identifier_type_map[ $identifier ] ) ? $identifier_type_map[ $identifier ] : array(\n-\t\t\t\t\t'type'  => 'unknown',\n-\t\t\t\t\t'value' => '',\n-\t\t\t\t);\n-\n-\t\t\t\tif ( false === $data ) {\n-\t\t\t\t\t$data = array(\n-\t\t\t\t\t\t'type'          => $id_info['type'],\n-\t\t\t\t\t\t'identifier'    => $id_info['value'], \u002F\u002F Store original identifier value.\n-\t\t\t\t\t\t'attempts'      => array(),\n-\t\t\t\t\t\t'blocked_until' => 0,\n-\t\t\t\t\t\t'total_blocks'  => 0,\n-\t\t\t\t\t\t'created'       => $now,\n-\t\t\t\t\t\t'last_attempt'  => $now,\n-\t\t\t\t\t);\n-\t\t\t\t} else {\n-\t\t\t\t\t\u002F\u002F Update type if not set or is 'identifier' or 'unknown'.\n-\t\t\t\t\tif ( ! isset( $data['type'] ) || 'identifier' === $data['type'] || 'unknown' === $data['type'] ) {\n-\t\t\t\t\t\t$data['type'] = $id_info['type'];\n-\t\t\t\t\t}\n-\t\t\t\t\t\u002F\u002F Store original identifier value if not set.\n-\t\t\t\t\tif ( ! isset( $data['identifier'] ) || empty( $data['identifier'] ) ) {\n-\t\t\t\t\t\t$data['identifier'] = $id_info['value'];\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\n-\t\t\t\tif ( ! isset( $data['blocked_until'] ) || $data['blocked_until'] \u003C $blocked_until ) {\n-\t\t\t\t\t$old_blocked_until     = isset( $data['blocked_until'] ) ? $data['blocked_until'] : 0;\n-\t\t\t\t\t$data['blocked_until'] = $blocked_until;\n-\t\t\t\t\t$data['block_reason']  = $block_reason;\n-\n-\t\t\t\t\t\u002F\u002F Store original identifier value for display (admin-only access, no masking needed).\n-\t\t\t\t\tif ( ! empty( $id_info['value'] ) ) {\n-\t\t\t\t\t\t$data['identifier'] = $id_info['value'];\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$this->storage->mosp_update_spam_data( $key, $data );\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get all identifiers for the current request.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $ip IP address.\n-\t\t * @param string $browser_id Browser fingerprint ID.\n-\t\t * @return array Array of identifiers\n-\t\t *\u002F\n-\t\tpublic function mosp_get_all_identifiers( $email, $phone, $ip, $browser_id ) {\n-\t\t\t$identifiers = array();\n-\n-\t\t\t$norm_email = $this->mosp_normalize_email_for_spam( $email );\n-\t\t\tif ( '' !== $norm_email ) {\n-\t\t\t\t$identifiers[] = 'email:' . $norm_email;\n-\t\t\t}\n-\n-\t\t\t$norm_phone = $this->mosp_normalize_phone_for_spam( $phone );\n-\t\t\tif ( '' !== $norm_phone ) {\n-\t\t\t\t$identifiers[] = 'phone:' . $norm_phone;\n-\t\t\t}\n-\n-\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t$identifiers[] = 'ip:' . $ip;\n-\t\t\t}\n-\n-\t\t\tif ( ! empty( $browser_id ) ) {\n-\t\t\t\t$identifiers[] = 'browser:' . $browser_id;\n-\t\t\t}\n-\n-\t\t\treturn $identifiers;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Normalize email for spam\u002Frate-limit keys (stable casing).\n-\t\t *\n-\t\t * @param string $email Email.\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tprivate function mosp_normalize_email_for_spam( $email ) {\n-\t\t\treturn strtolower( trim( (string) $email ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Normalize phone the same way as puzzle verification (MoUtility) so reset\u002Fclear hits the same DB rows as OTP send.\n-\t\t *\n-\t\t * @param string $phone Phone.\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tprivate function mosp_normalize_phone_for_spam( $phone ) {\n-\t\t\t$phone = trim( (string) $phone );\n-\t\t\tif ( '' === $phone ) {\n-\t\t\t\treturn '';\n-\t\t\t}\n-\t\t\tif ( class_exists( '\\OTP\\Helper\\MoUtility' ) ) {\n-\t\t\t\t$processed = MoUtility::process_phone_number( $phone );\n-\t\t\t\t$digits    = preg_replace( '\u002F\\D\u002F', '', (string) $processed );\n-\t\t\t\tif ( strlen( $digits ) >= 6 ) {\n-\t\t\t\t\treturn $processed;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn preg_replace( '\u002F[^0-9+]\u002F', '', $phone );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Cross-identifier strings (IP + credential) using normalized email\u002Fphone.\n-\t\t *\n-\t\t * @param string $email      Email.\n-\t\t * @param string $phone      Phone.\n-\t\t * @param string $ip         IP.\n-\t\t * @param string $browser_id Browser id.\n-\t\t * @return string[]\n-\t\t *\u002F\n-\t\tprivate function mosp_build_cross_identifier_strings( $email, $phone, $ip, $browser_id ) {\n-\t\t\tif ( empty( $ip ) ) {\n-\t\t\t\treturn array();\n-\t\t\t}\n-\t\t\t$cross = array();\n-\t\t\t$ne    = $this->mosp_normalize_email_for_spam( $email );\n-\t\t\tif ( '' !== $ne ) {\n-\t\t\t\t$cross[] = 'cross_ip_email:' . $ip . '|' . $ne;\n-\t\t\t}\n-\t\t\t$np = $this->mosp_normalize_phone_for_spam( $phone );\n-\t\t\tif ( '' !== $np ) {\n-\t\t\t\t$cross[] = 'cross_ip_phone:' . $ip . '|' . $np;\n-\t\t\t}\n-\t\t\tif ( ! empty( $browser_id ) ) {\n-\t\t\t\t$cross[] = 'cross_ip_browser:' . $ip . '|' . $browser_id;\n-\t\t\t}\n-\t\t\treturn $cross;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Every spam row to clear after puzzle success (canonical + legacy raw-phone keys).\n-\t\t *\n-\t\t * @param string $email      Email.\n-\t\t * @param string $phone      Phone.\n-\t\t * @param string $ip         IP.\n-\t\t * @param string $browser_id Browser id.\n-\t\t * @return string[]\n-\t\t *\u002F\n-\t\tprivate function mosp_get_identifiers_to_reset_on_puzzle_success( $email, $phone, $ip, $browser_id ) {\n-\t\t\t$out = array_merge(\n-\t\t\t\t$this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id ),\n-\t\t\t\t$this->mosp_build_cross_identifier_strings( $email, $phone, $ip, $browser_id )\n-\t\t\t);\n-\n-\t\t\t$norm_phone = $this->mosp_normalize_phone_for_spam( $phone );\n-\t\t\t$raw_phone  = trim( (string) $phone );\n-\t\t\tif ( '' !== $raw_phone ) {\n-\t\t\t\t$legacy_vals = array_unique(\n-\t\t\t\t\tarray_filter(\n-\t\t\t\t\t\tarray(\n-\t\t\t\t\t\t\t$raw_phone,\n-\t\t\t\t\t\t\tpreg_replace( '\u002F[^0-9+]\u002F', '', $raw_phone ),\n-\t\t\t\t\t\t)\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t\tforeach ( $legacy_vals as $lp ) {\n-\t\t\t\t\tif ( '' === $lp || $lp === $norm_phone ) {\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\t\t\t\t\t$out[] = 'phone:' . $lp;\n-\t\t\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t\t\t$out[] = 'cross_ip_phone:' . $ip . '|' . $lp;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn array_values( array_unique( array_filter( $out ) ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check cross-identifier blocking (IP + OTP type combination).\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $ip IP address.\n-\t\t * @param string $browser_id Browser fingerprint ID.\n-\t\t * @return bool True if should be blocked\n-\t\t *\u002F\n-\t\tprivate function mosp_is_cross_identifier_blocked( $email, $phone, $ip, $browser_id ) {\n-\t\t\tforeach ( $this->mosp_build_cross_identifier_strings( $email, $phone, $ip, $browser_id ) as $cross_identifier ) {\n-\t\t\t\tif ( $this->is_identifier_blocked( $cross_identifier ) ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Record cross-identifier attempt.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $ip IP address.\n-\t\t * @param string $browser_id Browser fingerprint ID.\n-\t\t * @param int    $current_time Current timestamp.\n-\t\t * @param array  $context Context array.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tprivate function mosp_record_cross_identifier_attempt( $email, $phone, $ip, $browser_id, $current_time, $context = array() ) {\n-\t\t\tforeach ( $this->mosp_build_cross_identifier_strings( $email, $phone, $ip, $browser_id ) as $cross_identifier ) {\n-\t\t\t\t$this->mosp_record_identifier_attempt( $cross_identifier, $current_time, $context );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get block data for identifiers.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $browser_id Browser fingerprint ID.\n-\t\t * @param string $context Context of the check ('otp_send' or 'timer_status').\n-\t\t * @return array Block data with remaining time\n-\t\t *\u002F\n-\t\tpublic function mosp_get_block_data( $email, $phone, $browser_id = '', $context = 'otp_send' ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'remaining_time' => 0,\n-\t\t\t\t\t'reason'         => '',\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$current_time = time();\n-\t\t\t$settings     = $this->storage->mosp_get_settings();\n-\n-\t\t\t$ip = $this->mosp_get_client_ip();\n-\n-\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t$is_whitelisted = $this->storage->mosp_is_whitelisted( $ip, 'ip' );\n-\t\t\t\tif ( $is_whitelisted ) {\n-\t\t\t\t\treturn array(\n-\t\t\t\t\t\t'remaining_time' => 0,\n-\t\t\t\t\t\t'reason'         => '',\n-\t\t\t\t\t);\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$max_remaining_time = 0;\n-\t\t\t$block_reason       = '';\n-\n-\t\t\tif ( $this->mosp_is_daily_limit_exceeded( $email, $phone, $settings, $context ) ) {\n-\t\t\t\t$max_remaining_time = $this->mosp_get_daily_limit_reset_time( $email, $phone );\n-\t\t\t\t$block_reason       = 'daily_limit_exceeded';\n-\t\t\t} elseif ( $this->mosp_is_hourly_limit_exceeded( $email, $phone, $settings, $context ) ) {\n-\t\t\t\t$max_remaining_time = $this->mosp_get_hourly_limit_reset_time( $email, $phone );\n-\t\t\t\t$block_reason       = 'hourly_limit_exceeded';\n-\t\t\t}\n-\n-\t\t\tif ( $max_remaining_time \u003C= 0 ) {\n-\t\t\t\t$identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id );\n-\n-\t\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t\t$block_data = $this->storage->mosp_is_blocked( $identifier );\n-\n-\t\t\t\t\tif ( $block_data['blocked'] ) {\n-\t\t\t\t\t\t$blocked_until = $block_data['blocked_until'];\n-\t\t\t\t\t\t$remaining     = $blocked_until - $current_time;\n-\t\t\t\t\t\tif ( $remaining > $max_remaining_time ) {\n-\t\t\t\t\t\t\t$max_remaining_time = $remaining;\n-\t\t\t\t\t\t\t$block_reason       = $block_data['reason'];\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$final_remaining = max( 0, $max_remaining_time );\n-\t\t\treturn array(\n-\t\t\t\t'remaining_time' => $final_remaining,\n-\t\t\t\t'reason'         => $block_reason,\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get block message with timer.\n-\t\t *\n-\t\t * @param int    $remaining_time Remaining block time in seconds.\n-\t\t * @param string $message_type Optional message type constant (defaults to USER_IS_BLOCKED_AJAX).\n-\t\t * @return string Block message with timer placeholder\n-\t\t *\u002F\n-\t\tpublic function mosp_get_block_message_with_timer( $remaining_time, $message_type = null ) {\n-\t\t\t$minutes = floor( $remaining_time \u002F 60 );\n-\t\t\t$seconds = $remaining_time % 60;\n-\n-\t\t\t$formatted_minutes = sprintf( '%02d', $minutes );\n-\t\t\t$formatted_seconds = sprintf( '%02d', $seconds );\n-\n-\t\t\t$message_constant = $message_type ? $message_type : MoMessages::USER_IS_BLOCKED_AJAX;\n-\t\t\t$message_template = MoMessages::showMessage( $message_constant );\n-\n-\t\t\t$message = $message_template;\n-\n-\t\t\tif ( strpos( $message, '{minutes}' ) !== false || strpos( $message, '{seconds}' ) !== false ) {\n-\t\t\t\t$message = str_replace(\n-\t\t\t\t\tarray( '{minutes}', '{seconds}' ),\n-\t\t\t\t\tarray( $formatted_minutes, $formatted_seconds ),\n-\t\t\t\t\t$message\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\tif ( strpos( $message, '{{remaining_time}}' ) !== false ) {\n-\t\t\t\t$time_display = sprintf( '%02d:%02d', $minutes, $seconds );\n-\t\t\t\t$message      = str_replace( '{{remaining_time}}', $time_display, $message );\n-\t\t\t}\n-\n-\t\t\tif ( strpos( $message, '%' ) !== false ) {\n-\t\t\t\t$time_display = sprintf( '%02d:%02d', $minutes, $seconds );\n-\t\t\t\t$message      = sprintf( $message, $time_display );\n-\t\t\t}\n-\n-\t\t\treturn $message;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if user would be blocked after recording one more attempt.\n-\t\t * This prevents race condition where OTP is sent successfully but user gets blocked immediately after\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $browser_id Browser fingerprint ID.\n-\t\t * @return bool True if would be blocked after attempt, false otherwise\n-\t\t *\u002F\n-\t\tpublic function mosp_would_be_blocked_after_attempt( $email, $phone, $browser_id = '' ) {\n-\t\t\t$result = $this->mosp_would_be_blocked_after_attempt_with_details( $email, $phone, $browser_id );\n-\t\t\treturn $result['would_be_blocked'];\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if user would be blocked after recording one more attempt, with detailed information.\n-\t\t * This prevents race condition where OTP is sent successfully but user gets blocked immediately after.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $browser_id Browser fingerprint ID.\n-\t\t * @return array Array with 'would_be_blocked' (bool), 'reason' (string), and 'remaining_time' (int)\n-\t\t *\u002F\n-\t\tpublic function mosp_would_be_blocked_after_attempt_with_details( $email, $phone, $browser_id = '' ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'would_be_blocked' => false,\n-\t\t\t\t\t'reason'           => '',\n-\t\t\t\t\t'remaining_time'   => 0,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$settings    = $this->storage->mosp_get_settings();\n-\t\t\t$ip          = $this->mosp_get_client_ip();\n-\t\t\t$identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id );\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$block_data = $this->storage->mosp_is_blocked( $identifier );\n-\t\t\t\tif ( $block_data['blocked'] && 'cooldown' === $block_data['reason'] ) {\n-\t\t\t\t\t$now            = time();\n-\t\t\t\t\t$remaining_time = $block_data['blocked_until'] - $now;\n-\t\t\t\t\tif ( $remaining_time > 0 ) {\n-\t\t\t\t\t\treturn array(\n-\t\t\t\t\t\t\t'would_be_blocked' => true,\n-\t\t\t\t\t\t\t'reason'           => 'cooldown',\n-\t\t\t\t\t\t\t'remaining_time'   => $remaining_time,\n-\t\t\t\t\t\t);\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$cooldown_result = $this->mosp_would_be_on_cooldown_after_attempt_with_details( $identifier, $settings );\n-\t\t\t\tif ( $cooldown_result['would_be_on_cooldown'] ) {\n-\t\t\t\t\treturn array(\n-\t\t\t\t\t\t'would_be_blocked' => true,\n-\t\t\t\t\t\t'reason'           => 'cooldown',\n-\t\t\t\t\t\t'remaining_time'   => $cooldown_result['remaining_time'],\n-\t\t\t\t\t);\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( $this->mosp_would_exceed_hourly_limit_after_attempt( $email, $phone, $settings ) ) {\n-\t\t\t\t$remaining_time = $this->mosp_get_hourly_limit_reset_time( $email, $phone );\n-\t\t\t\treturn array(\n-\t\t\t\t\t'would_be_blocked' => true,\n-\t\t\t\t\t'reason'           => 'hourly_limit_exceeded',\n-\t\t\t\t\t'remaining_time'   => $remaining_time,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\tif ( $this->mosp_would_exceed_daily_limit_after_attempt( $email, $phone, $settings ) ) {\n-\t\t\t\t$remaining_time = $this->mosp_get_daily_limit_reset_time( $email, $phone );\n-\t\t\t\treturn array(\n-\t\t\t\t\t'would_be_blocked' => true,\n-\t\t\t\t\t'reason'           => 'daily_limit_exceeded',\n-\t\t\t\t\t'remaining_time'   => $remaining_time,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$max_attempts_result = $this->mosp_would_exceed_max_attempts_after_attempt_with_details( $identifier, $settings );\n-\t\t\t\tif ( $max_attempts_result['would_exceed'] ) {\n-\t\t\t\t\treturn array(\n-\t\t\t\t\t\t'would_be_blocked' => true,\n-\t\t\t\t\t\t'reason'           => 'max_attempts_exceeded',\n-\t\t\t\t\t\t'remaining_time'   => $max_attempts_result['remaining_time'],\n-\t\t\t\t\t);\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn array(\n-\t\t\t\t'would_be_blocked' => false,\n-\t\t\t\t'reason'           => '',\n-\t\t\t\t'remaining_time'   => 0,\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if recording one more attempt would exceed hourly limit.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param array  $settings Plugin settings.\n-\t\t * @return bool True if would exceed hourly limit after attempt, false otherwise\n-\t\t *\u002F\n-\t\tprivate function mosp_would_exceed_hourly_limit_after_attempt( $email, $phone, $settings ) {\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\tif ( $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\t$current_attempts = MoRateLimitHelper::mosp_get_hourly_attempts( $identifier );\n-\t\t\t\tif ( ( $current_attempts + 1 ) > $settings['hourly_limit'] ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if recording one more attempt would exceed daily limit.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param array  $settings Plugin settings.\n-\t\t * @return bool True if would exceed daily limit after attempt, false otherwise\n-\t\t *\u002F\n-\t\tprivate function mosp_would_exceed_daily_limit_after_attempt( $email, $phone, $settings ) {\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\tif ( $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\t$current_attempts = MoRateLimitHelper::mosp_get_daily_attempts( $identifier );\n-\t\t\t\tif ( ( $current_attempts + 1 ) > $settings['daily_limit'] ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if recording one more attempt would exceed max attempts for identifier.\n-\t\t *\n-\t\t * @param string $identifier The identifier to check.\n-\t\t * @param array  $settings Plugin settings.\n-\t\t * @return bool True if would exceed max attempts after attempt, false otherwise\n-\t\t *\u002F\n-\t\tprivate function mosp_would_exceed_max_attempts_after_attempt( $identifier, $settings ) {\n-\t\t\t$result = $this->mosp_would_exceed_max_attempts_after_attempt_with_details( $identifier, $settings );\n-\t\t\treturn $result['would_exceed'];\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if recording one more attempt would exceed max attempts, with detailed information.\n-\t\t *\n-\t\t * @param string $identifier The identifier to check.\n-\t\t * @param array  $settings Plugin settings.\n-\t\t * @return array Array with 'would_exceed' (bool) and 'remaining_time' (int)\n-\t\t *\u002F\n-\t\tprivate function mosp_would_exceed_max_attempts_after_attempt_with_details( $identifier, $settings ) {\n-\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\n-\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\n-\t\t\t$now  = time();\n-\n-\t\t\tif ( false === $data || ! isset( $data['attempts'] ) || ! is_array( $data['attempts'] ) || empty( $data['attempts'] ) ) {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'would_exceed'   => false,\n-\t\t\t\t\t'remaining_time' => 0,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\tif ( isset( $data['blocked_until'] ) && $data['blocked_until'] > $now ) {\n-\t\t\t\t$block_reason = isset( $data['block_reason'] ) ? $data['block_reason'] : '';\n-\t\t\t\tif ( 'max_attempts_exceeded' === $block_reason ) {\n-\t\t\t\t\t$remaining_time = $data['blocked_until'] - $now;\n-\t\t\t\t\treturn array(\n-\t\t\t\t\t\t'would_exceed'   => true,\n-\t\t\t\t\t\t'remaining_time' => $remaining_time,\n-\t\t\t\t\t);\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$time_window     = MoSecurityHelper::COUNTING_WINDOW_SECONDS; \u002F\u002F 15 minutes.\n-\t\t\t$cutoff_time     = $now - $time_window;\n-\t\t\t$recent_attempts = array();\n-\n-\t\t\tforeach ( $data['attempts'] as $timestamp ) {\n-\t\t\t\tif ( $timestamp > $cutoff_time ) {\n-\t\t\t\t\t$recent_attempts[] = $timestamp;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$recent_attempts_count = count( $recent_attempts );\n-\n-\t\t\tif ( ( $recent_attempts_count + 1 ) > $settings['max_attempts'] ) {\n-\t\t\t\tif ( isset( $data['blocked_until'] ) && $data['blocked_until'] > $now ) {\n-\t\t\t\t\t$block_reason = isset( $data['block_reason'] ) ? $data['block_reason'] : '';\n-\t\t\t\t\tif ( 'max_attempts_exceeded' === $block_reason ) {\n-\t\t\t\t\t\t$remaining_time = $data['blocked_until'] - $now;\n-\t\t\t\t\t\treturn array(\n-\t\t\t\t\t\t\t'would_exceed'   => true,\n-\t\t\t\t\t\t\t'remaining_time' => $remaining_time,\n-\t\t\t\t\t\t);\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\n-\t\t\t\t$block_time_seconds = $settings['block_time'];\n-\t\t\t\t$remaining_time     = $block_time_seconds;\n-\n-\t\t\t\treturn array(\n-\t\t\t\t\t'would_exceed'   => true,\n-\t\t\t\t\t'remaining_time' => $remaining_time,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\treturn array(\n-\t\t\t\t'would_exceed'   => false,\n-\t\t\t\t'remaining_time' => 0,\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if recording one more attempt would put the identifier on cooldown.\n-\t\t * This checks if there's a recent attempt that would trigger cooldown after adding this attempt.\n-\t\t *\n-\t\t * @param string $identifier The identifier to check.\n-\t\t * @param array  $settings Plugin settings.\n-\t\t * @return bool True if would be on cooldown after attempt, false otherwise\n-\t\t *\u002F\n-\t\tprivate function mosp_would_be_on_cooldown_after_attempt( $identifier, $settings ) {\n-\t\t\t$result = $this->mosp_would_be_on_cooldown_after_attempt_with_details( $identifier, $settings );\n-\t\t\treturn $result['would_be_on_cooldown'];\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if recording one more attempt would put the identifier on cooldown, with detailed information.\n-\t\t * This checks if there's a recent attempt that would trigger cooldown after adding this attempt.\n-\t\t *\n-\t\t * @param string $identifier The identifier to check.\n-\t\t * @param array  $settings Plugin settings.\n-\t\t * @return array Array with 'would_be_on_cooldown' (bool) and 'remaining_time' (int)\n-\t\t *\u002F\n-\t\tprivate function mosp_would_be_on_cooldown_after_attempt_with_details( $identifier, $settings ) {\n-\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\n-\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\n-\t\t\t$now  = time();\n-\n-\t\t\tif ( false === $data || ! isset( $data['attempts'] ) || ! is_array( $data['attempts'] ) || empty( $data['attempts'] ) ) {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'would_be_on_cooldown' => false,\n-\t\t\t\t\t'remaining_time'       => 0,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$cooldown_time = $settings['cooldown_time'];\n-\t\t\t$attempts      = $data['attempts'];\n-\t\t\t$attempt_count = count( $attempts );\n-\n-\t\t\tif ( 1 === $attempt_count ) {\n-\t\t\t\t$most_recent_attempt    = max( $attempts );\n-\t\t\t\t$time_since_most_recent = $now - $most_recent_attempt;\n-\n-\t\t\t\tif ( $time_since_most_recent \u003C $cooldown_time ) {\n-\t\t\t\t\t$remaining_cooldown = $cooldown_time - $time_since_most_recent;\n-\t\t\t\t\treturn array(\n-\t\t\t\t\t\t'would_be_on_cooldown' => true,\n-\t\t\t\t\t\t'remaining_time'       => $remaining_cooldown,\n-\t\t\t\t\t);\n-\t\t\t\t}\n-\n-\t\t\t\treturn array(\n-\t\t\t\t\t'would_be_on_cooldown' => false,\n-\t\t\t\t\t'remaining_time'       => 0,\n-\t\t\t\t);\n-\t\t\t}\n-\t\t\t$most_recent_attempt = max( $attempts );\n-\n-\t\t\t$time_since_most_recent = $now - $most_recent_attempt;\n-\n-\t\t\tif ( $time_since_most_recent \u003C $cooldown_time ) {\n-\t\t\t\t$remaining_cooldown = $cooldown_time - $time_since_most_recent;\n-\t\t\t\tif ( $remaining_cooldown \u003C 0 ) {\n-\t\t\t\t\t$remaining_cooldown = 0;\n-\t\t\t\t}\n-\t\t\t\treturn array(\n-\t\t\t\t\t'would_be_on_cooldown' => true,\n-\t\t\t\t\t'remaining_time'       => $remaining_cooldown,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\treturn array(\n-\t\t\t\t'would_be_on_cooldown' => false,\n-\t\t\t\t'remaining_time'       => 0,\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if a specific identifier is blocked.\n-\t\t *\n-\t\t * @param string $identifier The identifier to check.\n-\t\t * @return bool True if blocked, false otherwise\n-\t\t *\u002F\n-\t\tprivate function is_identifier_blocked( $identifier ) {\n-\t\t\t\u002F\u002F Use the storage method that contains the complete blocking logic.\n-\t\t\t$block_data = $this->storage->mosp_is_blocked( $identifier );\n-\t\t\treturn $block_data['blocked'];\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Record an attempt for a specific identifier.\n-\t\t *\n-\t\t * @param string $identifier The identifier.\n-\t\t * @param int    $current_time Current timestamp.\n-\t\t * @param array  $context Context array.\n-\t\t *\u002F\n-\t\tprivate function mosp_record_identifier_attempt( $identifier, $current_time, $context = array() ) {\n-\t\t\t$this->storage->mosp_record_attempt_with_timestamp( $identifier, $current_time, $context );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check for spam before OTP is sent.\n-\t\t *\n-\t\t * @param bool   $allow Whether to allow OTP sending.\n-\t\t * @param string $user_login Username.\n-\t\t * @param string $user_email Email address.\n-\t\t * @param string $phone_number Phone number.\n-\t\t * @return bool|WP_Error\n-\t\t *\u002F\n-\t\tpublic function mosp_check_spam_before_otp_send( $allow, $user_login, $user_email, $phone_number ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn $allow;\n-\t\t\t}\n-\n-\t\t\t$settings = $this->storage->mosp_get_settings();\n-\n-\t\t\tif ( ! empty( $user_email ) ) {\n-\t\t\t\tMoPHPSessions::add_session_var( 'user_email', $user_email );\n-\t\t\t}\n-\t\t\tif ( ! empty( $phone_number ) ) {\n-\t\t\t\tMoPHPSessions::add_session_var( 'phone_number_mo', $phone_number );\n-\t\t\t}\n-\n-\t\t\t$identifiers = $this->mosp_get_request_identifiers( $user_email, $phone_number );\n-\n-\t\t\tforeach ( $identifiers as $type => $identifier ) {\n-\t\t\t\tif ( empty( $identifier ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\tif ( $this->storage->mosp_is_whitelisted( $identifier, $type ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\t$block_status = $this->storage->mosp_is_blocked( $identifier );\n-\n-\t\t\t\tif ( $block_status['blocked'] ) {\n-\t\t\t\t\treturn $this->create_block_error( $block_status, $type, $identifier );\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn $allow;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Record OTP attempt after successful send\n-\t\t *\n-\t\t * @param string $user_login Username.\n-\t\t * @param string $user_email Email address.\n-\t\t * @param string $phone_number Phone number.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic function mosp_record_otp_attempt( $user_login, $user_email, $phone_number ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$settings = $this->storage->mosp_get_settings();\n-\t\t\t$ip       = $this->mosp_get_client_ip();\n-\t\t\t$browser  = $this->get_browser_id();\n-\n-\t\t\t$identifiers = $this->mosp_get_request_identifiers( $user_email, $phone_number );\n-\t\t\t$context     = array(\n-\t\t\t\t'ip'         => $ip,\n-\t\t\t\t'browser_id' => $browser,\n-\t\t\t\t'email'      => isset( $identifiers['email'] ) ? $identifiers['email'] : '',\n-\t\t\t\t'phone'      => isset( $identifiers['phone'] ) ? $identifiers['phone'] : '',\n-\t\t\t);\n-\n-\t\t\tforeach ( $identifiers as $type => $identifier ) {\n-\t\t\t\tif ( empty( $identifier ) || $this->storage->mosp_is_whitelisted( $identifier, $type ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\t$this->storage->mosp_record_attempt( $identifier, $type, $context );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get all identifiers for the current request.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @return array Array of identifiers.\n-\t\t *\u002F\n-\t\tprivate function mosp_get_request_identifiers( $email, $phone ) {\n-\t\t\t$settings    = $this->storage->mosp_get_settings();\n-\t\t\t$identifiers = array();\n-\n-\t\t\tif ( $settings['track_email'] && ! empty( $email ) ) {\n-\t\t\t\t$identifiers['email'] = strtolower( trim( $email ) );\n-\t\t\t}\n-\n-\t\t\tif ( $settings['track_phone'] && ! empty( $phone ) ) {\n-\t\t\t\t$identifiers['phone'] = preg_replace( '\u002F[^0-9+]\u002F', '', $phone );\n-\t\t\t}\n-\n-\t\t\tif ( $settings['track_ip'] ) {\n-\t\t\t\t$ip = $this->mosp_get_client_ip();\n-\t\t\t\tif ( $ip ) {\n-\t\t\t\t\t$identifiers['ip'] = $ip;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( $settings['track_browser'] ) {\n-\t\t\t\t$browser_id = $this->get_browser_id();\n-\t\t\t\tif ( $browser_id ) {\n-\t\t\t\t\t$identifiers['browser'] = $browser_id;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn $identifiers;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get client IP address with anti-spoofing protection\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tpublic function mosp_get_client_ip() {\n-\t\t\t$ip_candidates = $this->get_ip_candidates();\n-\n-\t\t\tif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {\n-\t\t\t\t$remote_addr = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );\n-\t\t\t\tif ( filter_var( $remote_addr, FILTER_VALIDATE_IP ) &&\n-\t\t\t\t\t! filter_var( $remote_addr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {\n-\t\t\t\t\tif ( $this->storage->mosp_is_whitelisted( $remote_addr, 'ip' ) ) {\n-\t\t\t\t\t\treturn $remote_addr;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$validated_ip = $this->validate_ip_security( $ip_candidates );\n-\n-\t\t\treturn $validated_ip;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get all possible IP addresses from headers.\n-\t\t *\n-\t\t * @return array Array of IP candidates with their sources\n-\t\t *\u002F\n-\t\tprivate function get_ip_candidates() {\n-\t\t\t$candidates = array();\n-\n-\t\t\t$ip_sources = array(\n-\t\t\t\t'REMOTE_ADDR'              => array(\n-\t\t\t\t\t'priority'  => 1,\n-\t\t\t\t\t'spoofable' => false,\n-\t\t\t\t),\n-\t\t\t\t'HTTP_CLIENT_IP'           => array(\n-\t\t\t\t\t'priority'  => 2,\n-\t\t\t\t\t'spoofable' => true,\n-\t\t\t\t),\n-\t\t\t\t'HTTP_CF_CONNECTING_IP'    => array(\n-\t\t\t\t\t'priority'  => 3,\n-\t\t\t\t\t'spoofable' => false,\n-\t\t\t\t),\n-\t\t\t\t'HTTP_X_REAL_IP'           => array(\n-\t\t\t\t\t'priority'  => 4,\n-\t\t\t\t\t'spoofable' => true,\n-\t\t\t\t),\n-\t\t\t\t'HTTP_X_FORWARDED_FOR'     => array(\n-\t\t\t\t\t'priority'  => 5,\n-\t\t\t\t\t'spoofable' => true,\n-\t\t\t\t),\n-\t\t\t\t'HTTP_X_FORWARDED'         => array(\n-\t\t\t\t\t'priority'  => 6,\n-\t\t\t\t\t'spoofable' => true,\n-\t\t\t\t),\n-\t\t\t\t'HTTP_X_CLUSTER_CLIENT_IP' => array(\n-\t\t\t\t\t'priority'  => 7,\n-\t\t\t\t\t'spoofable' => true,\n-\t\t\t\t),\n-\t\t\t\t'HTTP_FORWARDED_FOR'       => array(\n-\t\t\t\t\t'priority'  => 8,\n-\t\t\t\t\t'spoofable' => true,\n-\t\t\t\t),\n-\t\t\t\t'HTTP_FORWARDED'           => array(\n-\t\t\t\t\t'priority'  => 9,\n-\t\t\t\t\t'spoofable' => true,\n-\t\t\t\t),\n-\t\t\t);\n-\n-\t\t\tforeach ( $ip_sources as $header => $config ) {\n-\t\t\t\tif ( ! empty( $_SERVER[ $header ] ) ) {\n-\t\t\t\t\t$raw_value = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );\n-\t\t\t\t\t$ips       = $this->parse_ip_header( $raw_value );\n-\n-\t\t\t\t\tforeach ( $ips as $ip ) {\n-\t\t\t\t\t\tif ( $this->is_valid_public_ip( $ip ) ) {\n-\t\t\t\t\t\t\t$candidates[] = array(\n-\t\t\t\t\t\t\t\t'ip'         => $ip,\n-\t\t\t\t\t\t\t\t'source'     => $header,\n-\t\t\t\t\t\t\t\t'priority'   => $config['priority'],\n-\t\t\t\t\t\t\t\t'spoofable'  => $config['spoofable'],\n-\t\t\t\t\t\t\t\t'raw_header' => $raw_value,\n-\t\t\t\t\t\t\t);\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn $candidates;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Parse IP header value (handles comma-separated lists).\n-\t\t *\n-\t\t * @param string $header_value Raw header value.\n-\t\t * @return array Array of IP addresses.\n-\t\t *\u002F\n-\t\tprivate function parse_ip_header( $header_value ) {\n-\t\t\t$ips = array();\n-\n-\t\t\tif ( strpos( $header_value, ',' ) !== false ) {\n-\t\t\t\t$parts = explode( ',', $header_value );\n-\t\t\t\tforeach ( $parts as $part ) {\n-\t\t\t\t\t$ip = trim( $part );\n-\t\t\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t\t\t$ips[] = $ip;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t} else {\n-\t\t\t\t$ips[] = trim( $header_value );\n-\t\t\t}\n-\n-\t\t\treturn $ips;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Validate IP with security checks.\n-\t\t *\n-\t\t * @param string $ip IP address to validate.\n-\t\t * @return bool True if valid public IP.\n-\t\t *\u002F\n-\t\tprivate function is_valid_public_ip( $ip ) {\n-\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( $this->is_suspicious_ip( $ip ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\treturn true;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if IP appears suspicious.\n-\t\t *\n-\t\t * @param string $ip IP address.\n-\t\t * @return bool True if suspicious.\n-\t\t *\u002F\n-\t\tprivate function is_suspicious_ip( $ip ) {\n-\t\t\t$suspicious_patterns = array(\n-\t\t\t\t'0.0.0.0',\n-\t\t\t\t'255.255.255.255',\n-\t\t\t\t'1.1.1.1',\n-\t\t\t\t'8.8.8.8',\n-\t\t\t\t'127.0.0.1',\n-\t\t\t\t'169.254.0.0',\n-\t\t\t\t'224.0.0.0',\n-\t\t\t\t'240.0.0.0',\n-\t\t\t);\n-\n-\t\t\tforeach ( $suspicious_patterns as $pattern ) {\n-\t\t\t\tif ( strpos( $ip, $pattern ) === 0 ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Validate IP security and select most trustworthy.\n-\t\t *\n-\t\t * @param array $candidates Array of IP candidates.\n-\t\t * @return string Most trustworthy IP address.\n-\t\t *\u002F\n-\t\tprivate function validate_ip_security( $candidates ) {\n-\t\t\tif ( empty( $candidates ) ) {\n-\t\t\t\treturn '';\n-\t\t\t}\n-\n-\t\t\tusort(\n-\t\t\t\t$candidates,\n-\t\t\t\tfunction ( $a, $b ) {\n-\t\t\t\t\treturn $a['priority'] - $b['priority'];\n-\t\t\t\t}\n-\t\t\t);\n-\n-\t\t\t$remote_addr     = $this->get_remote_addr_ip( $candidates );\n-\t\t\t$proxy_detection = $this->detect_proxy_environment();\n-\n-\t\t\tif ( ! $proxy_detection['behind_proxy'] ) {\n-\t\t\t\treturn $remote_addr ? $remote_addr : '';\n-\t\t\t}\n-\n-\t\t\tif ( $proxy_detection['trusted_proxy'] ) {\n-\t\t\t\tforeach ( $candidates as $candidate ) {\n-\t\t\t\t\tif ( 'HTTP_CF_CONNECTING_IP' === $candidate['source'] && ! $candidate['spoofable'] ) {\n-\t\t\t\t\t\treturn $candidate['ip'];\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t\tforeach ( $candidates as $candidate ) {\n-\t\t\t\t\tif ( ! $candidate['spoofable'] ) {\n-\t\t\t\t\t\treturn $candidate['ip'];\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn $remote_addr ? $remote_addr : $candidates[0]['ip'];\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get REMOTE_ADDR IP from candidates.\n-\t\t *\n-\t\t * @param array $candidates IP candidates.\n-\t\t * @return string|null REMOTE_ADDR IP or null.\n-\t\t *\u002F\n-\t\tprivate function get_remote_addr_ip( $candidates ) {\n-\t\t\tforeach ( $candidates as $candidate ) {\n-\t\t\t\tif ( 'REMOTE_ADDR' === $candidate['source'] ) {\n-\t\t\t\t\treturn $candidate['ip'];\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn null;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Detect proxy environment.\n-\t\t *\n-\t\t * @return array Proxy detection results.\n-\t\t *\u002F\n-\t\tprivate function detect_proxy_environment() {\n-\n-\t\t\t$result = array(\n-\t\t\t\t'behind_proxy'  => false,\n-\t\t\t\t'trusted_proxy' => false,\n-\t\t\t\t'proxy_type'    => 'none',\n-\t\t\t);\n-\n-\t\t\tif ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) || ! empty( $_SERVER['HTTP_CF_RAY'] ) ) {\n-\t\t\t\t$result['behind_proxy']  = true;\n-\t\t\t\t$result['trusted_proxy'] = true;\n-\t\t\t\t$result['proxy_type']    = 'cloudflare';\n-\t\t\t\treturn $result;\n-\t\t\t}\n-\n-\t\t\t$trusted_headers = array(\n-\t\t\t\t'HTTP_CLIENT_IP',\n-\t\t\t\t'HTTP_X_FORWARDED_FOR',\n-\t\t\t\t'HTTP_X_REAL_IP',\n-\t\t\t);\n-\n-\t\t\tforeach ( $trusted_headers as $header ) {\n-\t\t\t\tif ( ! empty( $_SERVER[ $header ] ) ) {\n-\t\t\t\t\t$result['behind_proxy'] = true;\n-\t\t\t\t\t$remote_addr            = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';\n-\t\t\t\t\tif ( $this->is_known_proxy_ip( $remote_addr ) ) {\n-\t\t\t\t\t\t$result['trusted_proxy'] = true;\n-\t\t\t\t\t}\n-\t\t\t\t\t$result['proxy_type'] = 'generic';\n-\t\t\t\t\tbreak;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn $result;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if IP belongs to known proxy services.\n-\t\t *\n-\t\t * @param string $ip IP address to check.\n-\t\t * @return bool True if known proxy IP\n-\t\t *\u002F\n-\t\tprivate function is_known_proxy_ip( $ip ) {\n-\t\t\tif ( empty( $ip ) || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Cloudflare IP ranges (simplified check).\n-\t\t\t$cloudflare_ranges = array(\n-\t\t\t\t'173.245.48.0\u002F20',\n-\t\t\t\t'103.21.244.0\u002F22',\n-\t\t\t\t'103.22.200.0\u002F22',\n-\t\t\t\t'103.31.4.0\u002F22',\n-\t\t\t\t'141.101.64.0\u002F18',\n-\t\t\t\t'108.162.192.0\u002F18',\n-\t\t\t\t'190.93.240.0\u002F20',\n-\t\t\t\t'188.114.96.0\u002F20',\n-\t\t\t\t'197.234.240.0\u002F22',\n-\t\t\t\t'198.41.128.0\u002F17',\n-\t\t\t\t'162.158.0.0\u002F15',\n-\t\t\t\t'104.16.0.0\u002F13',\n-\t\t\t\t'104.24.0.0\u002F14',\n-\t\t\t\t'172.64.0.0\u002F13',\n-\t\t\t\t'131.0.72.0\u002F22',\n-\t\t\t);\n-\n-\t\t\tforeach ( $cloudflare_ranges as $range ) {\n-\t\t\t\tif ( $this->ip_in_range( $ip, $range ) ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if IP is in CIDR range.\n-\t\t *\n-\t\t * @param string $ip IP to check.\n-\t\t * @param string $range CIDR range.\n-\t\t * @return bool True if IP is in range\n-\t\t *\u002F\n-\t\tprivate function ip_in_range( $ip, $range ) {\n-\t\t\tif ( strpos( $range, '\u002F' ) === false ) {\n-\t\t\t\treturn $ip === $range;\n-\t\t\t}\n-\n-\t\t\tlist($subnet, $bits) = explode( '\u002F', $range );\n-\n-\t\t\tif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) && filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {\n-\t\t\t\t$ip_long      = ip2long( $ip );\n-\t\t\t\t$subnet_long  = ip2long( $subnet );\n-\t\t\t\t$mask         = -1 \u003C\u003C ( 32 - (int) $bits );\n-\t\t\t\t$subnet_long &= $mask;\n-\t\t\t\treturn ( $ip_long & $mask ) === $subnet_long;\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Detect IP switching attacks.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $browser_id Browser ID.\n-\t\t * @param string $current_ip Current IP address.\n-\t\t * @return bool True if attack detected\n-\t\t *\u002F\n-\t\tprivate function detect_ip_switching_attack( $email, $phone, $browser_id, $current_ip ) {\n-\t\t\tif ( empty( $current_ip ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$tracking_key = '';\n-\t\t\tif ( ! empty( $email ) ) {\n-\t\t\t\t$tracking_key = 'email:' . $email;\n-\t\t\t} elseif ( ! empty( $phone ) ) {\n-\t\t\t\t$tracking_key = 'phone:' . $phone;\n-\t\t\t} elseif ( ! empty( $browser_id ) ) {\n-\t\t\t\t$tracking_key = 'browser:' . $browser_id;\n-\t\t\t}\n-\n-\t\t\tif ( empty( $tracking_key ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$ip_history_key = 'mo_osp_ip_history_' . md5( $tracking_key );\n-\t\t\t$ip_history     = MoPHPSessions::get_session_var( $ip_history_key );\n-\n-\t\t\tif ( false === $ip_history ) {\n-\t\t\t\t$ip_history = array();\n-\t\t\t}\n-\n-\t\t\t$current_time = time();\n-\t\t\t$ip_history[] = array(\n-\t\t\t\t'ip'        => $current_ip,\n-\t\t\t\t'timestamp' => $current_time,\n-\t\t\t);\n-\n-\t\t\t$cutoff_time = $current_time - 600;\n-\t\t\t$ip_history  = array_filter(\n-\t\t\t\t$ip_history,\n-\t\t\t\tfunction ( $entry ) use ( $cutoff_time ) {\n-\t\t\t\t\treturn $entry['timestamp'] > $cutoff_time;\n-\t\t\t\t}\n-\t\t\t);\n-\n-\t\t\t$unique_ips = array();\n-\t\t\tforeach ( $ip_history as $entry ) {\n-\t\t\t\t$unique_ips[ $entry['ip'] ] = true;\n-\t\t\t}\n-\n-\t\t\tMoPHPSessions::add_session_var( $ip_history_key, $ip_history ); \u002F\u002F 10 minutes\n-\n-\t\t\treturn count( $unique_ips ) > 3;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Log security events.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param string $ip IP address.\n-\t\t * @param string $browser_id Browser ID.\n-\t\t * @param string $event_type Event type.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tprivate function mosp_log_security_event( $email, $phone, $ip, $browser_id, $event_type = 'OTP_REQUEST' ) {\n-\t\t\tif ( 'OTP_REQUEST' === $event_type ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$log_entry = array(\n-\t\t\t\t'timestamp'  => current_time( 'mysql' ),\n-\t\t\t\t'event_type' => $event_type,\n-\t\t\t\t'email'      => $email ? wp_hash( $email ) : '',\n-\t\t\t\t'phone'      => $phone ? wp_hash( $phone ) : '',\n-\t\t\t\t'ip'         => $ip ? wp_hash( $ip ) : '',\n-\t\t\t\t'browser_id' => $browser_id,\n-\t\t\t\t'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', \u002F\u002Fphpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized within the function.\n-\t\t\t\t'referer'    => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', \u002F\u002Fphpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- esc_url_raw() handles sanitization.\n-\t\t\t);\n-\n-\t\t\t$log_key      = 'mo_osp_security_log';\n-\t\t\t$existing_log = get_mo_option( $log_key );\n-\n-\t\t\tif ( is_string( $existing_log ) ) {\n-\t\t\t\t$maybe        = maybe_unserialize( $existing_log );\n-\t\t\t\t$existing_log = is_array( $maybe ) ? $maybe : array();\n-\t\t\t} elseif ( ! is_array( $existing_log ) ) {\n-\t\t\t\t$existing_log = array();\n-\t\t\t}\n-\n-\t\t\tif ( count( $existing_log ) >= 100 ) {\n-\t\t\t\t$existing_log = array_slice( $existing_log, -99 );\n-\t\t\t}\n-\n-\t\t\t$existing_log[] = $log_entry;\n-\t\t\tupdate_mo_option( $log_key, $existing_log );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get browser identifier from request.\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tprivate function get_browser_id() {\n-\t\t\t\u002F\u002F phpcs:disable WordPress.Security.NonceVerification.Missing -- Called from OTP generation hook, no nonce available\n-\t\t\tif ( isset( $_POST['mo_osp_browser_id'] ) ) { \u002F\u002Fphpcs:ignore WordPress.Security.NonceVerification.Missing -- Called from OTP generation hook, no nonce available\n-\t\t\t\treturn sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ); \u002F\u002Fphpcs:ignore WordPress.Security.NonceVerification.Missing -- Sanitized within the function.\n-\t\t\t}\n-\n-\t\t\t$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; \u002F\u002Fphpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized within the function.\n-\t\t\tif ( $user_agent ) {\n-\t\t\t\treturn hash( 'sha256', $user_agent );\n-\t\t\t}\n-\n-\t\t\treturn '';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Create error for blocked request.\n-\t\t *\n-\t\t * @param array  $block_status Block status information.\n-\t\t * @param string $type Identifier type.\n-\t\t * @param string $identifier The identifier.\n-\t\t * @return WP_Error.\n-\t\t *\u002F\n-\t\tprivate function create_block_error( $block_status, $type, $identifier ) {\n-\t\t\t$masked_id = $this->storage->mosp_mask_identifier( $identifier, $type );\n-\n-\t\t\tswitch ( $block_status['reason'] ) {\n-\t\t\t\tcase 'cooldown':\n-\t\t\t\t\t$message = sprintf(\n-\t\t\t\t\t\t\u002F* translators: %1$s: masked identifier, %2$d: remaining seconds *\u002F\n-\t\t\t\t\t\t__( 'Please wait %2$d seconds before requesting another OTP for %1$s.', 'miniorange-otp-verification' ),\n-\t\t\t\t\t\t$masked_id,\n-\t\t\t\t\t\t$block_status['remaining']\n-\t\t\t\t\t);\n-\t\t\t\t\tbreak;\n-\n-\t\t\t\tcase 'max_attempts_exceeded':\n-\t\t\t\t\t$blocked_until = date_i18n( get_mo_option( 'time_format' ), $block_status['blocked_until'] );\n-\t\t\t\t\t$message       = sprintf(\n-\t\t\t\t\t\t\u002F* translators: %1$s: masked identifier, %2$s: time when block expires *\u002F\n-\t\t\t\t\t\tMoMessages::showMessage( MoMessages::USER_IS_BLOCKED_AJAX ),\n-\t\t\t\t\t\t$masked_id,\n-\t\t\t\t\t\t$blocked_until\n-\t\t\t\t\t);\n-\t\t\t\t\tbreak;\n-\n-\t\t\t\tcase 'temporarily_blocked':\n-\t\t\t\t\t$blocked_until = date_i18n( get_mo_option( 'time_format' ), $block_status['blocked_until'] );\n-\t\t\t\t\t$message       = sprintf(\n-\t\t\t\t\t\t\u002F* translators: %1$s: masked identifier, %2$s: time when block expires *\u002F\n-\t\t\t\t\t\t__( 'Access temporarily blocked for %1$s. Please try again after %2$s.', 'miniorange-otp-verification' ),\n-\t\t\t\t\t\t$masked_id,\n-\t\t\t\t\t\t$blocked_until\n-\t\t\t\t\t);\n-\t\t\t\t\tbreak;\n-\n-\t\t\t\tdefault:\n-\t\t\t\t\t$message = __( 'OTP request blocked due to spam prevention measures.', 'miniorange-otp-verification' );\n-\t\t\t}\n-\n-\t\t\treturn new \\WP_Error( 'otp_spam_blocked', $message );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if user requires puzzle verification.\n-\t\t *\n-\t\t * @param string $email      Email address.\n-\t\t * @param string $phone      Phone number.\n-\t\t * @param string $ip         IP address.\n-\t\t * @param string $browser_id Browser fingerprint.\n-\t\t * @return bool.\n-\t\t *\u002F\n-\t\tpublic function mosp_requires_puzzle_verification( $email, $phone, $ip, $browser_id ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\treturn $this->storage->mosp_is_puzzle_required_for_user( $email, $phone, $ip, $browser_id );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Clear puzzle requirement after successful verification.\n-\t\t *\n-\t\t * @param string $email      Email address.\n-\t\t * @param string $phone      Phone number.\n-\t\t * @param string $ip         IP address.\n-\t\t * @param string $browser_id Browser fingerprint.\n-\t\t * @return void.\n-\t\t *\u002F\n-\t\tpublic function mosp_clear_puzzle_requirements( $email, $phone, $ip, $browser_id ) {\n-\t\t\t$to_clear = array();\n-\n-\t\t\t$raw_email  = trim( (string) $email );\n-\t\t\t$norm_email = $this->mosp_normalize_email_for_spam( $email );\n-\t\t\tif ( '' !== $raw_email ) {\n-\t\t\t\t$to_clear[] = $raw_email;\n-\t\t\t}\n-\t\t\tif ( '' !== $norm_email && $norm_email !== $raw_email ) {\n-\t\t\t\t$to_clear[] = $norm_email;\n-\t\t\t}\n-\n-\t\t\t$raw_phone  = trim( (string) $phone );\n-\t\t\t$norm_phone = $this->mosp_normalize_phone_for_spam( $phone );\n-\t\t\tif ( '' !== $raw_phone ) {\n-\t\t\t\t$to_clear[] = $raw_phone;\n-\t\t\t}\n-\t\t\tif ( '' !== $norm_phone && $norm_phone !== $raw_phone ) {\n-\t\t\t\t$to_clear[] = $norm_phone;\n-\t\t\t}\n-\n-\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t$to_clear[] = $ip;\n-\t\t\t}\n-\t\t\tif ( ! empty( $browser_id ) ) {\n-\t\t\t\t$to_clear[] = $browser_id;\n-\t\t\t}\n-\n-\t\t\tforeach ( array_unique( array_filter( $to_clear ) ) as $identifier ) {\n-\t\t\t\t$this->storage->mosp_clear_puzzle_requirement( $identifier );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if user has completed puzzle verification for hourly\u002Fdaily limits.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @return bool True if puzzle was completed\n-\t\t *\u002F\n-\t\tpublic function mosp_has_completed_limit_puzzle( $email, $phone ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\tif ( ! $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn true;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Mark that user has completed puzzle verification for hourly\u002Fdaily limits.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t *\u002F\n-\t\tpublic function mosp_mark_limit_puzzle_completed( $email, $phone ) {\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$puzzle_key = 'limit_puzzle_' . $this->storage->mosp_hash_key( $identifier );\n-\t\t\t\tMoPHPSessions::add_session_var( $puzzle_key, 'completed' );\n-\n-\t\t\t\t$permanent_key = 'puzzle_ever_completed_' . $this->storage->mosp_hash_key( $identifier );\n-\t\t\t\tupdate_option( $permanent_key, time() );\n-\n-\t\t\t\tMoRateLimitHelper::mosp_clear_rate_limit( $identifier, 'hourly' );\n-\t\t\t\tMoRateLimitHelper::mosp_clear_rate_limit( $identifier, 'daily' );\n-\t\t\t}\n-\n-\t\t\t$this->mosp_reset_immediate_spam_protection( $email, $phone );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Reset immediate spam protection after puzzle completion.\n-\t\t *\n-\t\t * This clears cooldown timers, attempt counts in the 15-minute window, and blocks.\n-\t\t * Note: Daily\u002Fhourly rate limits are cleared separately in mosp_mark_limit_puzzle_completed().\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t *\u002F\n-\t\tpublic function mosp_reset_immediate_spam_protection( $email, $phone ) {\n-\t\t\t$ip         = $this->mosp_get_client_ip();\n-\t\t\t$browser_id = $this->get_browser_id();\n-\n-\t\t\t$identifiers = $this->mosp_get_identifiers_to_reset_on_puzzle_success( $email, $phone, $ip, $browser_id );\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\n-\t\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\n-\n-\t\t\t\tif ( false !== $data ) {\n-\t\t\t\t\t$data['attempts']      = array();\n-\t\t\t\t\t$data['blocked_until'] = 0;\n-\t\t\t\t\t$data['last_attempt']  = 0;\n-\t\t\t\t\tif ( isset( $data['block_count'] ) ) {\n-\t\t\t\t\t\t$data['block_count'] = 0;\n-\t\t\t\t\t}\n-\t\t\t\t\tif ( isset( $data['block_reason'] ) ) {\n-\t\t\t\t\t\t$data['block_reason'] = '';\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$this->storage->mosp_update_spam_data( $key, $data );\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$this->mosp_clear_puzzle_requirements( $email, $phone, $ip, $browser_id );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if user requires puzzle verification for hourly\u002Fdaily limits.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @return bool True if puzzle is required\n-\t\t *\u002F\n-\t\tpublic function mosp_requires_limit_puzzle_verification( $email, $phone ) {\n-\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$settings    = $this->storage->mosp_get_settings();\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$ip          = $this->mosp_get_client_ip();\n-\t\t\t$identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, '' );\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$block_data = $this->storage->mosp_is_blocked( $identifier );\n-\t\t\t\tif ( $block_data['blocked'] ) {\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$daily_exceeded        = false;\n-\t\t\t$hourly_exceeded       = false;\n-\t\t\t$max_attempts_exceeded = false;\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\tif ( $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\t$daily_attempts  = MoRateLimitHelper::mosp_get_daily_attempts( $identifier );\n-\t\t\t\t$hourly_attempts = MoRateLimitHelper::mosp_get_hourly_attempts( $identifier );\n-\n-\t\t\t\tif ( $daily_attempts >= $settings['daily_limit'] ) {\n-\t\t\t\t\t$daily_exceeded = true;\n-\t\t\t\t}\n-\t\t\t\tif ( $hourly_attempts >= $settings['hourly_limit'] ) {\n-\t\t\t\t\t$hourly_exceeded = true;\n-\t\t\t\t}\n-\t\t\t\tif ( $daily_exceeded || $hourly_exceeded ) {\n-\t\t\t\t\tbreak;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$ip              = $this->mosp_get_client_ip();\n-\t\t\t$all_identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, '' );\n-\n-\t\t\tforeach ( $all_identifiers as $identifier ) {\n-\t\t\t\t$identifier_data = $this->storage->mosp_get_spam_data( $this->storage->mosp_hash_key( $identifier ) );\n-\t\t\t\tif ( false !== $identifier_data && isset( $identifier_data['attempts'] ) && is_array( $identifier_data['attempts'] ) ) {\n-\t\t\t\t\t$time_window     = MoSecurityHelper::COUNTING_WINDOW_SECONDS; \u002F\u002F 15 minutes\n-\t\t\t\t\t$cutoff_time     = time() - $time_window;\n-\t\t\t\t\t$recent_attempts = 0;\n-\n-\t\t\t\t\tforeach ( $identifier_data['attempts'] as $timestamp ) {\n-\t\t\t\t\t\tif ( $timestamp > $cutoff_time ) {\n-\t\t\t\t\t\t\t++$recent_attempts;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tif ( $recent_attempts > $settings['max_attempts'] ) {\n-\t\t\t\t\t\t$max_attempts_exceeded = true;\n-\t\t\t\t\t\tbreak;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$requires_puzzle = $daily_exceeded || $hourly_exceeded || $max_attempts_exceeded;\n-\n-\t\t\treturn $requires_puzzle;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if daily OTP limit is exceeded for a user using sliding window.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param array  $settings Settings array.\n-\t\t * @param string $context Context of the check ('otp_send' or 'timer_status').\n-\t\t * @return bool True if daily limit exceeded\n-\t\t *\u002F\n-\t\tprivate function mosp_is_daily_limit_exceeded( $email, $phone, $settings, $context = 'otp_send' ) {\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\tif ( 'otp_send' === $context && $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\tif ( MoRateLimitHelper::mosp_is_daily_limit_exceeded( $identifier, $settings['daily_limit'] ) ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get remaining time until daily limit resets (sliding window).\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @return int Remaining seconds until oldest attempt expires.\n-\t\t *\u002F\n-\t\tprivate function mosp_get_daily_limit_reset_time( $email, $phone ) {\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\n-\t\t\t$max_remaining = 0;\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$remaining = MoRateLimitHelper::mosp_get_reset_time( $identifier, MoRateLimitHelper::DAILY_WINDOW, 'daily' );\n-\t\t\t\tif ( $remaining > $max_remaining ) {\n-\t\t\t\t\t$max_remaining = $remaining;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn $max_remaining;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get remaining time until hourly limit resets (sliding window).\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @return int Remaining seconds until oldest attempt expires.\n-\t\t *\u002F\n-\t\tprivate function mosp_get_hourly_limit_reset_time( $email, $phone ) {\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\n-\t\t\t$max_remaining = 0;\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\t$remaining = MoRateLimitHelper::mosp_get_reset_time( $identifier, MoRateLimitHelper::HOURLY_WINDOW, 'hourly' );\n-\t\t\t\tif ( $remaining > $max_remaining ) {\n-\t\t\t\t\t$max_remaining = $remaining;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn $max_remaining;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if hourly OTP limit is exceeded for a user using sliding window.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @param array  $settings Settings array.\n-\t\t * @param string $context Context of the check ('otp_send' or 'timer_status').\n-\t\t * @return bool True if hourly limit exceeded\n-\t\t *\u002F\n-\t\tprivate function mosp_is_hourly_limit_exceeded( $email, $phone, $settings, $context = 'otp_send' ) {\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\tif ( 'otp_send' === $context && $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\tif ( MoRateLimitHelper::mosp_is_hourly_limit_exceeded( $identifier, $settings['hourly_limit'] ) ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get user identifier (email or phone, whichever is available).\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @return string User identifier\n-\t\t *\u002F\n-\t\tprivate function mosp_get_user_identifier( $email, $phone ) {\n-\t\t\t$np = $this->mosp_normalize_phone_for_spam( $phone );\n-\t\t\tif ( '' !== $np ) {\n-\t\t\t\treturn 'phone:' . $np;\n-\t\t\t}\n-\t\t\t$ne = $this->mosp_normalize_email_for_spam( $email );\n-\t\t\tif ( '' !== $ne ) {\n-\t\t\t\treturn 'email:' . $ne;\n-\t\t\t}\n-\t\t\treturn '';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get identifiers used for hourly\u002Fdaily limits.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tprivate function mosp_get_limit_identifiers( $email, $phone ) {\n-\t\t\t$settings    = $this->storage->mosp_get_settings();\n-\t\t\t$identifiers = array();\n-\n-\t\t\tif ( $settings['track_phone'] && ! empty( $phone ) ) {\n-\t\t\t\t$np = $this->mosp_normalize_phone_for_spam( $phone );\n-\t\t\t\tif ( '' !== $np ) {\n-\t\t\t\t\t$identifiers[] = 'phone:' . $np;\n-\t\t\t\t}\n-\t\t\t\treturn $identifiers;\n-\t\t\t}\n-\n-\t\t\tif ( $settings['track_email'] && ! empty( $email ) ) {\n-\t\t\t\t$ne = $this->mosp_normalize_email_for_spam( $email );\n-\t\t\t\tif ( '' !== $ne ) {\n-\t\t\t\t\t$identifiers[] = 'email:' . $ne;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn $identifiers;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if user has completed limit puzzle verification for a single identifier.\n-\t\t *\n-\t\t * @param string $identifier Identifier for limit checks.\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tprivate function mosp_has_completed_limit_puzzle_for_identifier( $identifier ) {\n-\t\t\tif ( empty( $identifier ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$puzzle_key = 'limit_puzzle_' . $this->storage->mosp_hash_key( $identifier );\n-\t\t\t$completed  = MoPHPSessions::get_session_var( $puzzle_key );\n-\n-\t\t\treturn 'completed' === $completed;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Record daily and hourly attempts for a user using sliding window.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t *\u002F\n-\t\tprivate function mosp_record_daily_hourly_attempts( $email, $phone ) {\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\tMoRateLimitHelper::mosp_record_attempt_multi_window( $identifier );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Clear hourly limit for a user (for testing purposes).\n-\t\t *\n-\t\t * Usage: Call this method via WordPress admin or add to functions.php:\n-\t\t * $handler = OSP\\Handler\\MoOtpSpamPreventerHandler::instance();\n-\t\t * $handler->mosp_clear_hourly_limit('test@example.com', '');\n-\t\t *\n-\t\t * Or via database:\n-\t\t * DELETE FROM wp_options WHERE option_name LIKE 'mo_customer_validation_mo_osp_rate_limit_hourly_%';\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @return bool True if cleared successfully.\n-\t\t *\u002F\n-\t\tpublic function mosp_clear_hourly_limit( $email = '', $phone = '' ) {\n-\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\n-\t\t\tif ( empty( $identifiers ) ) {\n-\t\t\t\tglobal $wpdb;\n-\t\t\t\t$prefix       = 'mo_customer_validation_mo_osp_rate_limit_hourly_';\n-\t\t\t\t$cache_key    = 'mosp_hourly_limit_options';\n-\t\t\t\t$option_names = wp_cache_get( $cache_key, 'options' );\n-\n-\t\t\t\tif ( false === $option_names ) {\n-\t\t\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t\t\t$wpdb->esc_like( $prefix ) . '%'\n-\t\t\t\t\t\t)\n-\t\t\t\t\t);\n-\t\t\t\t\twp_cache_set( $cache_key, $option_names, 'options' );\n-\t\t\t\t}\n-\n-\t\t\t\tif ( empty( $option_names ) ) {\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\n-\t\t\t\tforeach ( $option_names as $option_name ) {\n-\t\t\t\t\tdelete_option( $option_name );\n-\t\t\t\t}\n-\n-\t\t\t\twp_cache_delete( $cache_key, 'options' );\n-\t\t\t\treturn true;\n-\t\t\t}\n-\n-\t\t\t$cleared = true;\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\tif ( ! MoRateLimitHelper::mosp_clear_rate_limit( $identifier, 'hourly' ) ) {\n-\t\t\t\t\t$cleared = false;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn $cleared;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if addon is enabled.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tprivate function mosp_is_addon_enabled() {\n-\t\t\t$settings = $this->storage->mosp_get_settings();\n-\t\t\treturn ! empty( $settings['enabled'] );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Unblock a user by identifier hash.\n-\t\t *\n-\t\t * @param string $identifier_hash The hashed identifier.\n-\t\t * @return array Result with 'success' and 'message' keys.\n-\t\t *\u002F\n-\t\tpublic function mosp_unblock_user_by_hash( $identifier_hash ) {\n-\t\t\tif ( empty( $identifier_hash ) ) {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'success' => false,\n-\t\t\t\t\t'message' => __( 'Invalid identifier hash.', 'miniorange-otp-verification' ),\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F The identifier_hash is already the hash, so use it directly.\n-\t\t\t$key           = $identifier_hash;\n-\t\t\t$data          = $this->storage->mosp_get_spam_data( $key );\n-\t\t\t$blocked_until = 0;\n-\t\t\t$block_reason  = '';\n-\n-\t\t\tif ( false !== $data ) {\n-\t\t\t\t$blocked_until = isset( $data['blocked_until'] ) ? (int) $data['blocked_until'] : 0;\n-\t\t\t\t$block_reason  = isset( $data['block_reason'] ) ? $data['block_reason'] : '';\n-\n-\t\t\t\t\u002F\u002F Clear block status.\n-\t\t\t\t$data['blocked_until'] = 0;\n-\t\t\t\t$data['block_reason']  = '';\n-\t\t\t\t$data['attempts']      = array();\n-\t\t\t\t$data['last_attempt']  = 0;\n-\n-\t\t\t\t$this->storage->mosp_update_spam_data( $key, $data );\n-\n-\t\t\t\t$related_identifiers = $this->mosp_build_related_identifiers( $data );\n-\t\t\t\t$this->mosp_clear_identifiers_data( $related_identifiers );\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Clear rate limit data for all window types using the helper.\n-\t\t\t$window_types = array( 'hourly', 'daily' );\n-\t\t\tforeach ( $window_types as $window_type ) {\n-\t\t\t\t$rate_key = 'rate_limit_' . $window_type . '_' . $identifier_hash;\n-\t\t\t\t$this->storage->mosp_delete_spam_data( $rate_key );\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Clear puzzle requirements.\n-\t\t\t$this->storage->mosp_clear_puzzle_requirement( $identifier_hash );\n-\n-\t\t\t\u002F\u002F Clear cache.\n-\t\t\twp_cache_delete( 'mosp_blocked_users_list', 'mo_osp' );\n-\t\t\twp_cache_delete( 'mosp_rate_limit_hourly_options', 'mo_osp' );\n-\t\t\twp_cache_delete( 'mosp_rate_limit_daily_options', 'mo_osp' );\n-\t\t\twp_cache_delete( 'mosp_spam_data_option_names', 'mo_osp' );\n-\n-\t\t\t$this->mosp_clear_related_blocks_by_reason( $blocked_until, $block_reason, $identifier_hash );\n-\n-\t\t\treturn array(\n-\t\t\t\t'success' => true,\n-\t\t\t\t'message' => __( 'User unblocked successfully.', 'miniorange-otp-verification' ),\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Clear all blocked-user data, rate limits, and puzzle flags (admin only).\n-\t\t *\n-\t\t * @return array{ success: bool, message: string, deleted: int }\n-\t\t *\u002F\n-\t\tpublic function mosp_clear_all_blocked_data() {\n-\t\t\t$deleted = $this->storage->mosp_clear_all_otp_spam_data();\n-\n-\t\t\tif ( 0 === $deleted ) {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'success' => false,\n-\t\t\t\t\t'deleted' => 0,\n-\t\t\t\t\t'message' => __( 'No entries found to clear.', 'miniorange-otp-verification' ),\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\treturn array(\n-\t\t\t\t'success' => true,\n-\t\t\t\t'deleted' => $deleted,\n-\t\t\t\t'message' => sprintf(\n-\t\t\t\t\t\u002F* translators: %d: number of database options removed *\u002F\n-\t\t\t\t\t_n(\n-\t\t\t\t\t\t'Cleared %d stored entry (blocks, rate limits, and puzzle flags).',\n-\t\t\t\t\t\t'Cleared %d stored entries (blocks, rate limits, and puzzle flags).',\n-\t\t\t\t\t\t$deleted,\n-\t\t\t\t\t\t'miniorange-otp-verification'\n-\t\t\t\t\t),\n-\t\t\t\t\t$deleted\n-\t\t\t\t),\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Build related identifiers from stored metadata.\n-\t\t *\n-\t\t * @param array $data Spam data.\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tprivate function mosp_build_related_identifiers( $data ) {\n-\t\t\t$related_identifiers = array();\n-\t\t\tif ( isset( $data['last_ip'] ) && filter_var( $data['last_ip'], FILTER_VALIDATE_IP ) ) {\n-\t\t\t\t$related_identifiers[] = $data['last_ip'];\n-\t\t\t\t$related_identifiers[] = 'ip:' . $data['last_ip'];\n-\t\t\t}\n-\t\t\tif ( isset( $data['last_browser'] ) && ! empty( $data['last_browser'] ) ) {\n-\t\t\t\t$related_identifiers[] = $data['last_browser'];\n-\t\t\t\t$related_identifiers[] = 'browser:' . $data['last_browser'];\n-\t\t\t}\n-\t\t\tif ( isset( $data['last_email'] ) && ! empty( $data['last_email'] ) ) {\n-\t\t\t\t$related_identifiers[] = $data['last_email'];\n-\t\t\t\t$related_identifiers[] = 'email:' . $data['last_email'];\n-\t\t\t}\n-\t\t\tif ( isset( $data['last_phone'] ) && ! empty( $data['last_phone'] ) ) {\n-\t\t\t\t$related_identifiers[] = $data['last_phone'];\n-\t\t\t\t$related_identifiers[] = 'phone:' . $data['last_phone'];\n-\t\t\t}\n-\n-\t\t\t$last_ip      = isset( $data['last_ip'] ) ? $data['last_ip'] : '';\n-\t\t\t$last_email   = isset( $data['last_email'] ) ? $data['last_email'] : '';\n-\t\t\t$last_phone   = isset( $data['last_phone'] ) ? $data['last_phone'] : '';\n-\t\t\t$last_browser = isset( $data['last_browser'] ) ? $data['last_browser'] : '';\n-\n-\t\t\tif ( $last_ip && $last_email ) {\n-\t\t\t\t$related_identifiers[] = 'cross_ip_email:' . $last_ip . '|' . $last_email;\n-\t\t\t}\n-\t\t\tif ( $last_ip && $last_phone ) {\n-\t\t\t\t$related_identifiers[] = 'cross_ip_phone:' . $last_ip . '|' . $last_phone;\n-\t\t\t}\n-\t\t\tif ( $last_ip && $last_browser ) {\n-\t\t\t\t$related_identifiers[] = 'cross_ip_browser:' . $last_ip . '|' . $last_browser;\n-\t\t\t}\n-\n-\t\t\treturn array_values( array_unique( array_filter( $related_identifiers ) ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Clear spam + rate-limit data for identifiers.\n-\t\t *\n-\t\t * @param array $identifiers Identifiers to clear.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tprivate function mosp_clear_identifiers_data( $identifiers ) {\n-\t\t\tif ( empty( $identifiers ) || ! is_array( $identifiers ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $identifier ) {\n-\t\t\t\tif ( empty( $identifier ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\t$hash = $this->storage->mosp_hash_key( $identifier );\n-\n-\t\t\t\t$identifier_data = $this->storage->mosp_get_spam_data( $hash );\n-\t\t\t\tif ( false !== $identifier_data ) {\n-\t\t\t\t\t$identifier_data['blocked_until'] = 0;\n-\t\t\t\t\t$identifier_data['block_reason']  = '';\n-\t\t\t\t\t$identifier_data['attempts']      = array();\n-\t\t\t\t\t$identifier_data['last_attempt']  = 0;\n-\t\t\t\t\t$this->storage->mosp_update_spam_data( $hash, $identifier_data );\n-\t\t\t\t}\n-\n-\t\t\t\t$window_types = array( 'hourly', 'daily' );\n-\t\t\t\tforeach ( $window_types as $window_type ) {\n-\t\t\t\t\t$this->storage->mosp_delete_spam_data( 'rate_limit_' . $window_type . '_' . $hash );\n-\t\t\t\t\tdelete_mo_option( 'mo_osp_rate_limit_' . $window_type . '_' . $hash );\n-\t\t\t\t}\n-\n-\t\t\t\t$this->storage->mosp_clear_puzzle_requirement( $hash );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Clear blocks that share the same block reason and time.\n-\t\t *\n-\t\t * @param int    $blocked_until Blocked until timestamp.\n-\t\t * @param string $block_reason Block reason.\n-\t\t * @param string $exclude_hash Identifier hash to skip.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tprivate function mosp_clear_related_blocks_by_reason( $blocked_until, $block_reason, $exclude_hash ) {\n-\t\t\tif ( empty( $blocked_until ) || empty( $block_reason ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tglobal $wpdb;\n-\n-\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . MoOtpSpamStorage::SPAM_DATA_PREFIX ) . '%'\n-\t\t\t\t)\n-\t\t\t);\n-\n-\t\t\tif ( empty( $option_names ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tforeach ( $option_names as $db_option_name ) {\n-\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\n-\t\t\t\t$hash_key   = str_replace( MoOtpSpamStorage::SPAM_DATA_PREFIX, '', $option_key );\n-\n-\t\t\t\tif ( $hash_key === $exclude_hash ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\t$spam_data = $this->storage->mosp_get_spam_data( $hash_key );\n-\t\t\t\tif ( false === $spam_data ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\t$spam_blocked_until = isset( $spam_data['blocked_until'] ) ? (int) $spam_data['blocked_until'] : 0;\n-\t\t\t\t$spam_block_reason  = isset( $spam_data['block_reason'] ) ? $spam_data['block_reason'] : '';\n-\n-\t\t\t\tif ( $spam_blocked_until !== (int) $blocked_until || $spam_block_reason !== $block_reason ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\t$spam_data['blocked_until'] = 0;\n-\t\t\t\t$spam_data['block_reason']  = '';\n-\t\t\t\t$spam_data['attempts']      = array();\n-\t\t\t\t$spam_data['last_attempt']  = 0;\n-\t\t\t\t$this->storage->mosp_update_spam_data( $hash_key, $spam_data );\n-\n-\t\t\t\t$window_types = array( 'hourly', 'daily' );\n-\t\t\t\tforeach ( $window_types as $window_type ) {\n-\t\t\t\t\t$rate_key = 'rate_limit_' . $window_type . '_' . $hash_key;\n-\t\t\t\t\t$this->storage->mosp_delete_spam_data( $rate_key );\n-\t\t\t\t}\n-\n-\t\t\t\t$this->storage->mosp_clear_puzzle_requirement( $hash_key );\n-\t\t\t}\n-\t\t}\n-\t}\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * OTP Spam Preventer Main Handler\r\n+ *\r\n+ * @package otpspampreventer\u002Fhandler\r\n+ *\u002F\r\n+\r\n+namespace OSP\\Handler;\r\n+\r\n+use OSP\\Handler\\MoOtpSpamStorage;\r\n+use OSP\\Helper\\MoRateLimitHelper;\r\n+use OSP\\Helper\\MoSecurityHelper;\r\n+use OSP\\Traits\\Instance;\r\n+use OTP\\Helper\\MoMessages;\r\n+use OTP\\Helper\\MoPHPSessions;\r\n+use OTP\\Helper\\MoUtility;\r\n+\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+if ( ! class_exists( 'MoOtpSpamPreventerHandler' ) ) {\r\n+\t\u002F**\r\n+\t * The main handler class for OTP Spam Prevention functionality.\r\n+\t * Integrates with the existing OTP verification flow.\r\n+\t * Updated: Fixed block expiration logic\r\n+\t *\u002F\r\n+\tclass MoOtpSpamPreventerHandler {\r\n+\r\n+\t\tuse Instance;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Storage instance\r\n+\t\t *\r\n+\t\t * @var MoOtpSpamStorage\r\n+\t\t *\u002F\r\n+\t\tprivate $storage;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Constructor\r\n+\t\t *\u002F\r\n+\t\tpublic function __construct() {\r\n+\t\t\t$this->storage = MoOtpSpamStorage::instance();\r\n+\t\t\tMoRateLimitHelper::init( $this->storage );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Save settings from POST data.\r\n+\t\t *\r\n+\t\t * @param array $posted POST data array.\r\n+\t\t * @return array Result array with 'success' and 'message' keys.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_save_settings( $posted ) {\r\n+\t\t\t$settings                  = array();\r\n+\t\t\t$settings['enabled']       = isset( $posted['mo_osp_enabled'] );\r\n+\t\t\t$settings['cooldown_time'] = isset( $posted['mo_osp_cooldown_time'] ) ? absint( $posted['mo_osp_cooldown_time'] ) : 60;\r\n+\r\n+\t\t\t$max_attempts             = isset( $posted['mo_osp_max_attempts'] ) ? absint( $posted['mo_osp_max_attempts'] ) : 3;\r\n+\t\t\t$settings['max_attempts'] = max( 1, min( 10, $max_attempts ) );\r\n+\r\n+\t\t\t$settings['block_time'] = isset( $posted['mo_osp_block_time'] ) ? absint( $posted['mo_osp_block_time'] ) : 900;\r\n+\r\n+\t\t\t$settings['daily_limit']  = isset( $posted['mo_osp_daily_limit'] ) ? absint( $posted['mo_osp_daily_limit'] ) : 10;\r\n+\t\t\t$settings['hourly_limit'] = isset( $posted['mo_osp_hourly_limit'] ) ? absint( $posted['mo_osp_hourly_limit'] ) : 5;\r\n+\r\n+\t\t\t$validation_errors = array();\r\n+\r\n+\t\t\tif ( $settings['hourly_limit'] \u003C= $settings['max_attempts'] ) {\r\n+\t\t\t\t$validation_errors[] = 'Hourly limit (' . $settings['hourly_limit'] . ') must be greater than max attempts per window (' . $settings['max_attempts'] . ')';\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $settings['daily_limit'] \u003C= $settings['hourly_limit'] ) {\r\n+\t\t\t\t$validation_errors[] = 'Daily limit (' . $settings['daily_limit'] . ') must be greater than hourly limit (' . $settings['hourly_limit'] . ')';\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! empty( $validation_errors ) ) {\r\n+\t\t\t\t$error_message = 'Settings validation failed: ' . implode( '; ', $validation_errors );\r\n+\t\t\t\tdo_action( 'mo_otp_verification_show_message', $error_message, 'ERROR' );\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'success' => false,\r\n+\t\t\t\t\t'message' => $error_message,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings['track_phone']   = true;\r\n+\t\t\t$settings['track_email']   = true;\r\n+\t\t\t$settings['track_ip']      = true;\r\n+\t\t\t$settings['track_browser'] = true;\r\n+\r\n+\t\t\t\u002F\u002F Process whitelists.\r\n+\t\t\t$whitelist_ips             = isset( $posted['mo_osp_whitelist_ips'] ) ? sanitize_textarea_field( wp_unslash( $posted['mo_osp_whitelist_ips'] ) ) : '';\r\n+\t\t\t$settings['whitelist_ips'] = array_filter( array_map( 'trim', explode( \"\\n\", $whitelist_ips ) ) );\r\n+\t\t\t$settings['whitelist_ips'] = array_values( $settings['whitelist_ips'] );\r\n+\r\n+\t\t\t$result = $this->storage->mosp_update_settings( $settings );\r\n+\r\n+\t\t\tif ( $result ) {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'success' => true,\r\n+\t\t\t\t\t'message' => __( 'Settings saved successfully!', 'miniorange-otp-verification' ),\r\n+\t\t\t\t);\r\n+\t\t\t} else {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'success' => false,\r\n+\t\t\t\t\t'message' => __( 'Failed to save settings!', 'miniorange-otp-verification' ),\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if identifier is whitelisted.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to check (IP, email, phone, etc.).\r\n+\t\t * @param string $type The type of identifier (ip, email, phone).\r\n+\t\t * @return bool True if whitelisted, false otherwise.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_is_whitelisted( $identifier, $type ) {\r\n+\t\t\treturn $this->storage->mosp_is_whitelisted( $identifier, $type );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if a request should be blocked due to spam prevention rules\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $browser_id Browser fingerprint ID.\r\n+\t\t * @param string $context Context of the check ('otp_send' or 'timer_status').\r\n+\t\t * @return bool True if blocked, false if allowed\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_is_blocked( $email, $phone, $browser_id = '', $context = 'otp_send' ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings = $this->storage->mosp_get_settings();\r\n+\r\n+\t\t\t$ip = $this->mosp_get_client_ip();\r\n+\r\n+\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t$is_whitelisted = $this->storage->mosp_is_whitelisted( $ip, 'ip' );\r\n+\t\t\t\tif ( $is_whitelisted ) {\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$this->mosp_log_security_event( $email, $phone, $ip, $browser_id );\r\n+\r\n+\t\t\t$ip_switching_detected = $this->detect_ip_switching_attack( $email, $phone, $browser_id, $ip );\r\n+\t\t\tif ( $ip_switching_detected ) {\r\n+\t\t\t\t$this->mosp_log_security_event( $email, $phone, $ip, $browser_id, 'IP_SWITCHING_DETECTED' );\r\n+\t\t\t\treturn true;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$daily_limit_exceeded = $this->mosp_is_daily_limit_exceeded( $email, $phone, $settings, $context );\r\n+\t\t\tif ( $daily_limit_exceeded ) {\r\n+\t\t\t\treturn true;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$hourly_limit_exceeded = $this->mosp_is_hourly_limit_exceeded( $email, $phone, $settings, $context );\r\n+\t\t\tif ( $hourly_limit_exceeded ) {\r\n+\t\t\t\treturn true;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id );\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$identifier_blocked = $this->is_identifier_blocked( $identifier );\r\n+\t\t\t\tif ( $identifier_blocked ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$cross_identifier_blocked = $this->mosp_is_cross_identifier_blocked( $email, $phone, $ip, $browser_id );\r\n+\t\t\tif ( $cross_identifier_blocked ) {\r\n+\t\t\t\treturn true;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Record an OTP attempt for rate limiting (new method for integration)\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $browser_id Browser fingerprint ID.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_record_attempt_for_identifiers( $email, $phone, $browser_id = '' ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings = $this->storage->mosp_get_settings();\r\n+\r\n+\t\t\t$ip = $this->mosp_get_client_ip();\r\n+\r\n+\t\t\t$identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id );\r\n+\r\n+\t\t\t$current_time = time();\r\n+\t\t\t$context      = array(\r\n+\t\t\t\t'ip'         => $ip,\r\n+\t\t\t\t'browser_id' => $browser_id,\r\n+\t\t\t\t'email'      => $email,\r\n+\t\t\t\t'phone'      => $phone,\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$this->mosp_record_identifier_attempt( $identifier, $current_time, $context );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$this->mosp_record_cross_identifier_attempt( $email, $phone, $ip, $browser_id, $current_time, $context );\r\n+\r\n+\t\t\t$this->mosp_record_daily_hourly_attempts( $email, $phone );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Store a block for all identifiers when a block is detected via \"would be blocked\" check.\r\n+\t\t * This ensures the block persists in the database so the timer doesn't reset on each click.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $browser_id Browser fingerprint ID.\r\n+\t\t * @param string $block_reason Reason for the block (e.g., 'max_attempts_exceeded', 'cooldown').\r\n+\t\t * @param int    $remaining_time Remaining time in seconds until block expires.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_store_block_for_identifiers( $email, $phone, $browser_id, $block_reason, $remaining_time ) {\r\n+\t\t\t$ip            = $this->mosp_get_client_ip();\r\n+\t\t\t$identifiers   = $this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id );\r\n+\t\t\t$now           = time();\r\n+\t\t\t$blocked_until = $now + $remaining_time;\r\n+\r\n+\t\t\t\u002F\u002F Extract identifier type and value for storage.\r\n+\t\t\t$identifier_type_map = array();\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\tif ( strpos( $identifier, 'email:' ) === 0 ) {\r\n+\t\t\t\t\t$identifier_type_map[ $identifier ] = array(\r\n+\t\t\t\t\t\t'type'  => 'email',\r\n+\t\t\t\t\t\t'value' => substr( $identifier, 6 ),\r\n+\t\t\t\t\t);\r\n+\t\t\t\t} elseif ( strpos( $identifier, 'phone:' ) === 0 ) {\r\n+\t\t\t\t\t$identifier_type_map[ $identifier ] = array(\r\n+\t\t\t\t\t\t'type'  => 'phone',\r\n+\t\t\t\t\t\t'value' => substr( $identifier, 6 ),\r\n+\t\t\t\t\t);\r\n+\t\t\t\t} elseif ( strpos( $identifier, 'ip:' ) === 0 ) {\r\n+\t\t\t\t\t$identifier_type_map[ $identifier ] = array(\r\n+\t\t\t\t\t\t'type'  => 'ip',\r\n+\t\t\t\t\t\t'value' => substr( $identifier, 3 ),\r\n+\t\t\t\t\t);\r\n+\t\t\t\t} elseif ( strpos( $identifier, 'browser:' ) === 0 ) {\r\n+\t\t\t\t\t$identifier_type_map[ $identifier ] = array(\r\n+\t\t\t\t\t\t'type'  => 'browser',\r\n+\t\t\t\t\t\t'value' => substr( $identifier, 8 ),\r\n+\t\t\t\t\t);\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\t$identifier_type_map[ $identifier ] = array(\r\n+\t\t\t\t\t\t'type'  => 'unknown',\r\n+\t\t\t\t\t\t'value' => $identifier,\r\n+\t\t\t\t\t);\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\r\n+\t\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\r\n+\r\n+\t\t\t\t$id_info = isset( $identifier_type_map[ $identifier ] ) ? $identifier_type_map[ $identifier ] : array(\r\n+\t\t\t\t\t'type'  => 'unknown',\r\n+\t\t\t\t\t'value' => '',\r\n+\t\t\t\t);\r\n+\r\n+\t\t\t\tif ( false === $data ) {\r\n+\t\t\t\t\t$data = array(\r\n+\t\t\t\t\t\t'type'          => $id_info['type'],\r\n+\t\t\t\t\t\t'identifier'    => $id_info['value'], \u002F\u002F Store original identifier value.\r\n+\t\t\t\t\t\t'attempts'      => array(),\r\n+\t\t\t\t\t\t'blocked_until' => 0,\r\n+\t\t\t\t\t\t'total_blocks'  => 0,\r\n+\t\t\t\t\t\t'created'       => $now,\r\n+\t\t\t\t\t\t'last_attempt'  => $now,\r\n+\t\t\t\t\t);\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\t\u002F\u002F Update type if not set or is 'identifier' or 'unknown'.\r\n+\t\t\t\t\tif ( ! isset( $data['type'] ) || 'identifier' === $data['type'] || 'unknown' === $data['type'] ) {\r\n+\t\t\t\t\t\t$data['type'] = $id_info['type'];\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t\u002F\u002F Store original identifier value if not set.\r\n+\t\t\t\t\tif ( ! isset( $data['identifier'] ) || empty( $data['identifier'] ) ) {\r\n+\t\t\t\t\t\t$data['identifier'] = $id_info['value'];\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\tif ( ! isset( $data['blocked_until'] ) || $data['blocked_until'] \u003C $blocked_until ) {\r\n+\t\t\t\t\t$old_blocked_until     = isset( $data['blocked_until'] ) ? $data['blocked_until'] : 0;\r\n+\t\t\t\t\t$data['blocked_until'] = $blocked_until;\r\n+\t\t\t\t\t$data['block_reason']  = $block_reason;\r\n+\r\n+\t\t\t\t\t\u002F\u002F Store original identifier value for display (admin-only access, no masking needed).\r\n+\t\t\t\t\tif ( ! empty( $id_info['value'] ) ) {\r\n+\t\t\t\t\t\t$data['identifier'] = $id_info['value'];\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$this->storage->mosp_update_spam_data( $key, $data );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get all identifiers for the current request.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $ip IP address.\r\n+\t\t * @param string $browser_id Browser fingerprint ID.\r\n+\t\t * @return array Array of identifiers\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_get_all_identifiers( $email, $phone, $ip, $browser_id ) {\r\n+\t\t\t$identifiers = array();\r\n+\r\n+\t\t\t$norm_email = $this->mosp_normalize_email_for_spam( $email );\r\n+\t\t\tif ( '' !== $norm_email ) {\r\n+\t\t\t\t$identifiers[] = 'email:' . $norm_email;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$norm_phone = $this->mosp_normalize_phone_for_spam( $phone );\r\n+\t\t\tif ( '' !== $norm_phone ) {\r\n+\t\t\t\t$identifiers[] = 'phone:' . $norm_phone;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t$identifiers[] = 'ip:' . $ip;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! empty( $browser_id ) ) {\r\n+\t\t\t\t$identifiers[] = 'browser:' . $browser_id;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $identifiers;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Normalize email for spam\u002Frate-limit keys (stable casing).\r\n+\t\t *\r\n+\t\t * @param string $email Email.\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_normalize_email_for_spam( $email ) {\r\n+\t\t\treturn strtolower( trim( (string) $email ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Normalize phone the same way as puzzle verification (MoUtility) so reset\u002Fclear hits the same DB rows as OTP send.\r\n+\t\t *\r\n+\t\t * @param string $phone Phone.\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_normalize_phone_for_spam( $phone ) {\r\n+\t\t\t$phone = trim( (string) $phone );\r\n+\t\t\tif ( '' === $phone ) {\r\n+\t\t\t\treturn '';\r\n+\t\t\t}\r\n+\t\t\tif ( class_exists( '\\OTP\\Helper\\MoUtility' ) ) {\r\n+\t\t\t\t$processed = MoUtility::process_phone_number( $phone );\r\n+\t\t\t\t$digits    = preg_replace( '\u002F\\D\u002F', '', (string) $processed );\r\n+\t\t\t\tif ( strlen( $digits ) >= 6 ) {\r\n+\t\t\t\t\treturn $processed;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn preg_replace( '\u002F[^0-9+]\u002F', '', $phone );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Cross-identifier strings (IP + credential) using normalized email\u002Fphone.\r\n+\t\t *\r\n+\t\t * @param string $email      Email.\r\n+\t\t * @param string $phone      Phone.\r\n+\t\t * @param string $ip         IP.\r\n+\t\t * @param string $browser_id Browser id.\r\n+\t\t * @return string[]\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_build_cross_identifier_strings( $email, $phone, $ip, $browser_id ) {\r\n+\t\t\tif ( empty( $ip ) ) {\r\n+\t\t\t\treturn array();\r\n+\t\t\t}\r\n+\t\t\t$cross = array();\r\n+\t\t\t$ne    = $this->mosp_normalize_email_for_spam( $email );\r\n+\t\t\tif ( '' !== $ne ) {\r\n+\t\t\t\t$cross[] = 'cross_ip_email:' . $ip . '|' . $ne;\r\n+\t\t\t}\r\n+\t\t\t$np = $this->mosp_normalize_phone_for_spam( $phone );\r\n+\t\t\tif ( '' !== $np ) {\r\n+\t\t\t\t$cross[] = 'cross_ip_phone:' . $ip . '|' . $np;\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $browser_id ) ) {\r\n+\t\t\t\t$cross[] = 'cross_ip_browser:' . $ip . '|' . $browser_id;\r\n+\t\t\t}\r\n+\t\t\treturn $cross;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Every spam row to clear after puzzle success (canonical + legacy raw-phone keys).\r\n+\t\t *\r\n+\t\t * @param string $email      Email.\r\n+\t\t * @param string $phone      Phone.\r\n+\t\t * @param string $ip         IP.\r\n+\t\t * @param string $browser_id Browser id.\r\n+\t\t * @return string[]\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_get_identifiers_to_reset_on_puzzle_success( $email, $phone, $ip, $browser_id ) {\r\n+\t\t\t$out = array_merge(\r\n+\t\t\t\t$this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id ),\r\n+\t\t\t\t$this->mosp_build_cross_identifier_strings( $email, $phone, $ip, $browser_id )\r\n+\t\t\t);\r\n+\r\n+\t\t\t$norm_phone = $this->mosp_normalize_phone_for_spam( $phone );\r\n+\t\t\t$raw_phone  = trim( (string) $phone );\r\n+\t\t\tif ( '' !== $raw_phone ) {\r\n+\t\t\t\t$legacy_vals = array_unique(\r\n+\t\t\t\t\tarray_filter(\r\n+\t\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t\t$raw_phone,\r\n+\t\t\t\t\t\t\tpreg_replace( '\u002F[^0-9+]\u002F', '', $raw_phone ),\r\n+\t\t\t\t\t\t)\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t\tforeach ( $legacy_vals as $lp ) {\r\n+\t\t\t\t\tif ( '' === $lp || $lp === $norm_phone ) {\r\n+\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t$out[] = 'phone:' . $lp;\r\n+\t\t\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t\t\t$out[] = 'cross_ip_phone:' . $ip . '|' . $lp;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn array_values( array_unique( array_filter( $out ) ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check cross-identifier blocking (IP + OTP type combination).\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $ip IP address.\r\n+\t\t * @param string $browser_id Browser fingerprint ID.\r\n+\t\t * @return bool True if should be blocked\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_is_cross_identifier_blocked( $email, $phone, $ip, $browser_id ) {\r\n+\t\t\tforeach ( $this->mosp_build_cross_identifier_strings( $email, $phone, $ip, $browser_id ) as $cross_identifier ) {\r\n+\t\t\t\tif ( $this->is_identifier_blocked( $cross_identifier ) ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Record cross-identifier attempt.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $ip IP address.\r\n+\t\t * @param string $browser_id Browser fingerprint ID.\r\n+\t\t * @param int    $current_time Current timestamp.\r\n+\t\t * @param array  $context Context array.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_record_cross_identifier_attempt( $email, $phone, $ip, $browser_id, $current_time, $context = array() ) {\r\n+\t\t\tforeach ( $this->mosp_build_cross_identifier_strings( $email, $phone, $ip, $browser_id ) as $cross_identifier ) {\r\n+\t\t\t\t$this->mosp_record_identifier_attempt( $cross_identifier, $current_time, $context );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get block data for identifiers.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $browser_id Browser fingerprint ID.\r\n+\t\t * @param string $context Context of the check ('otp_send' or 'timer_status').\r\n+\t\t * @return array Block data with remaining time\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_get_block_data( $email, $phone, $browser_id = '', $context = 'otp_send' ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'remaining_time' => 0,\r\n+\t\t\t\t\t'reason'         => '',\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$current_time = time();\r\n+\t\t\t$settings     = $this->storage->mosp_get_settings();\r\n+\r\n+\t\t\t$ip = $this->mosp_get_client_ip();\r\n+\r\n+\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t$is_whitelisted = $this->storage->mosp_is_whitelisted( $ip, 'ip' );\r\n+\t\t\t\tif ( $is_whitelisted ) {\r\n+\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t'remaining_time' => 0,\r\n+\t\t\t\t\t\t'reason'         => '',\r\n+\t\t\t\t\t);\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$max_remaining_time = 0;\r\n+\t\t\t$block_reason       = '';\r\n+\r\n+\t\t\tif ( $this->mosp_is_daily_limit_exceeded( $email, $phone, $settings, $context ) ) {\r\n+\t\t\t\t$max_remaining_time = $this->mosp_get_daily_limit_reset_time( $email, $phone );\r\n+\t\t\t\t$block_reason       = 'daily_limit_exceeded';\r\n+\t\t\t} elseif ( $this->mosp_is_hourly_limit_exceeded( $email, $phone, $settings, $context ) ) {\r\n+\t\t\t\t$max_remaining_time = $this->mosp_get_hourly_limit_reset_time( $email, $phone );\r\n+\t\t\t\t$block_reason       = 'hourly_limit_exceeded';\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $max_remaining_time \u003C= 0 ) {\r\n+\t\t\t\t$identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id );\r\n+\r\n+\t\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t\t$block_data = $this->storage->mosp_is_blocked( $identifier );\r\n+\r\n+\t\t\t\t\tif ( $block_data['blocked'] ) {\r\n+\t\t\t\t\t\t$blocked_until = $block_data['blocked_until'];\r\n+\t\t\t\t\t\t$remaining     = $blocked_until - $current_time;\r\n+\t\t\t\t\t\tif ( $remaining > $max_remaining_time ) {\r\n+\t\t\t\t\t\t\t$max_remaining_time = $remaining;\r\n+\t\t\t\t\t\t\t$block_reason       = $block_data['reason'];\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$final_remaining = max( 0, $max_remaining_time );\r\n+\t\t\treturn array(\r\n+\t\t\t\t'remaining_time' => $final_remaining,\r\n+\t\t\t\t'reason'         => $block_reason,\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get block message with timer.\r\n+\t\t *\r\n+\t\t * @param int    $remaining_time Remaining block time in seconds.\r\n+\t\t * @param string $message_type Optional message type constant (defaults to USER_IS_BLOCKED_AJAX).\r\n+\t\t * @return string Block message with timer placeholder\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_get_block_message_with_timer( $remaining_time, $message_type = null ) {\r\n+\t\t\t$minutes = floor( $remaining_time \u002F 60 );\r\n+\t\t\t$seconds = $remaining_time % 60;\r\n+\r\n+\t\t\t$formatted_minutes = sprintf( '%02d', $minutes );\r\n+\t\t\t$formatted_seconds = sprintf( '%02d', $seconds );\r\n+\r\n+\t\t\t$message_constant = $message_type ? $message_type : MoMessages::USER_IS_BLOCKED_AJAX;\r\n+\t\t\t$message_template = MoMessages::showMessage( $message_constant );\r\n+\r\n+\t\t\t$message = $message_template;\r\n+\r\n+\t\t\tif ( strpos( $message, '{minutes}' ) !== false || strpos( $message, '{seconds}' ) !== false ) {\r\n+\t\t\t\t$message = str_replace(\r\n+\t\t\t\t\tarray( '{minutes}', '{seconds}' ),\r\n+\t\t\t\t\tarray( $formatted_minutes, $formatted_seconds ),\r\n+\t\t\t\t\t$message\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( strpos( $message, '{{remaining_time}}' ) !== false ) {\r\n+\t\t\t\t$time_display = sprintf( '%02d:%02d', $minutes, $seconds );\r\n+\t\t\t\t$message      = str_replace( '{{remaining_time}}', $time_display, $message );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( strpos( $message, '%' ) !== false ) {\r\n+\t\t\t\t$time_display = sprintf( '%02d:%02d', $minutes, $seconds );\r\n+\t\t\t\t$message      = sprintf( $message, $time_display );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $message;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if user would be blocked after recording one more attempt.\r\n+\t\t * This prevents race condition where OTP is sent successfully but user gets blocked immediately after\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $browser_id Browser fingerprint ID.\r\n+\t\t * @return bool True if would be blocked after attempt, false otherwise\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_would_be_blocked_after_attempt( $email, $phone, $browser_id = '' ) {\r\n+\t\t\t$result = $this->mosp_would_be_blocked_after_attempt_with_details( $email, $phone, $browser_id );\r\n+\t\t\treturn $result['would_be_blocked'];\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if user would be blocked after recording one more attempt, with detailed information.\r\n+\t\t * This prevents race condition where OTP is sent successfully but user gets blocked immediately after.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $browser_id Browser fingerprint ID.\r\n+\t\t * @return array Array with 'would_be_blocked' (bool), 'reason' (string), and 'remaining_time' (int)\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_would_be_blocked_after_attempt_with_details( $email, $phone, $browser_id = '' ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'would_be_blocked' => false,\r\n+\t\t\t\t\t'reason'           => '',\r\n+\t\t\t\t\t'remaining_time'   => 0,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings    = $this->storage->mosp_get_settings();\r\n+\t\t\t$ip          = $this->mosp_get_client_ip();\r\n+\t\t\t$identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, $browser_id );\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$block_data = $this->storage->mosp_is_blocked( $identifier );\r\n+\t\t\t\tif ( $block_data['blocked'] && 'cooldown' === $block_data['reason'] ) {\r\n+\t\t\t\t\t$now            = time();\r\n+\t\t\t\t\t$remaining_time = $block_data['blocked_until'] - $now;\r\n+\t\t\t\t\tif ( $remaining_time > 0 ) {\r\n+\t\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t\t'would_be_blocked' => true,\r\n+\t\t\t\t\t\t\t'reason'           => 'cooldown',\r\n+\t\t\t\t\t\t\t'remaining_time'   => $remaining_time,\r\n+\t\t\t\t\t\t);\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$cooldown_result = $this->mosp_would_be_on_cooldown_after_attempt_with_details( $identifier, $settings );\r\n+\t\t\t\tif ( $cooldown_result['would_be_on_cooldown'] ) {\r\n+\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t'would_be_blocked' => true,\r\n+\t\t\t\t\t\t'reason'           => 'cooldown',\r\n+\t\t\t\t\t\t'remaining_time'   => $cooldown_result['remaining_time'],\r\n+\t\t\t\t\t);\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $this->mosp_would_exceed_hourly_limit_after_attempt( $email, $phone, $settings ) ) {\r\n+\t\t\t\t$remaining_time = $this->mosp_get_hourly_limit_reset_time( $email, $phone );\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'would_be_blocked' => true,\r\n+\t\t\t\t\t'reason'           => 'hourly_limit_exceeded',\r\n+\t\t\t\t\t'remaining_time'   => $remaining_time,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $this->mosp_would_exceed_daily_limit_after_attempt( $email, $phone, $settings ) ) {\r\n+\t\t\t\t$remaining_time = $this->mosp_get_daily_limit_reset_time( $email, $phone );\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'would_be_blocked' => true,\r\n+\t\t\t\t\t'reason'           => 'daily_limit_exceeded',\r\n+\t\t\t\t\t'remaining_time'   => $remaining_time,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$max_attempts_result = $this->mosp_would_exceed_max_attempts_after_attempt_with_details( $identifier, $settings );\r\n+\t\t\t\tif ( $max_attempts_result['would_exceed'] ) {\r\n+\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t'would_be_blocked' => true,\r\n+\t\t\t\t\t\t'reason'           => 'max_attempts_exceeded',\r\n+\t\t\t\t\t\t'remaining_time'   => $max_attempts_result['remaining_time'],\r\n+\t\t\t\t\t);\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn array(\r\n+\t\t\t\t'would_be_blocked' => false,\r\n+\t\t\t\t'reason'           => '',\r\n+\t\t\t\t'remaining_time'   => 0,\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if recording one more attempt would exceed hourly limit.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param array  $settings Plugin settings.\r\n+\t\t * @return bool True if would exceed hourly limit after attempt, false otherwise\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_would_exceed_hourly_limit_after_attempt( $email, $phone, $settings ) {\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\tif ( $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t\t$current_attempts = MoRateLimitHelper::mosp_get_hourly_attempts( $identifier );\r\n+\t\t\t\tif ( ( $current_attempts + 1 ) > $settings['hourly_limit'] ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if recording one more attempt would exceed daily limit.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param array  $settings Plugin settings.\r\n+\t\t * @return bool True if would exceed daily limit after attempt, false otherwise\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_would_exceed_daily_limit_after_attempt( $email, $phone, $settings ) {\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\tif ( $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t\t$current_attempts = MoRateLimitHelper::mosp_get_daily_attempts( $identifier );\r\n+\t\t\t\tif ( ( $current_attempts + 1 ) > $settings['daily_limit'] ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if recording one more attempt would exceed max attempts for identifier.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to check.\r\n+\t\t * @param array  $settings Plugin settings.\r\n+\t\t * @return bool True if would exceed max attempts after attempt, false otherwise\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_would_exceed_max_attempts_after_attempt( $identifier, $settings ) {\r\n+\t\t\t$result = $this->mosp_would_exceed_max_attempts_after_attempt_with_details( $identifier, $settings );\r\n+\t\t\treturn $result['would_exceed'];\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if recording one more attempt would exceed max attempts, with detailed information.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to check.\r\n+\t\t * @param array  $settings Plugin settings.\r\n+\t\t * @return array Array with 'would_exceed' (bool) and 'remaining_time' (int)\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_would_exceed_max_attempts_after_attempt_with_details( $identifier, $settings ) {\r\n+\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\r\n+\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\r\n+\t\t\t$now  = time();\r\n+\r\n+\t\t\tif ( false === $data || ! isset( $data['attempts'] ) || ! is_array( $data['attempts'] ) || empty( $data['attempts'] ) ) {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'would_exceed'   => false,\r\n+\t\t\t\t\t'remaining_time' => 0,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( isset( $data['blocked_until'] ) && $data['blocked_until'] > $now ) {\r\n+\t\t\t\t$block_reason = isset( $data['block_reason'] ) ? $data['block_reason'] : '';\r\n+\t\t\t\tif ( 'max_attempts_exceeded' === $block_reason ) {\r\n+\t\t\t\t\t$remaining_time = $data['blocked_until'] - $now;\r\n+\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t'would_exceed'   => true,\r\n+\t\t\t\t\t\t'remaining_time' => $remaining_time,\r\n+\t\t\t\t\t);\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$time_window     = MoSecurityHelper::COUNTING_WINDOW_SECONDS; \u002F\u002F 15 minutes.\r\n+\t\t\t$cutoff_time     = $now - $time_window;\r\n+\t\t\t$recent_attempts = array();\r\n+\r\n+\t\t\tforeach ( $data['attempts'] as $timestamp ) {\r\n+\t\t\t\tif ( $timestamp > $cutoff_time ) {\r\n+\t\t\t\t\t$recent_attempts[] = $timestamp;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$recent_attempts_count = count( $recent_attempts );\r\n+\r\n+\t\t\tif ( ( $recent_attempts_count + 1 ) > $settings['max_attempts'] ) {\r\n+\t\t\t\tif ( isset( $data['blocked_until'] ) && $data['blocked_until'] > $now ) {\r\n+\t\t\t\t\t$block_reason = isset( $data['block_reason'] ) ? $data['block_reason'] : '';\r\n+\t\t\t\t\tif ( 'max_attempts_exceeded' === $block_reason ) {\r\n+\t\t\t\t\t\t$remaining_time = $data['blocked_until'] - $now;\r\n+\t\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t\t'would_exceed'   => true,\r\n+\t\t\t\t\t\t\t'remaining_time' => $remaining_time,\r\n+\t\t\t\t\t\t);\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$block_time_seconds = $settings['block_time'];\r\n+\t\t\t\t$remaining_time     = $block_time_seconds;\r\n+\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'would_exceed'   => true,\r\n+\t\t\t\t\t'remaining_time' => $remaining_time,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn array(\r\n+\t\t\t\t'would_exceed'   => false,\r\n+\t\t\t\t'remaining_time' => 0,\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if recording one more attempt would put the identifier on cooldown.\r\n+\t\t * This checks if there's a recent attempt that would trigger cooldown after adding this attempt.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to check.\r\n+\t\t * @param array  $settings Plugin settings.\r\n+\t\t * @return bool True if would be on cooldown after attempt, false otherwise\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_would_be_on_cooldown_after_attempt( $identifier, $settings ) {\r\n+\t\t\t$result = $this->mosp_would_be_on_cooldown_after_attempt_with_details( $identifier, $settings );\r\n+\t\t\treturn $result['would_be_on_cooldown'];\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if recording one more attempt would put the identifier on cooldown, with detailed information.\r\n+\t\t * This checks if there's a recent attempt that would trigger cooldown after adding this attempt.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to check.\r\n+\t\t * @param array  $settings Plugin settings.\r\n+\t\t * @return array Array with 'would_be_on_cooldown' (bool) and 'remaining_time' (int)\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_would_be_on_cooldown_after_attempt_with_details( $identifier, $settings ) {\r\n+\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\r\n+\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\r\n+\t\t\t$now  = time();\r\n+\r\n+\t\t\tif ( false === $data || ! isset( $data['attempts'] ) || ! is_array( $data['attempts'] ) || empty( $data['attempts'] ) ) {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'would_be_on_cooldown' => false,\r\n+\t\t\t\t\t'remaining_time'       => 0,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$cooldown_time = $settings['cooldown_time'];\r\n+\t\t\t$attempts      = $data['attempts'];\r\n+\t\t\t$attempt_count = count( $attempts );\r\n+\r\n+\t\t\tif ( 1 === $attempt_count ) {\r\n+\t\t\t\t$most_recent_attempt    = max( $attempts );\r\n+\t\t\t\t$time_since_most_recent = $now - $most_recent_attempt;\r\n+\r\n+\t\t\t\tif ( $time_since_most_recent \u003C $cooldown_time ) {\r\n+\t\t\t\t\t$remaining_cooldown = $cooldown_time - $time_since_most_recent;\r\n+\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t'would_be_on_cooldown' => true,\r\n+\t\t\t\t\t\t'remaining_time'       => $remaining_cooldown,\r\n+\t\t\t\t\t);\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'would_be_on_cooldown' => false,\r\n+\t\t\t\t\t'remaining_time'       => 0,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\t\t\t$most_recent_attempt = max( $attempts );\r\n+\r\n+\t\t\t$time_since_most_recent = $now - $most_recent_attempt;\r\n+\r\n+\t\t\tif ( $time_since_most_recent \u003C $cooldown_time ) {\r\n+\t\t\t\t$remaining_cooldown = $cooldown_time - $time_since_most_recent;\r\n+\t\t\t\tif ( $remaining_cooldown \u003C 0 ) {\r\n+\t\t\t\t\t$remaining_cooldown = 0;\r\n+\t\t\t\t}\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'would_be_on_cooldown' => true,\r\n+\t\t\t\t\t'remaining_time'       => $remaining_cooldown,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn array(\r\n+\t\t\t\t'would_be_on_cooldown' => false,\r\n+\t\t\t\t'remaining_time'       => 0,\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if a specific identifier is blocked.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to check.\r\n+\t\t * @return bool True if blocked, false otherwise\r\n+\t\t *\u002F\r\n+\t\tprivate function is_identifier_blocked( $identifier ) {\r\n+\t\t\t\u002F\u002F Use the storage method that contains the complete blocking logic.\r\n+\t\t\t$block_data = $this->storage->mosp_is_blocked( $identifier );\r\n+\t\t\treturn $block_data['blocked'];\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Record an attempt for a specific identifier.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier.\r\n+\t\t * @param int    $current_time Current timestamp.\r\n+\t\t * @param array  $context Context array.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_record_identifier_attempt( $identifier, $current_time, $context = array() ) {\r\n+\t\t\t$this->storage->mosp_record_attempt_with_timestamp( $identifier, $current_time, $context );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check for spam before OTP is sent.\r\n+\t\t *\r\n+\t\t * @param bool   $allow Whether to allow OTP sending.\r\n+\t\t * @param string $user_login Username.\r\n+\t\t * @param string $user_email Email address.\r\n+\t\t * @param string $phone_number Phone number.\r\n+\t\t * @return bool|WP_Error\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_check_spam_before_otp_send( $allow, $user_login, $user_email, $phone_number ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn $allow;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings = $this->storage->mosp_get_settings();\r\n+\r\n+\t\t\tif ( ! empty( $user_email ) ) {\r\n+\t\t\t\tMoPHPSessions::add_session_var( 'user_email', $user_email );\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $phone_number ) ) {\r\n+\t\t\t\tMoPHPSessions::add_session_var( 'phone_number_mo', $phone_number );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$identifiers = $this->mosp_get_request_identifiers( $user_email, $phone_number );\r\n+\r\n+\t\t\tforeach ( $identifiers as $type => $identifier ) {\r\n+\t\t\t\tif ( empty( $identifier ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\tif ( $this->storage->mosp_is_whitelisted( $identifier, $type ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$block_status = $this->storage->mosp_is_blocked( $identifier );\r\n+\r\n+\t\t\t\tif ( $block_status['blocked'] ) {\r\n+\t\t\t\t\treturn $this->create_block_error( $block_status, $type, $identifier );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $allow;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Record OTP attempt after successful send\r\n+\t\t *\r\n+\t\t * @param string $user_login Username.\r\n+\t\t * @param string $user_email Email address.\r\n+\t\t * @param string $phone_number Phone number.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_record_otp_attempt( $user_login, $user_email, $phone_number ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings = $this->storage->mosp_get_settings();\r\n+\t\t\t$ip       = $this->mosp_get_client_ip();\r\n+\t\t\t$browser  = $this->get_browser_id();\r\n+\r\n+\t\t\t$identifiers = $this->mosp_get_request_identifiers( $user_email, $phone_number );\r\n+\t\t\t$context     = array(\r\n+\t\t\t\t'ip'         => $ip,\r\n+\t\t\t\t'browser_id' => $browser,\r\n+\t\t\t\t'email'      => isset( $identifiers['email'] ) ? $identifiers['email'] : '',\r\n+\t\t\t\t'phone'      => isset( $identifiers['phone'] ) ? $identifiers['phone'] : '',\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $identifiers as $type => $identifier ) {\r\n+\t\t\t\tif ( empty( $identifier ) || $this->storage->mosp_is_whitelisted( $identifier, $type ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$this->storage->mosp_record_attempt( $identifier, $type, $context );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get all identifiers for the current request.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @return array Array of identifiers.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_get_request_identifiers( $email, $phone ) {\r\n+\t\t\t$settings    = $this->storage->mosp_get_settings();\r\n+\t\t\t$identifiers = array();\r\n+\r\n+\t\t\tif ( $settings['track_email'] && ! empty( $email ) ) {\r\n+\t\t\t\t$identifiers['email'] = strtolower( trim( $email ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $settings['track_phone'] && ! empty( $phone ) ) {\r\n+\t\t\t\t$identifiers['phone'] = preg_replace( '\u002F[^0-9+]\u002F', '', $phone );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $settings['track_ip'] ) {\r\n+\t\t\t\t$ip = $this->mosp_get_client_ip();\r\n+\t\t\t\tif ( $ip ) {\r\n+\t\t\t\t\t$identifiers['ip'] = $ip;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $settings['track_browser'] ) {\r\n+\t\t\t\t$browser_id = $this->get_browser_id();\r\n+\t\t\t\tif ( $browser_id ) {\r\n+\t\t\t\t\t$identifiers['browser'] = $browser_id;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $identifiers;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get client IP address with anti-spoofing protection\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_get_client_ip() {\r\n+\t\t\t$ip_candidates = $this->get_ip_candidates();\r\n+\r\n+\t\t\tif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {\r\n+\t\t\t\t$remote_addr = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );\r\n+\t\t\t\tif ( filter_var( $remote_addr, FILTER_VALIDATE_IP ) &&\r\n+\t\t\t\t\t! filter_var( $remote_addr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {\r\n+\t\t\t\t\tif ( $this->storage->mosp_is_whitelisted( $remote_addr, 'ip' ) ) {\r\n+\t\t\t\t\t\treturn $remote_addr;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$validated_ip = $this->validate_ip_security( $ip_candidates );\r\n+\r\n+\t\t\treturn $validated_ip;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get all possible IP addresses from headers.\r\n+\t\t *\r\n+\t\t * @return array Array of IP candidates with their sources\r\n+\t\t *\u002F\r\n+\t\tprivate function get_ip_candidates() {\r\n+\t\t\t$candidates = array();\r\n+\r\n+\t\t\t$ip_sources = array(\r\n+\t\t\t\t'REMOTE_ADDR'              => array(\r\n+\t\t\t\t\t'priority'  => 1,\r\n+\t\t\t\t\t'spoofable' => false,\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_CLIENT_IP'           => array(\r\n+\t\t\t\t\t'priority'  => 2,\r\n+\t\t\t\t\t'spoofable' => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_CF_CONNECTING_IP'    => array(\r\n+\t\t\t\t\t'priority'  => 3,\r\n+\t\t\t\t\t'spoofable' => false,\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_X_REAL_IP'           => array(\r\n+\t\t\t\t\t'priority'  => 4,\r\n+\t\t\t\t\t'spoofable' => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_X_FORWARDED_FOR'     => array(\r\n+\t\t\t\t\t'priority'  => 5,\r\n+\t\t\t\t\t'spoofable' => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_X_FORWARDED'         => array(\r\n+\t\t\t\t\t'priority'  => 6,\r\n+\t\t\t\t\t'spoofable' => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_X_CLUSTER_CLIENT_IP' => array(\r\n+\t\t\t\t\t'priority'  => 7,\r\n+\t\t\t\t\t'spoofable' => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_FORWARDED_FOR'       => array(\r\n+\t\t\t\t\t'priority'  => 8,\r\n+\t\t\t\t\t'spoofable' => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_FORWARDED'           => array(\r\n+\t\t\t\t\t'priority'  => 9,\r\n+\t\t\t\t\t'spoofable' => true,\r\n+\t\t\t\t),\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $ip_sources as $header => $config ) {\r\n+\t\t\t\tif ( ! empty( $_SERVER[ $header ] ) ) {\r\n+\t\t\t\t\t$raw_value = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );\r\n+\t\t\t\t\t$ips       = $this->parse_ip_header( $raw_value );\r\n+\r\n+\t\t\t\t\tforeach ( $ips as $ip ) {\r\n+\t\t\t\t\t\tif ( $this->is_valid_public_ip( $ip ) ) {\r\n+\t\t\t\t\t\t\t$candidates[] = array(\r\n+\t\t\t\t\t\t\t\t'ip'         => $ip,\r\n+\t\t\t\t\t\t\t\t'source'     => $header,\r\n+\t\t\t\t\t\t\t\t'priority'   => $config['priority'],\r\n+\t\t\t\t\t\t\t\t'spoofable'  => $config['spoofable'],\r\n+\t\t\t\t\t\t\t\t'raw_header' => $raw_value,\r\n+\t\t\t\t\t\t\t);\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $candidates;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Parse IP header value (handles comma-separated lists).\r\n+\t\t *\r\n+\t\t * @param string $header_value Raw header value.\r\n+\t\t * @return array Array of IP addresses.\r\n+\t\t *\u002F\r\n+\t\tprivate function parse_ip_header( $header_value ) {\r\n+\t\t\t$ips = array();\r\n+\r\n+\t\t\tif ( strpos( $header_value, ',' ) !== false ) {\r\n+\t\t\t\t$parts = explode( ',', $header_value );\r\n+\t\t\t\tforeach ( $parts as $part ) {\r\n+\t\t\t\t\t$ip = trim( $part );\r\n+\t\t\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t\t\t$ips[] = $ip;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t} else {\r\n+\t\t\t\t$ips[] = trim( $header_value );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $ips;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Validate IP with security checks.\r\n+\t\t *\r\n+\t\t * @param string $ip IP address to validate.\r\n+\t\t * @return bool True if valid public IP.\r\n+\t\t *\u002F\r\n+\t\tprivate function is_valid_public_ip( $ip ) {\r\n+\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $this->is_suspicious_ip( $ip ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn true;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if IP appears suspicious.\r\n+\t\t *\r\n+\t\t * @param string $ip IP address.\r\n+\t\t * @return bool True if suspicious.\r\n+\t\t *\u002F\r\n+\t\tprivate function is_suspicious_ip( $ip ) {\r\n+\t\t\t$suspicious_patterns = array(\r\n+\t\t\t\t'0.0.0.0',\r\n+\t\t\t\t'255.255.255.255',\r\n+\t\t\t\t'1.1.1.1',\r\n+\t\t\t\t'8.8.8.8',\r\n+\t\t\t\t'127.0.0.1',\r\n+\t\t\t\t'169.254.0.0',\r\n+\t\t\t\t'224.0.0.0',\r\n+\t\t\t\t'240.0.0.0',\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $suspicious_patterns as $pattern ) {\r\n+\t\t\t\tif ( strpos( $ip, $pattern ) === 0 ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Validate IP security and select most trustworthy.\r\n+\t\t *\r\n+\t\t * @param array $candidates Array of IP candidates.\r\n+\t\t * @return string Most trustworthy IP address.\r\n+\t\t *\u002F\r\n+\t\tprivate function validate_ip_security( $candidates ) {\r\n+\t\t\tif ( empty( $candidates ) ) {\r\n+\t\t\t\treturn '';\r\n+\t\t\t}\r\n+\r\n+\t\t\tusort(\r\n+\t\t\t\t$candidates,\r\n+\t\t\t\tfunction ( $a, $b ) {\r\n+\t\t\t\t\treturn $a['priority'] - $b['priority'];\r\n+\t\t\t\t}\r\n+\t\t\t);\r\n+\r\n+\t\t\t$remote_addr     = $this->get_remote_addr_ip( $candidates );\r\n+\t\t\t$proxy_detection = $this->detect_proxy_environment();\r\n+\r\n+\t\t\tif ( ! $proxy_detection['behind_proxy'] ) {\r\n+\t\t\t\treturn $remote_addr ? $remote_addr : '';\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $proxy_detection['trusted_proxy'] ) {\r\n+\t\t\t\tforeach ( $candidates as $candidate ) {\r\n+\t\t\t\t\tif ( 'HTTP_CF_CONNECTING_IP' === $candidate['source'] && ! $candidate['spoofable'] ) {\r\n+\t\t\t\t\t\treturn $candidate['ip'];\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t\tforeach ( $candidates as $candidate ) {\r\n+\t\t\t\t\tif ( ! $candidate['spoofable'] ) {\r\n+\t\t\t\t\t\treturn $candidate['ip'];\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $remote_addr ? $remote_addr : $candidates[0]['ip'];\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get REMOTE_ADDR IP from candidates.\r\n+\t\t *\r\n+\t\t * @param array $candidates IP candidates.\r\n+\t\t * @return string|null REMOTE_ADDR IP or null.\r\n+\t\t *\u002F\r\n+\t\tprivate function get_remote_addr_ip( $candidates ) {\r\n+\t\t\tforeach ( $candidates as $candidate ) {\r\n+\t\t\t\tif ( 'REMOTE_ADDR' === $candidate['source'] ) {\r\n+\t\t\t\t\treturn $candidate['ip'];\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn null;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Detect proxy environment.\r\n+\t\t *\r\n+\t\t * @return array Proxy detection results.\r\n+\t\t *\u002F\r\n+\t\tprivate function detect_proxy_environment() {\r\n+\r\n+\t\t\t$result = array(\r\n+\t\t\t\t'behind_proxy'  => false,\r\n+\t\t\t\t'trusted_proxy' => false,\r\n+\t\t\t\t'proxy_type'    => 'none',\r\n+\t\t\t);\r\n+\r\n+\t\t\tif ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) || ! empty( $_SERVER['HTTP_CF_RAY'] ) ) {\r\n+\t\t\t\t$result['behind_proxy']  = true;\r\n+\t\t\t\t$result['trusted_proxy'] = true;\r\n+\t\t\t\t$result['proxy_type']    = 'cloudflare';\r\n+\t\t\t\treturn $result;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$trusted_headers = array(\r\n+\t\t\t\t'HTTP_CLIENT_IP',\r\n+\t\t\t\t'HTTP_X_FORWARDED_FOR',\r\n+\t\t\t\t'HTTP_X_REAL_IP',\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $trusted_headers as $header ) {\r\n+\t\t\t\tif ( ! empty( $_SERVER[ $header ] ) ) {\r\n+\t\t\t\t\t$result['behind_proxy'] = true;\r\n+\t\t\t\t\t$remote_addr            = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';\r\n+\t\t\t\t\tif ( $this->is_known_proxy_ip( $remote_addr ) ) {\r\n+\t\t\t\t\t\t$result['trusted_proxy'] = true;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t$result['proxy_type'] = 'generic';\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $result;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if IP belongs to known proxy services.\r\n+\t\t *\r\n+\t\t * @param string $ip IP address to check.\r\n+\t\t * @return bool True if known proxy IP\r\n+\t\t *\u002F\r\n+\t\tprivate function is_known_proxy_ip( $ip ) {\r\n+\t\t\tif ( empty( $ip ) || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Cloudflare IP ranges (simplified check).\r\n+\t\t\t$cloudflare_ranges = array(\r\n+\t\t\t\t'173.245.48.0\u002F20',\r\n+\t\t\t\t'103.21.244.0\u002F22',\r\n+\t\t\t\t'103.22.200.0\u002F22',\r\n+\t\t\t\t'103.31.4.0\u002F22',\r\n+\t\t\t\t'141.101.64.0\u002F18',\r\n+\t\t\t\t'108.162.192.0\u002F18',\r\n+\t\t\t\t'190.93.240.0\u002F20',\r\n+\t\t\t\t'188.114.96.0\u002F20',\r\n+\t\t\t\t'197.234.240.0\u002F22',\r\n+\t\t\t\t'198.41.128.0\u002F17',\r\n+\t\t\t\t'162.158.0.0\u002F15',\r\n+\t\t\t\t'104.16.0.0\u002F13',\r\n+\t\t\t\t'104.24.0.0\u002F14',\r\n+\t\t\t\t'172.64.0.0\u002F13',\r\n+\t\t\t\t'131.0.72.0\u002F22',\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $cloudflare_ranges as $range ) {\r\n+\t\t\t\tif ( $this->ip_in_range( $ip, $range ) ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if IP is in CIDR range.\r\n+\t\t *\r\n+\t\t * @param string $ip IP to check.\r\n+\t\t * @param string $range CIDR range.\r\n+\t\t * @return bool True if IP is in range\r\n+\t\t *\u002F\r\n+\t\tprivate function ip_in_range( $ip, $range ) {\r\n+\t\t\tif ( strpos( $range, '\u002F' ) === false ) {\r\n+\t\t\t\treturn $ip === $range;\r\n+\t\t\t}\r\n+\r\n+\t\t\tlist($subnet, $bits) = explode( '\u002F', $range );\r\n+\r\n+\t\t\tif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) && filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {\r\n+\t\t\t\t$ip_long      = ip2long( $ip );\r\n+\t\t\t\t$subnet_long  = ip2long( $subnet );\r\n+\t\t\t\t$mask         = -1 \u003C\u003C ( 32 - (int) $bits );\r\n+\t\t\t\t$subnet_long &= $mask;\r\n+\t\t\t\treturn ( $ip_long & $mask ) === $subnet_long;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Detect IP switching attacks.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $browser_id Browser ID.\r\n+\t\t * @param string $current_ip Current IP address.\r\n+\t\t * @return bool True if attack detected\r\n+\t\t *\u002F\r\n+\t\tprivate function detect_ip_switching_attack( $email, $phone, $browser_id, $current_ip ) {\r\n+\t\t\tif ( empty( $current_ip ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$tracking_key = '';\r\n+\t\t\tif ( ! empty( $email ) ) {\r\n+\t\t\t\t$tracking_key = 'email:' . $email;\r\n+\t\t\t} elseif ( ! empty( $phone ) ) {\r\n+\t\t\t\t$tracking_key = 'phone:' . $phone;\r\n+\t\t\t} elseif ( ! empty( $browser_id ) ) {\r\n+\t\t\t\t$tracking_key = 'browser:' . $browser_id;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( empty( $tracking_key ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$ip_history_key = 'mo_osp_ip_history_' . md5( $tracking_key );\r\n+\t\t\t$ip_history     = MoPHPSessions::get_session_var( $ip_history_key );\r\n+\r\n+\t\t\tif ( false === $ip_history ) {\r\n+\t\t\t\t$ip_history = array();\r\n+\t\t\t}\r\n+\r\n+\t\t\t$current_time = time();\r\n+\t\t\t$ip_history[] = array(\r\n+\t\t\t\t'ip'        => $current_ip,\r\n+\t\t\t\t'timestamp' => $current_time,\r\n+\t\t\t);\r\n+\r\n+\t\t\t$cutoff_time = $current_time - 600;\r\n+\t\t\t$ip_history  = array_filter(\r\n+\t\t\t\t$ip_history,\r\n+\t\t\t\tfunction ( $entry ) use ( $cutoff_time ) {\r\n+\t\t\t\t\treturn $entry['timestamp'] > $cutoff_time;\r\n+\t\t\t\t}\r\n+\t\t\t);\r\n+\r\n+\t\t\t$unique_ips = array();\r\n+\t\t\tforeach ( $ip_history as $entry ) {\r\n+\t\t\t\t$unique_ips[ $entry['ip'] ] = true;\r\n+\t\t\t}\r\n+\r\n+\t\t\tMoPHPSessions::add_session_var( $ip_history_key, $ip_history ); \u002F\u002F 10 minutes\r\n+\r\n+\t\t\treturn count( $unique_ips ) > 3;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Log security events.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param string $ip IP address.\r\n+\t\t * @param string $browser_id Browser ID.\r\n+\t\t * @param string $event_type Event type.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_log_security_event( $email, $phone, $ip, $browser_id, $event_type = 'OTP_REQUEST' ) {\r\n+\t\t\tif ( 'OTP_REQUEST' === $event_type ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$log_entry = array(\r\n+\t\t\t\t'timestamp'  => current_time( 'mysql' ),\r\n+\t\t\t\t'event_type' => $event_type,\r\n+\t\t\t\t'email'      => $email ? wp_hash( $email ) : '',\r\n+\t\t\t\t'phone'      => $phone ? wp_hash( $phone ) : '',\r\n+\t\t\t\t'ip'         => $ip ? wp_hash( $ip ) : '',\r\n+\t\t\t\t'browser_id' => $browser_id,\r\n+\t\t\t\t'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', \u002F\u002Fphpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized within the function.\r\n+\t\t\t\t'referer'    => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', \u002F\u002Fphpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- esc_url_raw() handles sanitization.\r\n+\t\t\t);\r\n+\r\n+\t\t\t$log_key      = 'mo_osp_security_log';\r\n+\t\t\t$existing_log = get_mo_option( $log_key );\r\n+\r\n+\t\t\tif ( is_string( $existing_log ) ) {\r\n+\t\t\t\t$maybe        = maybe_unserialize( $existing_log );\r\n+\t\t\t\t$existing_log = is_array( $maybe ) ? $maybe : array();\r\n+\t\t\t} elseif ( ! is_array( $existing_log ) ) {\r\n+\t\t\t\t$existing_log = array();\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( count( $existing_log ) >= 100 ) {\r\n+\t\t\t\t$existing_log = array_slice( $existing_log, -99 );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$existing_log[] = $log_entry;\r\n+\t\t\tupdate_mo_option( $log_key, $existing_log );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get browser identifier from request.\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tprivate function get_browser_id() {\r\n+\t\t\t\u002F\u002F phpcs:disable WordPress.Security.NonceVerification.Missing -- Called from OTP generation hook, no nonce available\r\n+\t\t\tif ( isset( $_POST['mo_osp_browser_id'] ) ) { \u002F\u002Fphpcs:ignore WordPress.Security.NonceVerification.Missing -- Called from OTP generation hook, no nonce available\r\n+\t\t\t\treturn sanitize_text_field( wp_unslash( $_POST['mo_osp_browser_id'] ) ); \u002F\u002Fphpcs:ignore WordPress.Security.NonceVerification.Missing -- Sanitized within the function.\r\n+\t\t\t}\r\n+\r\n+\t\t\t$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; \u002F\u002Fphpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized within the function.\r\n+\t\t\tif ( $user_agent ) {\r\n+\t\t\t\treturn hash( 'sha256', $user_agent );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn '';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Create error for blocked request.\r\n+\t\t *\r\n+\t\t * @param array  $block_status Block status information.\r\n+\t\t * @param string $type Identifier type.\r\n+\t\t * @param string $identifier The identifier.\r\n+\t\t * @return WP_Error.\r\n+\t\t *\u002F\r\n+\t\tprivate function create_block_error( $block_status, $type, $identifier ) {\r\n+\t\t\t$masked_id = $this->storage->mosp_mask_identifier( $identifier, $type );\r\n+\r\n+\t\t\tswitch ( $block_status['reason'] ) {\r\n+\t\t\t\tcase 'cooldown':\r\n+\t\t\t\t\t$message = sprintf(\r\n+\t\t\t\t\t\t\u002F* translators: %1$s: masked identifier, %2$d: remaining seconds *\u002F\r\n+\t\t\t\t\t\t__( 'Please wait %2$d seconds before requesting another OTP for %1$s.', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t\t$masked_id,\r\n+\t\t\t\t\t\t$block_status['remaining']\r\n+\t\t\t\t\t);\r\n+\t\t\t\t\tbreak;\r\n+\r\n+\t\t\t\tcase 'max_attempts_exceeded':\r\n+\t\t\t\t\t$blocked_until = date_i18n( get_mo_option( 'time_format' ), $block_status['blocked_until'] );\r\n+\t\t\t\t\t$message       = sprintf(\r\n+\t\t\t\t\t\t\u002F* translators: %1$s: masked identifier, %2$s: time when block expires *\u002F\r\n+\t\t\t\t\t\tMoMessages::showMessage( MoMessages::USER_IS_BLOCKED_AJAX ),\r\n+\t\t\t\t\t\t$masked_id,\r\n+\t\t\t\t\t\t$blocked_until\r\n+\t\t\t\t\t);\r\n+\t\t\t\t\tbreak;\r\n+\r\n+\t\t\t\tcase 'temporarily_blocked':\r\n+\t\t\t\t\t$blocked_until = date_i18n( get_mo_option( 'time_format' ), $block_status['blocked_until'] );\r\n+\t\t\t\t\t$message       = sprintf(\r\n+\t\t\t\t\t\t\u002F* translators: %1$s: masked identifier, %2$s: time when block expires *\u002F\r\n+\t\t\t\t\t\t__( 'Access temporarily blocked for %1$s. Please try again after %2$s.', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t\t$masked_id,\r\n+\t\t\t\t\t\t$blocked_until\r\n+\t\t\t\t\t);\r\n+\t\t\t\t\tbreak;\r\n+\r\n+\t\t\t\tdefault:\r\n+\t\t\t\t\t$message = __( 'OTP request blocked due to spam prevention measures.', 'miniorange-otp-verification' );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn new \\WP_Error( 'otp_spam_blocked', $message );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if user requires puzzle verification.\r\n+\t\t *\r\n+\t\t * @param string $email      Email address.\r\n+\t\t * @param string $phone      Phone number.\r\n+\t\t * @param string $ip         IP address.\r\n+\t\t * @param string $browser_id Browser fingerprint.\r\n+\t\t * @return bool.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_requires_puzzle_verification( $email, $phone, $ip, $browser_id ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $this->storage->mosp_is_puzzle_required_for_user( $email, $phone, $ip, $browser_id );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Clear puzzle requirement after successful verification.\r\n+\t\t *\r\n+\t\t * @param string $email      Email address.\r\n+\t\t * @param string $phone      Phone number.\r\n+\t\t * @param string $ip         IP address.\r\n+\t\t * @param string $browser_id Browser fingerprint.\r\n+\t\t * @return void.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_clear_puzzle_requirements( $email, $phone, $ip, $browser_id ) {\r\n+\t\t\t$to_clear = array();\r\n+\r\n+\t\t\t$raw_email  = trim( (string) $email );\r\n+\t\t\t$norm_email = $this->mosp_normalize_email_for_spam( $email );\r\n+\t\t\tif ( '' !== $raw_email ) {\r\n+\t\t\t\t$to_clear[] = $raw_email;\r\n+\t\t\t}\r\n+\t\t\tif ( '' !== $norm_email && $norm_email !== $raw_email ) {\r\n+\t\t\t\t$to_clear[] = $norm_email;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$raw_phone  = trim( (string) $phone );\r\n+\t\t\t$norm_phone = $this->mosp_normalize_phone_for_spam( $phone );\r\n+\t\t\tif ( '' !== $raw_phone ) {\r\n+\t\t\t\t$to_clear[] = $raw_phone;\r\n+\t\t\t}\r\n+\t\t\tif ( '' !== $norm_phone && $norm_phone !== $raw_phone ) {\r\n+\t\t\t\t$to_clear[] = $norm_phone;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t$to_clear[] = $ip;\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $browser_id ) ) {\r\n+\t\t\t\t$to_clear[] = $browser_id;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( array_unique( array_filter( $to_clear ) ) as $identifier ) {\r\n+\t\t\t\t$this->storage->mosp_clear_puzzle_requirement( $identifier );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if user has completed puzzle verification for hourly\u002Fdaily limits.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @return bool True if puzzle was completed\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_has_completed_limit_puzzle( $email, $phone ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\tif ( ! $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn true;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Mark that user has completed puzzle verification for hourly\u002Fdaily limits.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_mark_limit_puzzle_completed( $email, $phone ) {\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$puzzle_key = 'limit_puzzle_' . $this->storage->mosp_hash_key( $identifier );\r\n+\t\t\t\tMoPHPSessions::add_session_var( $puzzle_key, 'completed' );\r\n+\r\n+\t\t\t\t$permanent_key = 'puzzle_ever_completed_' . $this->storage->mosp_hash_key( $identifier );\r\n+\t\t\t\tupdate_option( $permanent_key, time() );\r\n+\r\n+\t\t\t\tMoRateLimitHelper::mosp_clear_rate_limit( $identifier, 'hourly' );\r\n+\t\t\t\tMoRateLimitHelper::mosp_clear_rate_limit( $identifier, 'daily' );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$this->mosp_reset_immediate_spam_protection( $email, $phone );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Reset immediate spam protection after puzzle completion.\r\n+\t\t *\r\n+\t\t * This clears cooldown timers, attempt counts in the 15-minute window, and blocks.\r\n+\t\t * Note: Daily\u002Fhourly rate limits are cleared separately in mosp_mark_limit_puzzle_completed().\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_reset_immediate_spam_protection( $email, $phone ) {\r\n+\t\t\t$ip         = $this->mosp_get_client_ip();\r\n+\t\t\t$browser_id = $this->get_browser_id();\r\n+\r\n+\t\t\t$identifiers = $this->mosp_get_identifiers_to_reset_on_puzzle_success( $email, $phone, $ip, $browser_id );\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$key  = $this->storage->mosp_hash_key( $identifier );\r\n+\t\t\t\t$data = $this->storage->mosp_get_spam_data( $key );\r\n+\r\n+\t\t\t\tif ( false !== $data ) {\r\n+\t\t\t\t\t$data['attempts']      = array();\r\n+\t\t\t\t\t$data['blocked_until'] = 0;\r\n+\t\t\t\t\t$data['last_attempt']  = 0;\r\n+\t\t\t\t\tif ( isset( $data['block_count'] ) ) {\r\n+\t\t\t\t\t\t$data['block_count'] = 0;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tif ( isset( $data['block_reason'] ) ) {\r\n+\t\t\t\t\t\t$data['block_reason'] = '';\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$this->storage->mosp_update_spam_data( $key, $data );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$this->mosp_clear_puzzle_requirements( $email, $phone, $ip, $browser_id );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if user requires puzzle verification for hourly\u002Fdaily limits.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @return bool True if puzzle is required\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_requires_limit_puzzle_verification( $email, $phone ) {\r\n+\t\t\tif ( ! $this->mosp_is_addon_enabled() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$settings    = $this->storage->mosp_get_settings();\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$ip          = $this->mosp_get_client_ip();\r\n+\t\t\t$identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, '' );\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$block_data = $this->storage->mosp_is_blocked( $identifier );\r\n+\t\t\t\tif ( $block_data['blocked'] ) {\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$daily_exceeded        = false;\r\n+\t\t\t$hourly_exceeded       = false;\r\n+\t\t\t$max_attempts_exceeded = false;\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\tif ( $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t\t$daily_attempts  = MoRateLimitHelper::mosp_get_daily_attempts( $identifier );\r\n+\t\t\t\t$hourly_attempts = MoRateLimitHelper::mosp_get_hourly_attempts( $identifier );\r\n+\r\n+\t\t\t\tif ( $daily_attempts >= $settings['daily_limit'] ) {\r\n+\t\t\t\t\t$daily_exceeded = true;\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( $hourly_attempts >= $settings['hourly_limit'] ) {\r\n+\t\t\t\t\t$hourly_exceeded = true;\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( $daily_exceeded || $hourly_exceeded ) {\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$ip              = $this->mosp_get_client_ip();\r\n+\t\t\t$all_identifiers = $this->mosp_get_all_identifiers( $email, $phone, $ip, '' );\r\n+\r\n+\t\t\tforeach ( $all_identifiers as $identifier ) {\r\n+\t\t\t\t$identifier_data = $this->storage->mosp_get_spam_data( $this->storage->mosp_hash_key( $identifier ) );\r\n+\t\t\t\tif ( false !== $identifier_data && isset( $identifier_data['attempts'] ) && is_array( $identifier_data['attempts'] ) ) {\r\n+\t\t\t\t\t$time_window     = MoSecurityHelper::COUNTING_WINDOW_SECONDS; \u002F\u002F 15 minutes\r\n+\t\t\t\t\t$cutoff_time     = time() - $time_window;\r\n+\t\t\t\t\t$recent_attempts = 0;\r\n+\r\n+\t\t\t\t\tforeach ( $identifier_data['attempts'] as $timestamp ) {\r\n+\t\t\t\t\t\tif ( $timestamp > $cutoff_time ) {\r\n+\t\t\t\t\t\t\t++$recent_attempts;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\tif ( $recent_attempts > $settings['max_attempts'] ) {\r\n+\t\t\t\t\t\t$max_attempts_exceeded = true;\r\n+\t\t\t\t\t\tbreak;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$requires_puzzle = $daily_exceeded || $hourly_exceeded || $max_attempts_exceeded;\r\n+\r\n+\t\t\treturn $requires_puzzle;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if daily OTP limit is exceeded for a user using sliding window.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param array  $settings Settings array.\r\n+\t\t * @param string $context Context of the check ('otp_send' or 'timer_status').\r\n+\t\t * @return bool True if daily limit exceeded\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_is_daily_limit_exceeded( $email, $phone, $settings, $context = 'otp_send' ) {\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\tif ( 'otp_send' === $context && $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( MoRateLimitHelper::mosp_is_daily_limit_exceeded( $identifier, $settings['daily_limit'] ) ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get remaining time until daily limit resets (sliding window).\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @return int Remaining seconds until oldest attempt expires.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_get_daily_limit_reset_time( $email, $phone ) {\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\treturn 0;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$max_remaining = 0;\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$remaining = MoRateLimitHelper::mosp_get_reset_time( $identifier, MoRateLimitHelper::DAILY_WINDOW, 'daily' );\r\n+\t\t\t\tif ( $remaining > $max_remaining ) {\r\n+\t\t\t\t\t$max_remaining = $remaining;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $max_remaining;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get remaining time until hourly limit resets (sliding window).\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @return int Remaining seconds until oldest attempt expires.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_get_hourly_limit_reset_time( $email, $phone ) {\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\treturn 0;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$max_remaining = 0;\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\t$remaining = MoRateLimitHelper::mosp_get_reset_time( $identifier, MoRateLimitHelper::HOURLY_WINDOW, 'hourly' );\r\n+\t\t\t\tif ( $remaining > $max_remaining ) {\r\n+\t\t\t\t\t$max_remaining = $remaining;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $max_remaining;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if hourly OTP limit is exceeded for a user using sliding window.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @param array  $settings Settings array.\r\n+\t\t * @param string $context Context of the check ('otp_send' or 'timer_status').\r\n+\t\t * @return bool True if hourly limit exceeded\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_is_hourly_limit_exceeded( $email, $phone, $settings, $context = 'otp_send' ) {\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\tif ( 'otp_send' === $context && $this->mosp_has_completed_limit_puzzle_for_identifier( $identifier ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( MoRateLimitHelper::mosp_is_hourly_limit_exceeded( $identifier, $settings['hourly_limit'] ) ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get user identifier (email or phone, whichever is available).\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @return string User identifier\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_get_user_identifier( $email, $phone ) {\r\n+\t\t\t$np = $this->mosp_normalize_phone_for_spam( $phone );\r\n+\t\t\tif ( '' !== $np ) {\r\n+\t\t\t\treturn 'phone:' . $np;\r\n+\t\t\t}\r\n+\t\t\t$ne = $this->mosp_normalize_email_for_spam( $email );\r\n+\t\t\tif ( '' !== $ne ) {\r\n+\t\t\t\treturn 'email:' . $ne;\r\n+\t\t\t}\r\n+\t\t\treturn '';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get identifiers used for hourly\u002Fdaily limits.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_get_limit_identifiers( $email, $phone ) {\r\n+\t\t\t$settings    = $this->storage->mosp_get_settings();\r\n+\t\t\t$identifiers = array();\r\n+\r\n+\t\t\tif ( $settings['track_phone'] && ! empty( $phone ) ) {\r\n+\t\t\t\t$np = $this->mosp_normalize_phone_for_spam( $phone );\r\n+\t\t\t\tif ( '' !== $np ) {\r\n+\t\t\t\t\t$identifiers[] = 'phone:' . $np;\r\n+\t\t\t\t}\r\n+\t\t\t\treturn $identifiers;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $settings['track_email'] && ! empty( $email ) ) {\r\n+\t\t\t\t$ne = $this->mosp_normalize_email_for_spam( $email );\r\n+\t\t\t\tif ( '' !== $ne ) {\r\n+\t\t\t\t\t$identifiers[] = 'email:' . $ne;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $identifiers;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if user has completed limit puzzle verification for a single identifier.\r\n+\t\t *\r\n+\t\t * @param string $identifier Identifier for limit checks.\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_has_completed_limit_puzzle_for_identifier( $identifier ) {\r\n+\t\t\tif ( empty( $identifier ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$puzzle_key = 'limit_puzzle_' . $this->storage->mosp_hash_key( $identifier );\r\n+\t\t\t$completed  = MoPHPSessions::get_session_var( $puzzle_key );\r\n+\r\n+\t\t\treturn 'completed' === $completed;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Record daily and hourly attempts for a user using sliding window.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_record_daily_hourly_attempts( $email, $phone ) {\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\tMoRateLimitHelper::mosp_record_attempt_multi_window( $identifier );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Clear hourly limit for a user (for testing purposes).\r\n+\t\t *\r\n+\t\t * Usage: Call this method via WordPress admin or add to functions.php:\r\n+\t\t * $handler = OSP\\Handler\\MoOtpSpamPreventerHandler::instance();\r\n+\t\t * $handler->mosp_clear_hourly_limit('test@example.com', '');\r\n+\t\t *\r\n+\t\t * Or via database:\r\n+\t\t * DELETE FROM wp_options WHERE option_name LIKE 'mo_customer_validation_mo_osp_rate_limit_hourly_%';\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @return bool True if cleared successfully.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_clear_hourly_limit( $email = '', $phone = '' ) {\r\n+\t\t\t$identifiers = $this->mosp_get_limit_identifiers( $email, $phone );\r\n+\t\t\tif ( empty( $identifiers ) ) {\r\n+\t\t\t\tglobal $wpdb;\r\n+\t\t\t\t$prefix       = 'mo_customer_validation_mo_osp_rate_limit_hourly_';\r\n+\t\t\t\t$cache_key    = 'mosp_hourly_limit_options';\r\n+\t\t\t\t$option_names = wp_cache_get( $cache_key, 'options' );\r\n+\r\n+\t\t\t\tif ( false === $option_names ) {\r\n+\t\t\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t\t\t$wpdb->esc_like( $prefix ) . '%'\r\n+\t\t\t\t\t\t)\r\n+\t\t\t\t\t);\r\n+\t\t\t\t\twp_cache_set( $cache_key, $option_names, 'options' );\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\tif ( empty( $option_names ) ) {\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\tforeach ( $option_names as $option_name ) {\r\n+\t\t\t\t\tdelete_option( $option_name );\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\twp_cache_delete( $cache_key, 'options' );\r\n+\t\t\t\treturn true;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$cleared = true;\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\tif ( ! MoRateLimitHelper::mosp_clear_rate_limit( $identifier, 'hourly' ) ) {\r\n+\t\t\t\t\t$cleared = false;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn $cleared;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if addon is enabled.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_is_addon_enabled() {\r\n+\t\t\t$settings = $this->storage->mosp_get_settings();\r\n+\t\t\treturn ! empty( $settings['enabled'] );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Unblock a user by identifier hash.\r\n+\t\t *\r\n+\t\t * @param string $identifier_hash The hashed identifier.\r\n+\t\t * @return array Result with 'success' and 'message' keys.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_unblock_user_by_hash( $identifier_hash ) {\r\n+\t\t\tif ( empty( $identifier_hash ) ) {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'success' => false,\r\n+\t\t\t\t\t'message' => __( 'Invalid identifier hash.', 'miniorange-otp-verification' ),\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F The identifier_hash is already the hash, so use it directly.\r\n+\t\t\t$key           = $identifier_hash;\r\n+\t\t\t$data          = $this->storage->mosp_get_spam_data( $key );\r\n+\t\t\t$blocked_until = 0;\r\n+\t\t\t$block_reason  = '';\r\n+\r\n+\t\t\tif ( false !== $data ) {\r\n+\t\t\t\t$blocked_until = isset( $data['blocked_until'] ) ? (int) $data['blocked_until'] : 0;\r\n+\t\t\t\t$block_reason  = isset( $data['block_reason'] ) ? $data['block_reason'] : '';\r\n+\r\n+\t\t\t\t\u002F\u002F Clear block status.\r\n+\t\t\t\t$data['blocked_until'] = 0;\r\n+\t\t\t\t$data['block_reason']  = '';\r\n+\t\t\t\t$data['attempts']      = array();\r\n+\t\t\t\t$data['last_attempt']  = 0;\r\n+\r\n+\t\t\t\t$this->storage->mosp_update_spam_data( $key, $data );\r\n+\r\n+\t\t\t\t$related_identifiers = $this->mosp_build_related_identifiers( $data );\r\n+\t\t\t\t$this->mosp_clear_identifiers_data( $related_identifiers );\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Clear rate limit data for all window types using the helper.\r\n+\t\t\t$window_types = array( 'hourly', 'daily' );\r\n+\t\t\tforeach ( $window_types as $window_type ) {\r\n+\t\t\t\t$rate_key = 'rate_limit_' . $window_type . '_' . $identifier_hash;\r\n+\t\t\t\t$this->storage->mosp_delete_spam_data( $rate_key );\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Clear puzzle requirements.\r\n+\t\t\t$this->storage->mosp_clear_puzzle_requirement( $identifier_hash );\r\n+\r\n+\t\t\t\u002F\u002F Clear cache.\r\n+\t\t\twp_cache_delete( 'mosp_blocked_users_list', 'mo_osp' );\r\n+\t\t\twp_cache_delete( 'mosp_rate_limit_hourly_options', 'mo_osp' );\r\n+\t\t\twp_cache_delete( 'mosp_rate_limit_daily_options', 'mo_osp' );\r\n+\t\t\twp_cache_delete( 'mosp_spam_data_option_names', 'mo_osp' );\r\n+\r\n+\t\t\t$this->mosp_clear_related_blocks_by_reason( $blocked_until, $block_reason, $identifier_hash );\r\n+\r\n+\t\t\treturn array(\r\n+\t\t\t\t'success' => true,\r\n+\t\t\t\t'message' => __( 'User unblocked successfully.', 'miniorange-otp-verification' ),\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Clear all blocked-user data, rate limits, and puzzle flags (admin only).\r\n+\t\t *\r\n+\t\t * @return array{ success: bool, message: string, deleted: int }\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_clear_all_blocked_data() {\r\n+\t\t\t$deleted = $this->storage->mosp_clear_all_otp_spam_data();\r\n+\r\n+\t\t\tif ( 0 === $deleted ) {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'success' => false,\r\n+\t\t\t\t\t'deleted' => 0,\r\n+\t\t\t\t\t'message' => __( 'No entries found to clear.', 'miniorange-otp-verification' ),\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn array(\r\n+\t\t\t\t'success' => true,\r\n+\t\t\t\t'deleted' => $deleted,\r\n+\t\t\t\t'message' => sprintf(\r\n+\t\t\t\t\t\u002F* translators: %d: number of database options removed *\u002F\r\n+\t\t\t\t\t_n(\r\n+\t\t\t\t\t\t'Cleared %d stored entry (blocks, rate limits, and puzzle flags).',\r\n+\t\t\t\t\t\t'Cleared %d stored entries (blocks, rate limits, and puzzle flags).',\r\n+\t\t\t\t\t\t$deleted,\r\n+\t\t\t\t\t\t'miniorange-otp-verification'\r\n+\t\t\t\t\t),\r\n+\t\t\t\t\t$deleted\r\n+\t\t\t\t),\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Build related identifiers from stored metadata.\r\n+\t\t *\r\n+\t\t * @param array $data Spam data.\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_build_related_identifiers( $data ) {\r\n+\t\t\t$related_identifiers = array();\r\n+\t\t\tif ( isset( $data['last_ip'] ) && filter_var( $data['last_ip'], FILTER_VALIDATE_IP ) ) {\r\n+\t\t\t\t$related_identifiers[] = $data['last_ip'];\r\n+\t\t\t\t$related_identifiers[] = 'ip:' . $data['last_ip'];\r\n+\t\t\t}\r\n+\t\t\tif ( isset( $data['last_browser'] ) && ! empty( $data['last_browser'] ) ) {\r\n+\t\t\t\t$related_identifiers[] = $data['last_browser'];\r\n+\t\t\t\t$related_identifiers[] = 'browser:' . $data['last_browser'];\r\n+\t\t\t}\r\n+\t\t\tif ( isset( $data['last_email'] ) && ! empty( $data['last_email'] ) ) {\r\n+\t\t\t\t$related_identifiers[] = $data['last_email'];\r\n+\t\t\t\t$related_identifiers[] = 'email:' . $data['last_email'];\r\n+\t\t\t}\r\n+\t\t\tif ( isset( $data['last_phone'] ) && ! empty( $data['last_phone'] ) ) {\r\n+\t\t\t\t$related_identifiers[] = $data['last_phone'];\r\n+\t\t\t\t$related_identifiers[] = 'phone:' . $data['last_phone'];\r\n+\t\t\t}\r\n+\r\n+\t\t\t$last_ip      = isset( $data['last_ip'] ) ? $data['last_ip'] : '';\r\n+\t\t\t$last_email   = isset( $data['last_email'] ) ? $data['last_email'] : '';\r\n+\t\t\t$last_phone   = isset( $data['last_phone'] ) ? $data['last_phone'] : '';\r\n+\t\t\t$last_browser = isset( $data['last_browser'] ) ? $data['last_browser'] : '';\r\n+\r\n+\t\t\tif ( $last_ip && $last_email ) {\r\n+\t\t\t\t$related_identifiers[] = 'cross_ip_email:' . $last_ip . '|' . $last_email;\r\n+\t\t\t}\r\n+\t\t\tif ( $last_ip && $last_phone ) {\r\n+\t\t\t\t$related_identifiers[] = 'cross_ip_phone:' . $last_ip . '|' . $last_phone;\r\n+\t\t\t}\r\n+\t\t\tif ( $last_ip && $last_browser ) {\r\n+\t\t\t\t$related_identifiers[] = 'cross_ip_browser:' . $last_ip . '|' . $last_browser;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn array_values( array_unique( array_filter( $related_identifiers ) ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Clear spam + rate-limit data for identifiers.\r\n+\t\t *\r\n+\t\t * @param array $identifiers Identifiers to clear.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_clear_identifiers_data( $identifiers ) {\r\n+\t\t\tif ( empty( $identifiers ) || ! is_array( $identifiers ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $identifier ) {\r\n+\t\t\t\tif ( empty( $identifier ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t\t$hash = $this->storage->mosp_hash_key( $identifier );\r\n+\r\n+\t\t\t\t$identifier_data = $this->storage->mosp_get_spam_data( $hash );\r\n+\t\t\t\tif ( false !== $identifier_data ) {\r\n+\t\t\t\t\t$identifier_data['blocked_until'] = 0;\r\n+\t\t\t\t\t$identifier_data['block_reason']  = '';\r\n+\t\t\t\t\t$identifier_data['attempts']      = array();\r\n+\t\t\t\t\t$identifier_data['last_attempt']  = 0;\r\n+\t\t\t\t\t$this->storage->mosp_update_spam_data( $hash, $identifier_data );\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$window_types = array( 'hourly', 'daily' );\r\n+\t\t\t\tforeach ( $window_types as $window_type ) {\r\n+\t\t\t\t\t$this->storage->mosp_delete_spam_data( 'rate_limit_' . $window_type . '_' . $hash );\r\n+\t\t\t\t\tdelete_mo_option( 'mo_osp_rate_limit_' . $window_type . '_' . $hash );\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$this->storage->mosp_clear_puzzle_requirement( $hash );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Clear blocks that share the same block reason and time.\r\n+\t\t *\r\n+\t\t * @param int    $blocked_until Blocked until timestamp.\r\n+\t\t * @param string $block_reason Block reason.\r\n+\t\t * @param string $exclude_hash Identifier hash to skip.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_clear_related_blocks_by_reason( $blocked_until, $block_reason, $exclude_hash ) {\r\n+\t\t\tif ( empty( $blocked_until ) || empty( $block_reason ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tglobal $wpdb;\r\n+\r\n+\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . MoOtpSpamStorage::SPAM_DATA_PREFIX ) . '%'\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\r\n+\t\t\tif ( empty( $option_names ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $option_names as $db_option_name ) {\r\n+\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\r\n+\t\t\t\t$hash_key   = str_replace( MoOtpSpamStorage::SPAM_DATA_PREFIX, '', $option_key );\r\n+\r\n+\t\t\t\tif ( $hash_key === $exclude_hash ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$spam_data = $this->storage->mosp_get_spam_data( $hash_key );\r\n+\t\t\t\tif ( false === $spam_data ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$spam_blocked_until = isset( $spam_data['blocked_until'] ) ? (int) $spam_data['blocked_until'] : 0;\r\n+\t\t\t\t$spam_block_reason  = isset( $spam_data['block_reason'] ) ? $spam_data['block_reason'] : '';\r\n+\r\n+\t\t\t\tif ( $spam_blocked_until !== (int) $blocked_until || $spam_block_reason !== $block_reason ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$spam_data['blocked_until'] = 0;\r\n+\t\t\t\t$spam_data['block_reason']  = '';\r\n+\t\t\t\t$spam_data['attempts']      = array();\r\n+\t\t\t\t$spam_data['last_attempt']  = 0;\r\n+\t\t\t\t$this->storage->mosp_update_spam_data( $hash_key, $spam_data );\r\n+\r\n+\t\t\t\t$window_types = array( 'hourly', 'daily' );\r\n+\t\t\t\tforeach ( $window_types as $window_type ) {\r\n+\t\t\t\t\t$rate_key = 'rate_limit_' . $window_type . '_' . $hash_key;\r\n+\t\t\t\t\t$this->storage->mosp_delete_spam_data( $rate_key );\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$this->storage->mosp_clear_puzzle_requirement( $hash_key );\r\n+\t\t\t}\r\n+\t\t}\r\n+\t}\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamstorage.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamstorage.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamstorage.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhandler\u002Fclass-mootpspamstorage.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,1624 +1,1624 @@\n-\u003C?php\n-\u002F**\n- * OTP Spam Storage Handler\n- *\n- * @package otpspampreventer\u002Fhandler\n- *\u002F\n-\n-namespace OSP\\Handler;\n-\n-use OSP\\Traits\\Instance;\n-use OSP\\Helper\\MoSecurityHelper;\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-if ( ! class_exists( 'MoOtpSpamStorage' ) ) {\n-\t\u002F**\n-\t * The class handles storage and retrieval of spam prevention data.\n-\t * Uses WordPress options table to store hashed keys and attempt data.\n-\t *\u002F\n-\tclass MoOtpSpamStorage {\n-\n-\t\tuse Instance;\n-\n-\t\t\u002F**\n-\t\t * Option name prefix for spam data\n-\t\t *\u002F\n-\t\tconst SPAM_DATA_PREFIX = 'mo_osp_spam_data_';\n-\n-\t\t\u002F**\n-\t\t * Option name for global settings\n-\t\t *\u002F\n-\t\tconst SETTINGS_OPTION = 'mo_osp_settings';\n-\n-\t\t\u002F**\n-\t\t * Maximum number of entries to keep in storage\n-\t\t *\u002F\n-\t\tconst MAX_ENTRIES = 10000;\n-\n-\t\t\u002F**\n-\t\t * Constructor\n-\t\t *\u002F\n-\t\tpublic function __construct() {\n-\t\t\t\u002F\u002F Schedule cleanup hook.\n-\t\t\tif ( ! wp_next_scheduled( 'mo_osp_cleanup_expired' ) ) {\n-\t\t\t\twp_schedule_event( time(), 'hourly', 'mo_osp_cleanup_expired' );\n-\t\t\t}\n-\t\t\tadd_action( 'mo_osp_cleanup_expired', array( $this, 'mosp_cleanup_expired_entries' ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Generate a secure hash for storing identifiers.\n-\t\t *\n-\t\t * @param string $value The value to hash (phone\u002Femail\u002Fip\u002Fbrowser_id).\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tpublic function mosp_hash_key( $value ) {\n-\t\t\treturn hash( 'sha256', strtolower( trim( (string) $value ) ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get spam data for a given key.\n-\t\t *\n-\t\t * @param string $key The hashed key.\n-\t\t * @return array|false\n-\t\t *\u002F\n-\t\tpublic function mosp_get_spam_data( $key ) {\n-\t\t\t$option_name = self::SPAM_DATA_PREFIX . $key;\n-\t\t\t$data        = get_mo_option( $option_name );\n-\t\t\tif ( false === $data ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( is_string( $data ) ) {\n-\t\t\t\t$data = maybe_unserialize( $data );\n-\t\t\t}\n-\n-\t\t\tif ( ! is_array( $data ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( isset( $data['attempts'] ) && ! is_array( $data['attempts'] ) ) {\n-\t\t\t\t$data['attempts'] = array();\n-\t\t\t} elseif ( ! isset( $data['attempts'] ) ) {\n-\t\t\t\t$data['attempts'] = array();\n-\t\t\t}\n-\n-\t\t\treturn $data;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Update spam data for a given key.\n-\t\t *\n-\t\t * @param string $key The hashed key.\n-\t\t * @param array  $data The spam data.\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic function mosp_update_spam_data( $key, $data ) {\n-\t\t\t$option_name = self::SPAM_DATA_PREFIX . $key;\n-\n-\t\t\tupdate_mo_option( $option_name, maybe_serialize( $data ) );\n-\n-\t\t\t$saved_data = $this->mosp_get_spam_data( $key );\n-\n-\t\t\t$success = false;\n-\t\t\tif ( false !== $saved_data && is_array( $saved_data ) ) {\n-\t\t\t\t$key_fields_match = true;\n-\t\t\t\tif ( isset( $data['blocked_until'] ) ) {\n-\t\t\t\t\t$key_fields_match = $key_fields_match && ( isset( $saved_data['blocked_until'] ) && (int) $saved_data['blocked_until'] === (int) $data['blocked_until'] );\n-\t\t\t\t}\n-\t\t\t\tif ( isset( $data['block_reason'] ) ) {\n-\t\t\t\t\t$key_fields_match = $key_fields_match && ( isset( $saved_data['block_reason'] ) && $saved_data['block_reason'] === $data['block_reason'] );\n-\t\t\t\t}\n-\t\t\t\t$success = $key_fields_match;\n-\t\t\t}\n-\n-\t\t\treturn $success;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Delete spam data for a given key.\n-\t\t *\n-\t\t * @param string $key The hashed key.\n-\t\t * @return bool|void\n-\t\t *\u002F\n-\t\tpublic function mosp_delete_spam_data( $key ) {\n-\t\t\t$option_name = self::SPAM_DATA_PREFIX . $key;\n-\t\t\twp_cache_delete( $option_name, 'mo_osp' );\n-\t\t\treturn delete_mo_option( $option_name );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Cached settings.\n-\t\t *\n-\t\t * @var array|null\n-\t\t *\u002F\n-\t\tprivate static $cached_settings = null;\n-\n-\t\t\u002F**\n-\t\t * Flag to track if settings have been logged (to avoid spam in logs).\n-\t\t *\n-\t\t * @var bool\n-\t\t *\u002F\n-\t\tprivate static $settings_logged = false;\n-\n-\t\t\u002F**\n-\t\t * Get addon settings.\n-\t\t *\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tpublic function mosp_get_settings() {\n-\t\t\tif ( null !== self::$cached_settings ) {\n-\t\t\t\treturn self::$cached_settings;\n-\t\t\t}\n-\n-\t\t\t$defaults = array(\n-\t\t\t\t'enabled'       => false,\n-\t\t\t\t'cooldown_time' => 60,\n-\t\t\t\t'max_attempts'  => 3,\n-\t\t\t\t'block_time'    => 900,\n-\t\t\t\t'daily_limit'   => 10,\n-\t\t\t\t'hourly_limit'  => 5,\n-\t\t\t\t'track_phone'   => true,\n-\t\t\t\t'track_email'   => true,\n-\t\t\t\t'track_ip'      => true,\n-\t\t\t\t'track_browser' => true,\n-\t\t\t\t'whitelist_ips' => array(),\n-\t\t\t);\n-\n-\t\t\t$settings = get_mo_option( self::SETTINGS_OPTION );\n-\n-\t\t\tif ( false === $settings || ! is_array( $settings ) ) {\n-\t\t\t\t$settings = $defaults;\n-\t\t\t} else {\n-\t\t\t\t$settings = wp_parse_args( $settings, $defaults );\n-\n-\t\t\t\tif ( isset( $settings['whitelist_ips'] ) && is_string( $settings['whitelist_ips'] ) ) {\n-\t\t\t\t\tif ( ! empty( $settings['whitelist_ips'] ) ) {\n-\t\t\t\t\t\t$split_by_newline = array_filter( array_map( 'trim', explode( \"\\n\", $settings['whitelist_ips'] ) ) );\n-\t\t\t\t\t\tif ( count( $split_by_newline ) === 1 && strpos( $split_by_newline[0], ' ' ) !== false ) {\n-\t\t\t\t\t\t\t$settings['whitelist_ips'] = array_filter( array_map( 'trim', explode( ' ', $settings['whitelist_ips'] ) ) );\n-\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t$settings['whitelist_ips'] = $split_by_newline;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\t$settings['whitelist_ips'] = array_values( $settings['whitelist_ips'] );\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\t$settings['whitelist_ips'] = array();\n-\t\t\t\t\t}\n-\t\t\t\t} elseif ( isset( $settings['whitelist_ips'] ) && is_array( $settings['whitelist_ips'] ) ) {\n-\t\t\t\t\t$cleaned_ips = array();\n-\t\t\t\t\tforeach ( $settings['whitelist_ips'] as $ip_item ) {\n-\t\t\t\t\t\t$ip_item = trim( $ip_item );\n-\t\t\t\t\t\tif ( empty( $ip_item ) ) {\n-\t\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\tif ( strpos( $ip_item, ' ' ) !== false ) {\n-\t\t\t\t\t\t\t$split_ips   = array_filter( array_map( 'trim', explode( ' ', $ip_item ) ) );\n-\t\t\t\t\t\t\t$cleaned_ips = array_merge( $cleaned_ips, $split_ips );\n-\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t$cleaned_ips[] = $ip_item;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t\t$settings['whitelist_ips'] = array_values( array_unique( $cleaned_ips ) );\n-\t\t\t\t} elseif ( ! isset( $settings['whitelist_ips'] ) || ! is_array( $settings['whitelist_ips'] ) ) {\n-\t\t\t\t\t$settings['whitelist_ips'] = array();\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tself::$cached_settings = $settings;\n-\n-\t\t\tif ( ! self::$settings_logged ) {\n-\t\t\t\tself::$settings_logged = true;\n-\t\t\t}\n-\n-\t\t\treturn $settings;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Update addon settings.\n-\t\t *\n-\t\t * @param array $settings The settings array.\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic function mosp_update_settings( $settings ) {\n-\t\t\tupdate_mo_option( self::SETTINGS_OPTION, $settings );\n-\n-\t\t\tself::$cached_settings = null;\n-\n-\t\t\t$saved_settings = get_mo_option( self::SETTINGS_OPTION );\n-\t\t\t$success        = ( $saved_settings === $settings );\n-\n-\t\t\treturn $success;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Record an OTP attempt.\n-\t\t *\n-\t\t * @param string $identifier The identifier (phone\u002Femail\u002Fip\u002Fbrowser).\n-\t\t * @param string $type The type of identifier.\n-\t\t * @return array The updated attempt data.\n-\t\t *\u002F\n-\t\tpublic function mosp_record_attempt( $identifier, $type, $context = array() ) {\n-\t\t\t$key  = $this->mosp_hash_key( $identifier );\n-\t\t\t$data = $this->mosp_get_spam_data( $key );\n-\t\t\t$now  = time();\n-\n-\t\t\tif ( false === $data ) {\n-\t\t\t\t$data = array(\n-\t\t\t\t\t'type'          => $type,\n-\t\t\t\t\t'attempts'      => array(),\n-\t\t\t\t\t'blocked_until' => 0,\n-\t\t\t\t\t'total_blocks'  => 0,\n-\t\t\t\t\t'created'       => $now,\n-\t\t\t\t\t'last_attempt'  => $now,\n-\t\t\t\t);\n-\t\t\t} else {\n-\t\t\t\tif ( ! isset( $data['type'] ) || 'identifier' === $data['type'] || 'unknown' === $data['type'] ) {\n-\t\t\t\t\t$data['type'] = $type;\n-\t\t\t\t}\n-\t\t\t\tif ( ! isset( $data['identifier'] ) && ! empty( $identifier ) ) {\n-\t\t\t\t\tif ( strpos( $identifier, 'email:' ) === 0 ) {\n-\t\t\t\t\t\t$data['identifier'] = substr( $identifier, 6 );\n-\t\t\t\t\t} elseif ( strpos( $identifier, 'phone:' ) === 0 ) {\n-\t\t\t\t\t\t$data['identifier'] = substr( $identifier, 6 );\n-\t\t\t\t\t} elseif ( strpos( $identifier, 'ip:' ) === 0 ) {\n-\t\t\t\t\t\t$data['identifier'] = substr( $identifier, 3 );\n-\t\t\t\t\t} elseif ( strpos( $identifier, 'browser:' ) === 0 ) {\n-\t\t\t\t\t\t$data['identifier'] = substr( $identifier, 8 );\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( is_array( $context ) ) {\n-\t\t\t\tif ( ! empty( $context['ip'] ) && filter_var( $context['ip'], FILTER_VALIDATE_IP ) ) {\n-\t\t\t\t\t$data['last_ip'] = $context['ip'];\n-\t\t\t\t}\n-\t\t\t\tif ( ! empty( $context['browser_id'] ) ) {\n-\t\t\t\t\t$data['last_browser'] = $context['browser_id'];\n-\t\t\t\t}\n-\t\t\t\tif ( ! empty( $context['email'] ) ) {\n-\t\t\t\t\t$data['last_email'] = strtolower( trim( (string) $context['email'] ) );\n-\t\t\t\t}\n-\t\t\t\tif ( ! empty( $context['phone'] ) ) {\n-\t\t\t\t\t$data['last_phone'] = trim( (string) $context['phone'] );\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$attempts_before = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\n-\n-\t\t\t$data['attempts'][]   = $now;\n-\t\t\t$data['last_attempt'] = $now;\n-\n-\t\t\t$settings    = $this->mosp_get_settings();\n-\t\t\t$time_window = MoSecurityHelper::COUNTING_WINDOW_SECONDS;\n-\t\t\t$cutoff_time = $now - $time_window;\n-\n-\t\t\t$data['attempts'] = array_filter(\n-\t\t\t\t$data['attempts'],\n-\t\t\t\tfunction ( $timestamp ) use ( $cutoff_time ) {\n-\t\t\t\t\treturn $timestamp > $cutoff_time;\n-\t\t\t\t}\n-\t\t\t);\n-\n-\t\t\t$data['attempts'] = array_values( $data['attempts'] );\n-\n-\t\t\t$attempts_after = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\n-\n-\t\t\t$this->mosp_update_spam_data( $key, $data );\n-\n-\t\t\treturn $data;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if an identifier is blocked.\n-\t\t *\n-\t\t * @param string $identifier The identifier to check.\n-\t\t * @return array Block status information.\n-\t\t *\u002F\n-\t\tpublic function mosp_is_blocked( $identifier ) {\n-\t\t\t$key      = $this->mosp_hash_key( $identifier );\n-\t\t\t$data     = $this->mosp_get_spam_data( $key );\n-\t\t\t$settings = $this->mosp_get_settings();\n-\t\t\t$now      = time();\n-\n-\t\t\tif ( false === $data ) {\n-\t\t\t\treturn array(\n-\t\t\t\t\t'blocked'       => false,\n-\t\t\t\t\t'reason'        => '',\n-\t\t\t\t\t'blocked_until' => 0,\n-\t\t\t\t\t'attempts'      => 0,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\tif ( isset( $data['blocked_until'] ) && $data['blocked_until'] > 0 && $data['blocked_until'] \u003C= $now ) {\n-\t\t\t\t$block_reason = isset( $data['block_reason'] ) ? $data['block_reason'] : 'unknown';\n-\n-\t\t\t\tif ( 'max_attempts_exceeded' === $block_reason ) {\n-\n-\t\t\t\t\tif ( strpos( $identifier, ':' ) !== false ) {\n-\t\t\t\t\t\tlist( $id_type, $id_value ) = explode( ':', $identifier, 2 );\n-\t\t\t\t\t\t$this->mosp_mark_puzzle_required( $id_value );\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\t$this->mosp_mark_puzzle_required( $identifier );\n-\t\t\t\t\t}\n-\t\t\t\t\t$attempts_before_clear = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\n-\t\t\t\t\t$data['attempts']      = array();\n-\t\t\t\t} else {\n-\t\t\t\t\t$time_window = MoSecurityHelper::COUNTING_WINDOW_SECONDS;\n-\t\t\t\t\t$cutoff_time = $now - $time_window;\n-\n-\t\t\t\t\t$attempts_before_clean = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\n-\n-\t\t\t\t\tif ( isset( $data['attempts'] ) && is_array( $data['attempts'] ) ) {\n-\t\t\t\t\t\t$data['attempts'] = array_filter(\n-\t\t\t\t\t\t\t$data['attempts'],\n-\t\t\t\t\t\t\tfunction ( $timestamp ) use ( $cutoff_time ) {\n-\t\t\t\t\t\t\t\treturn $timestamp > $cutoff_time;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t);\n-\t\t\t\t\t\t$data['attempts'] = array_values( $data['attempts'] );\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$attempts_after_clean = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\n-\t\t\t\t}\n-\n-\t\t\t\t$data['blocked_until'] = 0;\n-\t\t\t\t$data['block_reason']  = '';\n-\n-\t\t\t\t$this->mosp_update_spam_data( $this->mosp_hash_key( $identifier ), $data );\n-\t\t\t}\n-\n-\t\t\tif ( 0 === $data['blocked_until'] && isset( $data['block_count'] ) && $data['block_count'] > 0 ) {\n-\t\t\t\tif ( strpos( $identifier, ':' ) !== false ) {\n-\t\t\t\t\tlist( $id_type, $id_value ) = explode( ':', $identifier, 2 );\n-\t\t\t\t\t$existing_puzzle            = $this->mosp_is_puzzle_required( $id_value );\n-\t\t\t\t} else {\n-\t\t\t\t\t$existing_puzzle = $this->mosp_is_puzzle_required( $identifier );\n-\t\t\t\t}\n-\n-\t\t\t\tif ( ! $existing_puzzle ) {\n-\t\t\t\t\tif ( strpos( $identifier, ':' ) !== false ) {\n-\t\t\t\t\t\tlist( $id_type, $id_value ) = explode( ':', $identifier, 2 );\n-\t\t\t\t\t\t$this->mosp_mark_puzzle_required( $id_value );\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\t$this->mosp_mark_puzzle_required( $identifier );\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( $data['blocked_until'] > $now ) {\n-\t\t\t\t$remaining    = $data['blocked_until'] - $now;\n-\t\t\t\t$block_reason = isset( $data['block_reason'] ) && ! empty( $data['block_reason'] ) ? $data['block_reason'] : 'temporarily_blocked';\n-\t\t\t\treturn array(\n-\t\t\t\t\t'blocked'       => true,\n-\t\t\t\t\t'reason'        => $block_reason,\n-\t\t\t\t\t'blocked_until' => $data['blocked_until'],\n-\t\t\t\t\t'attempts'      => isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\t$cooldown_time = $settings['cooldown_time'];\n-\t\t\t$attempts      = isset( $data['attempts'] ) ? $data['attempts'] : array();\n-\n-\t\t\t$previous_attempt = null;\n-\t\t\tif ( count( $attempts ) >= 2 ) {\n-\t\t\t\t$sorted_attempts = $attempts;\n-\t\t\t\trsort( $sorted_attempts );\n-\t\t\t\t$most_recent_attempt    = $sorted_attempts[0];\n-\t\t\t\t$second_to_last_attempt = $sorted_attempts[1];\n-\n-\t\t\t\t$time_between_attempts = $most_recent_attempt - $second_to_last_attempt;\n-\n-\t\t\t\tif ( $time_between_attempts \u003C $cooldown_time ) {\n-\t\t\t\t\t$previous_attempt = $second_to_last_attempt;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( $previous_attempt && ( $now - $previous_attempt ) \u003C $cooldown_time ) {\n-\t\t\t\t$time_since_previous = $now - $previous_attempt;\n-\n-\t\t\t\t$calculated_blocked_until = $previous_attempt + $cooldown_time;\n-\n-\t\t\t\tif ( ! isset( $data['blocked_until'] ) || $data['blocked_until'] !== $calculated_blocked_until ) {\n-\t\t\t\t\tif ( $calculated_blocked_until > $now ) {\n-\t\t\t\t\t\t$data['blocked_until'] = $calculated_blocked_until;\n-\t\t\t\t\t\t$data['block_reason']  = 'cooldown';\n-\t\t\t\t\t\t$this->mosp_update_spam_data( $key, $data );\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\n-\t\t\t\t$blocked_until = isset( $data['blocked_until'] ) && $data['blocked_until'] > $now ? $data['blocked_until'] : $calculated_blocked_until;\n-\t\t\t\t$remaining     = $blocked_until - $now;\n-\n-\t\t\t\treturn array(\n-\t\t\t\t\t'blocked'       => true,\n-\t\t\t\t\t'reason'        => 'cooldown',\n-\t\t\t\t\t'blocked_until' => $blocked_until,\n-\t\t\t\t\t'attempts'      => isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0,\n-\t\t\t\t\t'remaining'     => $remaining,\n-\t\t\t\t);\n-\t\t\t} elseif ( $previous_attempt ) {\n-\t\t\t\t\t$time_since_previous = $now - $previous_attempt;\n-\t\t\t}\n-\n-\t\t\t$time_window      = MoSecurityHelper::COUNTING_WINDOW_SECONDS;\n-\t\t\t$cutoff_time      = $now - $time_window;\n-\t\t\t$data['attempts'] = array_filter(\n-\t\t\t\t$data['attempts'],\n-\t\t\t\tfunction ( $timestamp ) use ( $cutoff_time ) {\n-\t\t\t\t\treturn $timestamp > $cutoff_time;\n-\t\t\t\t}\n-\t\t\t);\n-\n-\t\t\t$max_attempts   = $settings['max_attempts'];\n-\t\t\t$attempts_count = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\n-\n-\t\t\tif ( $attempts_count > $max_attempts ) {\n-\t\t\t\t$block_time_seconds    = $settings['block_time'];\n-\t\t\t\t$data['blocked_until'] = $now + $block_time_seconds;\n-\t\t\t\t$data['block_reason']  = 'max_attempts_exceeded';\n-\t\t\t\tif ( ! isset( $data['total_blocks'] ) ) {\n-\t\t\t\t\t$data['total_blocks'] = 0;\n-\t\t\t\t}\n-\t\t\t\t++$data['total_blocks'];\n-\t\t\t\t$this->mosp_update_spam_data( $key, $data );\n-\n-\t\t\t\treturn array(\n-\t\t\t\t\t'blocked'       => true,\n-\t\t\t\t\t'reason'        => 'max_attempts_exceeded',\n-\t\t\t\t\t'blocked_until' => $data['blocked_until'],\n-\t\t\t\t\t'attempts'      => $attempts_count,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\treturn array(\n-\t\t\t\t'blocked'       => false,\n-\t\t\t\t'reason'        => '',\n-\t\t\t\t'blocked_until' => 0,\n-\t\t\t\t'attempts'      => isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0,\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if identifier is whitelisted.\n-\t\t *\n-\t\t * @param string $identifier The identifier to check.\n-\t\t * @param string $type The type of identifier.\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic function mosp_is_whitelisted( $identifier, $type ) {\n-\t\t\t$settings = $this->mosp_get_settings();\n-\n-\t\t\tswitch ( $type ) {\n-\t\t\t\tcase 'ip':\n-\t\t\t\t\t$identifier = trim( $identifier );\n-\n-\t\t\t\t\tif ( empty( $identifier ) || ! filter_var( $identifier, FILTER_VALIDATE_IP ) ) {\n-\t\t\t\t\t\treturn false;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$raw_whitelist = isset( $settings['whitelist_ips'] ) ? $settings['whitelist_ips'] : array();\n-\n-\t\t\t\t\tif ( is_string( $raw_whitelist ) ) {\n-\t\t\t\t\t\tif ( ! empty( $raw_whitelist ) ) {\n-\t\t\t\t\t\t\t$raw_whitelist = array_filter( array_map( 'trim', explode( \"\\n\", $raw_whitelist ) ) );\n-\t\t\t\t\t\t\t$raw_whitelist = array_values( $raw_whitelist );\n-\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t$raw_whitelist = array();\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tif ( ! empty( $raw_whitelist ) && is_array( $raw_whitelist ) ) {\n-\t\t\t\t\t\t$whitelist_ips = array_map( 'trim', $raw_whitelist );\n-\t\t\t\t\t\t$whitelist_ips = array_filter( $whitelist_ips );\n-\t\t\t\t\t\t$whitelist_ips = array_values( $whitelist_ips );\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\t$whitelist_ips = array();\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tforeach ( $whitelist_ips as $whitelist_ip ) {\n-\t\t\t\t\t\t$whitelist_ip = trim( $whitelist_ip );\n-\t\t\t\t\t\tif ( empty( $whitelist_ip ) ) {\n-\t\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\tif ( $identifier === $whitelist_ip ) {\n-\t\t\t\t\t\t\treturn true;\n-\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\tif ( strpos( $whitelist_ip, '\u002F' ) !== false ) {\n-\t\t\t\t\t\t\tif ( $this->mosp_ip_in_range( $identifier, $whitelist_ip ) ) {\n-\t\t\t\t\t\t\t\treturn true;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\t$identifier_is_ipv6 = filter_var( $identifier, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );\n-\t\t\t\t\t\t$whitelist_is_ipv6  = filter_var( $whitelist_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );\n-\n-\t\t\t\t\t\tif ( $identifier_is_ipv6 && $whitelist_is_ipv6 ) {\n-\t\t\t\t\t\t\t$normalized_identifier = $this->mosp_normalize_ipv6( $identifier );\n-\t\t\t\t\t\t\t$normalized_whitelist  = $this->mosp_normalize_ipv6( $whitelist_ip );\n-\t\t\t\t\t\t\tif ( $normalized_identifier === $normalized_whitelist ) {\n-\t\t\t\t\t\t\t\treturn true;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\t$identifier_is_ipv4 = filter_var( $identifier, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 );\n-\t\t\t\t\t\t$whitelist_is_ipv4  = filter_var( $whitelist_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 );\n-\n-\t\t\t\t\t\tif ( $identifier_is_ipv4 && $whitelist_is_ipv4 ) {\n-\t\t\t\t\t\t\tif ( $identifier === $whitelist_ip ) {\n-\t\t\t\t\t\t\t\treturn true;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t\treturn false;\n-\t\t\t\tdefault:\n-\t\t\t\t\treturn false;\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Normalize IPv6 address to canonical form.\n-\t\t *\n-\t\t * @param string $ip IPv6 address.\n-\t\t * @return string Normalized IPv6 address or original IP if not IPv6.\n-\t\t *\u002F\n-\t\tprivate function mosp_normalize_ipv6( $ip ) {\n-\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {\n-\t\t\t\treturn $ip;\n-\t\t\t}\n-\n-\t\t\tif ( function_exists( 'inet_pton' ) && function_exists( 'inet_ntop' ) ) {\n-\t\t\t\t$packed = inet_pton( $ip );\n-\t\t\t\tif ( false !== $packed ) {\n-\t\t\t\t\t$normalized = inet_ntop( $packed );\n-\t\t\t\t\tif ( false !== $normalized ) {\n-\t\t\t\t\t\treturn strtolower( $normalized );\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn strtolower( $ip );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if IP is in CIDR range (supports both IPv4 and IPv6).\n-\t\t *\n-\t\t * @param string $ip IP address to check.\n-\t\t * @param string $range CIDR range (e.g., \"192.168.1.0\u002F24\" or \"2001:db8::\u002F32\").\n-\t\t * @return bool True if IP is in range.\n-\t\t *\u002F\n-\t\tprivate function mosp_ip_in_range( $ip, $range ) {\n-\t\t\tif ( strpos( $range, '\u002F' ) === false ) {\n-\t\t\t\treturn $ip === $range;\n-\t\t\t}\n-\n-\t\t\tlist( $subnet, $bits ) = explode( '\u002F', $range );\n-\t\t\t$bits                  = (int) $bits;\n-\n-\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP ) || ! filter_var( $subnet, FILTER_VALIDATE_IP ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) && filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {\n-\t\t\t\tif ( $bits \u003C 0 || $bits > 32 ) {\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t\t$ip_long     = ip2long( $ip );\n-\t\t\t\t$subnet_long = ip2long( $subnet );\n-\t\t\t\tif ( false === $ip_long || false === $subnet_long ) {\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t\t$mask         = -1 \u003C\u003C ( 32 - $bits );\n-\t\t\t\t$subnet_long &= $mask;\n-\t\t\t\treturn ( $ip_long & $mask ) === $subnet_long;\n-\t\t\t}\n-\n-\t\t\tif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) && filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {\n-\t\t\t\tif ( $bits \u003C 0 || $bits > 128 ) {\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t\tif ( function_exists( 'inet_pton' ) ) {\n-\t\t\t\t\t$ip_packed     = inet_pton( $ip );\n-\t\t\t\t\t$subnet_packed = inet_pton( $subnet );\n-\t\t\t\t\tif ( false === $ip_packed || false === $subnet_packed ) {\n-\t\t\t\t\t\treturn false;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$ip_bytes     = unpack( 'C*', $ip_packed );\n-\t\t\t\t\t$subnet_bytes = unpack( 'C*', $subnet_packed );\n-\n-\t\t\t\t\t$full_bytes   = intval( $bits \u002F 8 );\n-\t\t\t\t\t$partial_bits = $bits % 8;\n-\n-\t\t\t\t\tfor ( $i = 1; $i \u003C= $full_bytes; $i++ ) {\n-\t\t\t\t\t\tif ( ! isset( $ip_bytes[ $i ] ) || ! isset( $subnet_bytes[ $i ] ) ) {\n-\t\t\t\t\t\t\treturn false;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\tif ( $ip_bytes[ $i ] !== $subnet_bytes[ $i ] ) {\n-\t\t\t\t\t\t\treturn false;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tif ( $partial_bits > 0 && $full_bytes \u003C 16 ) {\n-\t\t\t\t\t\t$byte_index = $full_bytes + 1;\n-\t\t\t\t\t\tif ( ! isset( $ip_bytes[ $byte_index ] ) || ! isset( $subnet_bytes[ $byte_index ] ) ) {\n-\t\t\t\t\t\t\treturn false;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\t$mask = 0xFF \u003C\u003C ( 8 - $partial_bits );\n-\t\t\t\t\t\tif ( ( $ip_bytes[ $byte_index ] & $mask ) !== ( $subnet_bytes[ $byte_index ] & $mask ) ) {\n-\t\t\t\t\t\t\treturn false;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\n-\t\t\t\t\treturn true;\n-\t\t\t\t} else {\n-\t\t\t\t\t$normalized_ip     = $this->mosp_normalize_ipv6( $ip );\n-\t\t\t\t\t$normalized_subnet = $this->mosp_normalize_ipv6( $subnet );\n-\t\t\t\t\tif ( 128 === $bits ) {\n-\t\t\t\t\t\treturn $normalized_ip === $normalized_subnet;\n-\t\t\t\t\t}\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Mark an identifier as requiring puzzle verification.\n-\t\t *\n-\t\t * @param string $identifier The identifier to mark.\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic function mosp_mark_puzzle_required( $identifier ) {\n-\t\t\t$key          = 'mo_osp_puzzle_' . $this->mosp_hash_key( $identifier );\n-\t\t\t$current_time = time();\n-\t\t\t$expiry       = $current_time + ( 24 * 60 * 60 ); \u002F\u002F 24 hours.\n-\n-\t\t\t$result = update_option( $key, $expiry );\n-\n-\t\t\treturn $result;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if an identifier requires puzzle verification.\n-\t\t *\n-\t\t * @param string $identifier The identifier to check.\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic function mosp_is_puzzle_required( $identifier ) {\n-\t\t\t$key          = 'mo_osp_puzzle_' . $this->mosp_hash_key( $identifier );\n-\t\t\t$expiry       = get_option( $key );\n-\t\t\t$current_time = time();\n-\n-\t\t\tif ( false === $expiry ) {\n-\t\t\t\t$expiry = 0;\n-\t\t\t}\n-\n-\t\t\t$required = ( $expiry && $expiry > $current_time );\n-\t\t\tif ( $required ) {\n-\t\t\t\t$remaining_time = $expiry - $current_time;\n-\t\t\t}\n-\n-\t\t\tif ( $required ) {\n-\t\t\t\t$remaining_time = $expiry - $current_time;\n-\t\t\t\treturn true;\n-\t\t\t}\n-\n-\t\t\tif ( $expiry ) {\n-\t\t\t\tdelete_option( $key );\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Clear puzzle requirement for an identifier.\n-\t\t *\n-\t\t * @param string $identifier The identifier to clear.\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic function mosp_clear_puzzle_requirement( $identifier ) {\n-\t\t\t$key    = 'mo_osp_puzzle_' . $this->mosp_hash_key( $identifier );\n-\t\t\t$result = delete_option( $key );\n-\t\t\treturn $result;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if user requires puzzle verification for any identifier.\n-\t\t *\n-\t\t * @param string $email      Email address.\n-\t\t * @param string $phone      Phone number.\n-\t\t * @param string $ip         IP address.\n-\t\t * @param string $browser_id Browser fingerprint.\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic function mosp_is_puzzle_required_for_user( $email, $phone, $ip, $browser_id ) {\n-\t\t\t$identifiers = array(\n-\t\t\t\t'email'   => $email,\n-\t\t\t\t'phone'   => $phone,\n-\t\t\t\t'ip'      => $ip,\n-\t\t\t\t'browser' => $browser_id,\n-\t\t\t);\n-\n-\t\t\t$prefixed_identifiers = array();\n-\t\t\tif ( ! empty( $email ) ) {\n-\t\t\t\t$prefixed_identifiers[] = 'email:' . $email;\n-\t\t\t}\n-\t\t\tif ( ! empty( $phone ) ) {\n-\t\t\t\t$prefixed_identifiers[] = 'phone:' . $phone;\n-\t\t\t}\n-\t\t\tif ( ! empty( $ip ) ) {\n-\t\t\t\t$prefixed_identifiers[] = 'ip:' . $ip;\n-\t\t\t}\n-\t\t\tif ( ! empty( $browser_id ) ) {\n-\t\t\t\t$prefixed_identifiers[] = 'browser:' . $browser_id;\n-\t\t\t}\n-\n-\t\t\tif ( empty( $email ) && empty( $phone ) && empty( $ip ) && empty( $browser_id ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tforeach ( $identifiers as $type => $identifier ) {\n-\t\t\t\tif ( ! empty( $identifier ) ) {\n-\t\t\t\t\t$required = $this->mosp_is_puzzle_required( $identifier );\n-\t\t\t\t\tif ( $required ) {\n-\t\t\t\t\t\treturn true;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tforeach ( $prefixed_identifiers as $prefixed_id ) {\n-\t\t\t\t$required = $this->mosp_is_puzzle_required( $prefixed_id );\n-\t\t\t\tif ( $required ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Cleanup expired entries.\n-\t\t *\u002F\n-\t\tpublic function mosp_cleanup_expired_entries() {\n-\t\t\tglobal $wpdb;\n-\n-\t\t\t$settings = $this->mosp_get_settings();\n-\t\t\t$now      = time();\n-\t\t\t$cutoff   = $now - ( MoSecurityHelper::COUNTING_WINDOW_SECONDS * 2 ); \u002F\u002F Keep data for 2x counting window (30 minutes).\n-\n-\t\t\t$deleted = $this->cleanup_spam_data( $cutoff );\n-\n-\t\t\t$deleted += $this->cleanup_rate_limiting_data( $now );\n-\n-\t\t\t$deleted += $this->cleanup_permanent_puzzle_flags( $now - ( 30 * 24 * 60 * 60 ) );\n-\n-\t\t\t$this->mosp_prune_if_needed();\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Cleanup main spam data entries.\n-\t\t *\n-\t\t * @param int $cutoff Cutoff timestamp.\n-\t\t * @return int Number of deleted entries.\n-\t\t *\u002F\n-\t\tprivate function cleanup_spam_data( $cutoff ) {\n-\t\t\tglobal $wpdb;\n-\n-\t\t\t$cache_key    = 'mosp_spam_data_option_names';\n-\t\t\t$option_names = wp_cache_get( $cache_key, 'mo_osp' );\n-\t\t\tif ( false === $option_names ) {\n-\t\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . self::SPAM_DATA_PREFIX ) . '%'\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t\twp_cache_set( $cache_key, $option_names, 'mo_osp' );\n-\t\t\t}\n-\n-\t\t\t$deleted = 0;\n-\t\t\tforeach ( $option_names as $db_option_name ) {\n-\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\n-\n-\t\t\t\t$data = get_mo_option( $option_key );\n-\n-\t\t\t\tif ( is_array( $data ) ) {\n-\t\t\t\t\tif ( isset( $data['last_attempt'] ) && $data['last_attempt'] \u003C $cutoff &&\n-\t\t\t\t\t\t( ! isset( $data['blocked_until'] ) || $data['blocked_until'] \u003C time() ) ) {\n-\t\t\t\t\t\tdelete_mo_option( $option_key );\n-\t\t\t\t\t\twp_cache_delete( $db_option_name, 'mo_osp' );\n-\t\t\t\t\t\t++$deleted;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( $deleted > 0 ) {\n-\t\t\t\twp_cache_delete( $cache_key, 'mo_osp' );\n-\t\t\t}\n-\n-\t\t\treturn $deleted;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Cleanup rate limiting data (hourly\u002Fdaily).\n-\t\t *\n-\t\t * @param int $now Current timestamp.\n-\t\t * @return int Number of deleted entries\n-\t\t *\u002F\n-\t\tprivate function cleanup_rate_limiting_data( $now ) {\n-\t\t\tglobal $wpdb;\n-\n-\t\t\t$deleted = 0;\n-\n-\t\t\t$hourly_cutoff  = $now - ( 2 * 60 * 60 );\n-\t\t\t$hourly_cache   = 'mosp_rate_limit_hourly_option_names';\n-\t\t\t$hourly_options = wp_cache_get( $hourly_cache, 'mo_osp' );\n-\t\t\tif ( false === $hourly_options ) {\n-\t\t\t\t$hourly_options = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_hourly_' ) . '%'\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t\twp_cache_set( $hourly_cache, $hourly_options, 'mo_osp' );\n-\t\t\t}\n-\n-\t\t\tforeach ( $hourly_options as $db_option_name ) {\n-\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\n-\n-\t\t\t\t$data = get_mo_option( $option_key );\n-\t\t\t\tif ( is_array( $data ) && isset( $data['last_attempt'] ) && $data['last_attempt'] \u003C $hourly_cutoff ) {\n-\t\t\t\t\tdelete_mo_option( $option_key );\n-\t\t\t\t\t++$deleted;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$daily_cutoff  = $now - ( 2 * 24 * 60 * 60 );\n-\t\t\t$daily_cache   = 'mosp_rate_limit_daily_option_names';\n-\t\t\t$daily_options = wp_cache_get( $daily_cache, 'mo_osp' );\n-\t\t\tif ( false === $daily_options ) {\n-\t\t\t\t$daily_options = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_daily_' ) . '%'\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t\twp_cache_set( $daily_cache, $daily_options, 'mo_osp' );\n-\t\t\t}\n-\n-\t\t\tforeach ( $daily_options as $db_option_name ) {\n-\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\n-\n-\t\t\t\t$data = get_mo_option( $option_key );\n-\t\t\t\tif ( is_array( $data ) && isset( $data['last_attempt'] ) && $data['last_attempt'] \u003C $daily_cutoff ) {\n-\t\t\t\t\tdelete_mo_option( $option_key );\n-\t\t\t\t\t++$deleted;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( $deleted > 0 ) {\n-\t\t\t\twp_cache_delete( $hourly_cache, 'mo_osp' );\n-\t\t\t\twp_cache_delete( $daily_cache, 'mo_osp' );\n-\t\t\t}\n-\n-\t\t\treturn $deleted;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Cleanup permanent puzzle completion flags.\n-\t\t *\n-\t\t * @param int $cutoff Cutoff timestamp (30 days ago).\n-\t\t * @return int Number of deleted entries\n-\t\t *\u002F\n-\t\tprivate function cleanup_permanent_puzzle_flags( $cutoff ) {\n-\t\t\tglobal $wpdb;\n-\n-\t\t\t$deleted        = 0;\n-\t\t\t$cache_key      = 'mosp_puzzle_completion_option_names';\n-\t\t\t$puzzle_options = wp_cache_get( $cache_key, 'mo_osp' );\n-\t\t\tif ( false === $puzzle_options ) {\n-\t\t\t\t$puzzle_options = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_puzzle_ever_completed_' ) . '%'\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t\twp_cache_set( $cache_key, $puzzle_options, 'mo_osp' );\n-\t\t\t}\n-\n-\t\t\tforeach ( $puzzle_options as $db_option_name ) {\n-\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\n-\n-\t\t\t\t$completion_time = get_mo_option( $option_key );\n-\t\t\t\tif ( is_numeric( $completion_time ) && $completion_time \u003C $cutoff ) {\n-\t\t\t\t\tdelete_mo_option( $option_key );\n-\t\t\t\t\t++$deleted;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( $deleted > 0 ) {\n-\t\t\t\twp_cache_delete( $cache_key, 'mo_osp' );\n-\t\t\t}\n-\n-\t\t\treturn $deleted;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Prune entries if still too many.\n-\t\t *\u002F\n-\t\tprivate function mosp_prune_if_needed() {\n-\t\t\tglobal $wpdb;\n-\n-\t\t\t$cache_key     = 'mosp_spam_storage_total_count';\n-\t\t\t$total_options = wp_cache_get( $cache_key, 'mo_osp' );\n-\t\t\tif ( false === $total_options ) {\n-\t\t\t\t$total_options = $wpdb->get_var( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s\",\n-\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . self::SPAM_DATA_PREFIX ) . '%',\n-\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_' ) . '%',\n-\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_puzzle_ever_completed_' ) . '%'\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t\twp_cache_set( $cache_key, $total_options, 'mo_osp' );\n-\t\t\t}\n-\n-\t\t\tif ( $total_options > self::MAX_ENTRIES ) {\n-\t\t\t\t$this->mosp_prune_old_entries( self::MAX_ENTRIES );\n-\t\t\t\twp_cache_delete( $cache_key, 'mo_osp' );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Prune old entries to keep storage bounded.\n-\t\t *\n-\t\t * @param int $max_entries Maximum entries to keep.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tprivate function mosp_prune_old_entries( $max_entries ) {\n-\t\t\tglobal $wpdb;\n-\n-\t\t\t$cache_key = 'mosp_spam_data_entries';\n-\t\t\t$results   = wp_cache_get( $cache_key, 'mo_osp' );\n-\t\t\tif ( false === $results ) {\n-\t\t\t\t$results = $wpdb->get_results( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s ORDER BY option_id DESC\",\n-\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . self::SPAM_DATA_PREFIX ) . '%'\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t\twp_cache_set( $cache_key, $results, 'mo_osp' );\n-\t\t\t}\n-\n-\t\t\tif ( count( $results ) \u003C= $max_entries ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$entries = array();\n-\t\t\tforeach ( $results as $result ) {\n-\t\t\t\t$data = maybe_unserialize( $result->option_value );\n-\t\t\t\tif ( is_array( $data ) && isset( $data['last_attempt'] ) ) {\n-\t\t\t\t\t$entries[] = array(\n-\t\t\t\t\t\t'option_name'  => $result->option_name,\n-\t\t\t\t\t\t'last_attempt' => $data['last_attempt'],\n-\t\t\t\t\t);\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tusort(\n-\t\t\t\t$entries,\n-\t\t\t\tfunction ( $a, $b ) {\n-\t\t\t\t\treturn $b['last_attempt'] - $a['last_attempt'];\n-\t\t\t\t}\n-\t\t\t);\n-\n-\t\t\t$to_delete = array_slice( $entries, $max_entries );\n-\t\t\tforeach ( $to_delete as $entry ) {\n-\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $entry['option_name'] );\n-\n-\t\t\t\tdelete_mo_option( $option_key );\n-\t\t\t\twp_cache_delete( $entry['option_name'], 'mo_osp' );\n-\t\t\t}\n-\n-\t\t\twp_cache_delete( $cache_key, 'mo_osp' );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get masked version of identifier for logging.\n-\t\t *\n-\t\t * @param string $identifier The identifier to mask.\n-\t\t * @param string $type The type of identifier.\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tpublic function mosp_mask_identifier( $identifier, $type ) {\n-\t\t\tswitch ( $type ) {\n-\t\t\t\tcase 'phone':\n-\t\t\t\t\tif ( strlen( $identifier ) > 4 ) {\n-\t\t\t\t\t\treturn str_repeat( 'X', strlen( $identifier ) - 4 ) . substr( $identifier, -4 );\n-\t\t\t\t\t}\n-\t\t\t\t\treturn $identifier;\n-\n-\t\t\t\tcase 'email':\n-\t\t\t\t\t$parts = explode( '@', $identifier );\n-\t\t\t\t\tif ( count( $parts ) === 2 ) {\n-\t\t\t\t\t\t$username        = $parts[0];\n-\t\t\t\t\t\t$domain          = $parts[1];\n-\t\t\t\t\t\t$masked_username = strlen( $username ) > 2 ? substr( $username, 0, 1 ) . str_repeat( '*', strlen( $username ) - 2 ) . substr( $username, -1 ) : $username;\n-\t\t\t\t\t\treturn $masked_username . '@' . $domain;\n-\t\t\t\t\t}\n-\t\t\t\t\treturn $identifier;\n-\n-\t\t\t\tcase 'ip':\n-\t\t\t\t\t$parts = explode( '.', $identifier );\n-\t\t\t\t\tif ( count( $parts ) === 4 ) {\n-\t\t\t\t\t\treturn $parts[0] . '.' . $parts[1] . '.XXX.XXX';\n-\t\t\t\t\t}\n-\t\t\t\t\treturn $identifier;\n-\n-\t\t\t\tdefault:\n-\t\t\t\t\treturn substr( $identifier, 0, 8 ) . '...';\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Record attempt with timestamp (new method for integration).\n-\t\t *\n-\t\t * @param string $identifier The full identifier (e.g., 'email:user@example.com').\n-\t\t * @param int    $timestamp The attempt timestamp.\n-\t\t * @param array  $context Optional context data.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic function mosp_record_attempt_with_timestamp( $identifier, $timestamp, $context = array() ) {\n-\t\t\t$key  = $this->mosp_hash_key( $identifier );\n-\t\t\t$data = $this->mosp_get_spam_data( $key );\n-\n-\t\t\tif ( false === $data ) {\n-\t\t\t\t$data = array(\n-\t\t\t\t\t'attempts'      => array(),\n-\t\t\t\t\t'blocked_until' => 0,\n-\t\t\t\t\t'created'       => $timestamp,\n-\t\t\t\t);\n-\t\t\t}\n-\n-\t\t\tif ( is_array( $context ) ) {\n-\t\t\t\tif ( ! empty( $context['ip'] ) && filter_var( $context['ip'], FILTER_VALIDATE_IP ) ) {\n-\t\t\t\t\t$data['last_ip'] = $context['ip'];\n-\t\t\t\t}\n-\t\t\t\tif ( ! empty( $context['browser_id'] ) ) {\n-\t\t\t\t\t$data['last_browser'] = $context['browser_id'];\n-\t\t\t\t}\n-\t\t\t\tif ( ! empty( $context['email'] ) ) {\n-\t\t\t\t\t$data['last_email'] = strtolower( trim( (string) $context['email'] ) );\n-\t\t\t\t}\n-\t\t\t\tif ( ! empty( $context['phone'] ) ) {\n-\t\t\t\t\t$data['last_phone'] = trim( (string) $context['phone'] );\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( is_string( $identifier ) && strpos( $identifier, ':' ) !== false ) {\n-\t\t\t\tlist( $id_type, $id_value ) = explode( ':', $identifier, 2 );\n-\t\t\t\t$id_value                   = trim( (string) $id_value );\n-\t\t\t\tif ( ! empty( $id_value ) ) {\n-\t\t\t\t\tif ( 'email' === $id_type ) {\n-\t\t\t\t\t\t$data['last_email'] = strtolower( $id_value );\n-\t\t\t\t\t} elseif ( 'phone' === $id_type ) {\n-\t\t\t\t\t\t$data['last_phone'] = $id_value;\n-\t\t\t\t\t} elseif ( 'ip' === $id_type ) {\n-\t\t\t\t\t\t$data['last_ip'] = $id_value;\n-\t\t\t\t\t} elseif ( 'browser' === $id_type ) {\n-\t\t\t\t\t\t$data['last_browser'] = $id_value;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( ! isset( $data['attempts'] ) ) {\n-\t\t\t\t$data['attempts'] = array();\n-\t\t\t}\n-\n-\t\t\t$data['attempts'][]   = $timestamp;\n-\t\t\t$data['last_attempt'] = $timestamp;\n-\n-\t\t\t$cutoff           = $timestamp - ( 24 * 60 * 60 );\n-\t\t\t$data['attempts'] = array_filter(\n-\t\t\t\t$data['attempts'],\n-\t\t\t\tfunction ( $time ) use ( $cutoff ) {\n-\t\t\t\t\treturn $time > $cutoff;\n-\t\t\t\t}\n-\t\t\t);\n-\n-\t\t\t$this->mosp_update_spam_data( $key, $data );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get all currently blocked users.\n-\t\t *\n-\t\t * @param int $limit Maximum number of entries to return (default 100).\n-\t\t * @param int $offset Offset for pagination (default 0).\n-\t\t * @return array Array of blocked user data.\n-\t\t *\u002F\n-\t\tpublic function mosp_get_all_blocked_users( $limit = 100, $offset = 0 ) {\n-\t\t\treturn $this->mosp_get_blocked_users_from_rate_limits( $limit, $offset );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Delete all spam\u002Fblock rows, rate-limit options, and puzzle-requirement flags (admin \"clear all\").\n-\t\t *\n-\t\t * @return int Number of options deleted.\n-\t\t *\u002F\n-\t\tpublic function mosp_clear_all_otp_spam_data() {\n-\t\t\tglobal $wpdb;\n-\n-\t\t\t$deleted = 0;\n-\n-\t\t\t$like_patterns = array(\n-\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . self::SPAM_DATA_PREFIX ) . '%',\n-\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_' ) . '%',\n-\t\t\t);\n-\n-\t\t\tforeach ( $like_patterns as $like ) {\n-\t\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t\t$like\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t\tforeach ( $option_names as $option_name ) {\n-\t\t\t\t\tdelete_site_option( $option_name );\n-\t\t\t\t\t++$deleted;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$puzzle_like  = $wpdb->esc_like( 'mo_osp_puzzle_' ) . '%';\n-\t\t\t$puzzle_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t$puzzle_like\n-\t\t\t\t)\n-\t\t\t);\n-\t\t\tforeach ( $puzzle_names as $option_name ) {\n-\t\t\t\tdelete_option( $option_name );\n-\t\t\t\t++$deleted;\n-\t\t\t}\n-\n-\t\t\twp_cache_delete( 'mosp_blocked_users_list', 'mo_osp' );\n-\t\t\twp_cache_delete( 'mosp_spam_data_option_names', 'mo_osp' );\n-\t\t\twp_cache_delete( 'mosp_uninstall_spam_option_names', 'mo_osp' );\n-\t\t\twp_cache_delete( 'mosp_rate_limit_hourly_options', 'mo_osp' );\n-\t\t\twp_cache_delete( 'mosp_rate_limit_daily_options', 'mo_osp' );\n-\t\t\twp_cache_delete( 'mosp_rate_limit_hourly_option_names', 'mo_osp' );\n-\t\t\twp_cache_delete( 'mosp_rate_limit_daily_option_names', 'mo_osp' );\n-\n-\t\t\treturn $deleted;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get blocked users by checking rate limit data and spam data.\n-\t\t *\n-\t\t * @param int $limit Maximum number of entries to return.\n-\t\t * @param int $offset Offset for pagination.\n-\t\t * @return array Array of blocked user data.\n-\t\t *\u002F\n-\t\tpublic function mosp_get_blocked_users_from_rate_limits( $limit = 100, $offset = 0 ) {\n-\t\t\tglobal $wpdb;\n-\n-\t\t\t$now                = time();\n-\t\t\t$blocked            = array();\n-\t\t\t$settings           = $this->mosp_get_settings();\n-\t\t\t$window_types       = array( 'hourly', 'daily' );\n-\t\t\t$seen_hashes        = array();\n-\t\t\t$hash_to_identifier = array();\n-\t\t\t$priority           = array(\n-\t\t\t\t'phone'   => 3,\n-\t\t\t\t'email'   => 2,\n-\t\t\t\t'ip'      => 1,\n-\t\t\t\t'browser' => 0,\n-\t\t\t);\n-\n-\t\t\tforeach ( $window_types as $window_type ) {\n-\t\t\t\t$cache_key    = 'mosp_rate_limit_' . $window_type . '_options';\n-\t\t\t\t$option_names = wp_cache_get( $cache_key, 'mo_osp' );\n-\n-\t\t\t\tif ( false === $option_names ) {\n-\t\t\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_' . $window_type . '_' ) . '%'\n-\t\t\t\t\t\t)\n-\t\t\t\t\t);\n-\t\t\t\t\twp_cache_set( $cache_key, $option_names, 'mo_osp', 300 );\n-\t\t\t\t}\n-\n-\t\t\t\tforeach ( $option_names as $db_option_name ) {\n-\t\t\t\t\t$option_key     = str_replace( 'mo_customer_validation_', '', $db_option_name );\n-\t\t\t\t\t$rate_limit_key = str_replace( self::SPAM_DATA_PREFIX, '', $option_key );\n-\n-\t\t\t\t\t$key_parts = explode( '_', $rate_limit_key );\n-\t\t\t\t\tif ( count( $key_parts ) >= 4 ) {\n-\t\t\t\t\t\t$identifier_hash = $key_parts[3];\n-\n-\t\t\t\t\t\tif ( ! isset( $hash_to_identifier[ $identifier_hash ] ) ) {\n-\t\t\t\t\t\t\t$hash_to_identifier[ $identifier_hash ] = null;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$cache_key         = 'mosp_spam_data_option_names';\n-\t\t\t$spam_option_names = wp_cache_get( $cache_key, 'mo_osp' );\n-\n-\t\t\tif ( false === $spam_option_names ) {\n-\t\t\t\t$spam_option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . self::SPAM_DATA_PREFIX ) . '%'\n-\t\t\t\t\t)\n-\t\t\t\t);\n-\t\t\t\twp_cache_set( $cache_key, $spam_option_names, 'mo_osp', 300 );\n-\t\t\t}\n-\n-\t\t\tforeach ( $spam_option_names as $db_option_name ) {\n-\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\n-\n-\t\t\t\t$hash_key = str_replace( self::SPAM_DATA_PREFIX, '', $option_key );\n-\n-\t\t\t\tif ( strpos( $option_key, 'rate_limit_' ) !== false ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\t$spam_data = $this->mosp_get_spam_data( $hash_key );\n-\n-\t\t\t\tif ( false === $spam_data || ! is_array( $spam_data ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\t$blocked_until = isset( $spam_data['blocked_until'] ) ? (int) $spam_data['blocked_until'] : 0;\n-\t\t\t\t$block_reason  = isset( $spam_data['block_reason'] ) ? $spam_data['block_reason'] : '';\n-\n-\t\t\t\tif ( $blocked_until > $now && in_array( $block_reason, array( 'hourly_limit_exceeded', 'daily_limit_exceeded', 'max_attempts_exceeded' ), true ) ) {\n-\t\t\t\t\t$remaining_time = $blocked_until - $now;\n-\n-\t\t\t\t\t$identifier_type  = isset( $spam_data['type'] ) ? $spam_data['type'] : 'unknown';\n-\t\t\t\t\t$identifier_value = isset( $spam_data['identifier'] ) ? $spam_data['identifier'] : '';\n-\n-\t\t\t\t\tif ( ! empty( $identifier_value ) ) {\n-\t\t\t\t\t\t$identifier_display = $identifier_value;\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\t$identifier_display = 'User: ' . substr( $hash_key, -8 );\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tif ( 'unknown' === $identifier_type || 'identifier' === $identifier_type ) {\n-\t\t\t\t\t\t$identifier_info = $this->mosp_infer_identifier_from_hash( $hash_key, $spam_data );\n-\t\t\t\t\t\t$identifier_type = $identifier_info['type'];\n-\t\t\t\t\t\tif ( empty( $identifier_value ) && ! empty( $identifier_info['value'] ) ) {\n-\t\t\t\t\t\t\t$identifier_value   = $identifier_info['value'];\n-\t\t\t\t\t\t\t$identifier_display = $identifier_value;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$user_key = $block_reason . '_' . $blocked_until;\n-\n-\t\t\t\t\tif ( in_array( $hash_key, $seen_hashes, true ) ) {\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$is_duplicate = false;\n-\t\t\t\t\tforeach ( $blocked as $existing ) {\n-\t\t\t\t\t\tif ( $existing['block_reason'] === $block_reason &&\n-\t\t\t\t\t\t\tabs( $existing['blocked_until'] - $blocked_until ) \u003C 5 && \u002F\u002F Within 5 seconds.\n-\t\t\t\t\t\t\t'unknown' !== $existing['identifier_type'] &&\n-\t\t\t\t\t\t\t'unknown' !== $identifier_type ) {\n-\t\t\t\t\t\t\t$existing_priority = isset( $priority[ $existing['identifier_type'] ] ) ? $priority[ $existing['identifier_type'] ] : 0;\n-\t\t\t\t\t\t\t$current_priority  = isset( $priority[ $identifier_type ] ) ? $priority[ $identifier_type ] : 0;\n-\n-\t\t\t\t\t\t\tif ( $current_priority > $existing_priority ) {\n-\t\t\t\t\t\t\t\t$blocked      = array_filter(\n-\t\t\t\t\t\t\t\t\t$blocked,\n-\t\t\t\t\t\t\t\t\tfunction ( $item ) use ( $existing ) {\n-\t\t\t\t\t\t\t\t\t\treturn $item['identifier_hash'] !== $existing['identifier_hash'];\n-\t\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\t);\n-\t\t\t\t\t\t\t\t$blocked      = array_values( $blocked );\n-\t\t\t\t\t\t\t\t$is_duplicate = false;\n-\t\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t\t$is_duplicate = true;\n-\t\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tif ( $is_duplicate ) {\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$blocked[] = array(\n-\t\t\t\t\t\t'identifier_hash'   => $hash_key,\n-\t\t\t\t\t\t'identifier_masked' => $identifier_display,\n-\t\t\t\t\t\t'identifier_type'   => $identifier_type,\n-\t\t\t\t\t\t'identifier_value'  => $identifier_value,\n-\t\t\t\t\t\t'block_reason'      => $block_reason,\n-\t\t\t\t\t\t'blocked_until'     => $blocked_until,\n-\t\t\t\t\t\t'remaining_time'    => $remaining_time,\n-\t\t\t\t\t);\n-\n-\t\t\t\t\t$seen_hashes[] = $hash_key;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tforeach ( $window_types as $window_type ) {\n-\t\t\t\t$cache_key    = 'mosp_rate_limit_' . $window_type . '_options';\n-\t\t\t\t$option_names = wp_cache_get( $cache_key, 'mo_osp' );\n-\n-\t\t\t\tif ( false === $option_names ) {\n-\t\t\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n-\t\t\t\t\t\t$wpdb->prepare(\n-\t\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\n-\t\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_' . $window_type . '_' ) . '%'\n-\t\t\t\t\t\t)\n-\t\t\t\t\t);\n-\t\t\t\t\twp_cache_set( $cache_key, $option_names, 'mo_osp', 300 );\n-\t\t\t\t}\n-\n-\t\t\t\t$limit_value = 'hourly' === $window_type ? $settings['hourly_limit'] : $settings['daily_limit'];\n-\n-\t\t\t\tforeach ( $option_names as $db_option_name ) {\n-\t\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\n-\n-\t\t\t\t\t$rate_limit_key = str_replace( self::SPAM_DATA_PREFIX, '', $option_key );\n-\t\t\t\t\t$rate_data      = $this->mosp_get_spam_data( $rate_limit_key );\n-\n-\t\t\t\t\tif ( false === $rate_data || ! is_array( $rate_data ) || ! isset( $rate_data['attempts'] ) || ! is_array( $rate_data['attempts'] ) ) {\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t$window_seconds   = 'hourly' === $window_type ? 3600 : 86400;\n-\t\t\t\t\t$window_start     = $now - $window_seconds;\n-\t\t\t\t\t$current_attempts = count(\n-\t\t\t\t\t\tarray_filter(\n-\t\t\t\t\t\t\t$rate_data['attempts'],\n-\t\t\t\t\t\t\tfunction ( $timestamp ) use ( $window_start ) {\n-\t\t\t\t\t\t\t\treturn $timestamp > $window_start;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t)\n-\t\t\t\t\t);\n-\n-\t\t\t\t\tif ( $current_attempts >= $limit_value ) {\n-\t\t\t\t\t\t$key_parts = explode( '_', $rate_limit_key );\n-\t\t\t\t\t\tif ( count( $key_parts ) >= 4 ) {\n-\t\t\t\t\t\t\t$identifier_hash = $key_parts[3];\n-\n-\t\t\t\t\t\t\tif ( in_array( $identifier_hash, $seen_hashes, true ) ) {\n-\t\t\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\t\t$in_window = array_filter(\n-\t\t\t\t\t\t\t\t$rate_data['attempts'],\n-\t\t\t\t\t\t\t\tfunction ( $timestamp ) use ( $window_start ) {\n-\t\t\t\t\t\t\t\t\treturn $timestamp > $window_start;\n-\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t);\n-\n-\t\t\t\t\t\t\tif ( ! empty( $in_window ) ) {\n-\t\t\t\t\t\t\t\t$oldest_attempt = min( $in_window );\n-\t\t\t\t\t\t\t\t$reset_time     = $oldest_attempt + $window_seconds;\n-\t\t\t\t\t\t\t\t$remaining_time = max( 0, $reset_time - $now );\n-\n-\t\t\t\t\t\t\t\t$spam_data = $this->mosp_get_spam_data( $identifier_hash );\n-\n-\t\t\t\t\t\t\t\t$blocked_until = 0;\n-\t\t\t\t\t\t\t\t$block_reason  = $window_type . '_limit_exceeded';\n-\n-\t\t\t\t\t\t\t\tif ( false !== $spam_data && is_array( $spam_data ) && isset( $spam_data['blocked_until'] ) && $spam_data['blocked_until'] > $now ) {\n-\t\t\t\t\t\t\t\t\t$blocked_until  = $spam_data['blocked_until'];\n-\t\t\t\t\t\t\t\t\t$block_reason   = isset( $spam_data['block_reason'] ) ? $spam_data['block_reason'] : $block_reason;\n-\t\t\t\t\t\t\t\t\t$remaining_time = $blocked_until - $now;\n-\t\t\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\t\t\t$identifier_type    = 'unknown';\n-\t\t\t\t\t\t\t\t$identifier_value   = '';\n-\t\t\t\t\t\t\t\t$identifier_display = 'User: ' . substr( $identifier_hash, -8 );\n-\n-\t\t\t\t\t\t\t\tif ( false !== $spam_data && is_array( $spam_data ) ) {\n-\t\t\t\t\t\t\t\t\tif ( isset( $spam_data['type'] ) ) {\n-\t\t\t\t\t\t\t\t\t\t$identifier_type = $spam_data['type'];\n-\t\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\t\tif ( isset( $spam_data['identifier'] ) && ! empty( $spam_data['identifier'] ) ) {\n-\t\t\t\t\t\t\t\t\t\t$identifier_value   = $spam_data['identifier'];\n-\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\n-\t\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\t\t\tif ( empty( $identifier_value ) && isset( $rate_data['identifier'] ) ) {\n-\t\t\t\t\t\t\t\t\t$rate_identifier = $rate_data['identifier'];\n-\t\t\t\t\t\t\t\t\tif ( strpos( $rate_identifier, 'phone:' ) === 0 ) {\n-\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'phone';\n-\t\t\t\t\t\t\t\t\t\t$identifier_value   = substr( $rate_identifier, 6 );\n-\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\n-\t\t\t\t\t\t\t\t\t} elseif ( strpos( $rate_identifier, 'email:' ) === 0 ) {\n-\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'email';\n-\t\t\t\t\t\t\t\t\t\t$identifier_value   = substr( $rate_identifier, 6 );\n-\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\n-\t\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\t\t\tif ( 'unknown' === $identifier_type || 'identifier' === $identifier_type ) {\n-\t\t\t\t\t\t\t\t\tif ( ! empty( $spam_data['last_email'] ) ) {\n-\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'email';\n-\t\t\t\t\t\t\t\t\t\t$identifier_value   = $spam_data['last_email'];\n-\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\n-\t\t\t\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_phone'] ) ) {\n-\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'phone';\n-\t\t\t\t\t\t\t\t\t\t$identifier_value   = $spam_data['last_phone'];\n-\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\n-\t\t\t\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_ip'] ) ) {\n-\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'ip';\n-\t\t\t\t\t\t\t\t\t\t$identifier_value   = $spam_data['last_ip'];\n-\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\n-\t\t\t\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_browser'] ) ) {\n-\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'browser';\n-\t\t\t\t\t\t\t\t\t\t$identifier_value   = $spam_data['last_browser'];\n-\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\n-\t\t\t\t\t\t\t\t\t} elseif ( ! empty( $identifier_value ) && strpos( $identifier_value, '@' ) !== false ) {\n-\t\t\t\t\t\t\t\t\t\t$identifier_type = 'email';\n-\t\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\t\t\t$calculated_blocked_until = $blocked_until > 0 ? $blocked_until : ( $now + $remaining_time );\n-\t\t\t\t\t\t\t\t$is_duplicate             = false;\n-\t\t\t\t\t\t\t\tforeach ( $blocked as $existing ) {\n-\t\t\t\t\t\t\t\t\tif ( $existing['block_reason'] === $block_reason &&\n-\t\t\t\t\t\t\t\t\t\tabs( $existing['blocked_until'] - $calculated_blocked_until ) \u003C 5 ) {\n-\t\t\t\t\t\t\t\t\t\t$existing_priority = isset( $priority[ $existing['identifier_type'] ] ) ? $priority[ $existing['identifier_type'] ] : 0;\n-\t\t\t\t\t\t\t\t\t\t$current_priority  = isset( $priority[ $identifier_type ] ) ? $priority[ $identifier_type ] : 0;\n-\n-\t\t\t\t\t\t\t\t\t\tif ( $current_priority > $existing_priority ) {\n-\t\t\t\t\t\t\t\t\t\t\t$blocked      = array_filter(\n-\t\t\t\t\t\t\t\t\t\t\t\t$blocked,\n-\t\t\t\t\t\t\t\t\t\t\t\tfunction ( $item ) use ( $existing ) {\n-\t\t\t\t\t\t\t\t\t\t\t\t\treturn $item['identifier_hash'] !== $existing['identifier_hash'];\n-\t\t\t\t\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\t\t\t\t);\n-\t\t\t\t\t\t\t\t\t\t\t$blocked      = array_values( $blocked );\n-\t\t\t\t\t\t\t\t\t\t\t$is_duplicate = false;\n-\t\t\t\t\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t\t\t\t\t$is_duplicate = true;\n-\t\t\t\t\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\t\t\tif ( $is_duplicate ) {\n-\t\t\t\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\t\t\t$blocked[] = array(\n-\t\t\t\t\t\t\t\t\t'identifier_hash'   => $identifier_hash,\n-\t\t\t\t\t\t\t\t\t'identifier_masked' => $identifier_display,\n-\t\t\t\t\t\t\t\t\t'identifier_type'   => $identifier_type,\n-\t\t\t\t\t\t\t\t\t'identifier_value'  => $identifier_value,\n-\t\t\t\t\t\t\t\t\t'block_reason'      => $block_reason,\n-\t\t\t\t\t\t\t\t\t'blocked_until'     => $calculated_blocked_until,\n-\t\t\t\t\t\t\t\t\t'remaining_time'    => $remaining_time,\n-\t\t\t\t\t\t\t\t);\n-\n-\t\t\t\t\t\t\t\t$seen_hashes[] = $identifier_hash;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Sort by remaining time (longest first).\n-\t\t\tusort(\n-\t\t\t\t$blocked,\n-\t\t\t\tfunction ( $a, $b ) {\n-\t\t\t\t\treturn $b['remaining_time'] - $a['remaining_time'];\n-\t\t\t\t}\n-\t\t\t);\n-\n-\t\t\t$total   = count( $blocked );\n-\t\t\t$blocked = array_slice( $blocked, $offset, $limit );\n-\n-\t\t\treturn array(\n-\t\t\t\t'users' => $blocked,\n-\t\t\t\t'total' => $total,\n-\t\t\t);\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Infer identifier type and value from hash by checking rate limit data.\n-\t\t *\n-\t\t * @param string $hash The identifier hash.\n-\t\t * @param array  $spam_data The spam data array.\n-\t\t * @return array Array with 'type', 'value', and 'masked' keys (masked now contains original value).\n-\t\t *\u002F\n-\t\tprivate function mosp_infer_identifier_from_hash( $hash, $spam_data ) {\n-\t\t\tglobal $wpdb;\n-\n-\t\t\t$result = array(\n-\t\t\t\t'type'   => 'unknown',\n-\t\t\t\t'value'  => '',\n-\t\t\t\t'masked' => 'User: ' . substr( $hash, -8 ),\n-\t\t\t);\n-\n-\t\t\tif ( isset( $spam_data['identifier'] ) && ! empty( $spam_data['identifier'] ) ) {\n-\t\t\t\t$result['value']  = $spam_data['identifier'];\n-\t\t\t\t$result['masked'] = $spam_data['identifier'];\n-\t\t\t}\n-\n-\t\t\tif ( isset( $spam_data['type'] ) && 'identifier' !== $spam_data['type'] && 'unknown' !== $spam_data['type'] ) {\n-\t\t\t\t$result['type'] = $spam_data['type'];\n-\t\t\t}\n-\n-\t\t\t$window_types = array( 'hourly', 'daily' );\n-\t\t\tforeach ( $window_types as $window_type ) {\n-\t\t\t\t$rate_limit_key = 'rate_limit_' . $window_type . '_' . $hash;\n-\t\t\t\t$rate_data      = $this->mosp_get_spam_data( $rate_limit_key );\n-\n-\t\t\t\tif ( false !== $rate_data && is_array( $rate_data ) ) {\n-\t\t\t\t\tif ( isset( $rate_data['identifier'] ) && ! empty( $rate_data['identifier'] ) ) {\n-\t\t\t\t\t\t$rate_identifier = $rate_data['identifier'];\n-\t\t\t\t\t\tif ( strpos( $rate_identifier, 'phone:' ) === 0 ) {\n-\t\t\t\t\t\t\t$result['type']   = 'phone';\n-\t\t\t\t\t\t\t$result['value']  = substr( $rate_identifier, 6 );\n-\t\t\t\t\t\t\t$result['masked'] = $result['value'];\n-\t\t\t\t\t\t} elseif ( strpos( $rate_identifier, 'email:' ) === 0 ) {\n-\t\t\t\t\t\t\t$result['type']   = 'email';\n-\t\t\t\t\t\t\t$result['value']  = substr( $rate_identifier, 6 );\n-\t\t\t\t\t\t\t$result['masked'] = $result['value'];\n-\t\t\t\t\t\t}\n-\t\t\t\t\t} elseif ( 'unknown' === $result['type'] ) {\n-\t\t\t\t\t\tif ( ! empty( $spam_data['last_email'] ) ) {\n-\t\t\t\t\t\t\t$result['type']   = 'email';\n-\t\t\t\t\t\t\t$result['value']  = $spam_data['last_email'];\n-\t\t\t\t\t\t\t$result['masked'] = $result['value'];\n-\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_phone'] ) ) {\n-\t\t\t\t\t\t\t$result['type']   = 'phone';\n-\t\t\t\t\t\t\t$result['value']  = $spam_data['last_phone'];\n-\t\t\t\t\t\t\t$result['masked'] = $result['value'];\n-\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_ip'] ) ) {\n-\t\t\t\t\t\t\t$result['type']   = 'ip';\n-\t\t\t\t\t\t\t$result['value']  = $spam_data['last_ip'];\n-\t\t\t\t\t\t\t$result['masked'] = $result['value'];\n-\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_browser'] ) ) {\n-\t\t\t\t\t\t\t$result['type']   = 'browser';\n-\t\t\t\t\t\t\t$result['value']  = $spam_data['last_browser'];\n-\t\t\t\t\t\t\t$result['masked'] = $result['value'];\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t\tbreak;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( 'unknown' === $result['type'] ) {\n-\t\t\t\tif ( ! empty( $spam_data['last_email'] ) ) {\n-\t\t\t\t\t$result['type']   = 'email';\n-\t\t\t\t\t$result['value']  = $spam_data['last_email'];\n-\t\t\t\t\t$result['masked'] = $result['value'];\n-\t\t\t\t} elseif ( ! empty( $spam_data['last_phone'] ) ) {\n-\t\t\t\t\t$result['type']   = 'phone';\n-\t\t\t\t\t$result['value']  = $spam_data['last_phone'];\n-\t\t\t\t\t$result['masked'] = $result['value'];\n-\t\t\t\t} elseif ( ! empty( $spam_data['last_ip'] ) ) {\n-\t\t\t\t\t$result['type']   = 'ip';\n-\t\t\t\t\t$result['value']  = $spam_data['last_ip'];\n-\t\t\t\t\t$result['masked'] = $result['value'];\n-\t\t\t\t} elseif ( ! empty( $spam_data['last_browser'] ) ) {\n-\t\t\t\t\t$result['type']   = 'browser';\n-\t\t\t\t\t$result['value']  = $spam_data['last_browser'];\n-\t\t\t\t\t$result['masked'] = $result['value'];\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn $result;\n-\t\t}\n-\t}\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * OTP Spam Storage Handler\r\n+ *\r\n+ * @package otpspampreventer\u002Fhandler\r\n+ *\u002F\r\n+\r\n+namespace OSP\\Handler;\r\n+\r\n+use OSP\\Traits\\Instance;\r\n+use OSP\\Helper\\MoSecurityHelper;\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+if ( ! class_exists( 'MoOtpSpamStorage' ) ) {\r\n+\t\u002F**\r\n+\t * The class handles storage and retrieval of spam prevention data.\r\n+\t * Uses WordPress options table to store hashed keys and attempt data.\r\n+\t *\u002F\r\n+\tclass MoOtpSpamStorage {\r\n+\r\n+\t\tuse Instance;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Option name prefix for spam data\r\n+\t\t *\u002F\r\n+\t\tconst SPAM_DATA_PREFIX = 'mo_osp_spam_data_';\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Option name for global settings\r\n+\t\t *\u002F\r\n+\t\tconst SETTINGS_OPTION = 'mo_osp_settings';\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Maximum number of entries to keep in storage\r\n+\t\t *\u002F\r\n+\t\tconst MAX_ENTRIES = 10000;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Constructor\r\n+\t\t *\u002F\r\n+\t\tpublic function __construct() {\r\n+\t\t\t\u002F\u002F Schedule cleanup hook.\r\n+\t\t\tif ( ! wp_next_scheduled( 'mo_osp_cleanup_expired' ) ) {\r\n+\t\t\t\twp_schedule_event( time(), 'hourly', 'mo_osp_cleanup_expired' );\r\n+\t\t\t}\r\n+\t\t\tadd_action( 'mo_osp_cleanup_expired', array( $this, 'mosp_cleanup_expired_entries' ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Generate a secure hash for storing identifiers.\r\n+\t\t *\r\n+\t\t * @param string $value The value to hash (phone\u002Femail\u002Fip\u002Fbrowser_id).\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_hash_key( $value ) {\r\n+\t\t\treturn hash( 'sha256', strtolower( trim( (string) $value ) ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get spam data for a given key.\r\n+\t\t *\r\n+\t\t * @param string $key The hashed key.\r\n+\t\t * @return array|false\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_get_spam_data( $key ) {\r\n+\t\t\t$option_name = self::SPAM_DATA_PREFIX . $key;\r\n+\t\t\t$data        = get_mo_option( $option_name );\r\n+\t\t\tif ( false === $data ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( is_string( $data ) ) {\r\n+\t\t\t\t$data = maybe_unserialize( $data );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! is_array( $data ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( isset( $data['attempts'] ) && ! is_array( $data['attempts'] ) ) {\r\n+\t\t\t\t$data['attempts'] = array();\r\n+\t\t\t} elseif ( ! isset( $data['attempts'] ) ) {\r\n+\t\t\t\t$data['attempts'] = array();\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $data;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Update spam data for a given key.\r\n+\t\t *\r\n+\t\t * @param string $key The hashed key.\r\n+\t\t * @param array  $data The spam data.\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_update_spam_data( $key, $data ) {\r\n+\t\t\t$option_name = self::SPAM_DATA_PREFIX . $key;\r\n+\r\n+\t\t\tupdate_mo_option( $option_name, maybe_serialize( $data ) );\r\n+\r\n+\t\t\t$saved_data = $this->mosp_get_spam_data( $key );\r\n+\r\n+\t\t\t$success = false;\r\n+\t\t\tif ( false !== $saved_data && is_array( $saved_data ) ) {\r\n+\t\t\t\t$key_fields_match = true;\r\n+\t\t\t\tif ( isset( $data['blocked_until'] ) ) {\r\n+\t\t\t\t\t$key_fields_match = $key_fields_match && ( isset( $saved_data['blocked_until'] ) && (int) $saved_data['blocked_until'] === (int) $data['blocked_until'] );\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( isset( $data['block_reason'] ) ) {\r\n+\t\t\t\t\t$key_fields_match = $key_fields_match && ( isset( $saved_data['block_reason'] ) && $saved_data['block_reason'] === $data['block_reason'] );\r\n+\t\t\t\t}\r\n+\t\t\t\t$success = $key_fields_match;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $success;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Delete spam data for a given key.\r\n+\t\t *\r\n+\t\t * @param string $key The hashed key.\r\n+\t\t * @return bool|void\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_delete_spam_data( $key ) {\r\n+\t\t\t$option_name = self::SPAM_DATA_PREFIX . $key;\r\n+\t\t\twp_cache_delete( $option_name, 'mo_osp' );\r\n+\t\t\treturn delete_mo_option( $option_name );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Cached settings.\r\n+\t\t *\r\n+\t\t * @var array|null\r\n+\t\t *\u002F\r\n+\t\tprivate static $cached_settings = null;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Flag to track if settings have been logged (to avoid spam in logs).\r\n+\t\t *\r\n+\t\t * @var bool\r\n+\t\t *\u002F\r\n+\t\tprivate static $settings_logged = false;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get addon settings.\r\n+\t\t *\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_get_settings() {\r\n+\t\t\tif ( null !== self::$cached_settings ) {\r\n+\t\t\t\treturn self::$cached_settings;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$defaults = array(\r\n+\t\t\t\t'enabled'       => false,\r\n+\t\t\t\t'cooldown_time' => 60,\r\n+\t\t\t\t'max_attempts'  => 3,\r\n+\t\t\t\t'block_time'    => 900,\r\n+\t\t\t\t'daily_limit'   => 10,\r\n+\t\t\t\t'hourly_limit'  => 5,\r\n+\t\t\t\t'track_phone'   => true,\r\n+\t\t\t\t'track_email'   => true,\r\n+\t\t\t\t'track_ip'      => true,\r\n+\t\t\t\t'track_browser' => true,\r\n+\t\t\t\t'whitelist_ips' => array(),\r\n+\t\t\t);\r\n+\r\n+\t\t\t$settings = get_mo_option( self::SETTINGS_OPTION );\r\n+\r\n+\t\t\tif ( false === $settings || ! is_array( $settings ) ) {\r\n+\t\t\t\t$settings = $defaults;\r\n+\t\t\t} else {\r\n+\t\t\t\t$settings = wp_parse_args( $settings, $defaults );\r\n+\r\n+\t\t\t\tif ( isset( $settings['whitelist_ips'] ) && is_string( $settings['whitelist_ips'] ) ) {\r\n+\t\t\t\t\tif ( ! empty( $settings['whitelist_ips'] ) ) {\r\n+\t\t\t\t\t\t$split_by_newline = array_filter( array_map( 'trim', explode( \"\\n\", $settings['whitelist_ips'] ) ) );\r\n+\t\t\t\t\t\tif ( count( $split_by_newline ) === 1 && strpos( $split_by_newline[0], ' ' ) !== false ) {\r\n+\t\t\t\t\t\t\t$settings['whitelist_ips'] = array_filter( array_map( 'trim', explode( ' ', $settings['whitelist_ips'] ) ) );\r\n+\t\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t\t$settings['whitelist_ips'] = $split_by_newline;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t$settings['whitelist_ips'] = array_values( $settings['whitelist_ips'] );\r\n+\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t$settings['whitelist_ips'] = array();\r\n+\t\t\t\t\t}\r\n+\t\t\t\t} elseif ( isset( $settings['whitelist_ips'] ) && is_array( $settings['whitelist_ips'] ) ) {\r\n+\t\t\t\t\t$cleaned_ips = array();\r\n+\t\t\t\t\tforeach ( $settings['whitelist_ips'] as $ip_item ) {\r\n+\t\t\t\t\t\t$ip_item = trim( $ip_item );\r\n+\t\t\t\t\t\tif ( empty( $ip_item ) ) {\r\n+\t\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t\tif ( strpos( $ip_item, ' ' ) !== false ) {\r\n+\t\t\t\t\t\t\t$split_ips   = array_filter( array_map( 'trim', explode( ' ', $ip_item ) ) );\r\n+\t\t\t\t\t\t\t$cleaned_ips = array_merge( $cleaned_ips, $split_ips );\r\n+\t\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t\t$cleaned_ips[] = $ip_item;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t$settings['whitelist_ips'] = array_values( array_unique( $cleaned_ips ) );\r\n+\t\t\t\t} elseif ( ! isset( $settings['whitelist_ips'] ) || ! is_array( $settings['whitelist_ips'] ) ) {\r\n+\t\t\t\t\t$settings['whitelist_ips'] = array();\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tself::$cached_settings = $settings;\r\n+\r\n+\t\t\tif ( ! self::$settings_logged ) {\r\n+\t\t\t\tself::$settings_logged = true;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $settings;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Update addon settings.\r\n+\t\t *\r\n+\t\t * @param array $settings The settings array.\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_update_settings( $settings ) {\r\n+\t\t\tupdate_mo_option( self::SETTINGS_OPTION, $settings );\r\n+\r\n+\t\t\tself::$cached_settings = null;\r\n+\r\n+\t\t\t$saved_settings = get_mo_option( self::SETTINGS_OPTION );\r\n+\t\t\t$success        = ( $saved_settings === $settings );\r\n+\r\n+\t\t\treturn $success;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Record an OTP attempt.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier (phone\u002Femail\u002Fip\u002Fbrowser).\r\n+\t\t * @param string $type The type of identifier.\r\n+\t\t * @return array The updated attempt data.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_record_attempt( $identifier, $type, $context = array() ) {\r\n+\t\t\t$key  = $this->mosp_hash_key( $identifier );\r\n+\t\t\t$data = $this->mosp_get_spam_data( $key );\r\n+\t\t\t$now  = time();\r\n+\r\n+\t\t\tif ( false === $data ) {\r\n+\t\t\t\t$data = array(\r\n+\t\t\t\t\t'type'          => $type,\r\n+\t\t\t\t\t'attempts'      => array(),\r\n+\t\t\t\t\t'blocked_until' => 0,\r\n+\t\t\t\t\t'total_blocks'  => 0,\r\n+\t\t\t\t\t'created'       => $now,\r\n+\t\t\t\t\t'last_attempt'  => $now,\r\n+\t\t\t\t);\r\n+\t\t\t} else {\r\n+\t\t\t\tif ( ! isset( $data['type'] ) || 'identifier' === $data['type'] || 'unknown' === $data['type'] ) {\r\n+\t\t\t\t\t$data['type'] = $type;\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( ! isset( $data['identifier'] ) && ! empty( $identifier ) ) {\r\n+\t\t\t\t\tif ( strpos( $identifier, 'email:' ) === 0 ) {\r\n+\t\t\t\t\t\t$data['identifier'] = substr( $identifier, 6 );\r\n+\t\t\t\t\t} elseif ( strpos( $identifier, 'phone:' ) === 0 ) {\r\n+\t\t\t\t\t\t$data['identifier'] = substr( $identifier, 6 );\r\n+\t\t\t\t\t} elseif ( strpos( $identifier, 'ip:' ) === 0 ) {\r\n+\t\t\t\t\t\t$data['identifier'] = substr( $identifier, 3 );\r\n+\t\t\t\t\t} elseif ( strpos( $identifier, 'browser:' ) === 0 ) {\r\n+\t\t\t\t\t\t$data['identifier'] = substr( $identifier, 8 );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( is_array( $context ) ) {\r\n+\t\t\t\tif ( ! empty( $context['ip'] ) && filter_var( $context['ip'], FILTER_VALIDATE_IP ) ) {\r\n+\t\t\t\t\t$data['last_ip'] = $context['ip'];\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( ! empty( $context['browser_id'] ) ) {\r\n+\t\t\t\t\t$data['last_browser'] = $context['browser_id'];\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( ! empty( $context['email'] ) ) {\r\n+\t\t\t\t\t$data['last_email'] = strtolower( trim( (string) $context['email'] ) );\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( ! empty( $context['phone'] ) ) {\r\n+\t\t\t\t\t$data['last_phone'] = trim( (string) $context['phone'] );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$attempts_before = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\r\n+\r\n+\t\t\t$data['attempts'][]   = $now;\r\n+\t\t\t$data['last_attempt'] = $now;\r\n+\r\n+\t\t\t$settings    = $this->mosp_get_settings();\r\n+\t\t\t$time_window = MoSecurityHelper::COUNTING_WINDOW_SECONDS;\r\n+\t\t\t$cutoff_time = $now - $time_window;\r\n+\r\n+\t\t\t$data['attempts'] = array_filter(\r\n+\t\t\t\t$data['attempts'],\r\n+\t\t\t\tfunction ( $timestamp ) use ( $cutoff_time ) {\r\n+\t\t\t\t\treturn $timestamp > $cutoff_time;\r\n+\t\t\t\t}\r\n+\t\t\t);\r\n+\r\n+\t\t\t$data['attempts'] = array_values( $data['attempts'] );\r\n+\r\n+\t\t\t$attempts_after = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\r\n+\r\n+\t\t\t$this->mosp_update_spam_data( $key, $data );\r\n+\r\n+\t\t\treturn $data;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if an identifier is blocked.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to check.\r\n+\t\t * @return array Block status information.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_is_blocked( $identifier ) {\r\n+\t\t\t$key      = $this->mosp_hash_key( $identifier );\r\n+\t\t\t$data     = $this->mosp_get_spam_data( $key );\r\n+\t\t\t$settings = $this->mosp_get_settings();\r\n+\t\t\t$now      = time();\r\n+\r\n+\t\t\tif ( false === $data ) {\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'blocked'       => false,\r\n+\t\t\t\t\t'reason'        => '',\r\n+\t\t\t\t\t'blocked_until' => 0,\r\n+\t\t\t\t\t'attempts'      => 0,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( isset( $data['blocked_until'] ) && $data['blocked_until'] > 0 && $data['blocked_until'] \u003C= $now ) {\r\n+\t\t\t\t$block_reason = isset( $data['block_reason'] ) ? $data['block_reason'] : 'unknown';\r\n+\r\n+\t\t\t\tif ( 'max_attempts_exceeded' === $block_reason ) {\r\n+\r\n+\t\t\t\t\tif ( strpos( $identifier, ':' ) !== false ) {\r\n+\t\t\t\t\t\tlist( $id_type, $id_value ) = explode( ':', $identifier, 2 );\r\n+\t\t\t\t\t\t$this->mosp_mark_puzzle_required( $id_value );\r\n+\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t$this->mosp_mark_puzzle_required( $identifier );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t$attempts_before_clear = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\r\n+\t\t\t\t\t$data['attempts']      = array();\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\t$time_window = MoSecurityHelper::COUNTING_WINDOW_SECONDS;\r\n+\t\t\t\t\t$cutoff_time = $now - $time_window;\r\n+\r\n+\t\t\t\t\t$attempts_before_clean = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\r\n+\r\n+\t\t\t\t\tif ( isset( $data['attempts'] ) && is_array( $data['attempts'] ) ) {\r\n+\t\t\t\t\t\t$data['attempts'] = array_filter(\r\n+\t\t\t\t\t\t\t$data['attempts'],\r\n+\t\t\t\t\t\t\tfunction ( $timestamp ) use ( $cutoff_time ) {\r\n+\t\t\t\t\t\t\t\treturn $timestamp > $cutoff_time;\r\n+\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t);\r\n+\t\t\t\t\t\t$data['attempts'] = array_values( $data['attempts'] );\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$attempts_after_clean = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$data['blocked_until'] = 0;\r\n+\t\t\t\t$data['block_reason']  = '';\r\n+\r\n+\t\t\t\t$this->mosp_update_spam_data( $this->mosp_hash_key( $identifier ), $data );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( 0 === $data['blocked_until'] && isset( $data['block_count'] ) && $data['block_count'] > 0 ) {\r\n+\t\t\t\tif ( strpos( $identifier, ':' ) !== false ) {\r\n+\t\t\t\t\tlist( $id_type, $id_value ) = explode( ':', $identifier, 2 );\r\n+\t\t\t\t\t$existing_puzzle            = $this->mosp_is_puzzle_required( $id_value );\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\t$existing_puzzle = $this->mosp_is_puzzle_required( $identifier );\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\tif ( ! $existing_puzzle ) {\r\n+\t\t\t\t\tif ( strpos( $identifier, ':' ) !== false ) {\r\n+\t\t\t\t\t\tlist( $id_type, $id_value ) = explode( ':', $identifier, 2 );\r\n+\t\t\t\t\t\t$this->mosp_mark_puzzle_required( $id_value );\r\n+\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t$this->mosp_mark_puzzle_required( $identifier );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $data['blocked_until'] > $now ) {\r\n+\t\t\t\t$remaining    = $data['blocked_until'] - $now;\r\n+\t\t\t\t$block_reason = isset( $data['block_reason'] ) && ! empty( $data['block_reason'] ) ? $data['block_reason'] : 'temporarily_blocked';\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'blocked'       => true,\r\n+\t\t\t\t\t'reason'        => $block_reason,\r\n+\t\t\t\t\t'blocked_until' => $data['blocked_until'],\r\n+\t\t\t\t\t'attempts'      => isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\t$cooldown_time = $settings['cooldown_time'];\r\n+\t\t\t$attempts      = isset( $data['attempts'] ) ? $data['attempts'] : array();\r\n+\r\n+\t\t\t$previous_attempt = null;\r\n+\t\t\tif ( count( $attempts ) >= 2 ) {\r\n+\t\t\t\t$sorted_attempts = $attempts;\r\n+\t\t\t\trsort( $sorted_attempts );\r\n+\t\t\t\t$most_recent_attempt    = $sorted_attempts[0];\r\n+\t\t\t\t$second_to_last_attempt = $sorted_attempts[1];\r\n+\r\n+\t\t\t\t$time_between_attempts = $most_recent_attempt - $second_to_last_attempt;\r\n+\r\n+\t\t\t\tif ( $time_between_attempts \u003C $cooldown_time ) {\r\n+\t\t\t\t\t$previous_attempt = $second_to_last_attempt;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $previous_attempt && ( $now - $previous_attempt ) \u003C $cooldown_time ) {\r\n+\t\t\t\t$time_since_previous = $now - $previous_attempt;\r\n+\r\n+\t\t\t\t$calculated_blocked_until = $previous_attempt + $cooldown_time;\r\n+\r\n+\t\t\t\tif ( ! isset( $data['blocked_until'] ) || $data['blocked_until'] !== $calculated_blocked_until ) {\r\n+\t\t\t\t\tif ( $calculated_blocked_until > $now ) {\r\n+\t\t\t\t\t\t$data['blocked_until'] = $calculated_blocked_until;\r\n+\t\t\t\t\t\t$data['block_reason']  = 'cooldown';\r\n+\t\t\t\t\t\t$this->mosp_update_spam_data( $key, $data );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$blocked_until = isset( $data['blocked_until'] ) && $data['blocked_until'] > $now ? $data['blocked_until'] : $calculated_blocked_until;\r\n+\t\t\t\t$remaining     = $blocked_until - $now;\r\n+\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'blocked'       => true,\r\n+\t\t\t\t\t'reason'        => 'cooldown',\r\n+\t\t\t\t\t'blocked_until' => $blocked_until,\r\n+\t\t\t\t\t'attempts'      => isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0,\r\n+\t\t\t\t\t'remaining'     => $remaining,\r\n+\t\t\t\t);\r\n+\t\t\t} elseif ( $previous_attempt ) {\r\n+\t\t\t\t\t$time_since_previous = $now - $previous_attempt;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$time_window      = MoSecurityHelper::COUNTING_WINDOW_SECONDS;\r\n+\t\t\t$cutoff_time      = $now - $time_window;\r\n+\t\t\t$data['attempts'] = array_filter(\r\n+\t\t\t\t$data['attempts'],\r\n+\t\t\t\tfunction ( $timestamp ) use ( $cutoff_time ) {\r\n+\t\t\t\t\treturn $timestamp > $cutoff_time;\r\n+\t\t\t\t}\r\n+\t\t\t);\r\n+\r\n+\t\t\t$max_attempts   = $settings['max_attempts'];\r\n+\t\t\t$attempts_count = isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0;\r\n+\r\n+\t\t\tif ( $attempts_count > $max_attempts ) {\r\n+\t\t\t\t$block_time_seconds    = $settings['block_time'];\r\n+\t\t\t\t$data['blocked_until'] = $now + $block_time_seconds;\r\n+\t\t\t\t$data['block_reason']  = 'max_attempts_exceeded';\r\n+\t\t\t\tif ( ! isset( $data['total_blocks'] ) ) {\r\n+\t\t\t\t\t$data['total_blocks'] = 0;\r\n+\t\t\t\t}\r\n+\t\t\t\t++$data['total_blocks'];\r\n+\t\t\t\t$this->mosp_update_spam_data( $key, $data );\r\n+\r\n+\t\t\t\treturn array(\r\n+\t\t\t\t\t'blocked'       => true,\r\n+\t\t\t\t\t'reason'        => 'max_attempts_exceeded',\r\n+\t\t\t\t\t'blocked_until' => $data['blocked_until'],\r\n+\t\t\t\t\t'attempts'      => $attempts_count,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn array(\r\n+\t\t\t\t'blocked'       => false,\r\n+\t\t\t\t'reason'        => '',\r\n+\t\t\t\t'blocked_until' => 0,\r\n+\t\t\t\t'attempts'      => isset( $data['attempts'] ) && is_array( $data['attempts'] ) ? count( $data['attempts'] ) : 0,\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if identifier is whitelisted.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to check.\r\n+\t\t * @param string $type The type of identifier.\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_is_whitelisted( $identifier, $type ) {\r\n+\t\t\t$settings = $this->mosp_get_settings();\r\n+\r\n+\t\t\tswitch ( $type ) {\r\n+\t\t\t\tcase 'ip':\r\n+\t\t\t\t\t$identifier = trim( $identifier );\r\n+\r\n+\t\t\t\t\tif ( empty( $identifier ) || ! filter_var( $identifier, FILTER_VALIDATE_IP ) ) {\r\n+\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$raw_whitelist = isset( $settings['whitelist_ips'] ) ? $settings['whitelist_ips'] : array();\r\n+\r\n+\t\t\t\t\tif ( is_string( $raw_whitelist ) ) {\r\n+\t\t\t\t\t\tif ( ! empty( $raw_whitelist ) ) {\r\n+\t\t\t\t\t\t\t$raw_whitelist = array_filter( array_map( 'trim', explode( \"\\n\", $raw_whitelist ) ) );\r\n+\t\t\t\t\t\t\t$raw_whitelist = array_values( $raw_whitelist );\r\n+\t\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t\t$raw_whitelist = array();\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\tif ( ! empty( $raw_whitelist ) && is_array( $raw_whitelist ) ) {\r\n+\t\t\t\t\t\t$whitelist_ips = array_map( 'trim', $raw_whitelist );\r\n+\t\t\t\t\t\t$whitelist_ips = array_filter( $whitelist_ips );\r\n+\t\t\t\t\t\t$whitelist_ips = array_values( $whitelist_ips );\r\n+\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t$whitelist_ips = array();\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\tforeach ( $whitelist_ips as $whitelist_ip ) {\r\n+\t\t\t\t\t\t$whitelist_ip = trim( $whitelist_ip );\r\n+\t\t\t\t\t\tif ( empty( $whitelist_ip ) ) {\r\n+\t\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\tif ( $identifier === $whitelist_ip ) {\r\n+\t\t\t\t\t\t\treturn true;\r\n+\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\tif ( strpos( $whitelist_ip, '\u002F' ) !== false ) {\r\n+\t\t\t\t\t\t\tif ( $this->mosp_ip_in_range( $identifier, $whitelist_ip ) ) {\r\n+\t\t\t\t\t\t\t\treturn true;\r\n+\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\t$identifier_is_ipv6 = filter_var( $identifier, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );\r\n+\t\t\t\t\t\t$whitelist_is_ipv6  = filter_var( $whitelist_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );\r\n+\r\n+\t\t\t\t\t\tif ( $identifier_is_ipv6 && $whitelist_is_ipv6 ) {\r\n+\t\t\t\t\t\t\t$normalized_identifier = $this->mosp_normalize_ipv6( $identifier );\r\n+\t\t\t\t\t\t\t$normalized_whitelist  = $this->mosp_normalize_ipv6( $whitelist_ip );\r\n+\t\t\t\t\t\t\tif ( $normalized_identifier === $normalized_whitelist ) {\r\n+\t\t\t\t\t\t\t\treturn true;\r\n+\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\t$identifier_is_ipv4 = filter_var( $identifier, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 );\r\n+\t\t\t\t\t\t$whitelist_is_ipv4  = filter_var( $whitelist_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 );\r\n+\r\n+\t\t\t\t\t\tif ( $identifier_is_ipv4 && $whitelist_is_ipv4 ) {\r\n+\t\t\t\t\t\t\tif ( $identifier === $whitelist_ip ) {\r\n+\t\t\t\t\t\t\t\treturn true;\r\n+\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\tdefault:\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Normalize IPv6 address to canonical form.\r\n+\t\t *\r\n+\t\t * @param string $ip IPv6 address.\r\n+\t\t * @return string Normalized IPv6 address or original IP if not IPv6.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_normalize_ipv6( $ip ) {\r\n+\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {\r\n+\t\t\t\treturn $ip;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( function_exists( 'inet_pton' ) && function_exists( 'inet_ntop' ) ) {\r\n+\t\t\t\t$packed = inet_pton( $ip );\r\n+\t\t\t\tif ( false !== $packed ) {\r\n+\t\t\t\t\t$normalized = inet_ntop( $packed );\r\n+\t\t\t\t\tif ( false !== $normalized ) {\r\n+\t\t\t\t\t\treturn strtolower( $normalized );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn strtolower( $ip );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if IP is in CIDR range (supports both IPv4 and IPv6).\r\n+\t\t *\r\n+\t\t * @param string $ip IP address to check.\r\n+\t\t * @param string $range CIDR range (e.g., \"192.168.1.0\u002F24\" or \"2001:db8::\u002F32\").\r\n+\t\t * @return bool True if IP is in range.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_ip_in_range( $ip, $range ) {\r\n+\t\t\tif ( strpos( $range, '\u002F' ) === false ) {\r\n+\t\t\t\treturn $ip === $range;\r\n+\t\t\t}\r\n+\r\n+\t\t\tlist( $subnet, $bits ) = explode( '\u002F', $range );\r\n+\t\t\t$bits                  = (int) $bits;\r\n+\r\n+\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP ) || ! filter_var( $subnet, FILTER_VALIDATE_IP ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) && filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {\r\n+\t\t\t\tif ( $bits \u003C 0 || $bits > 32 ) {\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t\t$ip_long     = ip2long( $ip );\r\n+\t\t\t\t$subnet_long = ip2long( $subnet );\r\n+\t\t\t\tif ( false === $ip_long || false === $subnet_long ) {\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t\t$mask         = -1 \u003C\u003C ( 32 - $bits );\r\n+\t\t\t\t$subnet_long &= $mask;\r\n+\t\t\t\treturn ( $ip_long & $mask ) === $subnet_long;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) && filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {\r\n+\t\t\t\tif ( $bits \u003C 0 || $bits > 128 ) {\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( function_exists( 'inet_pton' ) ) {\r\n+\t\t\t\t\t$ip_packed     = inet_pton( $ip );\r\n+\t\t\t\t\t$subnet_packed = inet_pton( $subnet );\r\n+\t\t\t\t\tif ( false === $ip_packed || false === $subnet_packed ) {\r\n+\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$ip_bytes     = unpack( 'C*', $ip_packed );\r\n+\t\t\t\t\t$subnet_bytes = unpack( 'C*', $subnet_packed );\r\n+\r\n+\t\t\t\t\t$full_bytes   = intval( $bits \u002F 8 );\r\n+\t\t\t\t\t$partial_bits = $bits % 8;\r\n+\r\n+\t\t\t\t\tfor ( $i = 1; $i \u003C= $full_bytes; $i++ ) {\r\n+\t\t\t\t\t\tif ( ! isset( $ip_bytes[ $i ] ) || ! isset( $subnet_bytes[ $i ] ) ) {\r\n+\t\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t\tif ( $ip_bytes[ $i ] !== $subnet_bytes[ $i ] ) {\r\n+\t\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\tif ( $partial_bits > 0 && $full_bytes \u003C 16 ) {\r\n+\t\t\t\t\t\t$byte_index = $full_bytes + 1;\r\n+\t\t\t\t\t\tif ( ! isset( $ip_bytes[ $byte_index ] ) || ! isset( $subnet_bytes[ $byte_index ] ) ) {\r\n+\t\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t$mask = 0xFF \u003C\u003C ( 8 - $partial_bits );\r\n+\t\t\t\t\t\tif ( ( $ip_bytes[ $byte_index ] & $mask ) !== ( $subnet_bytes[ $byte_index ] & $mask ) ) {\r\n+\t\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\t$normalized_ip     = $this->mosp_normalize_ipv6( $ip );\r\n+\t\t\t\t\t$normalized_subnet = $this->mosp_normalize_ipv6( $subnet );\r\n+\t\t\t\t\tif ( 128 === $bits ) {\r\n+\t\t\t\t\t\treturn $normalized_ip === $normalized_subnet;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Mark an identifier as requiring puzzle verification.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to mark.\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_mark_puzzle_required( $identifier ) {\r\n+\t\t\t$key          = 'mo_osp_puzzle_' . $this->mosp_hash_key( $identifier );\r\n+\t\t\t$current_time = time();\r\n+\t\t\t$expiry       = $current_time + ( 24 * 60 * 60 ); \u002F\u002F 24 hours.\r\n+\r\n+\t\t\t$result = update_option( $key, $expiry );\r\n+\r\n+\t\t\treturn $result;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if an identifier requires puzzle verification.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to check.\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_is_puzzle_required( $identifier ) {\r\n+\t\t\t$key          = 'mo_osp_puzzle_' . $this->mosp_hash_key( $identifier );\r\n+\t\t\t$expiry       = get_option( $key );\r\n+\t\t\t$current_time = time();\r\n+\r\n+\t\t\tif ( false === $expiry ) {\r\n+\t\t\t\t$expiry = 0;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$required = ( $expiry && $expiry > $current_time );\r\n+\t\t\tif ( $required ) {\r\n+\t\t\t\t$remaining_time = $expiry - $current_time;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $required ) {\r\n+\t\t\t\t$remaining_time = $expiry - $current_time;\r\n+\t\t\t\treturn true;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $expiry ) {\r\n+\t\t\t\tdelete_option( $key );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Clear puzzle requirement for an identifier.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to clear.\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_clear_puzzle_requirement( $identifier ) {\r\n+\t\t\t$key    = 'mo_osp_puzzle_' . $this->mosp_hash_key( $identifier );\r\n+\t\t\t$result = delete_option( $key );\r\n+\t\t\treturn $result;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if user requires puzzle verification for any identifier.\r\n+\t\t *\r\n+\t\t * @param string $email      Email address.\r\n+\t\t * @param string $phone      Phone number.\r\n+\t\t * @param string $ip         IP address.\r\n+\t\t * @param string $browser_id Browser fingerprint.\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_is_puzzle_required_for_user( $email, $phone, $ip, $browser_id ) {\r\n+\t\t\t$identifiers = array(\r\n+\t\t\t\t'email'   => $email,\r\n+\t\t\t\t'phone'   => $phone,\r\n+\t\t\t\t'ip'      => $ip,\r\n+\t\t\t\t'browser' => $browser_id,\r\n+\t\t\t);\r\n+\r\n+\t\t\t$prefixed_identifiers = array();\r\n+\t\t\tif ( ! empty( $email ) ) {\r\n+\t\t\t\t$prefixed_identifiers[] = 'email:' . $email;\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $phone ) ) {\r\n+\t\t\t\t$prefixed_identifiers[] = 'phone:' . $phone;\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $ip ) ) {\r\n+\t\t\t\t$prefixed_identifiers[] = 'ip:' . $ip;\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $browser_id ) ) {\r\n+\t\t\t\t$prefixed_identifiers[] = 'browser:' . $browser_id;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( empty( $email ) && empty( $phone ) && empty( $ip ) && empty( $browser_id ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $identifiers as $type => $identifier ) {\r\n+\t\t\t\tif ( ! empty( $identifier ) ) {\r\n+\t\t\t\t\t$required = $this->mosp_is_puzzle_required( $identifier );\r\n+\t\t\t\t\tif ( $required ) {\r\n+\t\t\t\t\t\treturn true;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $prefixed_identifiers as $prefixed_id ) {\r\n+\t\t\t\t$required = $this->mosp_is_puzzle_required( $prefixed_id );\r\n+\t\t\t\tif ( $required ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Cleanup expired entries.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_cleanup_expired_entries() {\r\n+\t\t\tglobal $wpdb;\r\n+\r\n+\t\t\t$settings = $this->mosp_get_settings();\r\n+\t\t\t$now      = time();\r\n+\t\t\t$cutoff   = $now - ( MoSecurityHelper::COUNTING_WINDOW_SECONDS * 2 ); \u002F\u002F Keep data for 2x counting window (30 minutes).\r\n+\r\n+\t\t\t$deleted = $this->cleanup_spam_data( $cutoff );\r\n+\r\n+\t\t\t$deleted += $this->cleanup_rate_limiting_data( $now );\r\n+\r\n+\t\t\t$deleted += $this->cleanup_permanent_puzzle_flags( $now - ( 30 * 24 * 60 * 60 ) );\r\n+\r\n+\t\t\t$this->mosp_prune_if_needed();\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Cleanup main spam data entries.\r\n+\t\t *\r\n+\t\t * @param int $cutoff Cutoff timestamp.\r\n+\t\t * @return int Number of deleted entries.\r\n+\t\t *\u002F\r\n+\t\tprivate function cleanup_spam_data( $cutoff ) {\r\n+\t\t\tglobal $wpdb;\r\n+\r\n+\t\t\t$cache_key    = 'mosp_spam_data_option_names';\r\n+\t\t\t$option_names = wp_cache_get( $cache_key, 'mo_osp' );\r\n+\t\t\tif ( false === $option_names ) {\r\n+\t\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . self::SPAM_DATA_PREFIX ) . '%'\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t\twp_cache_set( $cache_key, $option_names, 'mo_osp' );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$deleted = 0;\r\n+\t\t\tforeach ( $option_names as $db_option_name ) {\r\n+\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\r\n+\r\n+\t\t\t\t$data = get_mo_option( $option_key );\r\n+\r\n+\t\t\t\tif ( is_array( $data ) ) {\r\n+\t\t\t\t\tif ( isset( $data['last_attempt'] ) && $data['last_attempt'] \u003C $cutoff &&\r\n+\t\t\t\t\t\t( ! isset( $data['blocked_until'] ) || $data['blocked_until'] \u003C time() ) ) {\r\n+\t\t\t\t\t\tdelete_mo_option( $option_key );\r\n+\t\t\t\t\t\twp_cache_delete( $db_option_name, 'mo_osp' );\r\n+\t\t\t\t\t\t++$deleted;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $deleted > 0 ) {\r\n+\t\t\t\twp_cache_delete( $cache_key, 'mo_osp' );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $deleted;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Cleanup rate limiting data (hourly\u002Fdaily).\r\n+\t\t *\r\n+\t\t * @param int $now Current timestamp.\r\n+\t\t * @return int Number of deleted entries\r\n+\t\t *\u002F\r\n+\t\tprivate function cleanup_rate_limiting_data( $now ) {\r\n+\t\t\tglobal $wpdb;\r\n+\r\n+\t\t\t$deleted = 0;\r\n+\r\n+\t\t\t$hourly_cutoff  = $now - ( 2 * 60 * 60 );\r\n+\t\t\t$hourly_cache   = 'mosp_rate_limit_hourly_option_names';\r\n+\t\t\t$hourly_options = wp_cache_get( $hourly_cache, 'mo_osp' );\r\n+\t\t\tif ( false === $hourly_options ) {\r\n+\t\t\t\t$hourly_options = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_hourly_' ) . '%'\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t\twp_cache_set( $hourly_cache, $hourly_options, 'mo_osp' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $hourly_options as $db_option_name ) {\r\n+\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\r\n+\r\n+\t\t\t\t$data = get_mo_option( $option_key );\r\n+\t\t\t\tif ( is_array( $data ) && isset( $data['last_attempt'] ) && $data['last_attempt'] \u003C $hourly_cutoff ) {\r\n+\t\t\t\t\tdelete_mo_option( $option_key );\r\n+\t\t\t\t\t++$deleted;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$daily_cutoff  = $now - ( 2 * 24 * 60 * 60 );\r\n+\t\t\t$daily_cache   = 'mosp_rate_limit_daily_option_names';\r\n+\t\t\t$daily_options = wp_cache_get( $daily_cache, 'mo_osp' );\r\n+\t\t\tif ( false === $daily_options ) {\r\n+\t\t\t\t$daily_options = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_daily_' ) . '%'\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t\twp_cache_set( $daily_cache, $daily_options, 'mo_osp' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $daily_options as $db_option_name ) {\r\n+\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\r\n+\r\n+\t\t\t\t$data = get_mo_option( $option_key );\r\n+\t\t\t\tif ( is_array( $data ) && isset( $data['last_attempt'] ) && $data['last_attempt'] \u003C $daily_cutoff ) {\r\n+\t\t\t\t\tdelete_mo_option( $option_key );\r\n+\t\t\t\t\t++$deleted;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $deleted > 0 ) {\r\n+\t\t\t\twp_cache_delete( $hourly_cache, 'mo_osp' );\r\n+\t\t\t\twp_cache_delete( $daily_cache, 'mo_osp' );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $deleted;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Cleanup permanent puzzle completion flags.\r\n+\t\t *\r\n+\t\t * @param int $cutoff Cutoff timestamp (30 days ago).\r\n+\t\t * @return int Number of deleted entries\r\n+\t\t *\u002F\r\n+\t\tprivate function cleanup_permanent_puzzle_flags( $cutoff ) {\r\n+\t\t\tglobal $wpdb;\r\n+\r\n+\t\t\t$deleted        = 0;\r\n+\t\t\t$cache_key      = 'mosp_puzzle_completion_option_names';\r\n+\t\t\t$puzzle_options = wp_cache_get( $cache_key, 'mo_osp' );\r\n+\t\t\tif ( false === $puzzle_options ) {\r\n+\t\t\t\t$puzzle_options = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_puzzle_ever_completed_' ) . '%'\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t\twp_cache_set( $cache_key, $puzzle_options, 'mo_osp' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $puzzle_options as $db_option_name ) {\r\n+\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\r\n+\r\n+\t\t\t\t$completion_time = get_mo_option( $option_key );\r\n+\t\t\t\tif ( is_numeric( $completion_time ) && $completion_time \u003C $cutoff ) {\r\n+\t\t\t\t\tdelete_mo_option( $option_key );\r\n+\t\t\t\t\t++$deleted;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $deleted > 0 ) {\r\n+\t\t\t\twp_cache_delete( $cache_key, 'mo_osp' );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $deleted;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Prune entries if still too many.\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_prune_if_needed() {\r\n+\t\t\tglobal $wpdb;\r\n+\r\n+\t\t\t$cache_key     = 'mosp_spam_storage_total_count';\r\n+\t\t\t$total_options = wp_cache_get( $cache_key, 'mo_osp' );\r\n+\t\t\tif ( false === $total_options ) {\r\n+\t\t\t\t$total_options = $wpdb->get_var( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s\",\r\n+\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . self::SPAM_DATA_PREFIX ) . '%',\r\n+\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_' ) . '%',\r\n+\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_puzzle_ever_completed_' ) . '%'\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t\twp_cache_set( $cache_key, $total_options, 'mo_osp' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $total_options > self::MAX_ENTRIES ) {\r\n+\t\t\t\t$this->mosp_prune_old_entries( self::MAX_ENTRIES );\r\n+\t\t\t\twp_cache_delete( $cache_key, 'mo_osp' );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Prune old entries to keep storage bounded.\r\n+\t\t *\r\n+\t\t * @param int $max_entries Maximum entries to keep.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_prune_old_entries( $max_entries ) {\r\n+\t\t\tglobal $wpdb;\r\n+\r\n+\t\t\t$cache_key = 'mosp_spam_data_entries';\r\n+\t\t\t$results   = wp_cache_get( $cache_key, 'mo_osp' );\r\n+\t\t\tif ( false === $results ) {\r\n+\t\t\t\t$results = $wpdb->get_results( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s ORDER BY option_id DESC\",\r\n+\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . self::SPAM_DATA_PREFIX ) . '%'\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t\twp_cache_set( $cache_key, $results, 'mo_osp' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( count( $results ) \u003C= $max_entries ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$entries = array();\r\n+\t\t\tforeach ( $results as $result ) {\r\n+\t\t\t\t$data = maybe_unserialize( $result->option_value );\r\n+\t\t\t\tif ( is_array( $data ) && isset( $data['last_attempt'] ) ) {\r\n+\t\t\t\t\t$entries[] = array(\r\n+\t\t\t\t\t\t'option_name'  => $result->option_name,\r\n+\t\t\t\t\t\t'last_attempt' => $data['last_attempt'],\r\n+\t\t\t\t\t);\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tusort(\r\n+\t\t\t\t$entries,\r\n+\t\t\t\tfunction ( $a, $b ) {\r\n+\t\t\t\t\treturn $b['last_attempt'] - $a['last_attempt'];\r\n+\t\t\t\t}\r\n+\t\t\t);\r\n+\r\n+\t\t\t$to_delete = array_slice( $entries, $max_entries );\r\n+\t\t\tforeach ( $to_delete as $entry ) {\r\n+\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $entry['option_name'] );\r\n+\r\n+\t\t\t\tdelete_mo_option( $option_key );\r\n+\t\t\t\twp_cache_delete( $entry['option_name'], 'mo_osp' );\r\n+\t\t\t}\r\n+\r\n+\t\t\twp_cache_delete( $cache_key, 'mo_osp' );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get masked version of identifier for logging.\r\n+\t\t *\r\n+\t\t * @param string $identifier The identifier to mask.\r\n+\t\t * @param string $type The type of identifier.\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_mask_identifier( $identifier, $type ) {\r\n+\t\t\tswitch ( $type ) {\r\n+\t\t\t\tcase 'phone':\r\n+\t\t\t\t\tif ( strlen( $identifier ) > 4 ) {\r\n+\t\t\t\t\t\treturn str_repeat( 'X', strlen( $identifier ) - 4 ) . substr( $identifier, -4 );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\treturn $identifier;\r\n+\r\n+\t\t\t\tcase 'email':\r\n+\t\t\t\t\t$parts = explode( '@', $identifier );\r\n+\t\t\t\t\tif ( count( $parts ) === 2 ) {\r\n+\t\t\t\t\t\t$username        = $parts[0];\r\n+\t\t\t\t\t\t$domain          = $parts[1];\r\n+\t\t\t\t\t\t$masked_username = strlen( $username ) > 2 ? substr( $username, 0, 1 ) . str_repeat( '*', strlen( $username ) - 2 ) . substr( $username, -1 ) : $username;\r\n+\t\t\t\t\t\treturn $masked_username . '@' . $domain;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\treturn $identifier;\r\n+\r\n+\t\t\t\tcase 'ip':\r\n+\t\t\t\t\t$parts = explode( '.', $identifier );\r\n+\t\t\t\t\tif ( count( $parts ) === 4 ) {\r\n+\t\t\t\t\t\treturn $parts[0] . '.' . $parts[1] . '.XXX.XXX';\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\treturn $identifier;\r\n+\r\n+\t\t\t\tdefault:\r\n+\t\t\t\t\treturn substr( $identifier, 0, 8 ) . '...';\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Record attempt with timestamp (new method for integration).\r\n+\t\t *\r\n+\t\t * @param string $identifier The full identifier (e.g., 'email:user@example.com').\r\n+\t\t * @param int    $timestamp The attempt timestamp.\r\n+\t\t * @param array  $context Optional context data.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_record_attempt_with_timestamp( $identifier, $timestamp, $context = array() ) {\r\n+\t\t\t$key  = $this->mosp_hash_key( $identifier );\r\n+\t\t\t$data = $this->mosp_get_spam_data( $key );\r\n+\r\n+\t\t\tif ( false === $data ) {\r\n+\t\t\t\t$data = array(\r\n+\t\t\t\t\t'attempts'      => array(),\r\n+\t\t\t\t\t'blocked_until' => 0,\r\n+\t\t\t\t\t'created'       => $timestamp,\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( is_array( $context ) ) {\r\n+\t\t\t\tif ( ! empty( $context['ip'] ) && filter_var( $context['ip'], FILTER_VALIDATE_IP ) ) {\r\n+\t\t\t\t\t$data['last_ip'] = $context['ip'];\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( ! empty( $context['browser_id'] ) ) {\r\n+\t\t\t\t\t$data['last_browser'] = $context['browser_id'];\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( ! empty( $context['email'] ) ) {\r\n+\t\t\t\t\t$data['last_email'] = strtolower( trim( (string) $context['email'] ) );\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( ! empty( $context['phone'] ) ) {\r\n+\t\t\t\t\t$data['last_phone'] = trim( (string) $context['phone'] );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( is_string( $identifier ) && strpos( $identifier, ':' ) !== false ) {\r\n+\t\t\t\tlist( $id_type, $id_value ) = explode( ':', $identifier, 2 );\r\n+\t\t\t\t$id_value                   = trim( (string) $id_value );\r\n+\t\t\t\tif ( ! empty( $id_value ) ) {\r\n+\t\t\t\t\tif ( 'email' === $id_type ) {\r\n+\t\t\t\t\t\t$data['last_email'] = strtolower( $id_value );\r\n+\t\t\t\t\t} elseif ( 'phone' === $id_type ) {\r\n+\t\t\t\t\t\t$data['last_phone'] = $id_value;\r\n+\t\t\t\t\t} elseif ( 'ip' === $id_type ) {\r\n+\t\t\t\t\t\t$data['last_ip'] = $id_value;\r\n+\t\t\t\t\t} elseif ( 'browser' === $id_type ) {\r\n+\t\t\t\t\t\t$data['last_browser'] = $id_value;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! isset( $data['attempts'] ) ) {\r\n+\t\t\t\t$data['attempts'] = array();\r\n+\t\t\t}\r\n+\r\n+\t\t\t$data['attempts'][]   = $timestamp;\r\n+\t\t\t$data['last_attempt'] = $timestamp;\r\n+\r\n+\t\t\t$cutoff           = $timestamp - ( 24 * 60 * 60 );\r\n+\t\t\t$data['attempts'] = array_filter(\r\n+\t\t\t\t$data['attempts'],\r\n+\t\t\t\tfunction ( $time ) use ( $cutoff ) {\r\n+\t\t\t\t\treturn $time > $cutoff;\r\n+\t\t\t\t}\r\n+\t\t\t);\r\n+\r\n+\t\t\t$this->mosp_update_spam_data( $key, $data );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get all currently blocked users.\r\n+\t\t *\r\n+\t\t * @param int $limit Maximum number of entries to return (default 100).\r\n+\t\t * @param int $offset Offset for pagination (default 0).\r\n+\t\t * @return array Array of blocked user data.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_get_all_blocked_users( $limit = 100, $offset = 0 ) {\r\n+\t\t\treturn $this->mosp_get_blocked_users_from_rate_limits( $limit, $offset );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Delete all spam\u002Fblock rows, rate-limit options, and puzzle-requirement flags (admin \"clear all\").\r\n+\t\t *\r\n+\t\t * @return int Number of options deleted.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_clear_all_otp_spam_data() {\r\n+\t\t\tglobal $wpdb;\r\n+\r\n+\t\t\t$deleted = 0;\r\n+\r\n+\t\t\t$like_patterns = array(\r\n+\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . self::SPAM_DATA_PREFIX ) . '%',\r\n+\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_' ) . '%',\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $like_patterns as $like ) {\r\n+\t\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t\t$like\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t\tforeach ( $option_names as $option_name ) {\r\n+\t\t\t\t\tdelete_site_option( $option_name );\r\n+\t\t\t\t\t++$deleted;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$puzzle_like  = $wpdb->esc_like( 'mo_osp_puzzle_' ) . '%';\r\n+\t\t\t$puzzle_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t$puzzle_like\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t\tforeach ( $puzzle_names as $option_name ) {\r\n+\t\t\t\tdelete_option( $option_name );\r\n+\t\t\t\t++$deleted;\r\n+\t\t\t}\r\n+\r\n+\t\t\twp_cache_delete( 'mosp_blocked_users_list', 'mo_osp' );\r\n+\t\t\twp_cache_delete( 'mosp_spam_data_option_names', 'mo_osp' );\r\n+\t\t\twp_cache_delete( 'mosp_uninstall_spam_option_names', 'mo_osp' );\r\n+\t\t\twp_cache_delete( 'mosp_rate_limit_hourly_options', 'mo_osp' );\r\n+\t\t\twp_cache_delete( 'mosp_rate_limit_daily_options', 'mo_osp' );\r\n+\t\t\twp_cache_delete( 'mosp_rate_limit_hourly_option_names', 'mo_osp' );\r\n+\t\t\twp_cache_delete( 'mosp_rate_limit_daily_option_names', 'mo_osp' );\r\n+\r\n+\t\t\treturn $deleted;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get blocked users by checking rate limit data and spam data.\r\n+\t\t *\r\n+\t\t * @param int $limit Maximum number of entries to return.\r\n+\t\t * @param int $offset Offset for pagination.\r\n+\t\t * @return array Array of blocked user data.\r\n+\t\t *\u002F\r\n+\t\tpublic function mosp_get_blocked_users_from_rate_limits( $limit = 100, $offset = 0 ) {\r\n+\t\t\tglobal $wpdb;\r\n+\r\n+\t\t\t$now                = time();\r\n+\t\t\t$blocked            = array();\r\n+\t\t\t$settings           = $this->mosp_get_settings();\r\n+\t\t\t$window_types       = array( 'hourly', 'daily' );\r\n+\t\t\t$seen_hashes        = array();\r\n+\t\t\t$hash_to_identifier = array();\r\n+\t\t\t$priority           = array(\r\n+\t\t\t\t'phone'   => 3,\r\n+\t\t\t\t'email'   => 2,\r\n+\t\t\t\t'ip'      => 1,\r\n+\t\t\t\t'browser' => 0,\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $window_types as $window_type ) {\r\n+\t\t\t\t$cache_key    = 'mosp_rate_limit_' . $window_type . '_options';\r\n+\t\t\t\t$option_names = wp_cache_get( $cache_key, 'mo_osp' );\r\n+\r\n+\t\t\t\tif ( false === $option_names ) {\r\n+\t\t\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_' . $window_type . '_' ) . '%'\r\n+\t\t\t\t\t\t)\r\n+\t\t\t\t\t);\r\n+\t\t\t\t\twp_cache_set( $cache_key, $option_names, 'mo_osp', 300 );\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\tforeach ( $option_names as $db_option_name ) {\r\n+\t\t\t\t\t$option_key     = str_replace( 'mo_customer_validation_', '', $db_option_name );\r\n+\t\t\t\t\t$rate_limit_key = str_replace( self::SPAM_DATA_PREFIX, '', $option_key );\r\n+\r\n+\t\t\t\t\t$key_parts = explode( '_', $rate_limit_key );\r\n+\t\t\t\t\tif ( count( $key_parts ) >= 4 ) {\r\n+\t\t\t\t\t\t$identifier_hash = $key_parts[3];\r\n+\r\n+\t\t\t\t\t\tif ( ! isset( $hash_to_identifier[ $identifier_hash ] ) ) {\r\n+\t\t\t\t\t\t\t$hash_to_identifier[ $identifier_hash ] = null;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$cache_key         = 'mosp_spam_data_option_names';\r\n+\t\t\t$spam_option_names = wp_cache_get( $cache_key, 'mo_osp' );\r\n+\r\n+\t\t\tif ( false === $spam_option_names ) {\r\n+\t\t\t\t$spam_option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_' . self::SPAM_DATA_PREFIX ) . '%'\r\n+\t\t\t\t\t)\r\n+\t\t\t\t);\r\n+\t\t\t\twp_cache_set( $cache_key, $spam_option_names, 'mo_osp', 300 );\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $spam_option_names as $db_option_name ) {\r\n+\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\r\n+\r\n+\t\t\t\t$hash_key = str_replace( self::SPAM_DATA_PREFIX, '', $option_key );\r\n+\r\n+\t\t\t\tif ( strpos( $option_key, 'rate_limit_' ) !== false ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$spam_data = $this->mosp_get_spam_data( $hash_key );\r\n+\r\n+\t\t\t\tif ( false === $spam_data || ! is_array( $spam_data ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$blocked_until = isset( $spam_data['blocked_until'] ) ? (int) $spam_data['blocked_until'] : 0;\r\n+\t\t\t\t$block_reason  = isset( $spam_data['block_reason'] ) ? $spam_data['block_reason'] : '';\r\n+\r\n+\t\t\t\tif ( $blocked_until > $now && in_array( $block_reason, array( 'hourly_limit_exceeded', 'daily_limit_exceeded', 'max_attempts_exceeded' ), true ) ) {\r\n+\t\t\t\t\t$remaining_time = $blocked_until - $now;\r\n+\r\n+\t\t\t\t\t$identifier_type  = isset( $spam_data['type'] ) ? $spam_data['type'] : 'unknown';\r\n+\t\t\t\t\t$identifier_value = isset( $spam_data['identifier'] ) ? $spam_data['identifier'] : '';\r\n+\r\n+\t\t\t\t\tif ( ! empty( $identifier_value ) ) {\r\n+\t\t\t\t\t\t$identifier_display = $identifier_value;\r\n+\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t$identifier_display = 'User: ' . substr( $hash_key, -8 );\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\tif ( 'unknown' === $identifier_type || 'identifier' === $identifier_type ) {\r\n+\t\t\t\t\t\t$identifier_info = $this->mosp_infer_identifier_from_hash( $hash_key, $spam_data );\r\n+\t\t\t\t\t\t$identifier_type = $identifier_info['type'];\r\n+\t\t\t\t\t\tif ( empty( $identifier_value ) && ! empty( $identifier_info['value'] ) ) {\r\n+\t\t\t\t\t\t\t$identifier_value   = $identifier_info['value'];\r\n+\t\t\t\t\t\t\t$identifier_display = $identifier_value;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$user_key = $block_reason . '_' . $blocked_until;\r\n+\r\n+\t\t\t\t\tif ( in_array( $hash_key, $seen_hashes, true ) ) {\r\n+\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$is_duplicate = false;\r\n+\t\t\t\t\tforeach ( $blocked as $existing ) {\r\n+\t\t\t\t\t\tif ( $existing['block_reason'] === $block_reason &&\r\n+\t\t\t\t\t\t\tabs( $existing['blocked_until'] - $blocked_until ) \u003C 5 && \u002F\u002F Within 5 seconds.\r\n+\t\t\t\t\t\t\t'unknown' !== $existing['identifier_type'] &&\r\n+\t\t\t\t\t\t\t'unknown' !== $identifier_type ) {\r\n+\t\t\t\t\t\t\t$existing_priority = isset( $priority[ $existing['identifier_type'] ] ) ? $priority[ $existing['identifier_type'] ] : 0;\r\n+\t\t\t\t\t\t\t$current_priority  = isset( $priority[ $identifier_type ] ) ? $priority[ $identifier_type ] : 0;\r\n+\r\n+\t\t\t\t\t\t\tif ( $current_priority > $existing_priority ) {\r\n+\t\t\t\t\t\t\t\t$blocked      = array_filter(\r\n+\t\t\t\t\t\t\t\t\t$blocked,\r\n+\t\t\t\t\t\t\t\t\tfunction ( $item ) use ( $existing ) {\r\n+\t\t\t\t\t\t\t\t\t\treturn $item['identifier_hash'] !== $existing['identifier_hash'];\r\n+\t\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\t);\r\n+\t\t\t\t\t\t\t\t$blocked      = array_values( $blocked );\r\n+\t\t\t\t\t\t\t\t$is_duplicate = false;\r\n+\t\t\t\t\t\t\t\tbreak;\r\n+\t\t\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t\t\t$is_duplicate = true;\r\n+\t\t\t\t\t\t\t\tbreak;\r\n+\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\tif ( $is_duplicate ) {\r\n+\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$blocked[] = array(\r\n+\t\t\t\t\t\t'identifier_hash'   => $hash_key,\r\n+\t\t\t\t\t\t'identifier_masked' => $identifier_display,\r\n+\t\t\t\t\t\t'identifier_type'   => $identifier_type,\r\n+\t\t\t\t\t\t'identifier_value'  => $identifier_value,\r\n+\t\t\t\t\t\t'block_reason'      => $block_reason,\r\n+\t\t\t\t\t\t'blocked_until'     => $blocked_until,\r\n+\t\t\t\t\t\t'remaining_time'    => $remaining_time,\r\n+\t\t\t\t\t);\r\n+\r\n+\t\t\t\t\t$seen_hashes[] = $hash_key;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tforeach ( $window_types as $window_type ) {\r\n+\t\t\t\t$cache_key    = 'mosp_rate_limit_' . $window_type . '_options';\r\n+\t\t\t\t$option_names = wp_cache_get( $cache_key, 'mo_osp' );\r\n+\r\n+\t\t\t\tif ( false === $option_names ) {\r\n+\t\t\t\t\t$option_names = $wpdb->get_col( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\r\n+\t\t\t\t\t\t$wpdb->prepare(\r\n+\t\t\t\t\t\t\t\"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s\",\r\n+\t\t\t\t\t\t\t$wpdb->esc_like( 'mo_customer_validation_mo_osp_rate_limit_' . $window_type . '_' ) . '%'\r\n+\t\t\t\t\t\t)\r\n+\t\t\t\t\t);\r\n+\t\t\t\t\twp_cache_set( $cache_key, $option_names, 'mo_osp', 300 );\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$limit_value = 'hourly' === $window_type ? $settings['hourly_limit'] : $settings['daily_limit'];\r\n+\r\n+\t\t\t\tforeach ( $option_names as $db_option_name ) {\r\n+\t\t\t\t\t$option_key = str_replace( 'mo_customer_validation_', '', $db_option_name );\r\n+\r\n+\t\t\t\t\t$rate_limit_key = str_replace( self::SPAM_DATA_PREFIX, '', $option_key );\r\n+\t\t\t\t\t$rate_data      = $this->mosp_get_spam_data( $rate_limit_key );\r\n+\r\n+\t\t\t\t\tif ( false === $rate_data || ! is_array( $rate_data ) || ! isset( $rate_data['attempts'] ) || ! is_array( $rate_data['attempts'] ) ) {\r\n+\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t$window_seconds   = 'hourly' === $window_type ? 3600 : 86400;\r\n+\t\t\t\t\t$window_start     = $now - $window_seconds;\r\n+\t\t\t\t\t$current_attempts = count(\r\n+\t\t\t\t\t\tarray_filter(\r\n+\t\t\t\t\t\t\t$rate_data['attempts'],\r\n+\t\t\t\t\t\t\tfunction ( $timestamp ) use ( $window_start ) {\r\n+\t\t\t\t\t\t\t\treturn $timestamp > $window_start;\r\n+\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t)\r\n+\t\t\t\t\t);\r\n+\r\n+\t\t\t\t\tif ( $current_attempts >= $limit_value ) {\r\n+\t\t\t\t\t\t$key_parts = explode( '_', $rate_limit_key );\r\n+\t\t\t\t\t\tif ( count( $key_parts ) >= 4 ) {\r\n+\t\t\t\t\t\t\t$identifier_hash = $key_parts[3];\r\n+\r\n+\t\t\t\t\t\t\tif ( in_array( $identifier_hash, $seen_hashes, true ) ) {\r\n+\t\t\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\t\t$in_window = array_filter(\r\n+\t\t\t\t\t\t\t\t$rate_data['attempts'],\r\n+\t\t\t\t\t\t\t\tfunction ( $timestamp ) use ( $window_start ) {\r\n+\t\t\t\t\t\t\t\t\treturn $timestamp > $window_start;\r\n+\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t);\r\n+\r\n+\t\t\t\t\t\t\tif ( ! empty( $in_window ) ) {\r\n+\t\t\t\t\t\t\t\t$oldest_attempt = min( $in_window );\r\n+\t\t\t\t\t\t\t\t$reset_time     = $oldest_attempt + $window_seconds;\r\n+\t\t\t\t\t\t\t\t$remaining_time = max( 0, $reset_time - $now );\r\n+\r\n+\t\t\t\t\t\t\t\t$spam_data = $this->mosp_get_spam_data( $identifier_hash );\r\n+\r\n+\t\t\t\t\t\t\t\t$blocked_until = 0;\r\n+\t\t\t\t\t\t\t\t$block_reason  = $window_type . '_limit_exceeded';\r\n+\r\n+\t\t\t\t\t\t\t\tif ( false !== $spam_data && is_array( $spam_data ) && isset( $spam_data['blocked_until'] ) && $spam_data['blocked_until'] > $now ) {\r\n+\t\t\t\t\t\t\t\t\t$blocked_until  = $spam_data['blocked_until'];\r\n+\t\t\t\t\t\t\t\t\t$block_reason   = isset( $spam_data['block_reason'] ) ? $spam_data['block_reason'] : $block_reason;\r\n+\t\t\t\t\t\t\t\t\t$remaining_time = $blocked_until - $now;\r\n+\t\t\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\t\t\t$identifier_type    = 'unknown';\r\n+\t\t\t\t\t\t\t\t$identifier_value   = '';\r\n+\t\t\t\t\t\t\t\t$identifier_display = 'User: ' . substr( $identifier_hash, -8 );\r\n+\r\n+\t\t\t\t\t\t\t\tif ( false !== $spam_data && is_array( $spam_data ) ) {\r\n+\t\t\t\t\t\t\t\t\tif ( isset( $spam_data['type'] ) ) {\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_type = $spam_data['type'];\r\n+\t\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\t\tif ( isset( $spam_data['identifier'] ) && ! empty( $spam_data['identifier'] ) ) {\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_value   = $spam_data['identifier'];\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\r\n+\t\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\t\t\tif ( empty( $identifier_value ) && isset( $rate_data['identifier'] ) ) {\r\n+\t\t\t\t\t\t\t\t\t$rate_identifier = $rate_data['identifier'];\r\n+\t\t\t\t\t\t\t\t\tif ( strpos( $rate_identifier, 'phone:' ) === 0 ) {\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'phone';\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_value   = substr( $rate_identifier, 6 );\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\r\n+\t\t\t\t\t\t\t\t\t} elseif ( strpos( $rate_identifier, 'email:' ) === 0 ) {\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'email';\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_value   = substr( $rate_identifier, 6 );\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\r\n+\t\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\t\t\tif ( 'unknown' === $identifier_type || 'identifier' === $identifier_type ) {\r\n+\t\t\t\t\t\t\t\t\tif ( ! empty( $spam_data['last_email'] ) ) {\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'email';\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_value   = $spam_data['last_email'];\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\r\n+\t\t\t\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_phone'] ) ) {\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'phone';\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_value   = $spam_data['last_phone'];\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\r\n+\t\t\t\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_ip'] ) ) {\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'ip';\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_value   = $spam_data['last_ip'];\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\r\n+\t\t\t\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_browser'] ) ) {\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_type    = 'browser';\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_value   = $spam_data['last_browser'];\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_display = $identifier_value;\r\n+\t\t\t\t\t\t\t\t\t} elseif ( ! empty( $identifier_value ) && strpos( $identifier_value, '@' ) !== false ) {\r\n+\t\t\t\t\t\t\t\t\t\t$identifier_type = 'email';\r\n+\t\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\t\t\t$calculated_blocked_until = $blocked_until > 0 ? $blocked_until : ( $now + $remaining_time );\r\n+\t\t\t\t\t\t\t\t$is_duplicate             = false;\r\n+\t\t\t\t\t\t\t\tforeach ( $blocked as $existing ) {\r\n+\t\t\t\t\t\t\t\t\tif ( $existing['block_reason'] === $block_reason &&\r\n+\t\t\t\t\t\t\t\t\t\tabs( $existing['blocked_until'] - $calculated_blocked_until ) \u003C 5 ) {\r\n+\t\t\t\t\t\t\t\t\t\t$existing_priority = isset( $priority[ $existing['identifier_type'] ] ) ? $priority[ $existing['identifier_type'] ] : 0;\r\n+\t\t\t\t\t\t\t\t\t\t$current_priority  = isset( $priority[ $identifier_type ] ) ? $priority[ $identifier_type ] : 0;\r\n+\r\n+\t\t\t\t\t\t\t\t\t\tif ( $current_priority > $existing_priority ) {\r\n+\t\t\t\t\t\t\t\t\t\t\t$blocked      = array_filter(\r\n+\t\t\t\t\t\t\t\t\t\t\t\t$blocked,\r\n+\t\t\t\t\t\t\t\t\t\t\t\tfunction ( $item ) use ( $existing ) {\r\n+\t\t\t\t\t\t\t\t\t\t\t\t\treturn $item['identifier_hash'] !== $existing['identifier_hash'];\r\n+\t\t\t\t\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\t\t\t\t);\r\n+\t\t\t\t\t\t\t\t\t\t\t$blocked      = array_values( $blocked );\r\n+\t\t\t\t\t\t\t\t\t\t\t$is_duplicate = false;\r\n+\t\t\t\t\t\t\t\t\t\t\tbreak;\r\n+\t\t\t\t\t\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t\t\t\t\t\t$is_duplicate = true;\r\n+\t\t\t\t\t\t\t\t\t\t\tbreak;\r\n+\t\t\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\t\t\tif ( $is_duplicate ) {\r\n+\t\t\t\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\t\t\t\t$blocked[] = array(\r\n+\t\t\t\t\t\t\t\t\t'identifier_hash'   => $identifier_hash,\r\n+\t\t\t\t\t\t\t\t\t'identifier_masked' => $identifier_display,\r\n+\t\t\t\t\t\t\t\t\t'identifier_type'   => $identifier_type,\r\n+\t\t\t\t\t\t\t\t\t'identifier_value'  => $identifier_value,\r\n+\t\t\t\t\t\t\t\t\t'block_reason'      => $block_reason,\r\n+\t\t\t\t\t\t\t\t\t'blocked_until'     => $calculated_blocked_until,\r\n+\t\t\t\t\t\t\t\t\t'remaining_time'    => $remaining_time,\r\n+\t\t\t\t\t\t\t\t);\r\n+\r\n+\t\t\t\t\t\t\t\t$seen_hashes[] = $identifier_hash;\r\n+\t\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Sort by remaining time (longest first).\r\n+\t\t\tusort(\r\n+\t\t\t\t$blocked,\r\n+\t\t\t\tfunction ( $a, $b ) {\r\n+\t\t\t\t\treturn $b['remaining_time'] - $a['remaining_time'];\r\n+\t\t\t\t}\r\n+\t\t\t);\r\n+\r\n+\t\t\t$total   = count( $blocked );\r\n+\t\t\t$blocked = array_slice( $blocked, $offset, $limit );\r\n+\r\n+\t\t\treturn array(\r\n+\t\t\t\t'users' => $blocked,\r\n+\t\t\t\t'total' => $total,\r\n+\t\t\t);\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Infer identifier type and value from hash by checking rate limit data.\r\n+\t\t *\r\n+\t\t * @param string $hash The identifier hash.\r\n+\t\t * @param array  $spam_data The spam data array.\r\n+\t\t * @return array Array with 'type', 'value', and 'masked' keys (masked now contains original value).\r\n+\t\t *\u002F\r\n+\t\tprivate function mosp_infer_identifier_from_hash( $hash, $spam_data ) {\r\n+\t\t\tglobal $wpdb;\r\n+\r\n+\t\t\t$result = array(\r\n+\t\t\t\t'type'   => 'unknown',\r\n+\t\t\t\t'value'  => '',\r\n+\t\t\t\t'masked' => 'User: ' . substr( $hash, -8 ),\r\n+\t\t\t);\r\n+\r\n+\t\t\tif ( isset( $spam_data['identifier'] ) && ! empty( $spam_data['identifier'] ) ) {\r\n+\t\t\t\t$result['value']  = $spam_data['identifier'];\r\n+\t\t\t\t$result['masked'] = $spam_data['identifier'];\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( isset( $spam_data['type'] ) && 'identifier' !== $spam_data['type'] && 'unknown' !== $spam_data['type'] ) {\r\n+\t\t\t\t$result['type'] = $spam_data['type'];\r\n+\t\t\t}\r\n+\r\n+\t\t\t$window_types = array( 'hourly', 'daily' );\r\n+\t\t\tforeach ( $window_types as $window_type ) {\r\n+\t\t\t\t$rate_limit_key = 'rate_limit_' . $window_type . '_' . $hash;\r\n+\t\t\t\t$rate_data      = $this->mosp_get_spam_data( $rate_limit_key );\r\n+\r\n+\t\t\t\tif ( false !== $rate_data && is_array( $rate_data ) ) {\r\n+\t\t\t\t\tif ( isset( $rate_data['identifier'] ) && ! empty( $rate_data['identifier'] ) ) {\r\n+\t\t\t\t\t\t$rate_identifier = $rate_data['identifier'];\r\n+\t\t\t\t\t\tif ( strpos( $rate_identifier, 'phone:' ) === 0 ) {\r\n+\t\t\t\t\t\t\t$result['type']   = 'phone';\r\n+\t\t\t\t\t\t\t$result['value']  = substr( $rate_identifier, 6 );\r\n+\t\t\t\t\t\t\t$result['masked'] = $result['value'];\r\n+\t\t\t\t\t\t} elseif ( strpos( $rate_identifier, 'email:' ) === 0 ) {\r\n+\t\t\t\t\t\t\t$result['type']   = 'email';\r\n+\t\t\t\t\t\t\t$result['value']  = substr( $rate_identifier, 6 );\r\n+\t\t\t\t\t\t\t$result['masked'] = $result['value'];\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t} elseif ( 'unknown' === $result['type'] ) {\r\n+\t\t\t\t\t\tif ( ! empty( $spam_data['last_email'] ) ) {\r\n+\t\t\t\t\t\t\t$result['type']   = 'email';\r\n+\t\t\t\t\t\t\t$result['value']  = $spam_data['last_email'];\r\n+\t\t\t\t\t\t\t$result['masked'] = $result['value'];\r\n+\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_phone'] ) ) {\r\n+\t\t\t\t\t\t\t$result['type']   = 'phone';\r\n+\t\t\t\t\t\t\t$result['value']  = $spam_data['last_phone'];\r\n+\t\t\t\t\t\t\t$result['masked'] = $result['value'];\r\n+\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_ip'] ) ) {\r\n+\t\t\t\t\t\t\t$result['type']   = 'ip';\r\n+\t\t\t\t\t\t\t$result['value']  = $spam_data['last_ip'];\r\n+\t\t\t\t\t\t\t$result['masked'] = $result['value'];\r\n+\t\t\t\t\t\t} elseif ( ! empty( $spam_data['last_browser'] ) ) {\r\n+\t\t\t\t\t\t\t$result['type']   = 'browser';\r\n+\t\t\t\t\t\t\t$result['value']  = $spam_data['last_browser'];\r\n+\t\t\t\t\t\t\t$result['masked'] = $result['value'];\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( 'unknown' === $result['type'] ) {\r\n+\t\t\t\tif ( ! empty( $spam_data['last_email'] ) ) {\r\n+\t\t\t\t\t$result['type']   = 'email';\r\n+\t\t\t\t\t$result['value']  = $spam_data['last_email'];\r\n+\t\t\t\t\t$result['masked'] = $result['value'];\r\n+\t\t\t\t} elseif ( ! empty( $spam_data['last_phone'] ) ) {\r\n+\t\t\t\t\t$result['type']   = 'phone';\r\n+\t\t\t\t\t$result['value']  = $spam_data['last_phone'];\r\n+\t\t\t\t\t$result['masked'] = $result['value'];\r\n+\t\t\t\t} elseif ( ! empty( $spam_data['last_ip'] ) ) {\r\n+\t\t\t\t\t$result['type']   = 'ip';\r\n+\t\t\t\t\t$result['value']  = $spam_data['last_ip'];\r\n+\t\t\t\t\t$result['masked'] = $result['value'];\r\n+\t\t\t\t} elseif ( ! empty( $spam_data['last_browser'] ) ) {\r\n+\t\t\t\t\t$result['type']   = 'browser';\r\n+\t\t\t\t\t$result['value']  = $spam_data['last_browser'];\r\n+\t\t\t\t\t$result['masked'] = $result['value'];\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $result;\r\n+\t\t}\r\n+\t}\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhelper\u002Fclass-mopuzzlehelper.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhelper\u002Fclass-mopuzzlehelper.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhelper\u002Fclass-mopuzzlehelper.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhelper\u002Fclass-mopuzzlehelper.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,392 +1,392 @@\n-\u003C?php\n-\u002F**\n- * Puzzle Helper Class\n- *\n- * Contains all puzzle generation, validation, and management functions.\n- * This class handles the secure puzzle system for OTP spam prevention.\n- *\n- * @package otpspampreventer\u002Fhelper\n- *\u002F\n-\n-namespace OSP\\Helper;\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-use OTP\\Helper\\MoPHPSessions;\n-\n-if ( ! class_exists( 'MoPuzzleHelper' ) ) {\n-\t\u002F**\n-\t * Puzzle Helper Class\n-\t *\n-\t * Handles all puzzle-related operations including generation,\n-\t * validation, session management, and security checks.\n-\t *\u002F\n-\tclass MoPuzzleHelper {\n-\n-\t\t\u002F**\n-\t\t * Generate a secure mathematical puzzle\n-\t\t *\n-\t\t * Creates a random mathematical puzzle with varying difficulty levels.\n-\t\t * The puzzle data is designed to be stored securely on the server.\n-\t\t *\n-\t\t * @return array|false Array with 'question' and 'answer' keys, or false on failure.\n-\t\t *\u002F\n-\t\tpublic static function mosp_generate_secure_puzzle() {\n-\t\t\t$puzzle_templates = array(\n-\t\t\t\t\u002F\u002F Simple addition.\n-\t\t\t\tarray(\n-\t\t\t\t\t'type' => 'add',\n-\t\t\t\t\t'min'  => 1,\n-\t\t\t\t\t'max'  => 9,\n-\t\t\t\t),\n-\t\t\t\t\u002F\u002F Simple subtraction.\n-\t\t\t\tarray(\n-\t\t\t\t\t'type' => 'sub',\n-\t\t\t\t\t'min'  => 1,\n-\t\t\t\t\t'max'  => 9,\n-\t\t\t\t),\n-\t\t\t);\n-\n-\t\t\t$template = $puzzle_templates[ array_rand( $puzzle_templates ) ];\n-\n-\t\t\tswitch ( $template['type'] ) {\n-\t\t\t\tcase 'add':\n-\t\t\t\t\t$a = wp_rand( $template['min'], $template['max'] );\n-\t\t\t\t\t$b = wp_rand( $template['min'], $template['max'] );\n-\t\t\t\t\treturn array(\n-\t\t\t\t\t\t'question' => \"{$a} + {$b}\",\n-\t\t\t\t\t\t'answer'   => $a + $b,\n-\t\t\t\t\t);\n-\n-\t\t\t\tcase 'sub':\n-\t\t\t\t\t$a = wp_rand( $template['min'], $template['max'] );\n-\t\t\t\t\t$b = wp_rand( $template['min'], $a );\n-\t\t\t\t\treturn array(\n-\t\t\t\t\t\t'question' => \"{$a} - {$b}\",\n-\t\t\t\t\t\t'answer'   => $a - $b,\n-\t\t\t\t\t);\n-\n-\t\t\t\tdefault:\n-\t\t\t\t\t$a = wp_rand( 1, 9 );\n-\t\t\t\t\t$b = wp_rand( 1, 9 );\n-\t\t\t\t\treturn array(\n-\t\t\t\t\t\t'question' => \"{$a} + {$b}\",\n-\t\t\t\t\t\t'answer'   => $a + $b,\n-\t\t\t\t\t);\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Store puzzle data securely in session\n-\t\t *\n-\t\t * Stores puzzle data using WordPress session system with additional\n-\t\t * security metadata including IP, User-Agent, and timestamp.\n-\t\t * Uses fallback storage methods if primary session storage fails.\n-\t\t *\n-\t\t * @param string $question The puzzle question.\n-\t\t * @param int    $answer The correct answer.\n-\t\t * @param string $ip Client IP address.\n-\t\t * @param string $user_agent Client User-Agent string.\n-\t\t *\u002F\n-\t\tpublic static function mosp_store_puzzle_in_session( $question, $answer, $ip = '', $user_agent = '' ) {\n-\t\t\t$current_ip         = $ip ? $ip : MoSecurityHelper::mosp_get_client_ip();\n-\t\t\t$current_user_agent = $user_agent ? $user_agent : MoSecurityHelper::mosp_get_user_agent();\n-\n-\t\t\t$puzzle_data = array(\n-\t\t\t\t'question'   => sanitize_text_field( wp_unslash( $question ) ),\n-\t\t\t\t'answer'     => intval( $answer ),\n-\t\t\t\t'timestamp'  => time(),\n-\t\t\t\t'ip'         => $current_ip,\n-\t\t\t\t'user_agent' => $current_user_agent,\n-\t\t\t);\n-\n-\t\t\tMoPHPSessions::unset_session( 'mo_osp_current_puzzle' );\n-\n-\t\t\tMoPHPSessions::add_session_var( 'mo_osp_current_puzzle', $puzzle_data );\n-\n-\t\t\t$verification = MoPHPSessions::get_session_var( 'mo_osp_current_puzzle' );\n-\t\t\tif ( ! $verification ) {\n-\n-\t\t\t\t$user_key      = self::generate_user_puzzle_key( $current_ip, $current_user_agent );\n-\t\t\t\t$transient_key = 'mo_osp_puzzle_' . $user_key;\n-\n-\t\t\t\tMoPHPSessions::add_session_var( $transient_key, $puzzle_data );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Generate user-specific puzzle key for fallback storage\n-\t\t *\n-\t\t * @param string $ip Client IP address.\n-\t\t * @param string $user_agent Client User-Agent string.\n-\t\t * @return string Unique key for this user.\n-\t\t *\u002F\n-\t\tprivate static function generate_user_puzzle_key( $ip, $user_agent ) {\n-\t\t\t$unique_data = $ip . '|' . substr( $user_agent, 0, 100 ) . '|' . ( isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '' );\n-\t\t\treturn substr( md5( $unique_data ), 0, 16 );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get puzzle data from storage (primary session or fallback session)\n-\t\t *\n-\t\t * @return array|false Puzzle data or false if not found.\n-\t\t *\u002F\n-\t\tprivate static function get_puzzle_data_from_storage() {\n-\t\t\t$session_puzzle_data = MoPHPSessions::get_session_var( 'mo_osp_current_puzzle' );\n-\n-\t\t\tif ( $session_puzzle_data && is_array( $session_puzzle_data ) ) {\n-\t\t\t\treturn $session_puzzle_data;\n-\t\t\t}\n-\n-\t\t\t$current_ip         = MoSecurityHelper::mosp_get_client_ip();\n-\t\t\t$current_user_agent = MoSecurityHelper::mosp_get_user_agent();\n-\t\t\t$user_key           = self::generate_user_puzzle_key( $current_ip, $current_user_agent );\n-\t\t\t$transient_key      = 'mo_osp_puzzle_' . $user_key;\n-\n-\t\t\t$fallback_data = MoPHPSessions::get_session_var( $transient_key );\n-\t\t\tif ( $fallback_data && is_array( $fallback_data ) ) {\n-\t\t\t\treturn $fallback_data;\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Validate puzzle answer using session-stored data\n-\t\t *\n-\t\t * Validates the user's answer against securely stored puzzle data\n-\t\t * with comprehensive security checks including IP validation,\n-\t\t * User-Agent validation, and timestamp validation.\n-\t\t *\n-\t\t * @param int $user_answer The user's submitted answer.\n-\t\t * @return bool True if answer is correct and all security checks pass.\n-\t\t *\u002F\n-\t\tpublic static function mosp_validate_puzzle_answer_from_session( $user_answer ) {\n-\n-\t\t\t$session_puzzle_data = self::get_puzzle_data_from_storage();\n-\n-\t\t\tif ( ! $session_puzzle_data || ! is_array( $session_puzzle_data ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( ! isset( $session_puzzle_data['question'] ) || ! isset( $session_puzzle_data['answer'] ) || ! isset( $session_puzzle_data['timestamp'] ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$puzzle_age = time() - $session_puzzle_data['timestamp'];\n-\n-\t\t\tif ( $puzzle_age > 600 ) {\n-\t\t\t\tself::mosp_clear_puzzle_from_session();\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$current_ip         = MoSecurityHelper::mosp_get_client_ip();\n-\t\t\t$current_user_agent = MoSecurityHelper::mosp_get_user_agent();\n-\n-\t\t\t$expected_answer = intval( $session_puzzle_data['answer'] );\n-\t\t\t$user_answer_int = intval( $user_answer );\n-\t\t\t$is_correct      = ( $user_answer_int === $expected_answer );\n-\n-\t\t\tif ( $is_correct ) {\n-\t\t\t\tself::mosp_clear_puzzle_from_session();\n-\t\t\t}\n-\n-\t\t\treturn $is_correct;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Clear puzzle data from session\n-\t\t *\n-\t\t * Removes puzzle data from both primary and fallback storage for security cleanup.\n-\t\t *\u002F\n-\t\tpublic static function mosp_clear_puzzle_from_session() {\n-\t\t\tMoPHPSessions::unset_session( 'mo_osp_current_puzzle' );\n-\n-\t\t\t$current_ip         = MoSecurityHelper::mosp_get_client_ip();\n-\t\t\t$current_user_agent = MoSecurityHelper::mosp_get_user_agent();\n-\t\t\t$user_key           = self::generate_user_puzzle_key( $current_ip, $current_user_agent );\n-\t\t\t$transient_key      = 'mo_osp_puzzle_' . $user_key;\n-\n-\t\t\tMoPHPSessions::unset_session( $transient_key );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if puzzle data exists in session.\n-\t\t *\n-\t\t * @return bool True if valid puzzle data exists in session.\n-\t\t *\u002F\n-\t\tpublic static function mosp_has_puzzle_in_session() {\n-\t\t\t$session_puzzle_data = self::get_puzzle_data_from_storage();\n-\n-\t\t\tif ( ! $session_puzzle_data || ! is_array( $session_puzzle_data ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( isset( $session_puzzle_data['timestamp'] ) ) {\n-\t\t\t\t$puzzle_age = time() - $session_puzzle_data['timestamp'];\n-\t\t\t\tif ( $puzzle_age > 600 ) {\n-\t\t\t\t\tself::mosp_clear_puzzle_from_session();\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$has_required_fields = isset( $session_puzzle_data['question'] ) && isset( $session_puzzle_data['answer'] );\n-\n-\t\t\treturn $has_required_fields;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Generate puzzle question as a blurred image.\n-\t\t *\n-\t\t * Creates an image with the puzzle question text, adds distortion effects\n-\t\t * to prevent OCR\u002Fbot bypass, and returns the image as a data URI.\n-\t\t *\n-\t\t * @param string $question The puzzle question text.\n-\t\t * @return string|false The image data URI or false on failure.\n-\t\t *\u002F\n-\t\tpublic static function mosp_generate_puzzle_image( $question ) {\n-\t\t\tif ( ! function_exists( 'imagecreatetruecolor' ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$width  = 280;\n-\t\t\t$height = 60;\n-\n-\t\t\t$image = imagecreatetruecolor( $width, $height );\n-\t\t\tif ( ! $image ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$bg_color    = imagecolorallocate( $image, 255, 255, 255 );\n-\t\t\t$text_color  = imagecolorallocate( $image, 50, 50, 50 );\n-\t\t\t$noise_color = imagecolorallocate( $image, 200, 200, 200 );\n-\n-\t\t\timagefill( $image, 0, 0, $bg_color );\n-\n-\t\t\tfor ( $i = 0; $i \u003C 80; $i++ ) {\n-\t\t\t\timagesetpixel( $image, wp_rand( 0, $width ), wp_rand( 0, $height ), $noise_color );\n-\t\t\t}\n-\n-\t\t\tfor ( $i = 0; $i \u003C 2; $i++ ) {\n-\t\t\t\t$line_color = imagecolorallocate( $image, wp_rand( 220, 240 ), wp_rand( 220, 240 ), wp_rand( 220, 240 ) );\n-\t\t\t\timageline( $image, wp_rand( 0, $width ), wp_rand( 0, $height ), wp_rand( 0, $width ), wp_rand( 0, $height ), $line_color );\n-\t\t\t}\n-\n-\t\t\t$font_path = '';\n-\t\t\t$font_size = 20;\n-\n-\t\t\t$possible_fonts = array(\n-\t\t\t\t'\u002Fusr\u002Fshare\u002Ffonts\u002Ftruetype\u002Fdejavu\u002FDejaVuSans-Bold.ttf',\n-\t\t\t\t'\u002Fusr\u002Fshare\u002Ffonts\u002Ftruetype\u002Fliberation\u002FLiberationSans-Bold.ttf',\n-\t\t\t\t'C:\u002FWindows\u002FFonts\u002Farial.ttf',\n-\t\t\t\t'C:\u002FWindows\u002FFonts\u002Fverdana.ttf',\n-\t\t\t\t'\u002FSystem\u002FLibrary\u002FFonts\u002FHelvetica.ttc',\n-\t\t\t);\n-\n-\t\t\tforeach ( $possible_fonts as $font ) {\n-\t\t\t\tif ( file_exists( $font ) ) {\n-\t\t\t\t\t$font_path = $font;\n-\t\t\t\t\tbreak;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$x = 15;\n-\t\t\t$y = 40;\n-\n-\t\t\tif ( $font_path && function_exists( 'imagettftext' ) ) {\n-\t\t\t\t$angle = wp_rand( -5, 5 );\n-\t\t\t\timagettftext( $image, $font_size, $angle, $x, $y, $text_color, $font_path, $question );\n-\t\t\t} else {\n-\t\t\t\t$font = 5;\n-\t\t\t\timagestring( $image, $font, $x, 20, $question, $text_color );\n-\t\t\t}\n-\n-\t\t\tif ( function_exists( 'imagefilter' ) ) {\n-\t\t\t\timagefilter( $image, IMG_FILTER_GAUSSIAN_BLUR );\n-\t\t\t\timagefilter( $image, IMG_FILTER_SMOOTH, 2 );\n-\t\t\t}\n-\n-\t\t\tob_start();\n-\t\t\timagepng( $image, null, 9 );\n-\t\t\t$image_data = ob_get_clean();\n-\n-\t\t\timagedestroy( $image );\n-\n-\t\t\tif ( $image_data ) {\n-\t\t\t\t\u002F\u002F phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Needed for data URI encoding.\n-\t\t\t\treturn 'data:image\u002Fpng;base64,' . base64_encode( $image_data );\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Render the puzzle popup HTML.\n-\t\t *\n-\t\t * Outputs the puzzle verification popup HTML that can be used on any page.\n-\t\t * This method handles all the HTML markup for the puzzle modal\u002Foverlay.\n-\t\t *\n-\t\t * @return void Outputs HTML directly\n-\t\t *\u002F\n-\t\tpublic static function mosp_render_puzzle_popup() {\n-\t\t\t?>\n-\t\t\t\u003Cdiv id=\"mo-osp-puzzle-overlay\" class=\"mo-osp-puzzle-overlay mo-osp-hidden\">\n-\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-popup\">\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-header\">\n-\t\t\t\t\t\t\u003Ch3 class=\"mo-osp-puzzle-title\">\n-\t\t\t\t\t\t\t\u003Csvg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\u003Cpath d=\"M12 1L3 5V11C3 16.55 6.84 21.74 12 23C17.16 21.74 21 16.55 21 11V5L12 1ZM10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Security Verification', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\u003C\u002Fh3>\n-\t\t\t\t\t\t\u003Cbutton type=\"button\" class=\"mo-osp-puzzle-close\" aria-label=\"\u003C?php echo esc_attr( __( 'Close', 'miniorange-otp-verification' ) ); ?>\">\n-\t\t\t\t\t\t\t\u003Csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\u003Cpath d=\"M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\u003C\u002Fbutton>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-body\">\n-\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-message\">\n-\t\t\t\t\t\t\t\u003Cp>\u003C?php echo esc_html( __( 'For security purposes, please solve this simple puzzle to verify you are human before sending an OTP.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-question\">\n-\t\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-equation\">\n-\t\t\t\t\t\t\t\t\u003Cimg id=\"mo-osp-puzzle-image\" class=\"mo-osp-puzzle-image\" src=\"\" alt=\"\u003C?php echo esc_attr( __( 'Puzzle Question', 'miniorange-otp-verification' ) ); ?>\" style=\"display: none;\" \u002F>\n-\t\t\t\t\t\t\t\t\u003Cspan id=\"mo-osp-puzzle-text\" class=\"mo-osp-equation-text\" style=\"display: none;\">\u003C\u002Fspan>\n-\t\t\t\t\t\t\t\t\u003Cspan class=\"mo-osp-equals\">=\u003C\u002Fspan>\n-\t\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo-osp-puzzle-answer\" class=\"mo-osp-puzzle-input\" placeholder=\"?\" autocomplete=\"off\" \u002F>\n-\t\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-error\" id=\"mo-osp-puzzle-error\" style=\"display: none;\">\n-\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-error-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\u003Cpath d=\"M12 2C6.48 2 2 6.48 2 12S6.48 22 12 22 22 17.52 22 12 17.52 2 12 2ZM13 17H11V15H13V17ZM13 13H11V7H13V13Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\u003Cspan id=\"mo-osp-puzzle-error-text\">\u003C\u002Fspan>\n-\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-footer\">\n-\t\t\t\t\t\t\u003Cbutton type=\"button\" id=\"mo-osp-puzzle-refresh\" class=\"mo-osp-puzzle-btn mo-osp-btn-secondary\">\n-\t\t\t\t\t\t\t\u003Csvg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\u003Cpath d=\"M17.65 6.35C16.2 4.9 14.21 4 12 4C7.58 4 4 7.58 4 12S7.58 20 12 20C15.73 20 18.84 17.45 19.73 14H17.65C16.83 16.33 14.61 18 12 18C8.69 18 6 15.31 6 12S8.69 6 12 6C13.66 6 15.14 6.69 16.22 7.78L13 11H20V4L17.65 6.35Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'New Puzzle', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\u003C\u002Fbutton>\n-\t\t\t\t\t\t\u003Cbutton type=\"button\" id=\"mo-osp-puzzle-verify\" class=\"mo-osp-puzzle-btn mo-osp-btn-primary\">\n-\t\t\t\t\t\t\t\u003Csvg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\u003Cpath d=\"M9 16.17L4.83 12L3.41 13.41L9 19L21 7L19.59 5.59L9 16.17Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Verify & Send OTP', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\u003C\u002Fbutton>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\u003C\u002Fdiv>\n-\t\t\t\u003C?php\n-\t\t}\n-\t}\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * Puzzle Helper Class\r\n+ *\r\n+ * Contains all puzzle generation, validation, and management functions.\r\n+ * This class handles the secure puzzle system for OTP spam prevention.\r\n+ *\r\n+ * @package otpspampreventer\u002Fhelper\r\n+ *\u002F\r\n+\r\n+namespace OSP\\Helper;\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+use OTP\\Helper\\MoPHPSessions;\r\n+\r\n+if ( ! class_exists( 'MoPuzzleHelper' ) ) {\r\n+\t\u002F**\r\n+\t * Puzzle Helper Class\r\n+\t *\r\n+\t * Handles all puzzle-related operations including generation,\r\n+\t * validation, session management, and security checks.\r\n+\t *\u002F\r\n+\tclass MoPuzzleHelper {\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Generate a secure mathematical puzzle\r\n+\t\t *\r\n+\t\t * Creates a random mathematical puzzle with varying difficulty levels.\r\n+\t\t * The puzzle data is designed to be stored securely on the server.\r\n+\t\t *\r\n+\t\t * @return array|false Array with 'question' and 'answer' keys, or false on failure.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_generate_secure_puzzle() {\r\n+\t\t\t$puzzle_templates = array(\r\n+\t\t\t\t\u002F\u002F Simple addition.\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'type' => 'add',\r\n+\t\t\t\t\t'min'  => 1,\r\n+\t\t\t\t\t'max'  => 9,\r\n+\t\t\t\t),\r\n+\t\t\t\t\u002F\u002F Simple subtraction.\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'type' => 'sub',\r\n+\t\t\t\t\t'min'  => 1,\r\n+\t\t\t\t\t'max'  => 9,\r\n+\t\t\t\t),\r\n+\t\t\t);\r\n+\r\n+\t\t\t$template = $puzzle_templates[ array_rand( $puzzle_templates ) ];\r\n+\r\n+\t\t\tswitch ( $template['type'] ) {\r\n+\t\t\t\tcase 'add':\r\n+\t\t\t\t\t$a = wp_rand( $template['min'], $template['max'] );\r\n+\t\t\t\t\t$b = wp_rand( $template['min'], $template['max'] );\r\n+\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t'question' => \"{$a} + {$b}\",\r\n+\t\t\t\t\t\t'answer'   => $a + $b,\r\n+\t\t\t\t\t);\r\n+\r\n+\t\t\t\tcase 'sub':\r\n+\t\t\t\t\t$a = wp_rand( $template['min'], $template['max'] );\r\n+\t\t\t\t\t$b = wp_rand( $template['min'], $a );\r\n+\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t'question' => \"{$a} - {$b}\",\r\n+\t\t\t\t\t\t'answer'   => $a - $b,\r\n+\t\t\t\t\t);\r\n+\r\n+\t\t\t\tdefault:\r\n+\t\t\t\t\t$a = wp_rand( 1, 9 );\r\n+\t\t\t\t\t$b = wp_rand( 1, 9 );\r\n+\t\t\t\t\treturn array(\r\n+\t\t\t\t\t\t'question' => \"{$a} + {$b}\",\r\n+\t\t\t\t\t\t'answer'   => $a + $b,\r\n+\t\t\t\t\t);\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Store puzzle data securely in session\r\n+\t\t *\r\n+\t\t * Stores puzzle data using WordPress session system with additional\r\n+\t\t * security metadata including IP, User-Agent, and timestamp.\r\n+\t\t * Uses fallback storage methods if primary session storage fails.\r\n+\t\t *\r\n+\t\t * @param string $question The puzzle question.\r\n+\t\t * @param int    $answer The correct answer.\r\n+\t\t * @param string $ip Client IP address.\r\n+\t\t * @param string $user_agent Client User-Agent string.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_store_puzzle_in_session( $question, $answer, $ip = '', $user_agent = '' ) {\r\n+\t\t\t$current_ip         = $ip ? $ip : MoSecurityHelper::mosp_get_client_ip();\r\n+\t\t\t$current_user_agent = $user_agent ? $user_agent : MoSecurityHelper::mosp_get_user_agent();\r\n+\r\n+\t\t\t$puzzle_data = array(\r\n+\t\t\t\t'question'   => sanitize_text_field( wp_unslash( $question ) ),\r\n+\t\t\t\t'answer'     => intval( $answer ),\r\n+\t\t\t\t'timestamp'  => time(),\r\n+\t\t\t\t'ip'         => $current_ip,\r\n+\t\t\t\t'user_agent' => $current_user_agent,\r\n+\t\t\t);\r\n+\r\n+\t\t\tMoPHPSessions::unset_session( 'mo_osp_current_puzzle' );\r\n+\r\n+\t\t\tMoPHPSessions::add_session_var( 'mo_osp_current_puzzle', $puzzle_data );\r\n+\r\n+\t\t\t$verification = MoPHPSessions::get_session_var( 'mo_osp_current_puzzle' );\r\n+\t\t\tif ( ! $verification ) {\r\n+\r\n+\t\t\t\t$user_key      = self::generate_user_puzzle_key( $current_ip, $current_user_agent );\r\n+\t\t\t\t$transient_key = 'mo_osp_puzzle_' . $user_key;\r\n+\r\n+\t\t\t\tMoPHPSessions::add_session_var( $transient_key, $puzzle_data );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Generate user-specific puzzle key for fallback storage\r\n+\t\t *\r\n+\t\t * @param string $ip Client IP address.\r\n+\t\t * @param string $user_agent Client User-Agent string.\r\n+\t\t * @return string Unique key for this user.\r\n+\t\t *\u002F\r\n+\t\tprivate static function generate_user_puzzle_key( $ip, $user_agent ) {\r\n+\t\t\t$unique_data = $ip . '|' . substr( $user_agent, 0, 100 ) . '|' . ( isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '' );\r\n+\t\t\treturn substr( md5( $unique_data ), 0, 16 );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get puzzle data from storage (primary session or fallback session)\r\n+\t\t *\r\n+\t\t * @return array|false Puzzle data or false if not found.\r\n+\t\t *\u002F\r\n+\t\tprivate static function get_puzzle_data_from_storage() {\r\n+\t\t\t$session_puzzle_data = MoPHPSessions::get_session_var( 'mo_osp_current_puzzle' );\r\n+\r\n+\t\t\tif ( $session_puzzle_data && is_array( $session_puzzle_data ) ) {\r\n+\t\t\t\treturn $session_puzzle_data;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$current_ip         = MoSecurityHelper::mosp_get_client_ip();\r\n+\t\t\t$current_user_agent = MoSecurityHelper::mosp_get_user_agent();\r\n+\t\t\t$user_key           = self::generate_user_puzzle_key( $current_ip, $current_user_agent );\r\n+\t\t\t$transient_key      = 'mo_osp_puzzle_' . $user_key;\r\n+\r\n+\t\t\t$fallback_data = MoPHPSessions::get_session_var( $transient_key );\r\n+\t\t\tif ( $fallback_data && is_array( $fallback_data ) ) {\r\n+\t\t\t\treturn $fallback_data;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Validate puzzle answer using session-stored data\r\n+\t\t *\r\n+\t\t * Validates the user's answer against securely stored puzzle data\r\n+\t\t * with comprehensive security checks including IP validation,\r\n+\t\t * User-Agent validation, and timestamp validation.\r\n+\t\t *\r\n+\t\t * @param int $user_answer The user's submitted answer.\r\n+\t\t * @return bool True if answer is correct and all security checks pass.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_validate_puzzle_answer_from_session( $user_answer ) {\r\n+\r\n+\t\t\t$session_puzzle_data = self::get_puzzle_data_from_storage();\r\n+\r\n+\t\t\tif ( ! $session_puzzle_data || ! is_array( $session_puzzle_data ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! isset( $session_puzzle_data['question'] ) || ! isset( $session_puzzle_data['answer'] ) || ! isset( $session_puzzle_data['timestamp'] ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$puzzle_age = time() - $session_puzzle_data['timestamp'];\r\n+\r\n+\t\t\tif ( $puzzle_age > 600 ) {\r\n+\t\t\t\tself::mosp_clear_puzzle_from_session();\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$current_ip         = MoSecurityHelper::mosp_get_client_ip();\r\n+\t\t\t$current_user_agent = MoSecurityHelper::mosp_get_user_agent();\r\n+\r\n+\t\t\t$expected_answer = intval( $session_puzzle_data['answer'] );\r\n+\t\t\t$user_answer_int = intval( $user_answer );\r\n+\t\t\t$is_correct      = ( $user_answer_int === $expected_answer );\r\n+\r\n+\t\t\tif ( $is_correct ) {\r\n+\t\t\t\tself::mosp_clear_puzzle_from_session();\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $is_correct;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Clear puzzle data from session\r\n+\t\t *\r\n+\t\t * Removes puzzle data from both primary and fallback storage for security cleanup.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_clear_puzzle_from_session() {\r\n+\t\t\tMoPHPSessions::unset_session( 'mo_osp_current_puzzle' );\r\n+\r\n+\t\t\t$current_ip         = MoSecurityHelper::mosp_get_client_ip();\r\n+\t\t\t$current_user_agent = MoSecurityHelper::mosp_get_user_agent();\r\n+\t\t\t$user_key           = self::generate_user_puzzle_key( $current_ip, $current_user_agent );\r\n+\t\t\t$transient_key      = 'mo_osp_puzzle_' . $user_key;\r\n+\r\n+\t\t\tMoPHPSessions::unset_session( $transient_key );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if puzzle data exists in session.\r\n+\t\t *\r\n+\t\t * @return bool True if valid puzzle data exists in session.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_has_puzzle_in_session() {\r\n+\t\t\t$session_puzzle_data = self::get_puzzle_data_from_storage();\r\n+\r\n+\t\t\tif ( ! $session_puzzle_data || ! is_array( $session_puzzle_data ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( isset( $session_puzzle_data['timestamp'] ) ) {\r\n+\t\t\t\t$puzzle_age = time() - $session_puzzle_data['timestamp'];\r\n+\t\t\t\tif ( $puzzle_age > 600 ) {\r\n+\t\t\t\t\tself::mosp_clear_puzzle_from_session();\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$has_required_fields = isset( $session_puzzle_data['question'] ) && isset( $session_puzzle_data['answer'] );\r\n+\r\n+\t\t\treturn $has_required_fields;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Generate puzzle question as a blurred image.\r\n+\t\t *\r\n+\t\t * Creates an image with the puzzle question text, adds distortion effects\r\n+\t\t * to prevent OCR\u002Fbot bypass, and returns the image as a data URI.\r\n+\t\t *\r\n+\t\t * @param string $question The puzzle question text.\r\n+\t\t * @return string|false The image data URI or false on failure.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_generate_puzzle_image( $question ) {\r\n+\t\t\tif ( ! function_exists( 'imagecreatetruecolor' ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$width  = 280;\r\n+\t\t\t$height = 60;\r\n+\r\n+\t\t\t$image = imagecreatetruecolor( $width, $height );\r\n+\t\t\tif ( ! $image ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$bg_color    = imagecolorallocate( $image, 255, 255, 255 );\r\n+\t\t\t$text_color  = imagecolorallocate( $image, 50, 50, 50 );\r\n+\t\t\t$noise_color = imagecolorallocate( $image, 200, 200, 200 );\r\n+\r\n+\t\t\timagefill( $image, 0, 0, $bg_color );\r\n+\r\n+\t\t\tfor ( $i = 0; $i \u003C 80; $i++ ) {\r\n+\t\t\t\timagesetpixel( $image, wp_rand( 0, $width ), wp_rand( 0, $height ), $noise_color );\r\n+\t\t\t}\r\n+\r\n+\t\t\tfor ( $i = 0; $i \u003C 2; $i++ ) {\r\n+\t\t\t\t$line_color = imagecolorallocate( $image, wp_rand( 220, 240 ), wp_rand( 220, 240 ), wp_rand( 220, 240 ) );\r\n+\t\t\t\timageline( $image, wp_rand( 0, $width ), wp_rand( 0, $height ), wp_rand( 0, $width ), wp_rand( 0, $height ), $line_color );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$font_path = '';\r\n+\t\t\t$font_size = 20;\r\n+\r\n+\t\t\t$possible_fonts = array(\r\n+\t\t\t\t'\u002Fusr\u002Fshare\u002Ffonts\u002Ftruetype\u002Fdejavu\u002FDejaVuSans-Bold.ttf',\r\n+\t\t\t\t'\u002Fusr\u002Fshare\u002Ffonts\u002Ftruetype\u002Fliberation\u002FLiberationSans-Bold.ttf',\r\n+\t\t\t\t'C:\u002FWindows\u002FFonts\u002Farial.ttf',\r\n+\t\t\t\t'C:\u002FWindows\u002FFonts\u002Fverdana.ttf',\r\n+\t\t\t\t'\u002FSystem\u002FLibrary\u002FFonts\u002FHelvetica.ttc',\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $possible_fonts as $font ) {\r\n+\t\t\t\tif ( file_exists( $font ) ) {\r\n+\t\t\t\t\t$font_path = $font;\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$x = 15;\r\n+\t\t\t$y = 40;\r\n+\r\n+\t\t\tif ( $font_path && function_exists( 'imagettftext' ) ) {\r\n+\t\t\t\t$angle = wp_rand( -5, 5 );\r\n+\t\t\t\timagettftext( $image, $font_size, $angle, $x, $y, $text_color, $font_path, $question );\r\n+\t\t\t} else {\r\n+\t\t\t\t$font = 5;\r\n+\t\t\t\timagestring( $image, $font, $x, 20, $question, $text_color );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( function_exists( 'imagefilter' ) ) {\r\n+\t\t\t\timagefilter( $image, IMG_FILTER_GAUSSIAN_BLUR );\r\n+\t\t\t\timagefilter( $image, IMG_FILTER_SMOOTH, 2 );\r\n+\t\t\t}\r\n+\r\n+\t\t\tob_start();\r\n+\t\t\timagepng( $image, null, 9 );\r\n+\t\t\t$image_data = ob_get_clean();\r\n+\r\n+\t\t\timagedestroy( $image );\r\n+\r\n+\t\t\tif ( $image_data ) {\r\n+\t\t\t\t\u002F\u002F phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Needed for data URI encoding.\r\n+\t\t\t\treturn 'data:image\u002Fpng;base64,' . base64_encode( $image_data );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Render the puzzle popup HTML.\r\n+\t\t *\r\n+\t\t * Outputs the puzzle verification popup HTML that can be used on any page.\r\n+\t\t * This method handles all the HTML markup for the puzzle modal\u002Foverlay.\r\n+\t\t *\r\n+\t\t * @return void Outputs HTML directly\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_render_puzzle_popup() {\r\n+\t\t\t?>\r\n+\t\t\t\u003Cdiv id=\"mo-osp-puzzle-overlay\" class=\"mo-osp-puzzle-overlay mo-osp-hidden\">\r\n+\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-popup\">\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-header\">\r\n+\t\t\t\t\t\t\u003Ch3 class=\"mo-osp-puzzle-title\">\r\n+\t\t\t\t\t\t\t\u003Csvg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\u003Cpath d=\"M12 1L3 5V11C3 16.55 6.84 21.74 12 23C17.16 21.74 21 16.55 21 11V5L12 1ZM10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Security Verification', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\u003C\u002Fh3>\r\n+\t\t\t\t\t\t\u003Cbutton type=\"button\" class=\"mo-osp-puzzle-close\" aria-label=\"\u003C?php echo esc_attr( __( 'Close', 'miniorange-otp-verification' ) ); ?>\">\r\n+\t\t\t\t\t\t\t\u003Csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\u003Cpath d=\"M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\u003C\u002Fbutton>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-body\">\r\n+\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-message\">\r\n+\t\t\t\t\t\t\t\u003Cp>\u003C?php echo esc_html( __( 'For security purposes, please solve this simple puzzle to verify you are human before sending an OTP.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-question\">\r\n+\t\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-equation\">\r\n+\t\t\t\t\t\t\t\t\u003Cimg id=\"mo-osp-puzzle-image\" class=\"mo-osp-puzzle-image\" src=\"\" alt=\"\u003C?php echo esc_attr( __( 'Puzzle Question', 'miniorange-otp-verification' ) ); ?>\" style=\"display: none;\" \u002F>\r\n+\t\t\t\t\t\t\t\t\u003Cspan id=\"mo-osp-puzzle-text\" class=\"mo-osp-equation-text\" style=\"display: none;\">\u003C\u002Fspan>\r\n+\t\t\t\t\t\t\t\t\u003Cspan class=\"mo-osp-equals\">=\u003C\u002Fspan>\r\n+\t\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo-osp-puzzle-answer\" class=\"mo-osp-puzzle-input\" placeholder=\"?\" autocomplete=\"off\" \u002F>\r\n+\t\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-error\" id=\"mo-osp-puzzle-error\" style=\"display: none;\">\r\n+\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-error-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\u003Cpath d=\"M12 2C6.48 2 2 6.48 2 12S6.48 22 12 22 22 17.52 22 12 17.52 2 12 2ZM13 17H11V15H13V17ZM13 13H11V7H13V13Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\u003Cspan id=\"mo-osp-puzzle-error-text\">\u003C\u002Fspan>\r\n+\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-puzzle-footer\">\r\n+\t\t\t\t\t\t\u003Cbutton type=\"button\" id=\"mo-osp-puzzle-refresh\" class=\"mo-osp-puzzle-btn mo-osp-btn-secondary\">\r\n+\t\t\t\t\t\t\t\u003Csvg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\u003Cpath d=\"M17.65 6.35C16.2 4.9 14.21 4 12 4C7.58 4 4 7.58 4 12S7.58 20 12 20C15.73 20 18.84 17.45 19.73 14H17.65C16.83 16.33 14.61 18 12 18C8.69 18 6 15.31 6 12S8.69 6 12 6C13.66 6 15.14 6.69 16.22 7.78L13 11H20V4L17.65 6.35Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'New Puzzle', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\u003C\u002Fbutton>\r\n+\t\t\t\t\t\t\u003Cbutton type=\"button\" id=\"mo-osp-puzzle-verify\" class=\"mo-osp-puzzle-btn mo-osp-btn-primary\">\r\n+\t\t\t\t\t\t\t\u003Csvg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\u003Cpath d=\"M9 16.17L4.83 12L3.41 13.41L9 19L21 7L19.59 5.59L9 16.17Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Verify & Send OTP', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\u003C\u002Fbutton>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\u003C?php\r\n+\t\t}\r\n+\t}\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhelper\u002Fclass-mosecurityhelper.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhelper\u002Fclass-mosecurityhelper.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fhelper\u002Fclass-mosecurityhelper.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fhelper\u002Fclass-mosecurityhelper.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,275 +1,275 @@\n-\u003C?php\n-\u002F**\n- * Security Helper Class\n- *\n- * Contains all security-related validation and verification functions.\n- * This class handles nonce verification, token management, and security checks.\n- *\n- * @package otpspampreventer\u002Fhelper\n- *\u002F\n-\n-namespace OSP\\Helper;\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-use OTP\\Helper\\MoPHPSessions;\n-use OTP\\Helper\\MoUtility;\n-\n-if ( ! class_exists( 'MoSecurityHelper' ) ) {\n-\t\u002F**\n-\t * Security Helper Class\n-\t *\n-\t * Handles all security-related operations including nonce verification,\n-\t * token generation, verification token management, and security validation.\n-\t *\u002F\n-\tclass MoSecurityHelper {\n-\n-\t\t\u002F**\n-\t\t * Counting window for spam detection (15 minutes in seconds)\n-\t\t *\n-\t\t * This constant defines the time window used for counting attempts\n-\t\t * in spam detection algorithms. Change this value in one place to\n-\t\t * modify the counting window across the entire addon.\n-\t\t *\u002F\n-\t\tconst COUNTING_WINDOW_SECONDS = 900; \u002F\u002F 15 minutes (15 * 60)\n-\n-\t\t\u002F**\n-\t\t * Normalize email or phone the same way as puzzle verify \u002F OTP send paths.\n-\t\t *\n-\t\t * @param string $value Raw value.\n-\t\t * @param bool   $is_email Whether this value is an email field.\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tprivate static function mosp_identifier_for_puzzle_key( $value, $is_email ) {\n-\t\t\t$value = trim( (string) $value );\n-\t\t\tif ( '' === $value ) {\n-\t\t\t\treturn '';\n-\t\t\t}\n-\t\t\tif ( $is_email ) {\n-\t\t\t\treturn sanitize_email( $value );\n-\t\t\t}\n-\t\t\treturn MoUtility::process_phone_number( $value );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * All user-identifier strings that may have been used to build the puzzle token\n-\t\t * (WooCommerce phone checkout clears email in the spam hook but puzzle AJAX often sends both).\n-\t\t *\n-\t\t * @param string $user_email Email from current request context.\n-\t\t * @param string $phone_number Phone from current request context.\n-\t\t * @return string[] Unique normalized identifiers.\n-\t\t *\u002F\n-\t\tprivate static function mosp_collect_puzzle_verification_identifiers( $user_email, $phone_number ) {\n-\t\t\t$pairs = array(\n-\t\t\t\tarray( $user_email, true ),\n-\t\t\t\tarray( $phone_number, false ),\n-\t\t\t\tarray( MoPHPSessions::get_session_var( 'user_email' ), true ),\n-\t\t\t\tarray( MoPHPSessions::get_session_var( 'phone_number_mo' ), false ),\n-\t\t\t);\n-\t\t\t$ids   = array();\n-\t\t\tforeach ( $pairs as $pair ) {\n-\t\t\t\t$n = self::mosp_identifier_for_puzzle_key( $pair[0], $pair[1] );\n-\t\t\t\tif ( '' !== $n ) {\n-\t\t\t\t\t$ids[] = $n;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn array_values( array_unique( $ids ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Build session\u002Ftoken key string for one identifier (must match generate path).\n-\t\t *\n-\t\t * @param string $user_identifier Normalized email or phone.\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tprivate static function mosp_build_puzzle_verification_key_string( $user_identifier ) {\n-\t\t\t$session_id = session_id() ? session_id() : wp_get_session_token();\n-\t\t\t$ip         = self::mosp_get_client_ip();\n-\t\t\treturn 'mo_osp_puzzle_verified_' . md5( $user_identifier . $session_id . $ip );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Every puzzle verification key to try for this request (email-only, phone-only, session fallbacks).\n-\t\t *\n-\t\t * @param string $user_email Email address.\n-\t\t * @param string $phone_number Phone number.\n-\t\t * @return string[]\n-\t\t *\u002F\n-\t\tpublic static function mosp_get_puzzle_verification_keys( $user_email, $phone_number ) {\n-\t\t\t$keys = array();\n-\t\t\tforeach ( self::mosp_collect_puzzle_verification_identifiers( $user_email, $phone_number ) as $id ) {\n-\t\t\t\t$keys[] = self::mosp_build_puzzle_verification_key_string( $id );\n-\t\t\t}\n-\t\t\treturn array_values( array_unique( array_filter( $keys ) ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Verify puzzle completion through secure server-side validation\n-\t\t *\n-\t\t * This method replaces the vulnerable $_POST['mo_osp_puzzle_processed'] check\n-\t\t * with proper server-side validation using nonces and session data.\n-\t\t *\n-\t\t * @param string $user_email Email address.\n-\t\t * @param string $phone_number Phone number.\n-\t\t * @return bool True if puzzle verification is valid and recent.\n-\t\t *\u002F\n-\t\tpublic static function mosp_is_puzzle_verification_valid( $user_email, $phone_number ) {\n-\t\t\t$keys = self::mosp_get_puzzle_verification_keys( $user_email, $phone_number );\n-\n-\t\t\tforeach ( $keys as $verification_key ) {\n-\t\t\t\tif ( '' === $verification_key ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\t$verification_time = MoPHPSessions::get_session_var( $verification_key );\n-\t\t\t\tif ( $verification_time && ( time() - $verification_time ) \u003C= 300 ) {\n-\t\t\t\t\t$used_key = $verification_key . '_used';\n-\t\t\t\t\tif ( MoPHPSessions::get_session_var( $used_key ) ) {\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tMoPHPSessions::add_session_var( $used_key, time() );\n-\t\t\t\t\tMoPHPSessions::unset_session( $verification_key );\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t$posted_verified = isset( $_POST['puzzle_verified'] ) ? sanitize_text_field( wp_unslash( $_POST['puzzle_verified'] ) ) : '';\n-\t\t\t$posted_nonce    = isset( $_POST['mo_osp_puzzle_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_puzzle_nonce'] ) ) : '';\n-\t\t\t$posted_token    = isset( $_POST['verification_token'] ) ? sanitize_text_field( wp_unslash( $_POST['verification_token'] ) ) : '';\n-\n-\t\t\tif ( 'true' === $posted_verified && ! empty( $posted_nonce ) && ! empty( $posted_token ) ) {\n-\t\t\t\tif ( wp_verify_nonce( $posted_nonce, 'mo_osp_puzzle_verify' ) ) {\n-\t\t\t\t\tforeach ( $keys as $verification_key ) {\n-\t\t\t\t\t\tif ( '' === $verification_key || ! hash_equals( $verification_key, $posted_token ) ) {\n-\t\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\t$used_key = $verification_key . '_used';\n-\t\t\t\t\t\tif ( ! MoPHPSessions::get_session_var( $used_key ) ) {\n-\t\t\t\t\t\t\tMoPHPSessions::add_session_var( $used_key, time() );\n-\t\t\t\t\t\t\treturn true;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Mark puzzle verification as complete with secure server-side storage.\n-\t\t *\n-\t\t * @param string $user_email Email address.\n-\t\t * @param string $phone_number Phone number.\n-\t\t *\u002F\n-\t\tpublic static function mosp_mark_puzzle_verification_complete( $user_email, $phone_number ) {\n-\t\t\t$verification_key = self::mosp_get_puzzle_verification_key( $user_email, $phone_number );\n-\n-\t\t\tMoPHPSessions::add_session_var( $verification_key, time() );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get unique puzzle verification key for user.\n-\t\t *\n-\t\t * @param string $user_email Email address.\n-\t\t * @param string $phone_number Phone number.\n-\t\t * @return string Unique verification key.\n-\t\t *\u002F\n-\t\tpublic static function mosp_get_puzzle_verification_key( $user_email, $phone_number ) {\n-\t\t\tif ( ! empty( $user_email ) ) {\n-\t\t\t\t$id = self::mosp_identifier_for_puzzle_key( $user_email, true );\n-\t\t\t} else {\n-\t\t\t\t$id = self::mosp_identifier_for_puzzle_key( $phone_number, false );\n-\t\t\t}\n-\t\t\tif ( '' === $id ) {\n-\t\t\t\treturn '';\n-\t\t\t}\n-\t\t\treturn self::mosp_build_puzzle_verification_key_string( $id );\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Generate secure puzzle verification token.\n-\t\t *\n-\t\t * @param string $email Email address.\n-\t\t * @param string $phone Phone number.\n-\t\t * @return string Verification token.\n-\t\t *\u002F\n-\t\tpublic static function mosp_generate_puzzle_verification_token( $email, $phone ) {\n-\t\t\t$timestamp        = time();\n-\t\t\t$verification_key = self::mosp_get_puzzle_verification_key( $email, $phone );\n-\t\t\tif ( '' === $verification_key ) {\n-\t\t\t\treturn '';\n-\t\t\t}\n-\n-\t\t\tMoPHPSessions::add_session_var( $verification_key, $timestamp );\n-\n-\t\t\treturn $verification_key;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get client IP address with comprehensive header checking.\n-\t\t *\n-\t\t * This is the centralized IP detection method used across all helpers.\n-\t\t *\n-\t\t * @return string Client IP address.\n-\t\t *\u002F\n-\t\tpublic static function mosp_get_client_ip() {\n-\t\t\t$ip_headers = array(\n-\t\t\t\t'HTTP_CF_CONNECTING_IP',     \u002F\u002F Cloudflare.\n-\t\t\t\t'HTTP_CLIENT_IP',            \u002F\u002F Proxy.\n-\t\t\t\t'HTTP_X_FORWARDED_FOR',      \u002F\u002F Load balancer\u002Fproxy.\n-\t\t\t\t'HTTP_X_FORWARDED',          \u002F\u002F Proxy.\n-\t\t\t\t'HTTP_X_CLUSTER_CLIENT_IP',  \u002F\u002F Cluster.\n-\t\t\t\t'HTTP_FORWARDED_FOR',        \u002F\u002F Proxy.\n-\t\t\t\t'HTTP_FORWARDED',            \u002F\u002F Proxy.\n-\t\t\t\t'REMOTE_ADDR',                \u002F\u002F Standard.\n-\t\t\t);\n-\n-\t\t\tforeach ( $ip_headers as $header ) {\n-\t\t\t\tif ( ! empty( $_SERVER[ $header ] ) ) {\n-\t\t\t\t\t$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );\n-\t\t\t\t\tif ( strpos( $ip, ',' ) !== false ) {\n-\t\t\t\t\t\t$ip = trim( explode( ',', $ip )[0] );\n-\t\t\t\t\t}\n-\t\t\t\t\tif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {\n-\t\t\t\t\t\treturn $ip;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get client User-Agent string.\n-\t\t *\n-\t\t * This is the centralized User-Agent detection method used across all helpers.\n-\t\t *\n-\t\t * @return string Client User-Agent string.\n-\t\t *\u002F\n-\t\tpublic static function mosp_get_user_agent() {\n-\t\t\treturn isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Log security events with standardized format.\n-\t\t *\n-\t\t * Note: Callers should sanitize context data before passing to avoid logging sensitive information.\n-\t\t *\n-\t\t * @param string $event_type The type of security event.\n-\t\t * @param string $message The security message.\n-\t\t * @param array  $context Additional context data (should be sanitized by caller).\n-\t\t *\u002F\n-\t\tpublic static function mosp_log_security_event( $event_type, $message, $context = array() ) {\n-\t\t\t$log_message = 'MO_OSP: SECURITY - ' . strtoupper( $event_type ) . ' - ' . $message;\n-\n-\t\t\tif ( ! empty( $context ) ) {\n-\t\t\t\t$log_message .= ' - Context: ' . wp_json_encode( $context );\n-\t\t\t}\n-\n-\t\t\t$log_message = apply_filters( 'mo_osp_security_log_message', $log_message, $event_type, $message, $context );\n-\t\t}\n-\t}\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * Security Helper Class\r\n+ *\r\n+ * Contains all security-related validation and verification functions.\r\n+ * This class handles nonce verification, token management, and security checks.\r\n+ *\r\n+ * @package otpspampreventer\u002Fhelper\r\n+ *\u002F\r\n+\r\n+namespace OSP\\Helper;\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+use OTP\\Helper\\MoPHPSessions;\r\n+use OTP\\Helper\\MoUtility;\r\n+\r\n+if ( ! class_exists( 'MoSecurityHelper' ) ) {\r\n+\t\u002F**\r\n+\t * Security Helper Class\r\n+\t *\r\n+\t * Handles all security-related operations including nonce verification,\r\n+\t * token generation, verification token management, and security validation.\r\n+\t *\u002F\r\n+\tclass MoSecurityHelper {\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Counting window for spam detection (15 minutes in seconds)\r\n+\t\t *\r\n+\t\t * This constant defines the time window used for counting attempts\r\n+\t\t * in spam detection algorithms. Change this value in one place to\r\n+\t\t * modify the counting window across the entire addon.\r\n+\t\t *\u002F\r\n+\t\tconst COUNTING_WINDOW_SECONDS = 900; \u002F\u002F 15 minutes (15 * 60)\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Normalize email or phone the same way as puzzle verify \u002F OTP send paths.\r\n+\t\t *\r\n+\t\t * @param string $value Raw value.\r\n+\t\t * @param bool   $is_email Whether this value is an email field.\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tprivate static function mosp_identifier_for_puzzle_key( $value, $is_email ) {\r\n+\t\t\t$value = trim( (string) $value );\r\n+\t\t\tif ( '' === $value ) {\r\n+\t\t\t\treturn '';\r\n+\t\t\t}\r\n+\t\t\tif ( $is_email ) {\r\n+\t\t\t\treturn sanitize_email( $value );\r\n+\t\t\t}\r\n+\t\t\treturn MoUtility::process_phone_number( $value );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * All user-identifier strings that may have been used to build the puzzle token\r\n+\t\t * (WooCommerce phone checkout clears email in the spam hook but puzzle AJAX often sends both).\r\n+\t\t *\r\n+\t\t * @param string $user_email Email from current request context.\r\n+\t\t * @param string $phone_number Phone from current request context.\r\n+\t\t * @return string[] Unique normalized identifiers.\r\n+\t\t *\u002F\r\n+\t\tprivate static function mosp_collect_puzzle_verification_identifiers( $user_email, $phone_number ) {\r\n+\t\t\t$pairs = array(\r\n+\t\t\t\tarray( $user_email, true ),\r\n+\t\t\t\tarray( $phone_number, false ),\r\n+\t\t\t\tarray( MoPHPSessions::get_session_var( 'user_email' ), true ),\r\n+\t\t\t\tarray( MoPHPSessions::get_session_var( 'phone_number_mo' ), false ),\r\n+\t\t\t);\r\n+\t\t\t$ids   = array();\r\n+\t\t\tforeach ( $pairs as $pair ) {\r\n+\t\t\t\t$n = self::mosp_identifier_for_puzzle_key( $pair[0], $pair[1] );\r\n+\t\t\t\tif ( '' !== $n ) {\r\n+\t\t\t\t\t$ids[] = $n;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn array_values( array_unique( $ids ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Build session\u002Ftoken key string for one identifier (must match generate path).\r\n+\t\t *\r\n+\t\t * @param string $user_identifier Normalized email or phone.\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tprivate static function mosp_build_puzzle_verification_key_string( $user_identifier ) {\r\n+\t\t\t$session_id = session_id() ? session_id() : wp_get_session_token();\r\n+\t\t\t$ip         = self::mosp_get_client_ip();\r\n+\t\t\treturn 'mo_osp_puzzle_verified_' . md5( $user_identifier . $session_id . $ip );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Every puzzle verification key to try for this request (email-only, phone-only, session fallbacks).\r\n+\t\t *\r\n+\t\t * @param string $user_email Email address.\r\n+\t\t * @param string $phone_number Phone number.\r\n+\t\t * @return string[]\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_get_puzzle_verification_keys( $user_email, $phone_number ) {\r\n+\t\t\t$keys = array();\r\n+\t\t\tforeach ( self::mosp_collect_puzzle_verification_identifiers( $user_email, $phone_number ) as $id ) {\r\n+\t\t\t\t$keys[] = self::mosp_build_puzzle_verification_key_string( $id );\r\n+\t\t\t}\r\n+\t\t\treturn array_values( array_unique( array_filter( $keys ) ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Verify puzzle completion through secure server-side validation\r\n+\t\t *\r\n+\t\t * This method replaces the vulnerable $_POST['mo_osp_puzzle_processed'] check\r\n+\t\t * with proper server-side validation using nonces and session data.\r\n+\t\t *\r\n+\t\t * @param string $user_email Email address.\r\n+\t\t * @param string $phone_number Phone number.\r\n+\t\t * @return bool True if puzzle verification is valid and recent.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_is_puzzle_verification_valid( $user_email, $phone_number ) {\r\n+\t\t\t$keys = self::mosp_get_puzzle_verification_keys( $user_email, $phone_number );\r\n+\r\n+\t\t\tforeach ( $keys as $verification_key ) {\r\n+\t\t\t\tif ( '' === $verification_key ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t\t$verification_time = MoPHPSessions::get_session_var( $verification_key );\r\n+\t\t\t\tif ( $verification_time && ( time() - $verification_time ) \u003C= 300 ) {\r\n+\t\t\t\t\t$used_key = $verification_key . '_used';\r\n+\t\t\t\t\tif ( MoPHPSessions::get_session_var( $used_key ) ) {\r\n+\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\tMoPHPSessions::add_session_var( $used_key, time() );\r\n+\t\t\t\t\tMoPHPSessions::unset_session( $verification_key );\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t$posted_verified = isset( $_POST['puzzle_verified'] ) ? sanitize_text_field( wp_unslash( $_POST['puzzle_verified'] ) ) : '';\r\n+\t\t\t$posted_nonce    = isset( $_POST['mo_osp_puzzle_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['mo_osp_puzzle_nonce'] ) ) : '';\r\n+\t\t\t$posted_token    = isset( $_POST['verification_token'] ) ? sanitize_text_field( wp_unslash( $_POST['verification_token'] ) ) : '';\r\n+\r\n+\t\t\tif ( 'true' === $posted_verified && ! empty( $posted_nonce ) && ! empty( $posted_token ) ) {\r\n+\t\t\t\tif ( wp_verify_nonce( $posted_nonce, 'mo_osp_puzzle_verify' ) ) {\r\n+\t\t\t\t\tforeach ( $keys as $verification_key ) {\r\n+\t\t\t\t\t\tif ( '' === $verification_key || ! hash_equals( $verification_key, $posted_token ) ) {\r\n+\t\t\t\t\t\t\tcontinue;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t\t$used_key = $verification_key . '_used';\r\n+\t\t\t\t\t\tif ( ! MoPHPSessions::get_session_var( $used_key ) ) {\r\n+\t\t\t\t\t\t\tMoPHPSessions::add_session_var( $used_key, time() );\r\n+\t\t\t\t\t\t\treturn true;\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Mark puzzle verification as complete with secure server-side storage.\r\n+\t\t *\r\n+\t\t * @param string $user_email Email address.\r\n+\t\t * @param string $phone_number Phone number.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_mark_puzzle_verification_complete( $user_email, $phone_number ) {\r\n+\t\t\t$verification_key = self::mosp_get_puzzle_verification_key( $user_email, $phone_number );\r\n+\r\n+\t\t\tMoPHPSessions::add_session_var( $verification_key, time() );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get unique puzzle verification key for user.\r\n+\t\t *\r\n+\t\t * @param string $user_email Email address.\r\n+\t\t * @param string $phone_number Phone number.\r\n+\t\t * @return string Unique verification key.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_get_puzzle_verification_key( $user_email, $phone_number ) {\r\n+\t\t\tif ( ! empty( $user_email ) ) {\r\n+\t\t\t\t$id = self::mosp_identifier_for_puzzle_key( $user_email, true );\r\n+\t\t\t} else {\r\n+\t\t\t\t$id = self::mosp_identifier_for_puzzle_key( $phone_number, false );\r\n+\t\t\t}\r\n+\t\t\tif ( '' === $id ) {\r\n+\t\t\t\treturn '';\r\n+\t\t\t}\r\n+\t\t\treturn self::mosp_build_puzzle_verification_key_string( $id );\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Generate secure puzzle verification token.\r\n+\t\t *\r\n+\t\t * @param string $email Email address.\r\n+\t\t * @param string $phone Phone number.\r\n+\t\t * @return string Verification token.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_generate_puzzle_verification_token( $email, $phone ) {\r\n+\t\t\t$timestamp        = time();\r\n+\t\t\t$verification_key = self::mosp_get_puzzle_verification_key( $email, $phone );\r\n+\t\t\tif ( '' === $verification_key ) {\r\n+\t\t\t\treturn '';\r\n+\t\t\t}\r\n+\r\n+\t\t\tMoPHPSessions::add_session_var( $verification_key, $timestamp );\r\n+\r\n+\t\t\treturn $verification_key;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get client IP address with comprehensive header checking.\r\n+\t\t *\r\n+\t\t * This is the centralized IP detection method used across all helpers.\r\n+\t\t *\r\n+\t\t * @return string Client IP address.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_get_client_ip() {\r\n+\t\t\t$ip_headers = array(\r\n+\t\t\t\t'HTTP_CF_CONNECTING_IP',     \u002F\u002F Cloudflare.\r\n+\t\t\t\t'HTTP_CLIENT_IP',            \u002F\u002F Proxy.\r\n+\t\t\t\t'HTTP_X_FORWARDED_FOR',      \u002F\u002F Load balancer\u002Fproxy.\r\n+\t\t\t\t'HTTP_X_FORWARDED',          \u002F\u002F Proxy.\r\n+\t\t\t\t'HTTP_X_CLUSTER_CLIENT_IP',  \u002F\u002F Cluster.\r\n+\t\t\t\t'HTTP_FORWARDED_FOR',        \u002F\u002F Proxy.\r\n+\t\t\t\t'HTTP_FORWARDED',            \u002F\u002F Proxy.\r\n+\t\t\t\t'REMOTE_ADDR',                \u002F\u002F Standard.\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $ip_headers as $header ) {\r\n+\t\t\t\tif ( ! empty( $_SERVER[ $header ] ) ) {\r\n+\t\t\t\t\t$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );\r\n+\t\t\t\t\tif ( strpos( $ip, ',' ) !== false ) {\r\n+\t\t\t\t\t\t$ip = trim( explode( ',', $ip )[0] );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {\r\n+\t\t\t\t\t\treturn $ip;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get client User-Agent string.\r\n+\t\t *\r\n+\t\t * This is the centralized User-Agent detection method used across all helpers.\r\n+\t\t *\r\n+\t\t * @return string Client User-Agent string.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_get_user_agent() {\r\n+\t\t\treturn isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Log security events with standardized format.\r\n+\t\t *\r\n+\t\t * Note: Callers should sanitize context data before passing to avoid logging sensitive information.\r\n+\t\t *\r\n+\t\t * @param string $event_type The type of security event.\r\n+\t\t * @param string $message The security message.\r\n+\t\t * @param array  $context Additional context data (should be sanitized by caller).\r\n+\t\t *\u002F\r\n+\t\tpublic static function mosp_log_security_event( $event_type, $message, $context = array() ) {\r\n+\t\t\t$log_message = 'MO_OSP: SECURITY - ' . strtoupper( $event_type ) . ' - ' . $message;\r\n+\r\n+\t\t\tif ( ! empty( $context ) ) {\r\n+\t\t\t\t$log_message .= ' - Context: ' . wp_json_encode( $context );\r\n+\t\t\t}\r\n+\r\n+\t\t\t$log_message = apply_filters( 'mo_osp_security_log_message', $log_message, $event_type, $message, $context );\r\n+\t\t}\r\n+\t}\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fcss\u002Fmo-admin.css \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fcss\u002Fmo-admin.css\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fcss\u002Fmo-admin.css\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fcss\u002Fmo-admin.css\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,855 +1,855 @@\n-\u002F* OTP Spam Preventer Admin Styles *\u002F\n-\u002F* Professional spacing and layout *\u002F\n-\n-\u002F* Main Container *\u002F\n-.mo-osp-container {\n-    max-width: 1200px;\n-    margin: 0 auto;\n-}\n-\n-\u002F* Using plugin's existing mo-header classes - no custom header CSS needed *\u002F\n-\u002F* Addon toggle *\u002F\n-.mo-osp-addon-toggle {\n-    display: inline-flex;\n-    align-items: center;\n-    gap: 6px;\n-    font-size: 16px;\n-    color: rgb(51 65 85);\n-}\n-\n-.mo-osp-addon-toggle input {\n-    margin: 0;\n-}\n-\n-\u002F* Toggle placement above basic settings *\u002F\n-.mo-osp-addon-toggle-row {\n-    display: flex;\n-    align-items: center;\n-    justify-content: flex-start;\n-    gap: 12px;\n-    padding: 0 20px 12px 20px;\n-}\n-\n-.mo-osp-addon-toggle-emphasis {\n-    padding: 2px 0;\n-    font-weight: 700;\n-    color: rgb(30 64 175);\n-}\n-\n-.mo-osp-addon-toggle-emphasis input {\n-    transform: scale(1.2);\n-}\n-\n-\u002F* SVG Icon Styles *\u002F\n-.mo-osp-header-icon {\n-    margin-right: 8px;\n-    color: rgb(51 65 85);\n-    vertical-align: middle;\n-}\n-\n-.mo-osp-section-icon {\n-    margin-right: 8px;\n-    color: rgb(51 65 85);\n-    vertical-align: middle;\n-}\n-\n-.mo-osp-subsection-icon {\n-    margin-right: 6px;\n-    color: rgb(51 65 85);\n-    vertical-align: middle;\n-}\n-\n-.mo-osp-field-icon {\n-    margin-right: 6px;\n-    color: rgb(100 116 139);\n-    vertical-align: middle;\n-}\n-\n-\u002F* Icon alignment in titles and labels *\u002F\n-.mo-osp-section-title,\n-.mo-osp-subsection-title {\n-    display: flex;\n-    align-items: center;\n-}\n-\n-\n-.mo-input-label {\n-    display: flex !important;\n-    align-items: center;\n-}\n-\n-\u002F* Card Layout *\u002F\n-.mo-osp-card {\n-    background: rgb(255 255 255);\n-    border: 1px solid rgb(226 232 240);\n-    border-radius: 8px;\n-    margin-bottom: 24px;\n-    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);\n-    overflow: hidden;\n-    margin: 10px 20px;\n-}\n-\n-.mo-osp-card:last-child {\n-    margin-bottom: 32px; \u002F* Add bottom margin to advanced settings card *\u002F\n-}\n-\n-\u002F* Card Header *\u002F\n-.mo-osp-card-header {\n-    padding: 24px 24px 16px 24px;\n-    border-bottom: 1px solid rgb(241 245 249);\n-}\n-\n-.mo-osp-section-title {\n-    font-size: 18px;\n-    font-weight: 600;\n-    color: rgb(51 65 85);\n-    margin: 0 0 8px 0;\n-    line-height: 1.4;\n-}\n-\n-.mo-osp-section-desc {\n-    font-size: 14px;\n-    color: rgb(100 116 139);\n-    margin: 0;\n-    line-height: 1.5;\n-}\n-\n-\u002F* Card Body *\u002F\n-.mo-osp-card-body {\n-    padding: 20px 24px 24px 24px;\n-}\n-\n-\u002F* Fields Grid *\u002F\n-.mo-osp-fields-grid {\n-    display: grid;\n-    grid-template-columns: 1fr;\n-    gap: 24px;\n-}\n-\n-@media (min-width: 768px) {\n-    .mo-osp-fields-grid {\n-        grid-template-columns: 1fr 1fr;\n-        gap: 32px 24px;\n-    }\n-}\n-\n-\u002F* Field Groups *\u002F\n-.mo-osp-field-group {\n-    display: flex;\n-    flex-direction: column;\n-    gap: 8px;\n-}\n-\n-.mo-osp-field-full {\n-    grid-column: 1 \u002F -1;\n-}\n-\n-.mo-osp-field-desc {\n-    font-size: 13px;\n-    color: rgb(100 116 139);\n-    margin: 0;\n-    line-height: 1.4;\n-}\n-\n-\u002F* Toggle Button *\u002F\n-.mo-osp-toggle-btn {\n-    width: 100%;\n-    padding: 20px 24px;\n-    background: transparent;\n-    border: none;\n-    text-align: left;\n-    cursor: pointer;\n-    transition: background-color 0.15s ease;\n-}\n-\n-.mo-osp-toggle-btn:hover {\n-    background-color: rgb(248 250 252);\n-}\n-\n-.mo-osp-toggle-content {\n-    display: flex;\n-    align-items: center;\n-    justify-content: space-between;\n-    gap: 16px;\n-}\n-\n-.mo-osp-toggle-indicator {\n-    display: flex;\n-    align-items: center;\n-    gap: 8px;\n-}\n-\n-.mo-osp-toggle-text {\n-    font-size: 14px;\n-    color: rgb(100 116 139);\n-    font-weight: 500;\n-}\n-\n-.mo-osp-toggle-icon {\n-    font-size: 12px;\n-    color: rgb(100 116 139);\n-    transition: transform 0.2s ease;\n-    display: inline-block;\n-}\n-\n-.mo-osp-toggle-icon.rotate-180 {\n-    transform: rotate(180deg);\n-}\n-\n-\u002F* Advanced Settings *\u002F\n-.mo-osp-advanced-hidden {\n-    display: none;\n-}\n-\n-.mo-osp-advanced-visible {\n-    display: block;\n-}\n-\n-.mo-osp-advanced-content {\n-    padding: 0 24px 32px 24px; \u002F* Increased bottom padding *\u002F\n-    border-top: 1px solid rgb(241 245 249);\n-}\n-\n-\u002F* Subsections *\u002F\n-.mo-osp-subsection {\n-    margin-top: 32px;\n-}\n-\n-.mo-osp-subsection:last-child {\n-    margin-bottom: 0;\n-}\n-\n-.mo-osp-subsection-header {\n-    margin-bottom: 20px;\n-    padding-top: 24px;\n-}\n-\n-.mo-osp-subsection:first-child .mo-osp-subsection-header {\n-    padding-top: 0;\n-}\n-\n-.mo-osp-subsection-title {\n-    font-size: 16px;\n-    font-weight: 600;\n-    color: rgb(51 65 85);\n-    margin: 0 0 6px 0;\n-    line-height: 1.4;\n-}\n-\n-\u002F* Form Input Overrides *\u002F\n-.mo-osp-container .mo-form-input,\n-.mo-osp-container .mo-form-textarea {\n-    border-radius: 6px;\n-    border: 1px solid rgb(226 232 240);\n-    padding: 10px 12px;\n-    font-size: 14px;\n-    transition: border-color 0.15s ease, box-shadow 0.15s ease;\n-    background: rgb(255 255 255);\n-    color: rgb(51 65 85);\n-}\n-\n-.mo-osp-container .mo-form-input:focus,\n-.mo-osp-container .mo-form-textarea:focus {\n-    border-color: rgb(99 102 241);\n-    box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);\n-    outline: none;\n-}\n-\n-.mo-osp-container .mo-input-label {\n-    font-size: 14px;\n-    font-weight: 500;\n-    color: rgb(51 65 85);\n-    margin-bottom: 6px;\n-    display: block;\n-}\n-\n-.mo-osp-container .mo-form-textarea {\n-    resize: vertical;\n-    min-height: 120px;\n-}\n-\n-\u002F* Number Input Styling *\u002F\n-.mo-osp-container .mo-form-input[type=\"number\"] {\n-    -webkit-appearance: none;\n-    -moz-appearance: textfield;\n-}\n-\n-.mo-osp-container .mo-form-input[type=\"number\"]::-webkit-outer-spin-button,\n-.mo-osp-container .mo-form-input[type=\"number\"]::-webkit-inner-spin-button {\n-    -webkit-appearance: none;\n-    margin: 0;\n-}\n-\n-\u002F* Validation Error Styling *\u002F\n-.mo-osp-error-field {\n-    border-color: #ef4444 !important;\n-    box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;\n-}\n-\n-.mo-osp-validation-error {\n-    color: #ef4444;\n-    font-size: 12px;\n-    margin-top: 4px;\n-    display: block;\n-}\n-\n-\u002F* Responsive Design *\u002F\n-@media (max-width: 767px) {\n-    .mo-osp-container {\n-        padding: 0 16px;\n-    }\n-    \n-    .mo-osp-card-header,\n-    .mo-osp-card-body,\n-    .mo-osp-advanced-content {\n-        padding-left: 20px;\n-        padding-right: 20px;\n-    }\n-    \n-    .mo-osp-toggle-btn {\n-        padding-left: 20px;\n-        padding-right: 20px;\n-    }\n-    \n-    .mo-osp-section-title {\n-        font-size: 16px;\n-    }\n-    \n-    .mo-osp-subsection-title {\n-        font-size: 15px;\n-    }\n-}\n-\n-@media (max-width: 480px) {\n-    .mo-osp-container {\n-        padding: 0 12px;\n-    }\n-    \n-    .mo-osp-card-header,\n-    .mo-osp-card-body,\n-    .mo-osp-advanced-content {\n-        padding-left: 16px;\n-        padding-right: 16px;\n-    }\n-    \n-    .mo-osp-toggle-btn {\n-        padding-left: 16px;\n-        padding-right: 16px;\n-    }\n-}\n-\n-\u002F* Puzzle Verification Popup Styles *\u002F\n-.mo-osp-puzzle-overlay {\n-    position: fixed;\n-    top: 0;\n-    left: 0;\n-    width: 100%;\n-    height: 100%;\n-    background: rgba(0, 0, 0, 0.6);\n-    backdrop-filter: blur(4px);\n-    z-index: 100001 !important; \u002F* Higher than WooCommerce checkout popup (100000) and other modals *\u002F\n-    display: flex;\n-    align-items: center;\n-    justify-content: center;\n-    animation: mo-osp-fade-in 0.3s ease-out;\n-}\n-\n-.mo-osp-puzzle-overlay.mo-osp-hidden {\n-    display: none;\n-}\n-\n-\u002F* Puzzle popup outer wrapper - ensures it appears above WooCommerce checkout popup *\u002F\n-#mo-osp-puzzle-popup-outer-div {\n-    position: fixed;\n-    top: 0;\n-    left: 0;\n-    width: 100%;\n-    height: 100%;\n-    z-index: 100001 !important; \u002F* Higher than WooCommerce checkout popup (100000) *\u002F\n-    display: none;\n-}\n-\n-.mo-osp-puzzle-popup {\n-    background: rgb(255 255 255);\n-    border-radius: 12px;\n-    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);\n-    max-width: 480px;\n-    width: 90%;\n-    max-height: 90vh;\n-    overflow: hidden;\n-    animation: mo-osp-slide-up 0.3s ease-out;\n-    position: relative;\n-    z-index: 100002; \u002F* Higher than overlay to ensure popup content is on top *\u002F\n-}\n-\n-.mo-osp-puzzle-header {\n-    padding: 24px 24px 16px 24px;\n-    border-bottom: 1px solid rgb(241 245 249);\n-    display: flex;\n-    align-items: center;\n-    justify-content: space-between;\n-}\n-\n-.mo-osp-puzzle-title {\n-    display: flex;\n-    align-items: center;\n-    margin: 0;\n-    font-size: 18px;\n-    font-weight: 600;\n-    color: rgb(51 65 85);\n-}\n-\n-.mo-osp-puzzle-icon {\n-    margin-right: 8px;\n-    color: rgb(99 102 241);\n-}\n-\n-.mo-osp-puzzle-close {\n-    background: none;\n-    border: none;\n-    padding: 8px;\n-    border-radius: 6px;\n-    cursor: pointer;\n-    color: rgb(107 114 128);\n-    transition: all 0.2s ease;\n-}\n-\n-.mo-osp-puzzle-close:hover {\n-    background: rgb(243 244 246);\n-    color: rgb(75 85 99);\n-}\n-\n-.mo-osp-puzzle-body {\n-    padding: 24px;\n-}\n-\n-.mo-osp-puzzle-message {\n-    margin-bottom: 24px;\n-}\n-\n-.mo-osp-puzzle-message p {\n-    margin: 0;\n-    color: rgb(75 85 99);\n-    font-size: 14px;\n-    line-height: 1.5;\n-}\n-\n-.mo-osp-puzzle-question {\n-    margin-bottom: 20px;\n-}\n-\n-.mo-osp-puzzle-equation {\n-    display: flex;\n-    align-items: center;\n-    justify-content: center;\n-    gap: 16px;\n-    padding: 16px;\n-    background: rgb(248 250 252);\n-    border: 2px solid rgb(226 232 240);\n-    border-radius: 8px;\n-    font-size: 24px;\n-    font-weight: 600;\n-}\n-\n-.mo-osp-equation-text {\n-    color: rgb(51 65 85);\n-    font-family: 'Courier New', monospace;\n-}\n-\n-.mo-osp-puzzle-image {\n-    max-width: 280px;\n-    width: 100%;\n-    height: auto;\n-    border-radius: 4px;\n-    border: 2px solid rgb(226 232 240);\n-    background: rgb(255 255 255);\n-    padding: 2px;\n-    display: block;\n-}\n-\n-.mo-osp-equals {\n-    color: rgb(99 102 241);\n-    font-weight: 700;\n-}\n-\n-.mo-osp-puzzle-input {\n-    width: 80px;\n-    height: 48px;\n-    border: 2px solid rgb(226 232 240);\n-    border-radius: 6px;\n-    text-align: center;\n-    font-size: 20px;\n-    font-weight: 600;\n-    color: rgb(51 65 85);\n-    background: rgb(255 255 255);\n-    transition: border-color 0.2s ease, box-shadow 0.2s ease;\n-}\n-\n-.mo-osp-puzzle-input:focus {\n-    outline: none;\n-    border-color: rgb(99 102 241);\n-    box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);\n-}\n-\n-.mo-osp-puzzle-input::placeholder {\n-    color: rgb(156 163 175);\n-}\n-\n-.mo-osp-puzzle-error {\n-    display: flex;\n-    align-items: center;\n-    gap: 8px;\n-    padding: 12px 16px;\n-    background: rgb(254 242 242);\n-    border: 1px solid rgb(252 165 165);\n-    border-radius: 6px;\n-    color: rgb(220 38 38);\n-    font-size: 14px;\n-    margin-bottom: 20px;\n-}\n-\n-.mo-osp-error-icon {\n-    flex-shrink: 0;\n-}\n-\n-.mo-osp-puzzle-footer {\n-    padding: 16px 24px 24px 24px;\n-    display: flex;\n-    gap: 12px;\n-    justify-content: flex-end;\n-}\n-\n-.mo-osp-puzzle-btn {\n-    display: flex;\n-    align-items: center;\n-    gap: 8px;\n-    padding: 10px 16px;\n-    border-radius: 6px;\n-    font-size: 14px;\n-    font-weight: 500;\n-    cursor: pointer;\n-    transition: all 0.2s ease;\n-    border: none;\n-}\n-\n-.mo-osp-btn-secondary {\n-    background: rgb(243 244 246);\n-    color: rgb(75 85 99);\n-    border: 1px solid rgb(209 213 219);\n-}\n-\n-.mo-osp-btn-secondary:hover {\n-    background: rgb(229 231 235);\n-    color: rgb(55 65 81);\n-}\n-\n-.mo-osp-btn-primary {\n-    background: rgb(99 102 241);\n-    color: rgb(255 255 255);\n-}\n-\n-.mo-osp-btn-primary:hover {\n-    background: rgb(79 70 229);\n-}\n-\n-.mo-osp-btn-primary:disabled {\n-    background: rgb(156 163 175);\n-    cursor: not-allowed;\n-}\n-\n-\u002F* Animations *\u002F\n-@keyframes mo-osp-fade-in {\n-    from {\n-        opacity: 0;\n-    }\n-    to {\n-        opacity: 1;\n-    }\n-}\n-\n-@keyframes mo-osp-slide-up {\n-    from {\n-        opacity: 0;\n-        transform: translateY(20px) scale(0.95);\n-    }\n-    to {\n-        opacity: 1;\n-        transform: translateY(0) scale(1);\n-    }\n-}\n-\n-\u002F* Responsive design for puzzle popup *\u002F\n-@media (max-width: 480px) {\n-    .mo-osp-puzzle-popup {\n-        width: 95%;\n-        margin: 20px;\n-    }\n-    \n-    .mo-osp-puzzle-header,\n-    .mo-osp-puzzle-body,\n-    .mo-osp-puzzle-footer {\n-        padding-left: 20px;\n-        padding-right: 20px;\n-    }\n-    \n-    .mo-osp-puzzle-equation {\n-        font-size: 20px;\n-        gap: 12px;\n-        padding: 12px;\n-        flex-wrap: wrap;\n-    }\n-    \n-    .mo-osp-puzzle-image {\n-        max-width: 100%;\n-    }\n-    \n-    .mo-osp-puzzle-input {\n-        width: 70px;\n-        height: 44px;\n-        font-size: 18px;\n-    }\n-    \n-    .mo-osp-puzzle-footer {\n-        flex-direction: column;\n-    }\n-    \n-    .mo-osp-puzzle-btn {\n-        width: 100%;\n-        justify-content: center;\n-    }\n-}\n-\n-\u002F* Light theme only - no dark mode *\u002F\n-\n-\u002F* Send OTP Button Processing State *\u002F\n-.mo-osp-processing {\n-    opacity: 0.7 !important;\n-    cursor: not-allowed !important;\n-    position: relative;\n-}\n-\n-.mo-osp-processing::after {\n-    content: '';\n-    position: absolute;\n-    top: 50%;\n-    left: 50%;\n-    width: 16px;\n-    height: 16px;\n-    margin: -8px 0 0 -8px;\n-    border: 2px solid #ffffff;\n-    border-radius: 50%;\n-    border-top-color: transparent;\n-    animation: mo-osp-spin 1s linear infinite;\n-    z-index: 1;\n-}\n-\n-@keyframes mo-osp-spin {\n-    to {\n-        transform: rotate(360deg);\n-    }\n-}\n-\n-\u002F* Ensure button text is still visible during processing *\u002F\n-.mo-osp-processing {\n-    color: rgba(255, 255, 255, 0.8) !important;\n-}\n-\n-\u002F* Blocked Users Table Styles *\u002F\n-.mo-osp-blocked-users-table {\n-    width: 100%;\n-    border-collapse: collapse;\n-    margin-top: 16px;\n-    background: rgb(255 255 255);\n-    border-radius: 8px;\n-    overflow: hidden;\n-    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);\n-}\n-\n-.mo-osp-blocked-users-table thead {\n-    background: rgb(249 250 251);\n-    border-bottom: 2px solid rgb(229 231 235);\n-}\n-\n-.mo-osp-blocked-users-table th {\n-    padding: 12px 16px;\n-    text-align: left;\n-    font-weight: 600;\n-    font-size: 14px;\n-    color: rgb(55 65 81);\n-    border-bottom: 1px solid rgb(229 231 235);\n-}\n-\n-.mo-osp-blocked-users-table td {\n-    padding: 12px 16px;\n-    border-bottom: 1px solid rgb(243 244 246);\n-    font-size: 14px;\n-    color: rgb(75 85 99);\n-}\n-\n-.mo-osp-blocked-users-table tbody tr:hover {\n-    background: rgb(249 250 251);\n-}\n-\n-.mo-osp-blocked-users-table tbody tr.mo-osp-expired {\n-    opacity: 0.6;\n-}\n-\n-.mo-osp-blocked-users-table .mo-osp-no-data,\n-.mo-osp-blocked-users-table .mo-osp-error {\n-    text-align: center;\n-    padding: 24px;\n-    color: rgb(107 114 128);\n-    font-style: italic;\n-}\n-\n-.mo-osp-blocked-users-table .mo-osp-error {\n-    color: rgb(220 38 38);\n-}\n-\n-.mo-osp-identifier-type {\n-    display: inline-block;\n-    padding: 2px 8px;\n-    background: rgb(243 244 246);\n-    color: rgb(75 85 99);\n-    border-radius: 4px;\n-    font-size: 12px;\n-    font-weight: 500;\n-    margin-right: 8px;\n-    text-transform: capitalize;\n-}\n-\n-.mo-osp-identifier-masked {\n-    font-family: monospace;\n-    color: rgb(55 65 81);\n-}\n-\n-.mo-osp-block-reason {\n-    display: inline-block;\n-    padding: 4px 10px;\n-    background: rgb(254 242 242);\n-    color: rgb(185 28 28);\n-    border-radius: 4px;\n-    font-size: 12px;\n-    font-weight: 500;\n-}\n-\n-.mo-osp-remaining-time {\n-    font-weight: 600;\n-    color: rgb(220 38 38);\n-    font-family: monospace;\n-}\n-\n-.mo-osp-blocked-users-actions {\n-    margin-top: 16px;\n-    display: flex;\n-    justify-content: flex-end;\n-    gap: 12px;\n-}\n-\n-.mo-osp-blocked-users-pagination {\n-    margin-top: 16px;\n-    display: flex;\n-    justify-content: center;\n-    align-items: center;\n-    gap: 16px;\n-    padding: 12px 0;\n-}\n-\n-.mo-osp-blocked-users-pagination button {\n-    min-width: 100px;\n-}\n-\n-.mo-osp-blocked-users-pagination button:disabled {\n-    opacity: 0.5;\n-    cursor: not-allowed;\n-}\n-\n-.mo-osp-loading {\n-    text-align: center;\n-    padding: 24px;\n-    color: rgb(107 114 128);\n-}\n-\n-.mo-button-small {\n-    padding: 6px 12px;\n-    font-size: 13px;\n-    min-height: auto;\n-}\n-\n-.mo-button-small:hover {\n-    transform: none;\n-}\n-\n-\u002F* Unblock User Button Styling *\u002F\n-.mo-osp-unblock-user {\n-    background: rgb(220 38 38) !important;\n-    color: rgb(255 255 255) !important;\n-    border: 1px solid rgb(185 28 28) !important;\n-    border-radius: 6px !important;\n-    padding: 8px 16px !important;\n-    font-size: 13px !important;\n-    font-weight: 500 !important;\n-    cursor: pointer !important;\n-    transition: all 0.2s ease !important;\n-    display: inline-flex !important;\n-    align-items: center !important;\n-    gap: 6px !important;\n-    min-width: auto !important;\n-    text-decoration: none !important;\n-    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;\n-}\n-\n-.mo-osp-unblock-user:hover {\n-    background: rgb(185 28 28) !important;\n-    border-color: rgb(153 27 27) !important;\n-    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1) !important;\n-    transform: translateY(-1px) !important;\n-}\n-\n-.mo-osp-unblock-user:active {\n-    transform: translateY(0) !important;\n-    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;\n-}\n-\n-.mo-osp-unblock-user:disabled,\n-.mo-osp-unblock-user[disabled] {\n-    background: rgb(156 163 175) !important;\n-    border-color: rgb(156 163 175) !important;\n-    color: rgb(255 255 255) !important;\n-    cursor: not-allowed !important;\n-    opacity: 0.6 !important;\n-    transform: none !important;\n-}\n-\n-.mo-osp-unblock-user:disabled:hover,\n-.mo-osp-unblock-user[disabled]:hover {\n-    background: rgb(156 163 175) !important;\n-    border-color: rgb(156 163 175) !important;\n-    transform: none !important;\n-    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;\n-}\n-\n-\u002F* Responsive table *\u002F\n-@media (max-width: 768px) {\n-    .mo-osp-blocked-users-table-container {\n-        overflow-x: auto;\n-    }\n-\n-    .mo-osp-blocked-users-table {\n-        min-width: 600px;\n-    }\n-\n-    .mo-osp-blocked-users-table th,\n-    .mo-osp-blocked-users-table td {\n-        padding: 10px 12px;\n-        font-size: 13px;\n-    }\n-\n-    .mo-osp-blocked-users-actions {\n-        flex-direction: column;\n-    }\n-\n-    .mo-osp-blocked-users-actions button {\n-        width: 100%;\n-    }\n+\u002F* OTP Spam Preventer Admin Styles *\u002F\r\n+\u002F* Professional spacing and layout *\u002F\r\n+\r\n+\u002F* Main Container *\u002F\r\n+.mo-osp-container {\r\n+    max-width: 1200px;\r\n+    margin: 0 auto;\r\n+}\r\n+\r\n+\u002F* Using plugin's existing mo-header classes - no custom header CSS needed *\u002F\r\n+\u002F* Addon toggle *\u002F\r\n+.mo-osp-addon-toggle {\r\n+    display: inline-flex;\r\n+    align-items: center;\r\n+    gap: 6px;\r\n+    font-size: 16px;\r\n+    color: rgb(51 65 85);\r\n+}\r\n+\r\n+.mo-osp-addon-toggle input {\r\n+    margin: 0;\r\n+}\r\n+\r\n+\u002F* Toggle placement above basic settings *\u002F\r\n+.mo-osp-addon-toggle-row {\r\n+    display: flex;\r\n+    align-items: center;\r\n+    justify-content: flex-start;\r\n+    gap: 12px;\r\n+    padding: 0 20px 12px 20px;\r\n+}\r\n+\r\n+.mo-osp-addon-toggle-emphasis {\r\n+    padding: 2px 0;\r\n+    font-weight: 700;\r\n+    color: rgb(30 64 175);\r\n+}\r\n+\r\n+.mo-osp-addon-toggle-emphasis input {\r\n+    transform: scale(1.2);\r\n+}\r\n+\r\n+\u002F* SVG Icon Styles *\u002F\r\n+.mo-osp-header-icon {\r\n+    margin-right: 8px;\r\n+    color: rgb(51 65 85);\r\n+    vertical-align: middle;\r\n+}\r\n+\r\n+.mo-osp-section-icon {\r\n+    margin-right: 8px;\r\n+    color: rgb(51 65 85);\r\n+    vertical-align: middle;\r\n+}\r\n+\r\n+.mo-osp-subsection-icon {\r\n+    margin-right: 6px;\r\n+    color: rgb(51 65 85);\r\n+    vertical-align: middle;\r\n+}\r\n+\r\n+.mo-osp-field-icon {\r\n+    margin-right: 6px;\r\n+    color: rgb(100 116 139);\r\n+    vertical-align: middle;\r\n+}\r\n+\r\n+\u002F* Icon alignment in titles and labels *\u002F\r\n+.mo-osp-section-title,\r\n+.mo-osp-subsection-title {\r\n+    display: flex;\r\n+    align-items: center;\r\n+}\r\n+\r\n+\r\n+.mo-input-label {\r\n+    display: flex !important;\r\n+    align-items: center;\r\n+}\r\n+\r\n+\u002F* Card Layout *\u002F\r\n+.mo-osp-card {\r\n+    background: rgb(255 255 255);\r\n+    border: 1px solid rgb(226 232 240);\r\n+    border-radius: 8px;\r\n+    margin-bottom: 24px;\r\n+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);\r\n+    overflow: hidden;\r\n+    margin: 10px 20px;\r\n+}\r\n+\r\n+.mo-osp-card:last-child {\r\n+    margin-bottom: 32px; \u002F* Add bottom margin to advanced settings card *\u002F\r\n+}\r\n+\r\n+\u002F* Card Header *\u002F\r\n+.mo-osp-card-header {\r\n+    padding: 24px 24px 16px 24px;\r\n+    border-bottom: 1px solid rgb(241 245 249);\r\n+}\r\n+\r\n+.mo-osp-section-title {\r\n+    font-size: 18px;\r\n+    font-weight: 600;\r\n+    color: rgb(51 65 85);\r\n+    margin: 0 0 8px 0;\r\n+    line-height: 1.4;\r\n+}\r\n+\r\n+.mo-osp-section-desc {\r\n+    font-size: 14px;\r\n+    color: rgb(100 116 139);\r\n+    margin: 0;\r\n+    line-height: 1.5;\r\n+}\r\n+\r\n+\u002F* Card Body *\u002F\r\n+.mo-osp-card-body {\r\n+    padding: 20px 24px 24px 24px;\r\n+}\r\n+\r\n+\u002F* Fields Grid *\u002F\r\n+.mo-osp-fields-grid {\r\n+    display: grid;\r\n+    grid-template-columns: 1fr;\r\n+    gap: 24px;\r\n+}\r\n+\r\n+@media (min-width: 768px) {\r\n+    .mo-osp-fields-grid {\r\n+        grid-template-columns: 1fr 1fr;\r\n+        gap: 32px 24px;\r\n+    }\r\n+}\r\n+\r\n+\u002F* Field Groups *\u002F\r\n+.mo-osp-field-group {\r\n+    display: flex;\r\n+    flex-direction: column;\r\n+    gap: 8px;\r\n+}\r\n+\r\n+.mo-osp-field-full {\r\n+    grid-column: 1 \u002F -1;\r\n+}\r\n+\r\n+.mo-osp-field-desc {\r\n+    font-size: 13px;\r\n+    color: rgb(100 116 139);\r\n+    margin: 0;\r\n+    line-height: 1.4;\r\n+}\r\n+\r\n+\u002F* Toggle Button *\u002F\r\n+.mo-osp-toggle-btn {\r\n+    width: 100%;\r\n+    padding: 20px 24px;\r\n+    background: transparent;\r\n+    border: none;\r\n+    text-align: left;\r\n+    cursor: pointer;\r\n+    transition: background-color 0.15s ease;\r\n+}\r\n+\r\n+.mo-osp-toggle-btn:hover {\r\n+    background-color: rgb(248 250 252);\r\n+}\r\n+\r\n+.mo-osp-toggle-content {\r\n+    display: flex;\r\n+    align-items: center;\r\n+    justify-content: space-between;\r\n+    gap: 16px;\r\n+}\r\n+\r\n+.mo-osp-toggle-indicator {\r\n+    display: flex;\r\n+    align-items: center;\r\n+    gap: 8px;\r\n+}\r\n+\r\n+.mo-osp-toggle-text {\r\n+    font-size: 14px;\r\n+    color: rgb(100 116 139);\r\n+    font-weight: 500;\r\n+}\r\n+\r\n+.mo-osp-toggle-icon {\r\n+    font-size: 12px;\r\n+    color: rgb(100 116 139);\r\n+    transition: transform 0.2s ease;\r\n+    display: inline-block;\r\n+}\r\n+\r\n+.mo-osp-toggle-icon.rotate-180 {\r\n+    transform: rotate(180deg);\r\n+}\r\n+\r\n+\u002F* Advanced Settings *\u002F\r\n+.mo-osp-advanced-hidden {\r\n+    display: none;\r\n+}\r\n+\r\n+.mo-osp-advanced-visible {\r\n+    display: block;\r\n+}\r\n+\r\n+.mo-osp-advanced-content {\r\n+    padding: 0 24px 32px 24px; \u002F* Increased bottom padding *\u002F\r\n+    border-top: 1px solid rgb(241 245 249);\r\n+}\r\n+\r\n+\u002F* Subsections *\u002F\r\n+.mo-osp-subsection {\r\n+    margin-top: 32px;\r\n+}\r\n+\r\n+.mo-osp-subsection:last-child {\r\n+    margin-bottom: 0;\r\n+}\r\n+\r\n+.mo-osp-subsection-header {\r\n+    margin-bottom: 20px;\r\n+    padding-top: 24px;\r\n+}\r\n+\r\n+.mo-osp-subsection:first-child .mo-osp-subsection-header {\r\n+    padding-top: 0;\r\n+}\r\n+\r\n+.mo-osp-subsection-title {\r\n+    font-size: 16px;\r\n+    font-weight: 600;\r\n+    color: rgb(51 65 85);\r\n+    margin: 0 0 6px 0;\r\n+    line-height: 1.4;\r\n+}\r\n+\r\n+\u002F* Form Input Overrides *\u002F\r\n+.mo-osp-container .mo-form-input,\r\n+.mo-osp-container .mo-form-textarea {\r\n+    border-radius: 6px;\r\n+    border: 1px solid rgb(226 232 240);\r\n+    padding: 10px 12px;\r\n+    font-size: 14px;\r\n+    transition: border-color 0.15s ease, box-shadow 0.15s ease;\r\n+    background: rgb(255 255 255);\r\n+    color: rgb(51 65 85);\r\n+}\r\n+\r\n+.mo-osp-container .mo-form-input:focus,\r\n+.mo-osp-container .mo-form-textarea:focus {\r\n+    border-color: rgb(99 102 241);\r\n+    box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);\r\n+    outline: none;\r\n+}\r\n+\r\n+.mo-osp-container .mo-input-label {\r\n+    font-size: 14px;\r\n+    font-weight: 500;\r\n+    color: rgb(51 65 85);\r\n+    margin-bottom: 6px;\r\n+    display: block;\r\n+}\r\n+\r\n+.mo-osp-container .mo-form-textarea {\r\n+    resize: vertical;\r\n+    min-height: 120px;\r\n+}\r\n+\r\n+\u002F* Number Input Styling *\u002F\r\n+.mo-osp-container .mo-form-input[type=\"number\"] {\r\n+    -webkit-appearance: none;\r\n+    -moz-appearance: textfield;\r\n+}\r\n+\r\n+.mo-osp-container .mo-form-input[type=\"number\"]::-webkit-outer-spin-button,\r\n+.mo-osp-container .mo-form-input[type=\"number\"]::-webkit-inner-spin-button {\r\n+    -webkit-appearance: none;\r\n+    margin: 0;\r\n+}\r\n+\r\n+\u002F* Validation Error Styling *\u002F\r\n+.mo-osp-error-field {\r\n+    border-color: #ef4444 !important;\r\n+    box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;\r\n+}\r\n+\r\n+.mo-osp-validation-error {\r\n+    color: #ef4444;\r\n+    font-size: 12px;\r\n+    margin-top: 4px;\r\n+    display: block;\r\n+}\r\n+\r\n+\u002F* Responsive Design *\u002F\r\n+@media (max-width: 767px) {\r\n+    .mo-osp-container {\r\n+        padding: 0 16px;\r\n+    }\r\n+    \r\n+    .mo-osp-card-header,\r\n+    .mo-osp-card-body,\r\n+    .mo-osp-advanced-content {\r\n+        padding-left: 20px;\r\n+        padding-right: 20px;\r\n+    }\r\n+    \r\n+    .mo-osp-toggle-btn {\r\n+        padding-left: 20px;\r\n+        padding-right: 20px;\r\n+    }\r\n+    \r\n+    .mo-osp-section-title {\r\n+        font-size: 16px;\r\n+    }\r\n+    \r\n+    .mo-osp-subsection-title {\r\n+        font-size: 15px;\r\n+    }\r\n+}\r\n+\r\n+@media (max-width: 480px) {\r\n+    .mo-osp-container {\r\n+        padding: 0 12px;\r\n+    }\r\n+    \r\n+    .mo-osp-card-header,\r\n+    .mo-osp-card-body,\r\n+    .mo-osp-advanced-content {\r\n+        padding-left: 16px;\r\n+        padding-right: 16px;\r\n+    }\r\n+    \r\n+    .mo-osp-toggle-btn {\r\n+        padding-left: 16px;\r\n+        padding-right: 16px;\r\n+    }\r\n+}\r\n+\r\n+\u002F* Puzzle Verification Popup Styles *\u002F\r\n+.mo-osp-puzzle-overlay {\r\n+    position: fixed;\r\n+    top: 0;\r\n+    left: 0;\r\n+    width: 100%;\r\n+    height: 100%;\r\n+    background: rgba(0, 0, 0, 0.6);\r\n+    backdrop-filter: blur(4px);\r\n+    z-index: 100001 !important; \u002F* Higher than WooCommerce checkout popup (100000) and other modals *\u002F\r\n+    display: flex;\r\n+    align-items: center;\r\n+    justify-content: center;\r\n+    animation: mo-osp-fade-in 0.3s ease-out;\r\n+}\r\n+\r\n+.mo-osp-puzzle-overlay.mo-osp-hidden {\r\n+    display: none;\r\n+}\r\n+\r\n+\u002F* Puzzle popup outer wrapper - ensures it appears above WooCommerce checkout popup *\u002F\r\n+#mo-osp-puzzle-popup-outer-div {\r\n+    position: fixed;\r\n+    top: 0;\r\n+    left: 0;\r\n+    width: 100%;\r\n+    height: 100%;\r\n+    z-index: 100001 !important; \u002F* Higher than WooCommerce checkout popup (100000) *\u002F\r\n+    display: none;\r\n+}\r\n+\r\n+.mo-osp-puzzle-popup {\r\n+    background: rgb(255 255 255);\r\n+    border-radius: 12px;\r\n+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);\r\n+    max-width: 480px;\r\n+    width: 90%;\r\n+    max-height: 90vh;\r\n+    overflow: hidden;\r\n+    animation: mo-osp-slide-up 0.3s ease-out;\r\n+    position: relative;\r\n+    z-index: 100002; \u002F* Higher than overlay to ensure popup content is on top *\u002F\r\n+}\r\n+\r\n+.mo-osp-puzzle-header {\r\n+    padding: 24px 24px 16px 24px;\r\n+    border-bottom: 1px solid rgb(241 245 249);\r\n+    display: flex;\r\n+    align-items: center;\r\n+    justify-content: space-between;\r\n+}\r\n+\r\n+.mo-osp-puzzle-title {\r\n+    display: flex;\r\n+    align-items: center;\r\n+    margin: 0;\r\n+    font-size: 18px;\r\n+    font-weight: 600;\r\n+    color: rgb(51 65 85);\r\n+}\r\n+\r\n+.mo-osp-puzzle-icon {\r\n+    margin-right: 8px;\r\n+    color: rgb(99 102 241);\r\n+}\r\n+\r\n+.mo-osp-puzzle-close {\r\n+    background: none;\r\n+    border: none;\r\n+    padding: 8px;\r\n+    border-radius: 6px;\r\n+    cursor: pointer;\r\n+    color: rgb(107 114 128);\r\n+    transition: all 0.2s ease;\r\n+}\r\n+\r\n+.mo-osp-puzzle-close:hover {\r\n+    background: rgb(243 244 246);\r\n+    color: rgb(75 85 99);\r\n+}\r\n+\r\n+.mo-osp-puzzle-body {\r\n+    padding: 24px;\r\n+}\r\n+\r\n+.mo-osp-puzzle-message {\r\n+    margin-bottom: 24px;\r\n+}\r\n+\r\n+.mo-osp-puzzle-message p {\r\n+    margin: 0;\r\n+    color: rgb(75 85 99);\r\n+    font-size: 14px;\r\n+    line-height: 1.5;\r\n+}\r\n+\r\n+.mo-osp-puzzle-question {\r\n+    margin-bottom: 20px;\r\n+}\r\n+\r\n+.mo-osp-puzzle-equation {\r\n+    display: flex;\r\n+    align-items: center;\r\n+    justify-content: center;\r\n+    gap: 16px;\r\n+    padding: 16px;\r\n+    background: rgb(248 250 252);\r\n+    border: 2px solid rgb(226 232 240);\r\n+    border-radius: 8px;\r\n+    font-size: 24px;\r\n+    font-weight: 600;\r\n+}\r\n+\r\n+.mo-osp-equation-text {\r\n+    color: rgb(51 65 85);\r\n+    font-family: 'Courier New', monospace;\r\n+}\r\n+\r\n+.mo-osp-puzzle-image {\r\n+    max-width: 280px;\r\n+    width: 100%;\r\n+    height: auto;\r\n+    border-radius: 4px;\r\n+    border: 2px solid rgb(226 232 240);\r\n+    background: rgb(255 255 255);\r\n+    padding: 2px;\r\n+    display: block;\r\n+}\r\n+\r\n+.mo-osp-equals {\r\n+    color: rgb(99 102 241);\r\n+    font-weight: 700;\r\n+}\r\n+\r\n+.mo-osp-puzzle-input {\r\n+    width: 80px;\r\n+    height: 48px;\r\n+    border: 2px solid rgb(226 232 240);\r\n+    border-radius: 6px;\r\n+    text-align: center;\r\n+    font-size: 20px;\r\n+    font-weight: 600;\r\n+    color: rgb(51 65 85);\r\n+    background: rgb(255 255 255);\r\n+    transition: border-color 0.2s ease, box-shadow 0.2s ease;\r\n+}\r\n+\r\n+.mo-osp-puzzle-input:focus {\r\n+    outline: none;\r\n+    border-color: rgb(99 102 241);\r\n+    box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);\r\n+}\r\n+\r\n+.mo-osp-puzzle-input::placeholder {\r\n+    color: rgb(156 163 175);\r\n+}\r\n+\r\n+.mo-osp-puzzle-error {\r\n+    display: flex;\r\n+    align-items: center;\r\n+    gap: 8px;\r\n+    padding: 12px 16px;\r\n+    background: rgb(254 242 242);\r\n+    border: 1px solid rgb(252 165 165);\r\n+    border-radius: 6px;\r\n+    color: rgb(220 38 38);\r\n+    font-size: 14px;\r\n+    margin-bottom: 20px;\r\n+}\r\n+\r\n+.mo-osp-error-icon {\r\n+    flex-shrink: 0;\r\n+}\r\n+\r\n+.mo-osp-puzzle-footer {\r\n+    padding: 16px 24px 24px 24px;\r\n+    display: flex;\r\n+    gap: 12px;\r\n+    justify-content: flex-end;\r\n+}\r\n+\r\n+.mo-osp-puzzle-btn {\r\n+    display: flex;\r\n+    align-items: center;\r\n+    gap: 8px;\r\n+    padding: 10px 16px;\r\n+    border-radius: 6px;\r\n+    font-size: 14px;\r\n+    font-weight: 500;\r\n+    cursor: pointer;\r\n+    transition: all 0.2s ease;\r\n+    border: none;\r\n+}\r\n+\r\n+.mo-osp-btn-secondary {\r\n+    background: rgb(243 244 246);\r\n+    color: rgb(75 85 99);\r\n+    border: 1px solid rgb(209 213 219);\r\n+}\r\n+\r\n+.mo-osp-btn-secondary:hover {\r\n+    background: rgb(229 231 235);\r\n+    color: rgb(55 65 81);\r\n+}\r\n+\r\n+.mo-osp-btn-primary {\r\n+    background: rgb(99 102 241);\r\n+    color: rgb(255 255 255);\r\n+}\r\n+\r\n+.mo-osp-btn-primary:hover {\r\n+    background: rgb(79 70 229);\r\n+}\r\n+\r\n+.mo-osp-btn-primary:disabled {\r\n+    background: rgb(156 163 175);\r\n+    cursor: not-allowed;\r\n+}\r\n+\r\n+\u002F* Animations *\u002F\r\n+@keyframes mo-osp-fade-in {\r\n+    from {\r\n+        opacity: 0;\r\n+    }\r\n+    to {\r\n+        opacity: 1;\r\n+    }\r\n+}\r\n+\r\n+@keyframes mo-osp-slide-up {\r\n+    from {\r\n+        opacity: 0;\r\n+        transform: translateY(20px) scale(0.95);\r\n+    }\r\n+    to {\r\n+        opacity: 1;\r\n+        transform: translateY(0) scale(1);\r\n+    }\r\n+}\r\n+\r\n+\u002F* Responsive design for puzzle popup *\u002F\r\n+@media (max-width: 480px) {\r\n+    .mo-osp-puzzle-popup {\r\n+        width: 95%;\r\n+        margin: 20px;\r\n+    }\r\n+    \r\n+    .mo-osp-puzzle-header,\r\n+    .mo-osp-puzzle-body,\r\n+    .mo-osp-puzzle-footer {\r\n+        padding-left: 20px;\r\n+        padding-right: 20px;\r\n+    }\r\n+    \r\n+    .mo-osp-puzzle-equation {\r\n+        font-size: 20px;\r\n+        gap: 12px;\r\n+        padding: 12px;\r\n+        flex-wrap: wrap;\r\n+    }\r\n+    \r\n+    .mo-osp-puzzle-image {\r\n+        max-width: 100%;\r\n+    }\r\n+    \r\n+    .mo-osp-puzzle-input {\r\n+        width: 70px;\r\n+        height: 44px;\r\n+        font-size: 18px;\r\n+    }\r\n+    \r\n+    .mo-osp-puzzle-footer {\r\n+        flex-direction: column;\r\n+    }\r\n+    \r\n+    .mo-osp-puzzle-btn {\r\n+        width: 100%;\r\n+        justify-content: center;\r\n+    }\r\n+}\r\n+\r\n+\u002F* Light theme only - no dark mode *\u002F\r\n+\r\n+\u002F* Send OTP Button Processing State *\u002F\r\n+.mo-osp-processing {\r\n+    opacity: 0.7 !important;\r\n+    cursor: not-allowed !important;\r\n+    position: relative;\r\n+}\r\n+\r\n+.mo-osp-processing::after {\r\n+    content: '';\r\n+    position: absolute;\r\n+    top: 50%;\r\n+    left: 50%;\r\n+    width: 16px;\r\n+    height: 16px;\r\n+    margin: -8px 0 0 -8px;\r\n+    border: 2px solid #ffffff;\r\n+    border-radius: 50%;\r\n+    border-top-color: transparent;\r\n+    animation: mo-osp-spin 1s linear infinite;\r\n+    z-index: 1;\r\n+}\r\n+\r\n+@keyframes mo-osp-spin {\r\n+    to {\r\n+        transform: rotate(360deg);\r\n+    }\r\n+}\r\n+\r\n+\u002F* Ensure button text is still visible during processing *\u002F\r\n+.mo-osp-processing {\r\n+    color: rgba(255, 255, 255, 0.8) !important;\r\n+}\r\n+\r\n+\u002F* Blocked Users Table Styles *\u002F\r\n+.mo-osp-blocked-users-table {\r\n+    width: 100%;\r\n+    border-collapse: collapse;\r\n+    margin-top: 16px;\r\n+    background: rgb(255 255 255);\r\n+    border-radius: 8px;\r\n+    overflow: hidden;\r\n+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);\r\n+}\r\n+\r\n+.mo-osp-blocked-users-table thead {\r\n+    background: rgb(249 250 251);\r\n+    border-bottom: 2px solid rgb(229 231 235);\r\n+}\r\n+\r\n+.mo-osp-blocked-users-table th {\r\n+    padding: 12px 16px;\r\n+    text-align: left;\r\n+    font-weight: 600;\r\n+    font-size: 14px;\r\n+    color: rgb(55 65 81);\r\n+    border-bottom: 1px solid rgb(229 231 235);\r\n+}\r\n+\r\n+.mo-osp-blocked-users-table td {\r\n+    padding: 12px 16px;\r\n+    border-bottom: 1px solid rgb(243 244 246);\r\n+    font-size: 14px;\r\n+    color: rgb(75 85 99);\r\n+}\r\n+\r\n+.mo-osp-blocked-users-table tbody tr:hover {\r\n+    background: rgb(249 250 251);\r\n+}\r\n+\r\n+.mo-osp-blocked-users-table tbody tr.mo-osp-expired {\r\n+    opacity: 0.6;\r\n+}\r\n+\r\n+.mo-osp-blocked-users-table .mo-osp-no-data,\r\n+.mo-osp-blocked-users-table .mo-osp-error {\r\n+    text-align: center;\r\n+    padding: 24px;\r\n+    color: rgb(107 114 128);\r\n+    font-style: italic;\r\n+}\r\n+\r\n+.mo-osp-blocked-users-table .mo-osp-error {\r\n+    color: rgb(220 38 38);\r\n+}\r\n+\r\n+.mo-osp-identifier-type {\r\n+    display: inline-block;\r\n+    padding: 2px 8px;\r\n+    background: rgb(243 244 246);\r\n+    color: rgb(75 85 99);\r\n+    border-radius: 4px;\r\n+    font-size: 12px;\r\n+    font-weight: 500;\r\n+    margin-right: 8px;\r\n+    text-transform: capitalize;\r\n+}\r\n+\r\n+.mo-osp-identifier-masked {\r\n+    font-family: monospace;\r\n+    color: rgb(55 65 81);\r\n+}\r\n+\r\n+.mo-osp-block-reason {\r\n+    display: inline-block;\r\n+    padding: 4px 10px;\r\n+    background: rgb(254 242 242);\r\n+    color: rgb(185 28 28);\r\n+    border-radius: 4px;\r\n+    font-size: 12px;\r\n+    font-weight: 500;\r\n+}\r\n+\r\n+.mo-osp-remaining-time {\r\n+    font-weight: 600;\r\n+    color: rgb(220 38 38);\r\n+    font-family: monospace;\r\n+}\r\n+\r\n+.mo-osp-blocked-users-actions {\r\n+    margin-top: 16px;\r\n+    display: flex;\r\n+    justify-content: flex-end;\r\n+    gap: 12px;\r\n+}\r\n+\r\n+.mo-osp-blocked-users-pagination {\r\n+    margin-top: 16px;\r\n+    display: flex;\r\n+    justify-content: center;\r\n+    align-items: center;\r\n+    gap: 16px;\r\n+    padding: 12px 0;\r\n+}\r\n+\r\n+.mo-osp-blocked-users-pagination button {\r\n+    min-width: 100px;\r\n+}\r\n+\r\n+.mo-osp-blocked-users-pagination button:disabled {\r\n+    opacity: 0.5;\r\n+    cursor: not-allowed;\r\n+}\r\n+\r\n+.mo-osp-loading {\r\n+    text-align: center;\r\n+    padding: 24px;\r\n+    color: rgb(107 114 128);\r\n+}\r\n+\r\n+.mo-button-small {\r\n+    padding: 6px 12px;\r\n+    font-size: 13px;\r\n+    min-height: auto;\r\n+}\r\n+\r\n+.mo-button-small:hover {\r\n+    transform: none;\r\n+}\r\n+\r\n+\u002F* Unblock User Button Styling *\u002F\r\n+.mo-osp-unblock-user {\r\n+    background: rgb(220 38 38) !important;\r\n+    color: rgb(255 255 255) !important;\r\n+    border: 1px solid rgb(185 28 28) !important;\r\n+    border-radius: 6px !important;\r\n+    padding: 8px 16px !important;\r\n+    font-size: 13px !important;\r\n+    font-weight: 500 !important;\r\n+    cursor: pointer !important;\r\n+    transition: all 0.2s ease !important;\r\n+    display: inline-flex !important;\r\n+    align-items: center !important;\r\n+    gap: 6px !important;\r\n+    min-width: auto !important;\r\n+    text-decoration: none !important;\r\n+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;\r\n+}\r\n+\r\n+.mo-osp-unblock-user:hover {\r\n+    background: rgb(185 28 28) !important;\r\n+    border-color: rgb(153 27 27) !important;\r\n+    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1) !important;\r\n+    transform: translateY(-1px) !important;\r\n+}\r\n+\r\n+.mo-osp-unblock-user:active {\r\n+    transform: translateY(0) !important;\r\n+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;\r\n+}\r\n+\r\n+.mo-osp-unblock-user:disabled,\r\n+.mo-osp-unblock-user[disabled] {\r\n+    background: rgb(156 163 175) !important;\r\n+    border-color: rgb(156 163 175) !important;\r\n+    color: rgb(255 255 255) !important;\r\n+    cursor: not-allowed !important;\r\n+    opacity: 0.6 !important;\r\n+    transform: none !important;\r\n+}\r\n+\r\n+.mo-osp-unblock-user:disabled:hover,\r\n+.mo-osp-unblock-user[disabled]:hover {\r\n+    background: rgb(156 163 175) !important;\r\n+    border-color: rgb(156 163 175) !important;\r\n+    transform: none !important;\r\n+    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;\r\n+}\r\n+\r\n+\u002F* Responsive table *\u002F\r\n+@media (max-width: 768px) {\r\n+    .mo-osp-blocked-users-table-container {\r\n+        overflow-x: auto;\r\n+    }\r\n+\r\n+    .mo-osp-blocked-users-table {\r\n+        min-width: 600px;\r\n+    }\r\n+\r\n+    .mo-osp-blocked-users-table th,\r\n+    .mo-osp-blocked-users-table td {\r\n+        padding: 10px 12px;\r\n+        font-size: 13px;\r\n+    }\r\n+\r\n+    .mo-osp-blocked-users-actions {\r\n+        flex-direction: column;\r\n+    }\r\n+\r\n+    .mo-osp-blocked-users-actions button {\r\n+        width: 100%;\r\n+    }\r\n }\n\\ No newline at end of file\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fpuzzle-system.js \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fpuzzle-system.js\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fpuzzle-system.js\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fpuzzle-system.js\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,1050 +1,1050 @@\n-\u002F**\n- * OTP Puzzle Verification System\n- * \n- * Secure puzzle system for human verification before OTP sending.\n- * Features:\n- * - Server-side puzzle generation and validation\n- * - Session-based security with fallback storage\n- * - Multiple detection methods for puzzle requirements\n- * - Comprehensive error handling and logging\n- * \n- * @package otpspampreventer\n- *\u002F\n-\n-(function($mo) {\n-    'use strict';\n-\n-    \u002F\u002F Puzzle Verification System\n-    window.MO_OSP_Puzzle = {\n-        \u002F\u002F State variables\n-        currentPuzzle: null,\n-        currentAnswer: null,\n-        lastUserAnswer: null,\n-        pendingOtpData: null,\n-        isShowing: false,\n-        puzzleCheckCount: 0,\n-        lastVerificationResponse: null,\n-        isGenerating: false, \u002F\u002F Flag to prevent duplicate generate calls\n-        isVerifying: false, \u002F\u002F Flag to prevent duplicate verify calls\n-        eventsBound: false, \u002F\u002F Flag to prevent duplicate event binding\n-\n-        \u002F**\n-         * Initialize the puzzle system\n-         *\u002F\n-        init: function() {\n-            \u002F\u002F Only bind events once to prevent duplicate handlers\n-            if (!this.eventsBound) {\n-                this.bindEvents();\n-                this.eventsBound = true;\n-            }\n-            this.puzzleCheckCount = 0; \u002F\u002F Initialize counter to prevent infinite loops\n-        },\n-\n-        \u002F**\n-         * Bind all puzzle-related event handlers\n-         * CRITICAL: Unbind previous handlers first to prevent duplicate event binding\n-         *\u002F\n-        bindEvents: function() {\n-            var self = this;\n-            \n-            \u002F\u002F CRITICAL FIX: Unbind all previous handlers to prevent duplicate event binding\n-            $mo(document).off('click', '.mo-osp-puzzle-close');\n-            $mo(document).off('click', '.mo-osp-puzzle-popup');\n-            $mo(document).off('click', '#mo-osp-puzzle-refresh');\n-            $mo(document).off('click', '#mo-osp-puzzle-verify');\n-            $mo(document).off('keypress', '#mo-osp-puzzle-answer');\n-            $mo(document).off('input', '#mo-osp-puzzle-answer');\n-            \n-            \u002F\u002F Close puzzle popup\n-            $mo(document).on('click', '.mo-osp-puzzle-close', function() {\n-                self.closePuzzle();\n-            });\n-\n-            \u002F\u002F Prevent closing when clicking inside popup\n-            $mo(document).on('click', '.mo-osp-puzzle-popup', function(e) {\n-                e.stopPropagation();\n-            });\n-\n-            \u002F\u002F Refresh puzzle\n-            $mo(document).on('click', '#mo-osp-puzzle-refresh', function() {\n-                self.generatePuzzle();\n-                $mo('#mo-osp-puzzle-answer').val('').focus();\n-            });\n-\n-            \u002F\u002F Verify puzzle - prevent duplicate calls\n-            $mo(document).on('click', '#mo-osp-puzzle-verify', function(e) {\n-                e.preventDefault();\n-                e.stopPropagation();\n-                \n-                \u002F\u002F Prevent duplicate calls - check if verification is already in progress\n-                if (self.isVerifying) {\n-                    return false;\n-                }\n-                \n-                self.verifyPuzzle();\n-                return false;\n-            });\n-\n-            \u002F\u002F Enter key in puzzle input - prevent duplicate calls\n-            $mo(document).on('keypress', '#mo-osp-puzzle-answer', function(e) {\n-                if (e.which === 13) { \u002F\u002F Enter key\n-                    e.preventDefault();\n-                    \n-                    \u002F\u002F Prevent duplicate calls\n-                    if (self.isVerifying) {\n-                        return false;\n-                    }\n-                    \n-                    self.verifyPuzzle();\n-                    return false;\n-                }\n-            });\n-\n-            \u002F\u002F Clear error on input\n-            $mo(document).on('input', '#mo-osp-puzzle-answer', function() {\n-                self.hideError();\n-            });\n-\n-            \u002F\u002F Note: checkForPuzzleMessage() is now only called when OTP request is made\n-            \u002F\u002F This prevents puzzle popup from showing on every page load\n-        },\n-\n-        \u002F**\n-         * Check for puzzle requirement messages in DOM\n-         * Uses circuit breaker to prevent infinite loops\n-         *\u002F\n-        checkForPuzzleMessage: function() {\n-            var self = this;\n-            \n-            \u002F\u002F Prevent infinite loop - only check if puzzle is not already showing\n-            if (self.isShowing) {\n-                return;\n-            }\n-            \n-            \u002F\u002F Circuit breaker: Stop checking after 30 attempts (30 seconds) to prevent infinite loops\n-            if (!this.puzzleCheckCount) {\n-                this.puzzleCheckCount = 0;\n-            }\n-            this.puzzleCheckCount++;\n-            \n-            if (this.puzzleCheckCount > 30) {\n-                return;\n-            }\n-            \n-            \n-            \u002F\u002F Look for the puzzle message in various message containers\n-            var messageSelectors = [\n-                '#mo_message',\n-                '.mo_message', \n-                '[id*=\"mo_message\"]',\n-                '.woocommerce-message',\n-                '.notice',\n-                '.alert',\n-                '[class*=\"message\"]',\n-                '.mo-otp-message',\n-                'div[style*=\"color\"]', \u002F\u002F Catch styled message divs\n-                '.error',\n-                '.success',\n-                '.info'\n-            ];\n-            \n-            var found = false;\n-            var allMessages = [];\n-            \n-            \u002F\u002F Specific puzzle message patterns (must be precise to avoid false positives)\n-            var puzzlePatterns = [\n-                'Please complete the security verification to continue',\n-                'solve this simple puzzle to verify you are human before sending an OTP',\n-                'complete the security verification to continue',\n-                'puzzle to verify you are human before sending',\n-                'security purposes, please solve this simple puzzle'\n-            ];\n-            \n-            for (var i = 0; i \u003C messageSelectors.length && !found; i++) {\n-                $mo(messageSelectors[i]).each(function() {\n-                    var messageText = $mo(this).text().trim();\n-                    if (messageText) {\n-                        allMessages.push({\n-                            selector: messageSelectors[i],\n-                            text: messageText,\n-                            element: this\n-                        });\n-                    }\n-                    \n-                    \u002F\u002F Check against multiple puzzle patterns\n-                    if (messageText && typeof messageText.includes === 'function') {\n-                        var messageTextLower = messageText.toLowerCase();\n-                        for (var j = 0; j \u003C puzzlePatterns.length; j++) {\n-                            if (messageTextLower.includes(puzzlePatterns[j].toLowerCase())) {\n-                                \u002F\u002F DON'T remove the message box - just clear its content and hide it temporarily\n-                                $mo(this).empty().hide();\n-                                self.showPuzzle({});\n-                                found = true;\n-                                return false;\n-                            }\n-                        }\n-                    }\n-                });\n-            }\n-            \n-            if (!found) {\n-                \n-                \u002F\u002F Use exponential backoff to reduce performance impact\n-                var delay = Math.min(1000 + (this.puzzleCheckCount * 100), 3000); \u002F\u002F Max 3 seconds\n-                \n-                \u002F\u002F Continue checking with increasing delay\n-                setTimeout(function() {\n-                    self.checkForPuzzleMessage();\n-                }, delay);\n-            } else {\n-                \u002F\u002F Reset counter when puzzle is found and shown\n-                this.puzzleCheckCount = 0;\n-            }\n-        },\n-\n-        \u002F**\n-         * Generate a new puzzle using secure server-side generation\n-         *\u002F\n-        generatePuzzle: function() {\n-            var self = this;\n-            \n-            \u002F\u002F CRITICAL FIX: Prevent duplicate calls\n-            if (this.isGenerating) {\n-                return;\n-            }\n-            \n-            \u002F\u002F Set generating flag to prevent duplicate calls\n-            this.isGenerating = true;\n-            \n-            $mo.ajax({\n-                url: mo_osp_ajax.ajax_url,\n-                type: 'POST',\n-                data: {\n-                    action: 'mo_osp_generate_puzzle',\n-                    nonce: mo_osp_ajax.nonce\n-                },\n-                success: function(response) {\n-                    \u002F\u002F Reset generating flag\n-                    self.isGenerating = false;\n-                    \n-                    if (response.success && response.data.question) {\n-                        self.currentPuzzle = response.data.question;\n-                        \u002F\u002F SECURITY: Never store the answer client-side\n-                        self.currentAnswer = null;\n-                        \n-                        \u002F\u002F SECURITY ENHANCEMENT: Display puzzle as image to prevent bot bypass\n-                        if (response.data.image) {\n-                            \u002F\u002F Show image, hide text\n-                            $mo('#mo-osp-puzzle-image').attr('src', response.data.image).show();\n-                            $mo('#mo-osp-puzzle-text').hide();\n-                        } else {\n-                            \u002F\u002F Fallback to text if image generation failed\n-                            $mo('#mo-osp-puzzle-text').text(response.data.question).show();\n-                            $mo('#mo-osp-puzzle-image').hide();\n-                        }\n-                        \n-                        $mo('#mo-osp-puzzle-answer').val('').focus();\n-                        self.hideError();\n-                        \n-                    } else {\n-                        self.showError('Failed to generate puzzle. Please try again.');\n-                    }\n-                },\n-                error: function() {\n-                    \u002F\u002F Reset generating flag on error\n-                    self.isGenerating = false;\n-                    self.showError('Failed to generate puzzle. Please try again.');\n-                }\n-            });\n-        },\n-\n-        \u002F**\n-         * Show the puzzle popup\n-         *\u002F\n-        showPuzzle: function(otpData) {\n-            \u002F\u002F Prevent duplicate calls - if puzzle is already showing, don't show again\n-            if (this.isShowing) {\n-                return;\n-            }\n-            \n-            this.isShowing = true;\n-            this.pendingOtpData = otpData;\n-            this.generatePuzzle();\n-            \n-            \u002F\u002F CRITICAL: Ensure puzzle overlay has higher z-index than WooCommerce checkout popup\n-            \u002F\u002F WooCommerce checkout popup uses z-index: 100000, so puzzle needs to be higher\n-            var $puzzleOverlay = $mo('#mo-osp-puzzle-overlay');\n-            $puzzleOverlay.css('z-index', '100001');\n-            $puzzleOverlay.removeClass('mo-osp-hidden');\n-            \n-            \u002F\u002F Also ensure puzzle popup container has high z-index\n-            var $puzzlePopup = $mo('#mo-osp-puzzle-popup-outer-div');\n-            if ($puzzlePopup.length > 0) {\n-                $puzzlePopup.css('z-index', '100002');\n-            }\n-            \n-            $mo('body').addClass('mo-osp-puzzle-open');\n-            $mo('#mo-osp-puzzle-answer').focus();\n-        },\n-\n-        \u002F**\n-         * Close the puzzle popup\n-         *\u002F\n-        closePuzzle: function() {\n-            this.isShowing = false; \u002F\u002F Clear flag to allow future puzzle checks\n-            this.isGenerating = false; \u002F\u002F Reset generating flag\n-            this.isVerifying = false; \u002F\u002F Reset verifying flag\n-            \n-            \u002F\u002F Hide puzzle overlay\n-            $mo('#mo-osp-puzzle-overlay').addClass('mo-osp-hidden');\n-            \n-            \u002F\u002F CRITICAL: Also hide the outer wrapper div (especially important for WooCommerce checkout popup)\n-            $mo('#mo-osp-puzzle-popup-outer-div').hide();\n-            \n-            $mo('body').removeClass('mo-osp-puzzle-open');\n-            this.pendingOtpData = null;\n-            this.hideError();\n-            \u002F\u002F Re-enable verify button\n-            $mo('#mo-osp-puzzle-verify').prop('disabled', false);\n-        },\n-\n-        \u002F**\n-         * Verify puzzle answer using secure server-side validation\n-         *\u002F\n-        verifyPuzzle: function() {\n-            var self = this;\n-            \n-            \u002F\u002F CRITICAL FIX: Prevent duplicate calls\n-            if (this.isVerifying) {\n-                return;\n-            }\n-            \n-            var userAnswer = parseInt($mo('#mo-osp-puzzle-answer').val());\n-\n-            \n-            if (isNaN(userAnswer)) {\n-                this.showError('Please enter a valid number.');\n-                return;\n-            }\n-            \n-            \u002F\u002F Set verifying flag to prevent duplicate calls\n-            this.isVerifying = true;\n-            \n-            \u002F\u002F Disable verify button to prevent multiple clicks\n-            $mo('#mo-osp-puzzle-verify').prop('disabled', true);\n-            \n-            \u002F\u002F Store user's answer for later form submission\n-            this.lastUserAnswer = userAnswer;\n-            \n-            \n-            \u002F\u002F SECURITY ENHANCEMENT: Verify puzzle through secure session-based AJAX endpoint\n-            \n-            $mo.ajax({    \n-                url: mo_osp_ajax.ajax_url,\n-                type: 'POST',\n-                data: {\n-                    action: 'mo_osp_verify_puzzle',\n-                    nonce: mo_osp_ajax.nonce,\n-                    puzzle_answer: userAnswer,\n-                    \u002F\u002F SECURITY: No longer sending question - server validates against session\n-                    email: this.getEmailFromForm(),\n-                    phone: this.getPhoneFromForm(),\n-                    browser_id: window.mo_osp_browser_id || window.MO_OTP_SpamPreventer.browserID || ''\n-                },\n-                success: function(response) {\n-                    \u002F\u002F Reset verifying flag\n-                    self.isVerifying = false;\n-                    $mo('#mo-osp-puzzle-verify').prop('disabled', false);\n-                    \n-                    if (response.success) {\n-                        \n-                        \u002F\u002F Set global flag to indicate puzzle was just completed\n-                        window.mo_osp_puzzle_just_completed = true;\n-                        \n-                        \u002F\u002F Store verification data for secure form submission\n-                        self.lastVerificationResponse = response.data;\n-                        \n-                        self.closePuzzle();\n-                        \n-                        \u002F\u002F Notify spam preventer of successful puzzle completion\n-                        if (typeof window.MO_OSP_SpamPreventer_onPuzzleSuccess !== 'undefined') {\n-                            window.MO_OSP_SpamPreventer_onPuzzleSuccess();\n-                        }\n-                        \n-                        self.proceedWithOTP();\n-                    } else {\n-                        \u002F\u002F SECURITY: Check if puzzle was reset (new puzzle generated)\n-                        if (response.data && response.data.puzzle_reset) {\n-                            \n-                            \u002F\u002F Check if image element exists\n-                            var $puzzleImage = $mo('#mo-osp-puzzle-image');\n-                            var $puzzleText = $mo('#mo-osp-puzzle-text');\n-                            \n-                            \n-                            \u002F\u002F CRITICAL: Clear answer field FIRST before updating puzzle\n-                            $mo('#mo-osp-puzzle-answer').val('').attr('placeholder', '?');\n-                            \n-                            \u002F\u002F Handle puzzle image if provided\n-                            if (response.data.puzzle_image) {\n-                                \n-                                \u002F\u002F Display new puzzle image (question is server-side only for security)\n-                                \u002F\u002F Force image reload by adding timestamp to prevent caching\n-                                var imageUrl = response.data.puzzle_image;\n-                                var timestamp = new Date().getTime();\n-                                \n-                                \u002F\u002F Always add timestamp to force reload, even if URL already has query params\n-                                if (imageUrl.indexOf('?') === -1) {\n-                                    imageUrl += '?t=' + timestamp;\n-                                } else {\n-                                    imageUrl += '&t=' + timestamp;\n-                                }\n-                                \n-                                \n-                                if ($puzzleImage.length > 0) {\n-                                    \u002F\u002F CRITICAL: Use .on('load') to ensure image is loaded before showing\n-                                    $puzzleImage.off('load error').on('load', function() {\n-                                        $mo(this).show();\n-                                        $puzzleText.hide();\n-                                    }).on('error', function() {\n-                                        console.error('[Puzzle] ERROR: Failed to load puzzle image');\n-                                        \u002F\u002F Fallback: show text if image fails\n-                                        if ($puzzleText.length > 0) {\n-                                            $puzzleText.text('New puzzle generated. Please refresh if image does not appear.').show();\n-                                        }\n-                                        $mo(this).hide();\n-                                    });\n-                                    \n-                                    \u002F\u002F Set the src AFTER binding load handler\n-                                    var oldSrc = $puzzleImage.attr('src');\n-                                    \n-                                    \u002F\u002F Force image reload by setting src\n-                                    if (oldSrc === imageUrl) {\n-                                        \u002F\u002F If URL is same (shouldn't happen with timestamp), force reload by clearing first\n-                                        $puzzleImage.attr('src', '');\n-                                        setTimeout(function() {\n-                                            $puzzleImage.attr('src', imageUrl);\n-                                        }, 50);\n-                                    } else {\n-                                        $puzzleImage.attr('src', imageUrl);\n-                                    }\n-                                    \n-                                    \u002F\u002F Ensure image is visible (in case it was hidden)\n-                                    $puzzleImage.show();\n-                                    $puzzleText.hide();\n-                                } else {\n-                                    console.error('[Puzzle] ERROR: Puzzle image element not found!');\n-                                }\n-                            } else if (response.data.puzzle_question) {\n-                                \u002F\u002F Fallback: show puzzle question as text if image not available\n-                                if ($puzzleText.length > 0) {\n-                                    $puzzleText.text(response.data.puzzle_question).show();\n-                                    $puzzleImage.hide();\n-                                }\n-                            } else {\n-                                console.error('[Puzzle] ERROR: No puzzle image or question provided in response!');\n-                                \u002F\u002F Show generic message\n-                                if ($puzzleText.length > 0) {\n-                                    $puzzleText.text('A new puzzle has been generated. Please try again.').show();\n-                                }\n-                                $puzzleImage.hide();\n-                            }\n-                            \n-                            \u002F\u002F Clear puzzle state\n-                            self.currentPuzzle = null;\n-                            self.currentAnswer = null;\n-                            \n-                            \u002F\u002F Hide error temporarily while new puzzle loads\n-                            self.hideError();\n-                            \n-                            \u002F\u002F Show error message after a brief delay to ensure puzzle image is visible\n-                            setTimeout(function() {\n-                                self.showError(response.data.message || 'Incorrect answer. A new puzzle has been generated. Please solve it.');\n-                                $mo('#mo-osp-puzzle-answer').focus();\n-                            }, 200);\n-                        } else {\n-                            \u002F\u002F No puzzle reset - same puzzle, just show error and allow retry\n-                            \n-                            \u002F\u002F Just clear the answer field and show error - puzzle stays the same\n-                            self.showError(response.data.message || 'Incorrect answer. Please try again.');\n-                            $mo('#mo-osp-puzzle-answer').val('').focus();\n-                        }\n-                        \n-                        \u002F\u002F Ensure verify button stays visible after incorrect answer\n-                        $mo('#mo-osp-puzzle-verify').show().prop('disabled', false);\n-                    }\n-                },\n-                error: function(xhr, status, error) {\n-                    console.error('Puzzle verification AJAX error: ' + status + ' - ' + error);\n-                    \u002F\u002F Reset verifying flag\n-                    self.isVerifying = false;\n-                    self.showError('Verification failed. Please try again.');\n-                    \u002F\u002F Ensure verify button stays visible after error\n-                    $mo('#mo-osp-puzzle-verify').show().prop('disabled', false);\n-                }\n-            });\n-        },\n-\n-        \u002F**\n-         * Proceed with OTP sending after successful puzzle verification\n-         *\u002F\n-        proceedWithOTP: function() {\n-            var self = this; \u002F\u002F Store reference to this for use in nested functions\n-            \n-            \u002F\u002F Get verification data from the last successful verification\n-            var verificationData = this.lastVerificationResponse || {};\n-            \n-            \u002F\u002F Add secure puzzle verification data to all forms\n-            $mo('form').each(function() {\n-                var $moform = $mo(this);\n-                \n-                \u002F\u002F Remove any old puzzle processed flags\n-                $moform.find('input[name=\"mo_osp_puzzle_processed\"]').remove();\n-                \n-                \u002F\u002F Add puzzle verification flag\n-                if (!$moform.find('input[name=\"puzzle_verified\"]').length) {\n-                    $moform.append('\u003Cinput type=\"hidden\" name=\"puzzle_verified\" value=\"true\">');\n-                }\n-                \n-                if (verificationData.puzzle_nonce && !$moform.find('input[name=\"mo_osp_puzzle_nonce\"]').length) {\n-                    $moform.append('\u003Cinput type=\"hidden\" name=\"mo_osp_puzzle_nonce\" value=\"' + verificationData.puzzle_nonce + '\">');\n-                }\n-                \n-                if (verificationData.verification_token && !$moform.find('input[name=\"verification_token\"]').length) {\n-                    $moform.append('\u003Cinput type=\"hidden\" name=\"verification_token\" value=\"' + verificationData.verification_token + '\">');\n-                }\n-                \n-                \u002F\u002F Add the user's puzzle answer for server verification\n-                if (self.lastUserAnswer && !$moform.find('input[name=\"puzzle_answer\"]').length) {\n-                    $moform.append('\u003Cinput type=\"hidden\" name=\"puzzle_answer\" value=\"' + self.lastUserAnswer + '\">');\n-                }\n-                \n-                \u002F\u002F SECURITY: No longer sending puzzle question - server has it in session\n-            });\n-            \n-            \u002F\u002F Set global verification flag for AJAX interception\n-            window.mo_osp_puzzle_verified = true;\n-            \n-            \n-            \u002F\u002F PRIORITY 1: Check for external popup callback (highest priority)\n-            if (typeof window.MO_OSP_Puzzle_onExternalPopupSuccess === 'function') {\n-                try {\n-                    \u002F\u002F Close puzzle before calling callback\n-                    self.closePuzzle();\n-                    window.MO_OSP_Puzzle_onExternalPopupSuccess(verificationData);\n-                } catch (e) {\n-                    console.error('Error in external popup success callback:', e);\n-                    \u002F\u002F Ensure puzzle is closed even on error\n-                    self.closePuzzle();\n-                }\n-                return;\n-            }\n-            \n-            \u002F\u002F PRIORITY 2: Check if we're in AJAX form context (callback registered by spam-preventer.js)\n-            if (typeof window.MO_OSP_Puzzle_onAjaxSuccess === 'function') {\n-                try {\n-                    \u002F\u002F Close puzzle before calling callback\n-                    self.closePuzzle();\n-                    window.MO_OSP_Puzzle_onAjaxSuccess(verificationData);\n-                } catch (e) {\n-                    console.error('Error in AJAX success callback:', e);\n-                    \u002F\u002F Ensure puzzle is closed even on error\n-                    self.closePuzzle();\n-                }\n-                return;\n-            }\n-            \n-            \u002F\u002F Check if we're in popup context (callback registered by popup-timer.js or inline script)\n-            if (typeof window.MO_OSP_Puzzle_onPopupSuccess === 'function') {\n-                try {\n-                    \u002F\u002F Close puzzle before calling callback\n-                    self.closePuzzle();\n-                    window.MO_OSP_Puzzle_onPopupSuccess();\n-                } catch (e) {\n-                    console.error('Error in popup success callback:', e);\n-                    \u002F\u002F Ensure puzzle is closed even on error\n-                    self.closePuzzle();\n-                }\n-                return;\n-            }\n-            \n-            \n-            \u002F\u002F Check if we're in a popup (DefaultPopup) - look for resend form\n-            var resendForm = document.getElementById('verification_resend_otp_form');\n-            if (resendForm) {\n-                \u002F\u002F CRITICAL: Close puzzle before submitting form\n-                self.closePuzzle();\n-                \n-                \u002F\u002F Add puzzle verification data to the resend form\n-                var puzzleVerifiedInput = document.createElement('input');\n-                puzzleVerifiedInput.type = 'hidden';\n-                puzzleVerifiedInput.name = 'puzzle_verified';\n-                puzzleVerifiedInput.value = 'true';\n-                resendForm.appendChild(puzzleVerifiedInput);\n-                \n-                if (verificationData.puzzle_nonce) {\n-                    var nonceInput = document.createElement('input');\n-                    nonceInput.type = 'hidden';\n-                    nonceInput.name = 'mo_osp_puzzle_nonce';\n-                    nonceInput.value = verificationData.puzzle_nonce;\n-                    resendForm.appendChild(nonceInput);\n-                }\n-                \n-                if (verificationData.verification_token) {\n-                    var tokenInput = document.createElement('input');\n-                    tokenInput.type = 'hidden';\n-                    tokenInput.name = 'verification_token';\n-                    tokenInput.value = verificationData.verification_token;\n-                    resendForm.appendChild(tokenInput);\n-                }\n-                \n-                \u002F\u002F Submit the resend form to trigger OTP sending\n-                resendForm.submit();\n-                return;\n-            }\n-            \n-            \n-            \u002F\u002F CRITICAL FIX: For popup context, show OTP form instead of reloading\n-            \u002F\u002F Check if we're in a popup (DefaultPopup was shown)\n-            var $moPopup = $mo('#mo_site_otp_form, .mo_customer_validation-modal');\n-            if ($moPopup.length > 0) {\n-                \n-                \u002F\u002F CRITICAL: Close puzzle before showing OTP popup again\n-                self.closePuzzle();\n-                \n-                \u002F\u002F Show the popup again (it was hidden when puzzle was shown)\n-                $mo('#mo_site_otp_form').show();\n-                $mo('.mo_customer_validation-modal').show();\n-                $mo('.mo-modal-backdrop').show();\n-                \n-                \u002F\u002F Update the message in popup to show OTP form message\n-                var $moPopupBody = $mo('.mo_customer_validation-modal-body');\n-                if ($moPopupBody.length > 0) {\n-                    \u002F\u002F Check if OTP form exists in popup\n-                    var $moOtpForm = $mo('#mo_validate_form');\n-                    if ($moOtpForm.length > 0) {\n-                        \u002F\u002F OTP form exists, trigger OTP sending via resend link\n-                        var $moResendLink = $mo('a.mo-resend, a[onclick*=\"mo_otp_verification_resend\"]');\n-                        if ($moResendLink.length > 0) {\n-                            \u002F\u002F Add puzzle verification data to the OTP form first\n-                            var $moOtpFormInputs = $moOtpForm;\n-                            if (!$moOtpFormInputs.find('input[name=\"puzzle_verified\"]').length) {\n-                                $moOtpFormInputs.append('\u003Cinput type=\"hidden\" name=\"puzzle_verified\" value=\"true\">');\n-                            }\n-                            if (verificationData.puzzle_nonce && !$moOtpFormInputs.find('input[name=\"mo_osp_puzzle_nonce\"]').length) {\n-                                $moOtpFormInputs.append('\u003Cinput type=\"hidden\" name=\"mo_osp_puzzle_nonce\" value=\"' + verificationData.puzzle_nonce + '\">');\n-                            }\n-                            if (verificationData.verification_token && !$moOtpFormInputs.find('input[name=\"verification_token\"]').length) {\n-                                $moOtpFormInputs.append('\u003Cinput type=\"hidden\" name=\"verification_token\" value=\"' + verificationData.verification_token + '\">');\n-                            }\n-                            \u002F\u002F Trigger resend to send OTP\n-                            $moResendLink.trigger('click');\n-                            return;\n-                        }\n-                    }\n-                }\n-            }\n-            \n-            \u002F\u002F Fallback: For regular forms, we need to resubmit the original form\n-            \u002F\u002F Since puzzle is verified, the next OTP request should succeed\n-            \n-            \u002F\u002F Try to find the original login\u002Fsubmission form\n-            var $moOriginalForm = $mo('form[name=\"loginform\"], form#loginform, form.wp-login-form');\n-            if ($moOriginalForm.length === 0) {\n-                \u002F\u002F Try other common form selectors\n-                $moOriginalForm = $mo('form').not('#mo_validate_form').not('#validation_goBack_form').not('#verification_resend_otp_form').first();\n-            }\n-            \n-            if ($moOriginalForm.length > 0) {\n-                \u002F\u002F Add puzzle verification data to the form\n-                if (!$moOriginalForm.find('input[name=\"puzzle_verified\"]').length) {\n-                    $moOriginalForm.append('\u003Cinput type=\"hidden\" name=\"puzzle_verified\" value=\"true\">');\n-                }\n-                if (verificationData.puzzle_nonce && !$moOriginalForm.find('input[name=\"mo_osp_puzzle_nonce\"]').length) {\n-                    $moOriginalForm.append('\u003Cinput type=\"hidden\" name=\"mo_osp_puzzle_nonce\" value=\"' + verificationData.puzzle_nonce + '\">');\n-                }\n-                if (verificationData.verification_token && !$moOriginalForm.find('input[name=\"verification_token\"]').length) {\n-                    $moOriginalForm.append('\u003Cinput type=\"hidden\" name=\"verification_token\" value=\"' + verificationData.verification_token + '\">');\n-                }\n-                \u002F\u002F Submit the form to trigger OTP sending\n-                $moOriginalForm.submit();\n-                return;\n-            }\n-            \n-            \u002F\u002F Last resort: Reload the page\n-            \u002F\u002F Store puzzle completion flag in sessionStorage\n-            sessionStorage.setItem('mo_osp_puzzle_completed', 'true');\n-            \u002F\u002F Reload to show OTP form\n-            window.location.reload();\n-        },\n-\n-        \u002F**\n-         * Click the Send OTP button after puzzle verification\n-         *\u002F\n-        clickSendOTPButton: function() {\n-            \n-            \u002F\u002F Find and click the send OTP button\n-            var sendButton = this.findSendOTPButton();\n-            \n-            if (sendButton && sendButton.length > 0) {\n-                \u002F\u002F Trigger OTP send directly\n-                this.triggerOTPSendDirectly();\n-            } else {\n-                \u002F\u002F Reset the flag after a short delay to allow user to click manually\n-                setTimeout(function() {\n-                    window.mo_otp_button_clicked = false;\n-                }, 2000);\n-            }\n-        },\n-\n-        \u002F**\n-         * Trigger OTP send directly\n-         *\u002F\n-        triggerOTPSendDirectly: function() {\n-            \n-            \u002F\u002F Find the send OTP button\n-            var sendButton = this.findSendOTPButton();\n-            \n-            if (sendButton && sendButton.length > 0) {\n-                \n-                \u002F\u002F Trigger the button click\n-                sendButton.trigger('click');\n-                \n-            } else {\n-                \u002F\u002F Show error message\n-                var messageBox = $mo('#mo_message, .mo_message, [id*=\"mo_message\"]').first();\n-                if (messageBox.length > 0) {\n-                    messageBox.empty().append('Error: Could not find Send OTP button.').css({\n-                        'color': '#ff5b5b',\n-                        'background': '#ffefef',\n-                        'padding': '10px',\n-                        'border-radius': '5px'\n-                    });\n-                } else {\n-                    \u002F\u002F Fallback: Create temporary message\n-                    $mo('body').append('\u003Cdiv id=\"mo_osp_temp_message\" style=\"position: fixed; top: 20px; right: 20px; background: #ffefef; color: #ff5b5b; padding: 10px; border-radius: 5px; z-index: 9999;\">Error: Could not find Send OTP button.\u003C\u002Fdiv>');\n-                    setTimeout(function() {\n-                        $mo('#mo_osp_temp_message').remove();\n-                    }, 5000);\n-                }\n-            }\n-        },\n-\n-        \u002F**\n-         * Find the Send OTP button on the page\n-         *\u002F\n-        findSendOTPButton: function() {\n-            \u002F\u002F Try multiple selectors to find the send OTP button\n-            var buttonSelectors = [\n-                'button#miniorange_wc_popup_send_otp_token',\n-                '#mo_wc_send_otp',\n-                'input[id*=\"send_otp\"]',\n-                'button[id*=\"send_otp\"]',\n-                'input[value*=\"Send OTP\"]',\n-                'button[value*=\"Send OTP\"]',\n-                'input[id*=\"mo_wc_send_otp\"]',\n-                'button[id*=\"mo_wc_send_otp\"]',\n-                '.mo-send-otp-button',\n-                '[class*=\"send-otp\"]',\n-                'input[name*=\"send_otp\"]',\n-                'button[name*=\"send_otp\"]'\n-            ];\n-            \n-            for (var i = 0; i \u003C buttonSelectors.length; i++) {\n-                var button = $mo(buttonSelectors[i]);\n-                if (button.length > 0) {\n-                    return button.first();\n-                }\n-            }\n-            return null;\n-        },\n-\n-        \u002F**\n-         * Intercept AJAX calls to add puzzle verification data\n-         *\u002F\n-        interceptAjaxCalls: function() {\n-            \n-            var self = this;\n-            var originalAjax = $mo.ajax;\n-            \n-            $mo.ajax = function(options) {\n-                \u002F\u002F Get verification data\n-                var verificationData = self.lastVerificationResponse || {};\n-                \n-                \u002F\u002F Check for AJAX actions that might be OTP-related\n-                var otpActions = [\n-                    'miniorange_ajax_otp',\n-                    'mo_ajax_form_validate',\n-                    'mo_send_otp',\n-                    'mo_resend_otp',\n-                    'woocommerce_checkout'\n-                ];\n-                \n-                var isOtpRelated = false;\n-                if (options.data) {\n-                    var dataStr = typeof options.data === 'string' ? options.data : JSON.stringify(options.data);\n-                    for (var i = 0; i \u003C otpActions.length; i++) {\n-                        if (dataStr.includes(otpActions[i]) || \n-                            dataStr.includes('send_otp') || dataStr.includes('verify_otp')) {\n-                            isOtpRelated = true;\n-                            break;\n-                        }\n-                    }\n-                }\n-                \n-                if (isOtpRelated && window.mo_osp_puzzle_verified) {\n-                    \n-                    \u002F\u002F Add puzzle verification data to AJAX request\n-                    if (typeof options.data === 'string') {\n-                        if (!options.data.includes('puzzle_verified')) {\n-                            options.data += '&puzzle_verified=true';\n-                        }\n-                        if (verificationData.puzzle_nonce && !options.data.includes('mo_osp_puzzle_nonce')) {\n-                            options.data += '&mo_osp_puzzle_nonce=' + encodeURIComponent(verificationData.puzzle_nonce);\n-                        }\n-                        if (verificationData.verification_token && !options.data.includes('verification_token')) {\n-                            options.data += '&verification_token=' + encodeURIComponent(verificationData.verification_token);\n-                        }\n-                        if (window.MO_OSP_Puzzle.lastUserAnswer && !options.data.includes('puzzle_answer')) {\n-                            options.data += '&puzzle_answer=' + encodeURIComponent(window.MO_OSP_Puzzle.lastUserAnswer);\n-                        }\n-                        if (!options.data.includes('mo_osp_browser_id')) {\n-                            options.data += '&mo_osp_browser_id=' + encodeURIComponent(window.mo_osp_browser_id || '');\n-                        }\n-                    } else {\n-                        \u002F\u002F If data is null\u002Fundefined, create new data object with secure verification\n-                        options.data = options.data || {};\n-                        options.data.mo_osp_browser_id = window.mo_osp_browser_id || '';\n-                        \n-                        if (window.mo_osp_puzzle_verified) {\n-                            options.data.puzzle_verified = 'true';\n-                            if (verificationData.puzzle_nonce) {\n-                                options.data.mo_osp_puzzle_nonce = verificationData.puzzle_nonce;\n-                            }\n-                            if (verificationData.verification_token) {\n-                                options.data.verification_token = verificationData.verification_token;\n-                            }\n-                            if (window.MO_OSP_Puzzle.lastUserAnswer) {\n-                                options.data.puzzle_answer = window.MO_OSP_Puzzle.lastUserAnswer;\n-                            }\n-                            \u002F\u002F SECURITY: No longer sending puzzle question - server has it in session\n-                        }\n-                    }\n-                    \n-                    \n-                    \u002F\u002F Wrap the success callback for universal handling\n-                    var originalSuccess = options.success;\n-                    options.success = function(response) {\n-                        \n-                        \u002F\u002F Check if response contains puzzle requirement (multiple detection methods)\n-                        var isPuzzleRequired = false;\n-                        var detectionMethod = '';\n-                        \n-                        \u002F\u002F Method 1: Check structured response format\n-                        if (response && (response.puzzle_required === true || response.authType === 'PUZZLE_REQUIRED' || response.result === 'puzzle_required')) {\n-                            isPuzzleRequired = true;\n-                            detectionMethod = 'structured_response';\n-                        }\n-                        \u002F\u002F Method 2: Check message content (fallback)\n-                        else if (response && response.message && \n-                            response.message.includes('Please complete the security verification to continue')) {\n-                            isPuzzleRequired = true;\n-                            detectionMethod = 'message_content';\n-                        }\n-                        \n-                        if (isPuzzleRequired) {\n-                            \n-                            \u002F\u002F Call original success handler first\n-                            if (originalSuccess && typeof originalSuccess === 'function') {\n-                                originalSuccess.call(this, response);\n-                            }\n-                            \n-                            \u002F\u002F Wait a moment for DOM to update, then show puzzle\n-                            setTimeout(function() {\n-                                if (typeof MO_OSP_Puzzle !== 'undefined' && !MO_OSP_Puzzle.isShowing) {\n-                                    MO_OSP_Puzzle.showPuzzle({});\n-                                }\n-                            }, 500);\n-                            \n-                            \u002F\u002F Also trigger manual puzzle check as fallback\n-                            setTimeout(function() {\n-                                if (typeof MO_OSP_Puzzle !== 'undefined') {\n-                                    MO_OSP_Puzzle.triggerPuzzleCheck();\n-                                }\n-                            }, 1000);\n-                            return;\n-                        }\n-                        \n-                        \u002F\u002F Call original success handler first - let it handle all UI updates\n-                        if (originalSuccess && typeof originalSuccess === 'function') {\n-                            originalSuccess.call(this, response);\n-                        }\n-                    };\n-                    \n-                    \u002F\u002F Also wrap error callback to catch puzzle requirements in error responses\n-                    var originalError = options.error;\n-                    options.error = function(xhr, status, error) {\n-                        \n-                        \u002F\u002F Try to parse error response for puzzle requirements\n-                        try {\n-                            var errorResponse = JSON.parse(xhr.responseText);\n-                            if (errorResponse && (errorResponse.puzzle_required === true || errorResponse.authType === 'PUZZLE_REQUIRED' || errorResponse.result === 'puzzle_required')) {\n-                                \n-                                \n-                                \u002F\u002F Show puzzle for error response\n-                                setTimeout(function() {\n-                                    if (typeof MO_OSP_Puzzle !== 'undefined' && !MO_OSP_Puzzle.isShowing) {\n-                                        MO_OSP_Puzzle.showPuzzle({});\n-                                    }\n-                                }, 500);\n-                                \n-                                \u002F\u002F Don't call original error handler for puzzle requirements\n-                                return;\n-                            }\n-                        } catch (e) {\n-                            \u002F\u002F Not JSON or parsing failed, continue with normal error handling\n-                        }\n-                        \n-                        \u002F\u002F Call original error handler for non-puzzle errors\n-                        if (originalError && typeof originalError === 'function') {\n-                            originalError.call(this, xhr, status, error);\n-                        }\n-                    };\n-                }\n-                \n-                \u002F\u002F Call the original ajax function\n-                return originalAjax.call(this, options);\n-            };\n-            \n-            \u002F\u002F Restore original AJAX after 10 seconds (longer for universal coverage)\n-            setTimeout(function() {\n-                $mo.ajax = originalAjax;\n-                \n-            }, 10000);\n-        },\n-\n-        \u002F**\n-         * Manual trigger for puzzle when we know it should appear (after OTP request)\n-         *\u002F\n-        triggerPuzzleCheck: function() {\n-            \n-            var self = this;\n-            \n-            \u002F\u002F Reset counter for new check sequence\n-            this.puzzleCheckCount = 0;\n-            \n-            \u002F\u002F Check immediately\n-            this.checkForPuzzleMessage();\n-            \n-            \u002F\u002F Only check a few more times with reasonable delays (not aggressive)\n-            setTimeout(function() { self.checkForPuzzleMessage(); }, 500);\n-            setTimeout(function() { self.checkForPuzzleMessage(); }, 1500);\n-        },\n-\n-        \u002F**\n-         * Get email from form fields\n-         *\u002F\n-        getEmailFromForm: function() {\n-            var email = '';\n-            \n-            \u002F\u002F Try to get from visible email inputs first\n-            $mo('input[type=\"email\"]:visible, input[name*=\"email\"]:visible, input[id*=\"email\"]:visible').each(function() {\n-                var $field = $mo(this);\n-                var type = ($field.attr('type') || '').toLowerCase();\n-                if (type === 'button' || type === 'submit' || type === 'reset') {\n-                    return;\n-                }\n-                var value = $field.val();\n-                if (value && !\u002Fsend\\s+otp|verify\\s+otp\u002Fi.test(value)) {\n-                    email = value;\n-                    return false; \u002F\u002F Break loop\n-                }\n-            });\n-            \n-            \u002F\u002F Fallback to hidden inputs (from popup forms with extra_post_data)\n-            if (!email) {\n-                $mo('input[type=\"hidden\"][name*=\"email\"], input[type=\"hidden\"][id*=\"email\"]').each(function() {\n-                    if ($mo(this).val()) {\n-                        email = $mo(this).val();\n-                        return false; \u002F\u002F Break loop\n-                    }\n-                });\n-            }\n-            \n-            \n-            return email;\n-        },\n-\n-        \u002F**\n-         * Get phone from form fields\n-         *\u002F\n-        getPhoneFromForm: function() {\n-            var phone = '';\n-            \n-            \u002F\u002F Try to get from visible phone inputs first\n-            $mo('input[type=\"tel\"]:visible, input[name*=\"phone\"]:visible, input[id*=\"phone\"]:visible, input[name*=\"mobile\"]:visible').each(function() {\n-                var $field = $mo(this);\n-                var type = ($field.attr('type') || '').toLowerCase();\n-                if (type === 'button' || type === 'submit' || type === 'reset') {\n-                    return;\n-                }\n-                var value = $field.val();\n-                if (!value || \u002Fsend\\s+otp|verify\\s+otp\u002Fi.test(value)) {\n-                    return;\n-                }\n-                \u002F\u002F Normalize to digits\u002F+ and require a minimum length to avoid tokens like \"6ff2c895dc\".\n-                var normalized = String(value).replace(\u002F[^0-9+]\u002Fg, '');\n-                var digitCount = normalized.replace(\u002F\\D\u002Fg, '').length;\n-                if (digitCount >= 6) {\n-                    phone = normalized;\n-                    return false; \u002F\u002F Break loop\n-                }\n-            });\n-            \n-            \u002F\u002F Fallback to hidden inputs (from popup forms with extra_post_data)\n-            if (!phone) {\n-            $mo('input[type=\"hidden\"][name*=\"phone\"], input[type=\"hidden\"][id*=\"phone\"], input[type=\"hidden\"][name*=\"mobile\"]').each(function() {\n-                    var value = $mo(this).val();\n-                    if (!value || \u002Fsend\\s+otp|verify\\s+otp\u002Fi.test(value)) {\n-                        return;\n-                    }\n-                    var normalized = String(value).replace(\u002F[^0-9+]\u002Fg, '');\n-                    var digitCount = normalized.replace(\u002F\\D\u002Fg, '').length;\n-                    if (digitCount >= 6) {\n-                        phone = normalized;\n-                        return false; \u002F\u002F Break loop\n-                    }\n-                });\n-            }\n-            \n-            \n-            return phone;\n-        },\n-\n-        \u002F**\n-         * Show error message in puzzle popup\n-         *\u002F\n-        showError: function(message) {\n-            $mo('#mo-osp-puzzle-error-text').text(message);\n-            $mo('#mo-osp-puzzle-error').show();\n-        },\n-\n-        \u002F**\n-         * Hide error message in puzzle popup\n-         *\u002F\n-        hideError: function() {\n-            $mo('#mo-osp-puzzle-error').hide();\n-        }\n-    };\n-\n-    \u002F\u002F Initialize puzzle system when document is ready\n-    $mo(document).ready(function() {\n-        \u002F\u002F Initialize puzzle system only when needed (not automatically on every page load)\n-        \u002F\u002F Puzzle will be initialized when OTP request triggers puzzle requirement\n-        if (typeof MO_OSP_Puzzle !== 'undefined') {\n-            \u002F\u002F Initialize fully - bind events and mark as initialized\n-            MO_OSP_Puzzle.init();\n-            MO_OSP_Puzzle.initialized = true;\n-        }\n-    });\n-\n-})(jQuery);\n+\u002F**\r\n+ * OTP Puzzle Verification System\r\n+ * \r\n+ * Secure puzzle system for human verification before OTP sending.\r\n+ * Features:\r\n+ * - Server-side puzzle generation and validation\r\n+ * - Session-based security with fallback storage\r\n+ * - Multiple detection methods for puzzle requirements\r\n+ * - Comprehensive error handling and logging\r\n+ * \r\n+ * @package otpspampreventer\r\n+ *\u002F\r\n+\r\n+(function($mo) {\r\n+    'use strict';\r\n+\r\n+    \u002F\u002F Puzzle Verification System\r\n+    window.MO_OSP_Puzzle = {\r\n+        \u002F\u002F State variables\r\n+        currentPuzzle: null,\r\n+        currentAnswer: null,\r\n+        lastUserAnswer: null,\r\n+        pendingOtpData: null,\r\n+        isShowing: false,\r\n+        puzzleCheckCount: 0,\r\n+        lastVerificationResponse: null,\r\n+        isGenerating: false, \u002F\u002F Flag to prevent duplicate generate calls\r\n+        isVerifying: false, \u002F\u002F Flag to prevent duplicate verify calls\r\n+        eventsBound: false, \u002F\u002F Flag to prevent duplicate event binding\r\n+\r\n+        \u002F**\r\n+         * Initialize the puzzle system\r\n+         *\u002F\r\n+        init: function() {\r\n+            \u002F\u002F Only bind events once to prevent duplicate handlers\r\n+            if (!this.eventsBound) {\r\n+                this.bindEvents();\r\n+                this.eventsBound = true;\r\n+            }\r\n+            this.puzzleCheckCount = 0; \u002F\u002F Initialize counter to prevent infinite loops\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Bind all puzzle-related event handlers\r\n+         * CRITICAL: Unbind previous handlers first to prevent duplicate event binding\r\n+         *\u002F\r\n+        bindEvents: function() {\r\n+            var self = this;\r\n+            \r\n+            \u002F\u002F CRITICAL FIX: Unbind all previous handlers to prevent duplicate event binding\r\n+            $mo(document).off('click', '.mo-osp-puzzle-close');\r\n+            $mo(document).off('click', '.mo-osp-puzzle-popup');\r\n+            $mo(document).off('click', '#mo-osp-puzzle-refresh');\r\n+            $mo(document).off('click', '#mo-osp-puzzle-verify');\r\n+            $mo(document).off('keypress', '#mo-osp-puzzle-answer');\r\n+            $mo(document).off('input', '#mo-osp-puzzle-answer');\r\n+            \r\n+            \u002F\u002F Close puzzle popup\r\n+            $mo(document).on('click', '.mo-osp-puzzle-close', function() {\r\n+                self.closePuzzle();\r\n+            });\r\n+\r\n+            \u002F\u002F Prevent closing when clicking inside popup\r\n+            $mo(document).on('click', '.mo-osp-puzzle-popup', function(e) {\r\n+                e.stopPropagation();\r\n+            });\r\n+\r\n+            \u002F\u002F Refresh puzzle\r\n+            $mo(document).on('click', '#mo-osp-puzzle-refresh', function() {\r\n+                self.generatePuzzle();\r\n+                $mo('#mo-osp-puzzle-answer').val('').focus();\r\n+            });\r\n+\r\n+            \u002F\u002F Verify puzzle - prevent duplicate calls\r\n+            $mo(document).on('click', '#mo-osp-puzzle-verify', function(e) {\r\n+                e.preventDefault();\r\n+                e.stopPropagation();\r\n+                \r\n+                \u002F\u002F Prevent duplicate calls - check if verification is already in progress\r\n+                if (self.isVerifying) {\r\n+                    return false;\r\n+                }\r\n+                \r\n+                self.verifyPuzzle();\r\n+                return false;\r\n+            });\r\n+\r\n+            \u002F\u002F Enter key in puzzle input - prevent duplicate calls\r\n+            $mo(document).on('keypress', '#mo-osp-puzzle-answer', function(e) {\r\n+                if (e.which === 13) { \u002F\u002F Enter key\r\n+                    e.preventDefault();\r\n+                    \r\n+                    \u002F\u002F Prevent duplicate calls\r\n+                    if (self.isVerifying) {\r\n+                        return false;\r\n+                    }\r\n+                    \r\n+                    self.verifyPuzzle();\r\n+                    return false;\r\n+                }\r\n+            });\r\n+\r\n+            \u002F\u002F Clear error on input\r\n+            $mo(document).on('input', '#mo-osp-puzzle-answer', function() {\r\n+                self.hideError();\r\n+            });\r\n+\r\n+            \u002F\u002F Note: checkForPuzzleMessage() is now only called when OTP request is made\r\n+            \u002F\u002F This prevents puzzle popup from showing on every page load\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Check for puzzle requirement messages in DOM\r\n+         * Uses circuit breaker to prevent infinite loops\r\n+         *\u002F\r\n+        checkForPuzzleMessage: function() {\r\n+            var self = this;\r\n+            \r\n+            \u002F\u002F Prevent infinite loop - only check if puzzle is not already showing\r\n+            if (self.isShowing) {\r\n+                return;\r\n+            }\r\n+            \r\n+            \u002F\u002F Circuit breaker: Stop checking after 30 attempts (30 seconds) to prevent infinite loops\r\n+            if (!this.puzzleCheckCount) {\r\n+                this.puzzleCheckCount = 0;\r\n+            }\r\n+            this.puzzleCheckCount++;\r\n+            \r\n+            if (this.puzzleCheckCount > 30) {\r\n+                return;\r\n+            }\r\n+            \r\n+            \r\n+            \u002F\u002F Look for the puzzle message in various message containers\r\n+            var messageSelectors = [\r\n+                '#mo_message',\r\n+                '.mo_message', \r\n+                '[id*=\"mo_message\"]',\r\n+                '.woocommerce-message',\r\n+                '.notice',\r\n+                '.alert',\r\n+                '[class*=\"message\"]',\r\n+                '.mo-otp-message',\r\n+                'div[style*=\"color\"]', \u002F\u002F Catch styled message divs\r\n+                '.error',\r\n+                '.success',\r\n+                '.info'\r\n+            ];\r\n+            \r\n+            var found = false;\r\n+            var allMessages = [];\r\n+            \r\n+            \u002F\u002F Specific puzzle message patterns (must be precise to avoid false positives)\r\n+            var puzzlePatterns = [\r\n+                'Please complete the security verification to continue',\r\n+                'solve this simple puzzle to verify you are human before sending an OTP',\r\n+                'complete the security verification to continue',\r\n+                'puzzle to verify you are human before sending',\r\n+                'security purposes, please solve this simple puzzle'\r\n+            ];\r\n+            \r\n+            for (var i = 0; i \u003C messageSelectors.length && !found; i++) {\r\n+                $mo(messageSelectors[i]).each(function() {\r\n+                    var messageText = $mo(this).text().trim();\r\n+                    if (messageText) {\r\n+                        allMessages.push({\r\n+                            selector: messageSelectors[i],\r\n+                            text: messageText,\r\n+                            element: this\r\n+                        });\r\n+                    }\r\n+                    \r\n+                    \u002F\u002F Check against multiple puzzle patterns\r\n+                    if (messageText && typeof messageText.includes === 'function') {\r\n+                        var messageTextLower = messageText.toLowerCase();\r\n+                        for (var j = 0; j \u003C puzzlePatterns.length; j++) {\r\n+                            if (messageTextLower.includes(puzzlePatterns[j].toLowerCase())) {\r\n+                                \u002F\u002F DON'T remove the message box - just clear its content and hide it temporarily\r\n+                                $mo(this).empty().hide();\r\n+                                self.showPuzzle({});\r\n+                                found = true;\r\n+                                return false;\r\n+                            }\r\n+                        }\r\n+                    }\r\n+                });\r\n+            }\r\n+            \r\n+            if (!found) {\r\n+                \r\n+                \u002F\u002F Use exponential backoff to reduce performance impact\r\n+                var delay = Math.min(1000 + (this.puzzleCheckCount * 100), 3000); \u002F\u002F Max 3 seconds\r\n+                \r\n+                \u002F\u002F Continue checking with increasing delay\r\n+                setTimeout(function() {\r\n+                    self.checkForPuzzleMessage();\r\n+                }, delay);\r\n+            } else {\r\n+                \u002F\u002F Reset counter when puzzle is found and shown\r\n+                this.puzzleCheckCount = 0;\r\n+            }\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Generate a new puzzle using secure server-side generation\r\n+         *\u002F\r\n+        generatePuzzle: function() {\r\n+            var self = this;\r\n+            \r\n+            \u002F\u002F CRITICAL FIX: Prevent duplicate calls\r\n+            if (this.isGenerating) {\r\n+                return;\r\n+            }\r\n+            \r\n+            \u002F\u002F Set generating flag to prevent duplicate calls\r\n+            this.isGenerating = true;\r\n+            \r\n+            $mo.ajax({\r\n+                url: mo_osp_ajax.ajax_url,\r\n+                type: 'POST',\r\n+                data: {\r\n+                    action: 'mo_osp_generate_puzzle',\r\n+                    nonce: mo_osp_ajax.nonce\r\n+                },\r\n+                success: function(response) {\r\n+                    \u002F\u002F Reset generating flag\r\n+                    self.isGenerating = false;\r\n+                    \r\n+                    if (response.success && response.data.question) {\r\n+                        self.currentPuzzle = response.data.question;\r\n+                        \u002F\u002F SECURITY: Never store the answer client-side\r\n+                        self.currentAnswer = null;\r\n+                        \r\n+                        \u002F\u002F SECURITY ENHANCEMENT: Display puzzle as image to prevent bot bypass\r\n+                        if (response.data.image) {\r\n+                            \u002F\u002F Show image, hide text\r\n+                            $mo('#mo-osp-puzzle-image').attr('src', response.data.image).show();\r\n+                            $mo('#mo-osp-puzzle-text').hide();\r\n+                        } else {\r\n+                            \u002F\u002F Fallback to text if image generation failed\r\n+                            $mo('#mo-osp-puzzle-text').text(response.data.question).show();\r\n+                            $mo('#mo-osp-puzzle-image').hide();\r\n+                        }\r\n+                        \r\n+                        $mo('#mo-osp-puzzle-answer').val('').focus();\r\n+                        self.hideError();\r\n+                        \r\n+                    } else {\r\n+                        self.showError('Failed to generate puzzle. Please try again.');\r\n+                    }\r\n+                },\r\n+                error: function() {\r\n+                    \u002F\u002F Reset generating flag on error\r\n+                    self.isGenerating = false;\r\n+                    self.showError('Failed to generate puzzle. Please try again.');\r\n+                }\r\n+            });\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Show the puzzle popup\r\n+         *\u002F\r\n+        showPuzzle: function(otpData) {\r\n+            \u002F\u002F Prevent duplicate calls - if puzzle is already showing, don't show again\r\n+            if (this.isShowing) {\r\n+                return;\r\n+            }\r\n+            \r\n+            this.isShowing = true;\r\n+            this.pendingOtpData = otpData;\r\n+            this.generatePuzzle();\r\n+            \r\n+            \u002F\u002F CRITICAL: Ensure puzzle overlay has higher z-index than WooCommerce checkout popup\r\n+            \u002F\u002F WooCommerce checkout popup uses z-index: 100000, so puzzle needs to be higher\r\n+            var $puzzleOverlay = $mo('#mo-osp-puzzle-overlay');\r\n+            $puzzleOverlay.css('z-index', '100001');\r\n+            $puzzleOverlay.removeClass('mo-osp-hidden');\r\n+            \r\n+            \u002F\u002F Also ensure puzzle popup container has high z-index\r\n+            var $puzzlePopup = $mo('#mo-osp-puzzle-popup-outer-div');\r\n+            if ($puzzlePopup.length > 0) {\r\n+                $puzzlePopup.css('z-index', '100002');\r\n+            }\r\n+            \r\n+            $mo('body').addClass('mo-osp-puzzle-open');\r\n+            $mo('#mo-osp-puzzle-answer').focus();\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Close the puzzle popup\r\n+         *\u002F\r\n+        closePuzzle: function() {\r\n+            this.isShowing = false; \u002F\u002F Clear flag to allow future puzzle checks\r\n+            this.isGenerating = false; \u002F\u002F Reset generating flag\r\n+            this.isVerifying = false; \u002F\u002F Reset verifying flag\r\n+            \r\n+            \u002F\u002F Hide puzzle overlay\r\n+            $mo('#mo-osp-puzzle-overlay').addClass('mo-osp-hidden');\r\n+            \r\n+            \u002F\u002F CRITICAL: Also hide the outer wrapper div (especially important for WooCommerce checkout popup)\r\n+            $mo('#mo-osp-puzzle-popup-outer-div').hide();\r\n+            \r\n+            $mo('body').removeClass('mo-osp-puzzle-open');\r\n+            this.pendingOtpData = null;\r\n+            this.hideError();\r\n+            \u002F\u002F Re-enable verify button\r\n+            $mo('#mo-osp-puzzle-verify').prop('disabled', false);\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Verify puzzle answer using secure server-side validation\r\n+         *\u002F\r\n+        verifyPuzzle: function() {\r\n+            var self = this;\r\n+            \r\n+            \u002F\u002F CRITICAL FIX: Prevent duplicate calls\r\n+            if (this.isVerifying) {\r\n+                return;\r\n+            }\r\n+            \r\n+            var userAnswer = parseInt($mo('#mo-osp-puzzle-answer').val());\r\n+\r\n+            \r\n+            if (isNaN(userAnswer)) {\r\n+                this.showError('Please enter a valid number.');\r\n+                return;\r\n+            }\r\n+            \r\n+            \u002F\u002F Set verifying flag to prevent duplicate calls\r\n+            this.isVerifying = true;\r\n+            \r\n+            \u002F\u002F Disable verify button to prevent multiple clicks\r\n+            $mo('#mo-osp-puzzle-verify').prop('disabled', true);\r\n+            \r\n+            \u002F\u002F Store user's answer for later form submission\r\n+            this.lastUserAnswer = userAnswer;\r\n+            \r\n+            \r\n+            \u002F\u002F SECURITY ENHANCEMENT: Verify puzzle through secure session-based AJAX endpoint\r\n+            \r\n+            $mo.ajax({    \r\n+                url: mo_osp_ajax.ajax_url,\r\n+                type: 'POST',\r\n+                data: {\r\n+                    action: 'mo_osp_verify_puzzle',\r\n+                    nonce: mo_osp_ajax.nonce,\r\n+                    puzzle_answer: userAnswer,\r\n+                    \u002F\u002F SECURITY: No longer sending question - server validates against session\r\n+                    email: this.getEmailFromForm(),\r\n+                    phone: this.getPhoneFromForm(),\r\n+                    browser_id: window.mo_osp_browser_id || window.MO_OTP_SpamPreventer.browserID || ''\r\n+                },\r\n+                success: function(response) {\r\n+                    \u002F\u002F Reset verifying flag\r\n+                    self.isVerifying = false;\r\n+                    $mo('#mo-osp-puzzle-verify').prop('disabled', false);\r\n+                    \r\n+                    if (response.success) {\r\n+                        \r\n+                        \u002F\u002F Set global flag to indicate puzzle was just completed\r\n+                        window.mo_osp_puzzle_just_completed = true;\r\n+                        \r\n+                        \u002F\u002F Store verification data for secure form submission\r\n+                        self.lastVerificationResponse = response.data;\r\n+                        \r\n+                        self.closePuzzle();\r\n+                        \r\n+                        \u002F\u002F Notify spam preventer of successful puzzle completion\r\n+                        if (typeof window.MO_OSP_SpamPreventer_onPuzzleSuccess !== 'undefined') {\r\n+                            window.MO_OSP_SpamPreventer_onPuzzleSuccess();\r\n+                        }\r\n+                        \r\n+                        self.proceedWithOTP();\r\n+                    } else {\r\n+                        \u002F\u002F SECURITY: Check if puzzle was reset (new puzzle generated)\r\n+                        if (response.data && response.data.puzzle_reset) {\r\n+                            \r\n+                            \u002F\u002F Check if image element exists\r\n+                            var $puzzleImage = $mo('#mo-osp-puzzle-image');\r\n+                            var $puzzleText = $mo('#mo-osp-puzzle-text');\r\n+                            \r\n+                            \r\n+                            \u002F\u002F CRITICAL: Clear answer field FIRST before updating puzzle\r\n+                            $mo('#mo-osp-puzzle-answer').val('').attr('placeholder', '?');\r\n+                            \r\n+                            \u002F\u002F Handle puzzle image if provided\r\n+                            if (response.data.puzzle_image) {\r\n+                                \r\n+                                \u002F\u002F Display new puzzle image (question is server-side only for security)\r\n+                                \u002F\u002F Force image reload by adding timestamp to prevent caching\r\n+                                var imageUrl = response.data.puzzle_image;\r\n+                                var timestamp = new Date().getTime();\r\n+                                \r\n+                                \u002F\u002F Always add timestamp to force reload, even if URL already has query params\r\n+                                if (imageUrl.indexOf('?') === -1) {\r\n+                                    imageUrl += '?t=' + timestamp;\r\n+                                } else {\r\n+                                    imageUrl += '&t=' + timestamp;\r\n+                                }\r\n+                                \r\n+                                \r\n+                                if ($puzzleImage.length > 0) {\r\n+                                    \u002F\u002F CRITICAL: Use .on('load') to ensure image is loaded before showing\r\n+                                    $puzzleImage.off('load error').on('load', function() {\r\n+                                        $mo(this).show();\r\n+                                        $puzzleText.hide();\r\n+                                    }).on('error', function() {\r\n+                                        console.error('[Puzzle] ERROR: Failed to load puzzle image');\r\n+                                        \u002F\u002F Fallback: show text if image fails\r\n+                                        if ($puzzleText.length > 0) {\r\n+                                            $puzzleText.text('New puzzle generated. Please refresh if image does not appear.').show();\r\n+                                        }\r\n+                                        $mo(this).hide();\r\n+                                    });\r\n+                                    \r\n+                                    \u002F\u002F Set the src AFTER binding load handler\r\n+                                    var oldSrc = $puzzleImage.attr('src');\r\n+                                    \r\n+                                    \u002F\u002F Force image reload by setting src\r\n+                                    if (oldSrc === imageUrl) {\r\n+                                        \u002F\u002F If URL is same (shouldn't happen with timestamp), force reload by clearing first\r\n+                                        $puzzleImage.attr('src', '');\r\n+                                        setTimeout(function() {\r\n+                                            $puzzleImage.attr('src', imageUrl);\r\n+                                        }, 50);\r\n+                                    } else {\r\n+                                        $puzzleImage.attr('src', imageUrl);\r\n+                                    }\r\n+                                    \r\n+                                    \u002F\u002F Ensure image is visible (in case it was hidden)\r\n+                                    $puzzleImage.show();\r\n+                                    $puzzleText.hide();\r\n+                                } else {\r\n+                                    console.error('[Puzzle] ERROR: Puzzle image element not found!');\r\n+                                }\r\n+                            } else if (response.data.puzzle_question) {\r\n+                                \u002F\u002F Fallback: show puzzle question as text if image not available\r\n+                                if ($puzzleText.length > 0) {\r\n+                                    $puzzleText.text(response.data.puzzle_question).show();\r\n+                                    $puzzleImage.hide();\r\n+                                }\r\n+                            } else {\r\n+                                console.error('[Puzzle] ERROR: No puzzle image or question provided in response!');\r\n+                                \u002F\u002F Show generic message\r\n+                                if ($puzzleText.length > 0) {\r\n+                                    $puzzleText.text('A new puzzle has been generated. Please try again.').show();\r\n+                                }\r\n+                                $puzzleImage.hide();\r\n+                            }\r\n+                            \r\n+                            \u002F\u002F Clear puzzle state\r\n+                            self.currentPuzzle = null;\r\n+                            self.currentAnswer = null;\r\n+                            \r\n+                            \u002F\u002F Hide error temporarily while new puzzle loads\r\n+                            self.hideError();\r\n+                            \r\n+                            \u002F\u002F Show error message after a brief delay to ensure puzzle image is visible\r\n+                            setTimeout(function() {\r\n+                                self.showError(response.data.message || 'Incorrect answer. A new puzzle has been generated. Please solve it.');\r\n+                                $mo('#mo-osp-puzzle-answer').focus();\r\n+                            }, 200);\r\n+                        } else {\r\n+                            \u002F\u002F No puzzle reset - same puzzle, just show error and allow retry\r\n+                            \r\n+                            \u002F\u002F Just clear the answer field and show error - puzzle stays the same\r\n+                            self.showError(response.data.message || 'Incorrect answer. Please try again.');\r\n+                            $mo('#mo-osp-puzzle-answer').val('').focus();\r\n+                        }\r\n+                        \r\n+                        \u002F\u002F Ensure verify button stays visible after incorrect answer\r\n+                        $mo('#mo-osp-puzzle-verify').show().prop('disabled', false);\r\n+                    }\r\n+                },\r\n+                error: function(xhr, status, error) {\r\n+                    console.error('Puzzle verification AJAX error: ' + status + ' - ' + error);\r\n+                    \u002F\u002F Reset verifying flag\r\n+                    self.isVerifying = false;\r\n+                    self.showError('Verification failed. Please try again.');\r\n+                    \u002F\u002F Ensure verify button stays visible after error\r\n+                    $mo('#mo-osp-puzzle-verify').show().prop('disabled', false);\r\n+                }\r\n+            });\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Proceed with OTP sending after successful puzzle verification\r\n+         *\u002F\r\n+        proceedWithOTP: function() {\r\n+            var self = this; \u002F\u002F Store reference to this for use in nested functions\r\n+            \r\n+            \u002F\u002F Get verification data from the last successful verification\r\n+            var verificationData = this.lastVerificationResponse || {};\r\n+            \r\n+            \u002F\u002F Add secure puzzle verification data to all forms\r\n+            $mo('form').each(function() {\r\n+                var $moform = $mo(this);\r\n+                \r\n+                \u002F\u002F Remove any old puzzle processed flags\r\n+                $moform.find('input[name=\"mo_osp_puzzle_processed\"]').remove();\r\n+                \r\n+                \u002F\u002F Add puzzle verification flag\r\n+                if (!$moform.find('input[name=\"puzzle_verified\"]').length) {\r\n+                    $moform.append('\u003Cinput type=\"hidden\" name=\"puzzle_verified\" value=\"true\">');\r\n+                }\r\n+                \r\n+                if (verificationData.puzzle_nonce && !$moform.find('input[name=\"mo_osp_puzzle_nonce\"]').length) {\r\n+                    $moform.append('\u003Cinput type=\"hidden\" name=\"mo_osp_puzzle_nonce\" value=\"' + verificationData.puzzle_nonce + '\">');\r\n+                }\r\n+                \r\n+                if (verificationData.verification_token && !$moform.find('input[name=\"verification_token\"]').length) {\r\n+                    $moform.append('\u003Cinput type=\"hidden\" name=\"verification_token\" value=\"' + verificationData.verification_token + '\">');\r\n+                }\r\n+                \r\n+                \u002F\u002F Add the user's puzzle answer for server verification\r\n+                if (self.lastUserAnswer && !$moform.find('input[name=\"puzzle_answer\"]').length) {\r\n+                    $moform.append('\u003Cinput type=\"hidden\" name=\"puzzle_answer\" value=\"' + self.lastUserAnswer + '\">');\r\n+                }\r\n+                \r\n+                \u002F\u002F SECURITY: No longer sending puzzle question - server has it in session\r\n+            });\r\n+            \r\n+            \u002F\u002F Set global verification flag for AJAX interception\r\n+            window.mo_osp_puzzle_verified = true;\r\n+            \r\n+            \r\n+            \u002F\u002F PRIORITY 1: Check for external popup callback (highest priority)\r\n+            if (typeof window.MO_OSP_Puzzle_onExternalPopupSuccess === 'function') {\r\n+                try {\r\n+                    \u002F\u002F Close puzzle before calling callback\r\n+                    self.closePuzzle();\r\n+                    window.MO_OSP_Puzzle_onExternalPopupSuccess(verificationData);\r\n+                } catch (e) {\r\n+                    console.error('Error in external popup success callback:', e);\r\n+                    \u002F\u002F Ensure puzzle is closed even on error\r\n+                    self.closePuzzle();\r\n+                }\r\n+                return;\r\n+            }\r\n+            \r\n+            \u002F\u002F PRIORITY 2: Check if we're in AJAX form context (callback registered by spam-preventer.js)\r\n+            if (typeof window.MO_OSP_Puzzle_onAjaxSuccess === 'function') {\r\n+                try {\r\n+                    \u002F\u002F Close puzzle before calling callback\r\n+                    self.closePuzzle();\r\n+                    window.MO_OSP_Puzzle_onAjaxSuccess(verificationData);\r\n+                } catch (e) {\r\n+                    console.error('Error in AJAX success callback:', e);\r\n+                    \u002F\u002F Ensure puzzle is closed even on error\r\n+                    self.closePuzzle();\r\n+                }\r\n+                return;\r\n+            }\r\n+            \r\n+            \u002F\u002F Check if we're in popup context (callback registered by popup-timer.js or inline script)\r\n+            if (typeof window.MO_OSP_Puzzle_onPopupSuccess === 'function') {\r\n+                try {\r\n+                    \u002F\u002F Close puzzle before calling callback\r\n+                    self.closePuzzle();\r\n+                    window.MO_OSP_Puzzle_onPopupSuccess();\r\n+                } catch (e) {\r\n+                    console.error('Error in popup success callback:', e);\r\n+                    \u002F\u002F Ensure puzzle is closed even on error\r\n+                    self.closePuzzle();\r\n+                }\r\n+                return;\r\n+            }\r\n+            \r\n+            \r\n+            \u002F\u002F Check if we're in a popup (DefaultPopup) - look for resend form\r\n+            var resendForm = document.getElementById('verification_resend_otp_form');\r\n+            if (resendForm) {\r\n+                \u002F\u002F CRITICAL: Close puzzle before submitting form\r\n+                self.closePuzzle();\r\n+                \r\n+                \u002F\u002F Add puzzle verification data to the resend form\r\n+                var puzzleVerifiedInput = document.createElement('input');\r\n+                puzzleVerifiedInput.type = 'hidden';\r\n+                puzzleVerifiedInput.name = 'puzzle_verified';\r\n+                puzzleVerifiedInput.value = 'true';\r\n+                resendForm.appendChild(puzzleVerifiedInput);\r\n+                \r\n+                if (verificationData.puzzle_nonce) {\r\n+                    var nonceInput = document.createElement('input');\r\n+                    nonceInput.type = 'hidden';\r\n+                    nonceInput.name = 'mo_osp_puzzle_nonce';\r\n+                    nonceInput.value = verificationData.puzzle_nonce;\r\n+                    resendForm.appendChild(nonceInput);\r\n+                }\r\n+                \r\n+                if (verificationData.verification_token) {\r\n+                    var tokenInput = document.createElement('input');\r\n+                    tokenInput.type = 'hidden';\r\n+                    tokenInput.name = 'verification_token';\r\n+                    tokenInput.value = verificationData.verification_token;\r\n+                    resendForm.appendChild(tokenInput);\r\n+                }\r\n+                \r\n+                \u002F\u002F Submit the resend form to trigger OTP sending\r\n+                resendForm.submit();\r\n+                return;\r\n+            }\r\n+            \r\n+            \r\n+            \u002F\u002F CRITICAL FIX: For popup context, show OTP form instead of reloading\r\n+            \u002F\u002F Check if we're in a popup (DefaultPopup was shown)\r\n+            var $moPopup = $mo('#mo_site_otp_form, .mo_customer_validation-modal');\r\n+            if ($moPopup.length > 0) {\r\n+                \r\n+                \u002F\u002F CRITICAL: Close puzzle before showing OTP popup again\r\n+                self.closePuzzle();\r\n+                \r\n+                \u002F\u002F Show the popup again (it was hidden when puzzle was shown)\r\n+                $mo('#mo_site_otp_form').show();\r\n+                $mo('.mo_customer_validation-modal').show();\r\n+                $mo('.mo-modal-backdrop').show();\r\n+                \r\n+                \u002F\u002F Update the message in popup to show OTP form message\r\n+                var $moPopupBody = $mo('.mo_customer_validation-modal-body');\r\n+                if ($moPopupBody.length > 0) {\r\n+                    \u002F\u002F Check if OTP form exists in popup\r\n+                    var $moOtpForm = $mo('#mo_validate_form');\r\n+                    if ($moOtpForm.length > 0) {\r\n+                        \u002F\u002F OTP form exists, trigger OTP sending via resend link\r\n+                        var $moResendLink = $mo('a.mo-resend, a[onclick*=\"mo_otp_verification_resend\"]');\r\n+                        if ($moResendLink.length > 0) {\r\n+                            \u002F\u002F Add puzzle verification data to the OTP form first\r\n+                            var $moOtpFormInputs = $moOtpForm;\r\n+                            if (!$moOtpFormInputs.find('input[name=\"puzzle_verified\"]').length) {\r\n+                                $moOtpFormInputs.append('\u003Cinput type=\"hidden\" name=\"puzzle_verified\" value=\"true\">');\r\n+                            }\r\n+                            if (verificationData.puzzle_nonce && !$moOtpFormInputs.find('input[name=\"mo_osp_puzzle_nonce\"]').length) {\r\n+                                $moOtpFormInputs.append('\u003Cinput type=\"hidden\" name=\"mo_osp_puzzle_nonce\" value=\"' + verificationData.puzzle_nonce + '\">');\r\n+                            }\r\n+                            if (verificationData.verification_token && !$moOtpFormInputs.find('input[name=\"verification_token\"]').length) {\r\n+                                $moOtpFormInputs.append('\u003Cinput type=\"hidden\" name=\"verification_token\" value=\"' + verificationData.verification_token + '\">');\r\n+                            }\r\n+                            \u002F\u002F Trigger resend to send OTP\r\n+                            $moResendLink.trigger('click');\r\n+                            return;\r\n+                        }\r\n+                    }\r\n+                }\r\n+            }\r\n+            \r\n+            \u002F\u002F Fallback: For regular forms, we need to resubmit the original form\r\n+            \u002F\u002F Since puzzle is verified, the next OTP request should succeed\r\n+            \r\n+            \u002F\u002F Try to find the original login\u002Fsubmission form\r\n+            var $moOriginalForm = $mo('form[name=\"loginform\"], form#loginform, form.wp-login-form');\r\n+            if ($moOriginalForm.length === 0) {\r\n+                \u002F\u002F Try other common form selectors\r\n+                $moOriginalForm = $mo('form').not('#mo_validate_form').not('#validation_goBack_form').not('#verification_resend_otp_form').first();\r\n+            }\r\n+            \r\n+            if ($moOriginalForm.length > 0) {\r\n+                \u002F\u002F Add puzzle verification data to the form\r\n+                if (!$moOriginalForm.find('input[name=\"puzzle_verified\"]').length) {\r\n+                    $moOriginalForm.append('\u003Cinput type=\"hidden\" name=\"puzzle_verified\" value=\"true\">');\r\n+                }\r\n+                if (verificationData.puzzle_nonce && !$moOriginalForm.find('input[name=\"mo_osp_puzzle_nonce\"]').length) {\r\n+                    $moOriginalForm.append('\u003Cinput type=\"hidden\" name=\"mo_osp_puzzle_nonce\" value=\"' + verificationData.puzzle_nonce + '\">');\r\n+                }\r\n+                if (verificationData.verification_token && !$moOriginalForm.find('input[name=\"verification_token\"]').length) {\r\n+                    $moOriginalForm.append('\u003Cinput type=\"hidden\" name=\"verification_token\" value=\"' + verificationData.verification_token + '\">');\r\n+                }\r\n+                \u002F\u002F Submit the form to trigger OTP sending\r\n+                $moOriginalForm.submit();\r\n+                return;\r\n+            }\r\n+            \r\n+            \u002F\u002F Last resort: Reload the page\r\n+            \u002F\u002F Store puzzle completion flag in sessionStorage\r\n+            sessionStorage.setItem('mo_osp_puzzle_completed', 'true');\r\n+            \u002F\u002F Reload to show OTP form\r\n+            window.location.reload();\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Click the Send OTP button after puzzle verification\r\n+         *\u002F\r\n+        clickSendOTPButton: function() {\r\n+            \r\n+            \u002F\u002F Find and click the send OTP button\r\n+            var sendButton = this.findSendOTPButton();\r\n+            \r\n+            if (sendButton && sendButton.length > 0) {\r\n+                \u002F\u002F Trigger OTP send directly\r\n+                this.triggerOTPSendDirectly();\r\n+            } else {\r\n+                \u002F\u002F Reset the flag after a short delay to allow user to click manually\r\n+                setTimeout(function() {\r\n+                    window.mo_otp_button_clicked = false;\r\n+                }, 2000);\r\n+            }\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Trigger OTP send directly\r\n+         *\u002F\r\n+        triggerOTPSendDirectly: function() {\r\n+            \r\n+            \u002F\u002F Find the send OTP button\r\n+            var sendButton = this.findSendOTPButton();\r\n+            \r\n+            if (sendButton && sendButton.length > 0) {\r\n+                \r\n+                \u002F\u002F Trigger the button click\r\n+                sendButton.trigger('click');\r\n+                \r\n+            } else {\r\n+                \u002F\u002F Show error message\r\n+                var messageBox = $mo('#mo_message, .mo_message, [id*=\"mo_message\"]').first();\r\n+                if (messageBox.length > 0) {\r\n+                    messageBox.empty().append('Error: Could not find Send OTP button.').css({\r\n+                        'color': '#ff5b5b',\r\n+                        'background': '#ffefef',\r\n+                        'padding': '10px',\r\n+                        'border-radius': '5px'\r\n+                    });\r\n+                } else {\r\n+                    \u002F\u002F Fallback: Create temporary message\r\n+                    $mo('body').append('\u003Cdiv id=\"mo_osp_temp_message\" style=\"position: fixed; top: 20px; right: 20px; background: #ffefef; color: #ff5b5b; padding: 10px; border-radius: 5px; z-index: 9999;\">Error: Could not find Send OTP button.\u003C\u002Fdiv>');\r\n+                    setTimeout(function() {\r\n+                        $mo('#mo_osp_temp_message').remove();\r\n+                    }, 5000);\r\n+                }\r\n+            }\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Find the Send OTP button on the page\r\n+         *\u002F\r\n+        findSendOTPButton: function() {\r\n+            \u002F\u002F Try multiple selectors to find the send OTP button\r\n+            var buttonSelectors = [\r\n+                'button#miniorange_wc_popup_send_otp_token',\r\n+                '#mo_wc_send_otp',\r\n+                'input[id*=\"send_otp\"]',\r\n+                'button[id*=\"send_otp\"]',\r\n+                'input[value*=\"Send OTP\"]',\r\n+                'button[value*=\"Send OTP\"]',\r\n+                'input[id*=\"mo_wc_send_otp\"]',\r\n+                'button[id*=\"mo_wc_send_otp\"]',\r\n+                '.mo-send-otp-button',\r\n+                '[class*=\"send-otp\"]',\r\n+                'input[name*=\"send_otp\"]',\r\n+                'button[name*=\"send_otp\"]'\r\n+            ];\r\n+            \r\n+            for (var i = 0; i \u003C buttonSelectors.length; i++) {\r\n+                var button = $mo(buttonSelectors[i]);\r\n+                if (button.length > 0) {\r\n+                    return button.first();\r\n+                }\r\n+            }\r\n+            return null;\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Intercept AJAX calls to add puzzle verification data\r\n+         *\u002F\r\n+        interceptAjaxCalls: function() {\r\n+            \r\n+            var self = this;\r\n+            var originalAjax = $mo.ajax;\r\n+            \r\n+            $mo.ajax = function(options) {\r\n+                \u002F\u002F Get verification data\r\n+                var verificationData = self.lastVerificationResponse || {};\r\n+                \r\n+                \u002F\u002F Check for AJAX actions that might be OTP-related\r\n+                var otpActions = [\r\n+                    'miniorange_ajax_otp',\r\n+                    'mo_ajax_form_validate',\r\n+                    'mo_send_otp',\r\n+                    'mo_resend_otp',\r\n+                    'woocommerce_checkout'\r\n+                ];\r\n+                \r\n+                var isOtpRelated = false;\r\n+                if (options.data) {\r\n+                    var dataStr = typeof options.data === 'string' ? options.data : JSON.stringify(options.data);\r\n+                    for (var i = 0; i \u003C otpActions.length; i++) {\r\n+                        if (dataStr.includes(otpActions[i]) || \r\n+                            dataStr.includes('send_otp') || dataStr.includes('verify_otp')) {\r\n+                            isOtpRelated = true;\r\n+                            break;\r\n+                        }\r\n+                    }\r\n+                }\r\n+                \r\n+                if (isOtpRelated && window.mo_osp_puzzle_verified) {\r\n+                    \r\n+                    \u002F\u002F Add puzzle verification data to AJAX request\r\n+                    if (typeof options.data === 'string') {\r\n+                        if (!options.data.includes('puzzle_verified')) {\r\n+                            options.data += '&puzzle_verified=true';\r\n+                        }\r\n+                        if (verificationData.puzzle_nonce && !options.data.includes('mo_osp_puzzle_nonce')) {\r\n+                            options.data += '&mo_osp_puzzle_nonce=' + encodeURIComponent(verificationData.puzzle_nonce);\r\n+                        }\r\n+                        if (verificationData.verification_token && !options.data.includes('verification_token')) {\r\n+                            options.data += '&verification_token=' + encodeURIComponent(verificationData.verification_token);\r\n+                        }\r\n+                        if (window.MO_OSP_Puzzle.lastUserAnswer && !options.data.includes('puzzle_answer')) {\r\n+                            options.data += '&puzzle_answer=' + encodeURIComponent(window.MO_OSP_Puzzle.lastUserAnswer);\r\n+                        }\r\n+                        if (!options.data.includes('mo_osp_browser_id')) {\r\n+                            options.data += '&mo_osp_browser_id=' + encodeURIComponent(window.mo_osp_browser_id || '');\r\n+                        }\r\n+                    } else {\r\n+                        \u002F\u002F If data is null\u002Fundefined, create new data object with secure verification\r\n+                        options.data = options.data || {};\r\n+                        options.data.mo_osp_browser_id = window.mo_osp_browser_id || '';\r\n+                        \r\n+                        if (window.mo_osp_puzzle_verified) {\r\n+                            options.data.puzzle_verified = 'true';\r\n+                            if (verificationData.puzzle_nonce) {\r\n+                                options.data.mo_osp_puzzle_nonce = verificationData.puzzle_nonce;\r\n+                            }\r\n+                            if (verificationData.verification_token) {\r\n+                                options.data.verification_token = verificationData.verification_token;\r\n+                            }\r\n+                            if (window.MO_OSP_Puzzle.lastUserAnswer) {\r\n+                                options.data.puzzle_answer = window.MO_OSP_Puzzle.lastUserAnswer;\r\n+                            }\r\n+                            \u002F\u002F SECURITY: No longer sending puzzle question - server has it in session\r\n+                        }\r\n+                    }\r\n+                    \r\n+                    \r\n+                    \u002F\u002F Wrap the success callback for universal handling\r\n+                    var originalSuccess = options.success;\r\n+                    options.success = function(response) {\r\n+                        \r\n+                        \u002F\u002F Check if response contains puzzle requirement (multiple detection methods)\r\n+                        var isPuzzleRequired = false;\r\n+                        var detectionMethod = '';\r\n+                        \r\n+                        \u002F\u002F Method 1: Check structured response format\r\n+                        if (response && (response.puzzle_required === true || response.authType === 'PUZZLE_REQUIRED' || response.result === 'puzzle_required')) {\r\n+                            isPuzzleRequired = true;\r\n+                            detectionMethod = 'structured_response';\r\n+                        }\r\n+                        \u002F\u002F Method 2: Check message content (fallback)\r\n+                        else if (response && response.message && \r\n+                            response.message.includes('Please complete the security verification to continue')) {\r\n+                            isPuzzleRequired = true;\r\n+                            detectionMethod = 'message_content';\r\n+                        }\r\n+                        \r\n+                        if (isPuzzleRequired) {\r\n+                            \r\n+                            \u002F\u002F Call original success handler first\r\n+                            if (originalSuccess && typeof originalSuccess === 'function') {\r\n+                                originalSuccess.call(this, response);\r\n+                            }\r\n+                            \r\n+                            \u002F\u002F Wait a moment for DOM to update, then show puzzle\r\n+                            setTimeout(function() {\r\n+                                if (typeof MO_OSP_Puzzle !== 'undefined' && !MO_OSP_Puzzle.isShowing) {\r\n+                                    MO_OSP_Puzzle.showPuzzle({});\r\n+                                }\r\n+                            }, 500);\r\n+                            \r\n+                            \u002F\u002F Also trigger manual puzzle check as fallback\r\n+                            setTimeout(function() {\r\n+                                if (typeof MO_OSP_Puzzle !== 'undefined') {\r\n+                                    MO_OSP_Puzzle.triggerPuzzleCheck();\r\n+                                }\r\n+                            }, 1000);\r\n+                            return;\r\n+                        }\r\n+                        \r\n+                        \u002F\u002F Call original success handler first - let it handle all UI updates\r\n+                        if (originalSuccess && typeof originalSuccess === 'function') {\r\n+                            originalSuccess.call(this, response);\r\n+                        }\r\n+                    };\r\n+                    \r\n+                    \u002F\u002F Also wrap error callback to catch puzzle requirements in error responses\r\n+                    var originalError = options.error;\r\n+                    options.error = function(xhr, status, error) {\r\n+                        \r\n+                        \u002F\u002F Try to parse error response for puzzle requirements\r\n+                        try {\r\n+                            var errorResponse = JSON.parse(xhr.responseText);\r\n+                            if (errorResponse && (errorResponse.puzzle_required === true || errorResponse.authType === 'PUZZLE_REQUIRED' || errorResponse.result === 'puzzle_required')) {\r\n+                                \r\n+                                \r\n+                                \u002F\u002F Show puzzle for error response\r\n+                                setTimeout(function() {\r\n+                                    if (typeof MO_OSP_Puzzle !== 'undefined' && !MO_OSP_Puzzle.isShowing) {\r\n+                                        MO_OSP_Puzzle.showPuzzle({});\r\n+                                    }\r\n+                                }, 500);\r\n+                                \r\n+                                \u002F\u002F Don't call original error handler for puzzle requirements\r\n+                                return;\r\n+                            }\r\n+                        } catch (e) {\r\n+                            \u002F\u002F Not JSON or parsing failed, continue with normal error handling\r\n+                        }\r\n+                        \r\n+                        \u002F\u002F Call original error handler for non-puzzle errors\r\n+                        if (originalError && typeof originalError === 'function') {\r\n+                            originalError.call(this, xhr, status, error);\r\n+                        }\r\n+                    };\r\n+                }\r\n+                \r\n+                \u002F\u002F Call the original ajax function\r\n+                return originalAjax.call(this, options);\r\n+            };\r\n+            \r\n+            \u002F\u002F Restore original AJAX after 10 seconds (longer for universal coverage)\r\n+            setTimeout(function() {\r\n+                $mo.ajax = originalAjax;\r\n+                \r\n+            }, 10000);\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Manual trigger for puzzle when we know it should appear (after OTP request)\r\n+         *\u002F\r\n+        triggerPuzzleCheck: function() {\r\n+            \r\n+            var self = this;\r\n+            \r\n+            \u002F\u002F Reset counter for new check sequence\r\n+            this.puzzleCheckCount = 0;\r\n+            \r\n+            \u002F\u002F Check immediately\r\n+            this.checkForPuzzleMessage();\r\n+            \r\n+            \u002F\u002F Only check a few more times with reasonable delays (not aggressive)\r\n+            setTimeout(function() { self.checkForPuzzleMessage(); }, 500);\r\n+            setTimeout(function() { self.checkForPuzzleMessage(); }, 1500);\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Get email from form fields\r\n+         *\u002F\r\n+        getEmailFromForm: function() {\r\n+            var email = '';\r\n+            \r\n+            \u002F\u002F Try to get from visible email inputs first\r\n+            $mo('input[type=\"email\"]:visible, input[name*=\"email\"]:visible, input[id*=\"email\"]:visible').each(function() {\r\n+                var $field = $mo(this);\r\n+                var type = ($field.attr('type') || '').toLowerCase();\r\n+                if (type === 'button' || type === 'submit' || type === 'reset') {\r\n+                    return;\r\n+                }\r\n+                var value = $field.val();\r\n+                if (value && !\u002Fsend\\s+otp|verify\\s+otp\u002Fi.test(value)) {\r\n+                    email = value;\r\n+                    return false; \u002F\u002F Break loop\r\n+                }\r\n+            });\r\n+            \r\n+            \u002F\u002F Fallback to hidden inputs (from popup forms with extra_post_data)\r\n+            if (!email) {\r\n+                $mo('input[type=\"hidden\"][name*=\"email\"], input[type=\"hidden\"][id*=\"email\"]').each(function() {\r\n+                    if ($mo(this).val()) {\r\n+                        email = $mo(this).val();\r\n+                        return false; \u002F\u002F Break loop\r\n+                    }\r\n+                });\r\n+            }\r\n+            \r\n+            \r\n+            return email;\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Get phone from form fields\r\n+         *\u002F\r\n+        getPhoneFromForm: function() {\r\n+            var phone = '';\r\n+            \r\n+            \u002F\u002F Try to get from visible phone inputs first\r\n+            $mo('input[type=\"tel\"]:visible, input[name*=\"phone\"]:visible, input[id*=\"phone\"]:visible, input[name*=\"mobile\"]:visible').each(function() {\r\n+                var $field = $mo(this);\r\n+                var type = ($field.attr('type') || '').toLowerCase();\r\n+                if (type === 'button' || type === 'submit' || type === 'reset') {\r\n+                    return;\r\n+                }\r\n+                var value = $field.val();\r\n+                if (!value || \u002Fsend\\s+otp|verify\\s+otp\u002Fi.test(value)) {\r\n+                    return;\r\n+                }\r\n+                \u002F\u002F Normalize to digits\u002F+ and require a minimum length to avoid tokens like \"6ff2c895dc\".\r\n+                var normalized = String(value).replace(\u002F[^0-9+]\u002Fg, '');\r\n+                var digitCount = normalized.replace(\u002F\\D\u002Fg, '').length;\r\n+                if (digitCount >= 6) {\r\n+                    phone = normalized;\r\n+                    return false; \u002F\u002F Break loop\r\n+                }\r\n+            });\r\n+            \r\n+            \u002F\u002F Fallback to hidden inputs (from popup forms with extra_post_data)\r\n+            if (!phone) {\r\n+            $mo('input[type=\"hidden\"][name*=\"phone\"], input[type=\"hidden\"][id*=\"phone\"], input[type=\"hidden\"][name*=\"mobile\"]').each(function() {\r\n+                    var value = $mo(this).val();\r\n+                    if (!value || \u002Fsend\\s+otp|verify\\s+otp\u002Fi.test(value)) {\r\n+                        return;\r\n+                    }\r\n+                    var normalized = String(value).replace(\u002F[^0-9+]\u002Fg, '');\r\n+                    var digitCount = normalized.replace(\u002F\\D\u002Fg, '').length;\r\n+                    if (digitCount >= 6) {\r\n+                        phone = normalized;\r\n+                        return false; \u002F\u002F Break loop\r\n+                    }\r\n+                });\r\n+            }\r\n+            \r\n+            \r\n+            return phone;\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Show error message in puzzle popup\r\n+         *\u002F\r\n+        showError: function(message) {\r\n+            $mo('#mo-osp-puzzle-error-text').text(message);\r\n+            $mo('#mo-osp-puzzle-error').show();\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Hide error message in puzzle popup\r\n+         *\u002F\r\n+        hideError: function() {\r\n+            $mo('#mo-osp-puzzle-error').hide();\r\n+        }\r\n+    };\r\n+\r\n+    \u002F\u002F Initialize puzzle system when document is ready\r\n+    $mo(document).ready(function() {\r\n+        \u002F\u002F Initialize puzzle system only when needed (not automatically on every page load)\r\n+        \u002F\u002F Puzzle will be initialized when OTP request triggers puzzle requirement\r\n+        if (typeof MO_OSP_Puzzle !== 'undefined') {\r\n+            \u002F\u002F Initialize fully - bind events and mark as initialized\r\n+            MO_OSP_Puzzle.init();\r\n+            MO_OSP_Puzzle.initialized = true;\r\n+        }\r\n+    });\r\n+\r\n+})(jQuery);\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fspam-preventer-admin.js \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fspam-preventer-admin.js\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fspam-preventer-admin.js\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fspam-preventer-admin.js\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,638 +1,638 @@\n-\u002F**\n- * OTP Spam Preventer Admin JavaScript\n- *\n- * @package miniorange-otp-verification\u002Faddons\n- *\u002F\n-\n-(function($mo) {\n-    'use strict';\n-\n-    var MO_OSP_Admin = {\n-        currentPage: 0,\n-        pageSize: 50,\n-        totalUsers: 0,\n-        \u002F** Auto-hide admin notices after this many ms (success \u002F error). *\u002F\n-        noticeAutoDismissMs: 10000,\n-        noticeDismissTimer: null,\n-\n-        init: function() {\n-            this.bindEvents();\n-            this.initializeAdvancedSettings();\n-            this.initializeBlockedUsers();\n-        },\n-\n-        bindEvents: function() {\n-            var self = this;\n-            \n-            \u002F\u002F Settings form validation\n-            $mo(document).on('submit', '#mo_osp_settings_form', function(e) {\n-                var isValid = self.validateSettings();\n-                if (!isValid) {\n-                    e.preventDefault();\n-                }\n-            });\n-\n-            \u002F\u002F Advanced settings toggle\n-            $mo(document).on('click', '#mo-osp-toggle-advanced', function(e) {\n-                e.preventDefault();\n-                self.toggleAdvancedSettings();\n-            });\n-\n-            \u002F\u002F Real-time validation\n-            $mo(document).on('input', '.mo-form-input', function() {\n-                self.validateField($mo(this));\n-                \n-                \u002F\u002F Also validate cross-field relationships for relevant fields\n-                var fieldId = $mo(this).attr('id');\n-                if (fieldId === 'mo_osp_max_attempts' || fieldId === 'mo_osp_hourly_limit' || fieldId === 'mo_osp_daily_limit') {\n-                    \u002F\u002F Clear previous validation summary to avoid confusion\n-                    $mo('.mo-osp-validation-summary').remove();\n-                    \u002F\u002F Validate cross-field relationships\n-                    self.validateCrossFieldRelationships();\n-                }\n-            });\n-\n-            \u002F\u002F Clear auto-dismiss timer when user dismisses the notice (core adds .notice-dismiss after wp-notice-added).\n-            $mo(document).on('click', '#mo-osp-admin-notice-container .notice-dismiss', function() {\n-                if (self.noticeDismissTimer) {\n-                    clearTimeout(self.noticeDismissTimer);\n-                    self.noticeDismissTimer = null;\n-                }\n-            });\n-\n-            \u002F\u002F Blocked users refresh\n-            $mo(document).on('click', '#mo-osp-refresh-blocked-users', function() {\n-                self.loadBlockedUsers();\n-            });\n-\n-            \u002F\u002F Clear all blocked users \u002F limits \u002F puzzle flags\n-            $mo(document).on('click', '#mo-osp-clear-all-blocked-users', function() {\n-                self.clearAllBlockedUsers();\n-            });\n-\n-            \u002F\u002F Addon enable\u002Fdisable toggle\n-            $mo(document).on('change', '#mo_osp_enabled', function() {\n-                self.toggleAddonStatus($mo(this));\n-            });\n-\n-            \u002F\u002F Unblock user\n-            $mo(document).on('click', '.mo-osp-unblock-user', function() {\n-                var identifierHash = $mo(this).data('hash');\n-                self.unblockUser(identifierHash);\n-            });\n-\n-            \u002F\u002F Pagination\n-            $mo(document).on('click', '#mo-osp-prev-page', function() {\n-                if (self.currentPage > 0) {\n-                    self.currentPage--;\n-                    self.loadBlockedUsers();\n-                }\n-            });\n-\n-            $mo(document).on('click', '#mo-osp-next-page', function() {\n-                var maxPage = Math.ceil(self.totalUsers \u002F self.pageSize) - 1;\n-                if (self.currentPage \u003C maxPage) {\n-                    self.currentPage++;\n-                    self.loadBlockedUsers();\n-                }\n-            });\n-        },\n-\n-        \u002F**\n-         * Initialize advanced settings state\n-         *\u002F\n-        initializeAdvancedSettings: function() {\n-            \u002F\u002F Always start hidden by default, ignore localStorage for initial state\n-            this.hideAdvancedSettings();\n-        },\n-\n-        \u002F**\n-         * Toggle advanced settings visibility\n-         *\u002F\n-        toggleAdvancedSettings: function() {\n-            var $moadvancedSection = $mo('#mo-osp-advanced-settings');\n-            var $motoggleButton = $mo('#mo-osp-toggle-advanced');\n-            var $motoggleText = $mo('#mo-osp-toggle-text');\n-            var $motoggleIcon = $mo('#mo-osp-toggle-icon');\n-\n-            if ($moadvancedSection.hasClass('mo-osp-advanced-hidden')) {\n-                this.showAdvancedSettings();\n-            } else {\n-                this.hideAdvancedSettings();\n-            }\n-        },\n-\n-        \u002F**\n-         * Show advanced settings\n-         *\u002F\n-        showAdvancedSettings: function() {\n-            var $moadvancedSection = $mo('#mo-osp-advanced-settings');\n-            var $motoggleText = $mo('#mo-osp-toggle-text');\n-            var $motoggleIcon = $mo('#mo-osp-toggle-icon');\n-\n-            $moadvancedSection.removeClass('mo-osp-advanced-hidden').addClass('mo-osp-advanced-visible');\n-            $motoggleText.text('Hide Advanced');\n-            $motoggleIcon.addClass('rotate-180');\n-            \n-            localStorage.setItem('mo_osp_advanced_expanded', 'true');\n-        },\n-\n-        \u002F**\n-         * Hide advanced settings\n-         *\u002F\n-        hideAdvancedSettings: function() {\n-            var $moadvancedSection = $mo('#mo-osp-advanced-settings');\n-            var $motoggleText = $mo('#mo-osp-toggle-text');\n-            var $motoggleIcon = $mo('#mo-osp-toggle-icon');\n-\n-            $moadvancedSection.removeClass('mo-osp-advanced-visible').addClass('mo-osp-advanced-hidden');\n-            $motoggleText.text('Show Advanced');\n-            $motoggleIcon.removeClass('rotate-180');\n-            \n-            localStorage.setItem('mo_osp_advanced_expanded', 'false');\n-        },\n-\n-        \u002F**\n-         * Validate individual field\n-         *\u002F\n-        validateField: function($mofield) {\n-            var fieldId = $mofield.attr('id');\n-            var value = $mofield.val();\n-            var isValid = true;\n-            var errorMessage = '';\n-\n-            \u002F\u002F Remove existing error styling\n-            $mofield.removeClass('mo-osp-error-field');\n-            $mofield.siblings('.mo-osp-validation-error').remove();\n-\n-            switch (fieldId) {\n-                case 'mo_osp_cooldown_time':\n-                    var cooldownTime = parseInt(value);\n-                    if (isNaN(cooldownTime) || cooldownTime \u003C 0 || cooldownTime > 86400) {\n-                        isValid = false;\n-                        errorMessage = 'Wait time must be between 0 and 86400 seconds (24 hours)';\n-                    }\n-                    break;\n-\n-                case 'mo_osp_max_attempts':\n-                    var maxAttempts = parseInt(value);\n-                    if (isNaN(maxAttempts) || maxAttempts \u003C 3 || maxAttempts > 10) {\n-                        isValid = false;\n-                        errorMessage = 'Maximum attempts must be between 3 and 10';\n-                    }\n-                    break;\n-\n-                case 'mo_osp_block_time':\n-                    var blockTime = parseInt(value);\n-                    if (isNaN(blockTime) || blockTime \u003C 60 || blockTime > 604800) {\n-                        isValid = false;\n-                        errorMessage = 'Block time must be between 60 seconds and 604800 seconds (7 days)';\n-                    }\n-                    break;\n-\n-                case 'mo_osp_daily_limit':\n-                    var dailyLimit = parseInt(value);\n-                    if (isNaN(dailyLimit) || dailyLimit \u003C 1 || dailyLimit > 1000) {\n-                        isValid = false;\n-                        errorMessage = 'Daily limit must be between 1 and 1000';\n-                    }\n-                    break;\n-\n-                case 'mo_osp_hourly_limit':\n-                    var hourlyLimit = parseInt(value);\n-                    if (isNaN(hourlyLimit) || hourlyLimit \u003C 1 || hourlyLimit > 100) {\n-                        isValid = false;\n-                        errorMessage = 'Hourly limit must be between 1 and 100';\n-                    }\n-                    break;\n-            }\n-\n-            if (!isValid) {\n-                $mofield.addClass('mo-osp-error-field');\n-                $mofield.parent().append('\u003Cspan class=\"mo-osp-validation-error\">' + errorMessage + '\u003C\u002Fspan>');\n-            }\n-\n-            return isValid;\n-        },\n-\n-        \u002F**\n-         * Validate entire settings form\n-         *\u002F\n-        validateSettings: function() {\n-            var isValid = true;\n-            var errors = [];\n-\n-            \u002F\u002F Clear previous errors\n-            $mo('.mo-osp-error-field').removeClass('mo-osp-error-field');\n-            $mo('.mo-osp-validation-error').remove();\n-\n-            \u002F\u002F Validate all form fields\n-            var $mofields = $mo('#mo_osp_settings_form .mo-form-input');\n-            var self = this;\n-            $mofields.each(function() {\n-                if (!self.validateField($mo(this))) {\n-                    isValid = false;\n-                }\n-            });\n-\n-            \u002F\u002F Validate cross-field relationships\n-            if (isValid && !this.validateCrossFieldRelationships()) {\n-                isValid = false;\n-            }\n-\n-            \u002F\u002F Show summary if there are errors\n-            if (!isValid && errors.length === 0) {\n-                this.showValidationSummary('Please correct the highlighted fields before saving.');\n-            }\n-\n-            return isValid;\n-        },\n-\n-        \u002F**\n-         * Show validation summary\n-         *\u002F\n-        showValidationSummary: function(message) {\n-            \u002F\u002F Remove existing summary\n-            $mo('.mo-osp-validation-summary').remove();\n-            \n-            \u002F\u002F Add new summary\n-            var $mosummary = $mo('\u003Cdiv class=\"mo-osp-validation-error mo-osp-validation-summary\" style=\"margin-bottom: 20px; padding: 12px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 6px;\">' + message + '\u003C\u002Fdiv>');\n-            $mo('#mo_osp_settings_form').prepend($mosummary);\n-            \n-            \u002F\u002F Scroll to top\n-            $mo('html, body').animate({\n-                scrollTop: $mosummary.offset().top - 100\n-            }, 500);\n-        },\n-\n-        \u002F**\n-         * Validate cross-field relationships to ensure settings make logical sense\n-         *\u002F\n-        validateCrossFieldRelationships: function() {\n-            var maxAttempts = parseInt($mo('#mo_osp_max_attempts').val()) || 3;\n-            var hourlyLimit = parseInt($mo('#mo_osp_hourly_limit').val()) || 5;\n-            var dailyLimit = parseInt($mo('#mo_osp_daily_limit').val()) || 10;\n-            \n-            var errors = [];\n-            \n-            \u002F\u002F Hourly limit must be greater than max attempts per window\n-            if (hourlyLimit \u003C= maxAttempts) {\n-                errors.push('Hourly limit (' + hourlyLimit + ') must be greater than max attempts per window (' + maxAttempts + ')');\n-                $mo('#mo_osp_hourly_limit').addClass('mo-osp-error-field');\n-            } else {\n-                $mo('#mo_osp_hourly_limit').removeClass('mo-osp-error-field');\n-            }\n-            \n-            \u002F\u002F Daily limit must be greater than hourly limit\n-            if (dailyLimit \u003C= hourlyLimit) {\n-                errors.push('Daily limit (' + dailyLimit + ') must be greater than hourly limit (' + hourlyLimit + ')');\n-                $mo('#mo_osp_daily_limit').addClass('mo-osp-error-field');\n-            } else {\n-                $mo('#mo_osp_daily_limit').removeClass('mo-osp-error-field');\n-            }\n-            \n-            \u002F\u002F Show cross-field validation errors\n-            if (errors.length > 0) {\n-                this.showValidationSummary('Settings validation failed: ' + errors.join('; '));\n-                return false;\n-            }\n-            \n-            return true;\n-        },\n-\n-        \u002F**\n-         * Initialize blocked users section\n-         *\u002F\n-        initializeBlockedUsers: function() {\n-            this.loadBlockedUsers();\n-        },\n-\n-        \u002F**\n-         * Load blocked users list\n-         *\u002F\n-        loadBlockedUsers: function() {\n-            var self = this;\n-            var $mocontainer = $mo('#mo-osp-blocked-users-container');\n-            var $moloading = $mo('#mo-osp-blocked-users-loading');\n-            var $motbody = $mo('#mo-osp-blocked-users-tbody');\n-\n-            $moloading.show();\n-            $motbody.html('');\n-\n-            $mo.ajax({\n-                url: mo_osp_admin_ajax.ajax_url,\n-                type: 'POST',\n-                data: {\n-                    action: 'mo_osp_get_blocked_users',\n-                    security: mo_osp_admin_ajax.nonce,\n-                    limit: self.pageSize,\n-                    offset: self.currentPage * self.pageSize\n-                },\n-                success: function(response) {\n-                    $moloading.hide();\n-                    if (response.success && response.data && response.data.users) {\n-                        self.totalUsers = response.data.total || 0;\n-                        self.renderBlockedUsers(response.data.users);\n-                        self.updatePagination();\n-                    } else {\n-                        $motbody.html('\u003Ctr>\u003Ctd colspan=\"4\" class=\"mo-osp-no-data\">' + \n-                            (response.data && response.data.message ? response.data.message : 'No blocked users found.') + \n-                            '\u003C\u002Ftd>\u003C\u002Ftr>');\n-                    }\n-                },\n-                error: function() {\n-                    $moloading.hide();\n-                    $motbody.html('\u003Ctr>\u003Ctd colspan=\"4\" class=\"mo-osp-error\">Error loading blocked users. Please try again.\u003C\u002Ftd>\u003C\u002Ftr>');\n-                }\n-            });\n-        },\n-\n-        \u002F**\n-         * Render blocked users in table\n-         *\u002F\n-        renderBlockedUsers: function(users) {\n-            var $motbody = $mo('#mo-osp-blocked-users-tbody');\n-            $motbody.empty();\n-\n-            if (users.length === 0) {\n-                $motbody.html('\u003Ctr>\u003Ctd colspan=\"4\" class=\"mo-osp-no-data\">No blocked users found.\u003C\u002Ftd>\u003C\u002Ftr>');\n-                return;\n-            }\n-\n-            users.forEach(function(user) {\n-                var row = '\u003Ctr data-hash=\"' + user.identifier_hash + '\">' +\n-                    '\u003Ctd>\u003Cspan class=\"mo-osp-identifier-type\">' + user.identifier_type + '\u003C\u002Fspan> ' + \n-                    '\u003Cspan class=\"mo-osp-identifier-masked\">' + user.identifier_masked + '\u003C\u002Fspan>\u003C\u002Ftd>' +\n-                    '\u003Ctd>\u003Cspan class=\"mo-osp-block-reason\">' + user.block_reason_label + '\u003C\u002Fspan>\u003C\u002Ftd>' +\n-                    '\u003Ctd>\u003Cspan class=\"mo-osp-remaining-time\" data-remaining=\"' + user.remaining_time + '\">' + \n-                    user.remaining_time_formatted + '\u003C\u002Fspan>\u003C\u002Ftd>' +\n-                    '\u003Ctd>\u003Cbutton type=\"button\" class=\"mo-osp-unblock-user\" ' +\n-                    'data-hash=\"' + user.identifier_hash + '\">' +\n-                    '\u003Csvg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" style=\"vertical-align: middle;\">' +\n-                    '\u003Cpath d=\"M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z\" fill=\"currentColor\"\u002F>' +\n-                    '\u003C\u002Fsvg>' +\n-                    ' Unblock\u003C\u002Fbutton>\u003C\u002Ftd>' +\n-                    '\u003C\u002Ftr>';\n-                $motbody.append(row);\n-            });\n-\n-            \u002F\u002F Start countdown timers\n-            this.startCountdownTimers();\n-        },\n-\n-        \u002F**\n-         * Start countdown timers for remaining time\n-         *\u002F\n-        startCountdownTimers: function() {\n-            var self = this;\n-            $mo('.mo-osp-remaining-time').each(function() {\n-                var $motime = $mo(this);\n-                var remaining = parseInt($motime.data('remaining')) || 0;\n-                \n-                if (remaining > 0) {\n-                    var interval = setInterval(function() {\n-                        remaining--;\n-                        if (remaining \u003C= 0) {\n-                            clearInterval(interval);\n-                            $motime.text('Expired');\n-                            $motime.closest('tr').addClass('mo-osp-expired');\n-                        } else {\n-                            $motime.text(self.formatTime(remaining));\n-                            $motime.data('remaining', remaining);\n-                        }\n-                    }, 1000);\n-                }\n-            });\n-        },\n-\n-        \u002F**\n-         * Format time in seconds to human-readable format\n-         *\u002F\n-        formatTime: function(seconds) {\n-            if (seconds \u003C 60) {\n-                return seconds + 's';\n-            } else if (seconds \u003C 3600) {\n-                var minutes = Math.floor(seconds \u002F 60);\n-                var secs = seconds % 60;\n-                return minutes + 'm ' + (secs > 0 ? secs + 's' : '');\n-            } else {\n-                var hours = Math.floor(seconds \u002F 3600);\n-                var minutes = Math.floor((seconds % 3600) \u002F 60);\n-                return hours + 'h ' + (minutes > 0 ? minutes + 'm' : '');\n-            }\n-        },\n-\n-        \u002F**\n-         * Update pagination controls\n-         *\u002F\n-        updatePagination: function() {\n-            var $mopagination = $mo('#mo-osp-blocked-users-pagination');\n-            var $moprev = $mo('#mo-osp-prev-page');\n-            var $monext = $mo('#mo-osp-next-page');\n-            var $moinfo = $mo('#mo-osp-page-info');\n-\n-            if (this.totalUsers === 0) {\n-                $mopagination.hide();\n-                return;\n-            }\n-\n-            $mopagination.show();\n-            var maxPage = Math.ceil(this.totalUsers \u002F this.pageSize) - 1;\n-            var start = this.currentPage * this.pageSize + 1;\n-            var end = Math.min((this.currentPage + 1) * this.pageSize, this.totalUsers);\n-\n-            $moinfo.text('Showing ' + start + '-' + end + ' of ' + this.totalUsers);\n-            $moprev.prop('disabled', this.currentPage === 0);\n-            $monext.prop('disabled', this.currentPage >= maxPage);\n-        },\n-\n-        \u002F**\n-         * Unblock a user\n-         *\u002F\n-        \u002F**\n-         * Clear all block data (spam rows, rate limits, puzzle flags)\n-         *\u002F\n-        clearAllBlockedUsers: function() {\n-            var self = this;\n-            var $mobtn = $mo('#mo-osp-clear-all-blocked-users');\n-            var originalHtml = $mobtn.html();\n-\n-            if (!window.confirm('This will remove all blocked users from the list, reset hourly\u002Fdaily rate limits, and clear puzzle requirements stored by this addon. This cannot be undone. Continue?')) {\n-                return;\n-            }\n-\n-            $mobtn.prop('disabled', true);\n-\n-            $mo.ajax({\n-                url: mo_osp_admin_ajax.ajax_url,\n-                type: 'POST',\n-                data: {\n-                    action: 'mo_osp_clear_all_blocked_users',\n-                    security: mo_osp_admin_ajax.nonce\n-                },\n-                success: function(response) {\n-                    var msg = response.data && response.data.message ? response.data.message : '';\n-                    if (response.success && msg) {\n-                        self.currentPage = 0;\n-                        self.loadBlockedUsers();\n-                        self.showAdminNotice(msg, 'success');\n-                    } else if (msg) {\n-                        self.showAdminNotice(msg, 'error');\n-                    } else {\n-                        self.showAdminNotice('Failed to clear data.', 'error');\n-                    }\n-                },\n-                error: function(xhr) {\n-                    var errMsg = 'Error clearing data. Please try again.';\n-                    if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {\n-                        errMsg = xhr.responseJSON.data.message;\n-                    }\n-                    self.showAdminNotice(errMsg, 'error');\n-                },\n-                complete: function() {\n-                    $mobtn.prop('disabled', false).html(originalHtml);\n-                }\n-            });\n-        },\n-\n-        unblockUser: function(identifierHash) {\n-            var self = this;\n-            var $mobutton = $mo('.mo-osp-unblock-user[data-hash=\"' + identifierHash + '\"]');\n-            var originalText = $mobutton.text();\n-\n-            if (!confirm('Are you sure you want to unblock this user?')) {\n-                return;\n-            }\n-\n-            $mobutton.prop('disabled', true).text('Unblocking...');\n-\n-            $mo.ajax({\n-                url: mo_osp_admin_ajax.ajax_url,\n-                type: 'POST',\n-                data: {\n-                    action: 'mo_osp_unblock_user_by_hash',\n-                    security: mo_osp_admin_ajax.nonce,\n-                    identifier_hash: identifierHash\n-                },\n-                success: function(response) {\n-                    if (response.success) {\n-                        \u002F\u002F Remove row or reload list\n-                        $mobutton.closest('tr').fadeOut(300, function() {\n-                            $mo(this).remove();\n-                            self.loadBlockedUsers();\n-                        });\n-                    } else {\n-                        self.showAdminNotice(\n-                            response.data && response.data.message ? response.data.message : 'Failed to unblock user.',\n-                            'error'\n-                        );\n-                        $mobutton.prop('disabled', false).text(originalText);\n-                    }\n-                },\n-                error: function(xhr) {\n-                    var errMsg = 'Error unblocking user. Please try again.';\n-                    if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {\n-                        errMsg = xhr.responseJSON.data.message;\n-                    }\n-                    self.showAdminNotice(errMsg, 'error');\n-                    $mobutton.prop('disabled', false).text(originalText);\n-                }\n-            });\n-        },\n-\n-        \u002F**\n-         * Toggle addon enabled status via AJAX\n-         *\u002F\n-        toggleAddonStatus: function($mocheckbox) {\n-            var self = this;\n-            var enabled = $mocheckbox.is(':checked') ? 1 : 0;\n-            var previousState = !enabled;\n-\n-            $mocheckbox.prop('disabled', true);\n-            self.showAdminNotice('', '');\n-\n-            $mo.ajax({\n-                url: mo_osp_admin_ajax.ajax_url,\n-                type: 'POST',\n-                data: {\n-                    action: 'mo_osp_toggle_addon',\n-                    security: mo_osp_admin_ajax.nonce,\n-                    enabled: enabled\n-                },\n-                success: function(response) {\n-                    if (response.success) {\n-                        var messageType = enabled ? 'success' : 'error';\n-                        self.showAdminNotice(response.data && response.data.message ? response.data.message : 'Addon status updated.', messageType);\n-                    } else {\n-                        $mocheckbox.prop('checked', previousState);\n-                        self.showAdminNotice(response.data && response.data.message ? response.data.message : 'Failed to update addon status.', 'error');\n-                    }\n-                },\n-                error: function() {\n-                    $mocheckbox.prop('checked', previousState);\n-                    self.showAdminNotice('Error updating addon status. Please try again.', 'error');\n-                },\n-                complete: function() {\n-                    $mocheckbox.prop('disabled', false);\n-                }\n-            });\n-        },\n-\n-        \u002F**\n-         * Show admin notice message (auto-dismisses after noticeAutoDismissMs).\n-         *\u002F\n-        showAdminNotice: function(message, type) {\n-            var self = this;\n-            var $mocontainer = $mo('#mo-osp-admin-notice-container');\n-\n-            if (this.noticeDismissTimer) {\n-                clearTimeout(this.noticeDismissTimer);\n-                this.noticeDismissTimer = null;\n-            }\n-\n-            $mocontainer.empty();\n-\n-            if (!message) {\n-                return;\n-            }\n-\n-            var noticeClass = 'notice notice-warning';\n-            if (type === 'success') {\n-                noticeClass = 'notice notice-success mo-notice-success';\n-            } else if (type === 'error') {\n-                noticeClass = 'notice notice-error mo-notice-error';\n-            }\n-\n-            var $monotice = $mo(\n-                '\u003Cdiv class=\"' + noticeClass + ' is-dismissible mo-admin-notif\" style=\"margin-top:1%;\">' +\n-                '\u003Cp>' + message + '\u003C\u002Fp>' +\n-                '\u003C\u002Fdiv>'\n-            );\n-\n-            $mocontainer.append($monotice);\n-            $mo(document).trigger('wp-notice-added', [$monotice]);\n-\n-            this.noticeDismissTimer = setTimeout(function() {\n-                self.noticeDismissTimer = null;\n-                if (!$monotice.length || !$monotice[0].ownerDocument.documentElement.contains($monotice[0])) {\n-                    return;\n-                }\n-                $monotice.fadeOut(300, function() {\n-                    $mo(this).remove();\n-                });\n-            }, this.noticeAutoDismissMs);\n-        },\n-\n-    };\n-\n-    \u002F\u002F Initialize when document is ready\n-    $mo(document).ready(function() {\n-        if (typeof mo_osp_admin_ajax !== 'undefined') {\n-            MO_OSP_Admin.init();\n-        }\n-    });\n-\n-    \u002F\u002F Make it globally accessible for debugging\n-    window.MO_OSP_Admin = MO_OSP_Admin;\n-\n+\u002F**\r\n+ * OTP Spam Preventer Admin JavaScript\r\n+ *\r\n+ * @package miniorange-otp-verification\u002Faddons\r\n+ *\u002F\r\n+\r\n+(function($mo) {\r\n+    'use strict';\r\n+\r\n+    var MO_OSP_Admin = {\r\n+        currentPage: 0,\r\n+        pageSize: 50,\r\n+        totalUsers: 0,\r\n+        \u002F** Auto-hide admin notices after this many ms (success \u002F error). *\u002F\r\n+        noticeAutoDismissMs: 10000,\r\n+        noticeDismissTimer: null,\r\n+\r\n+        init: function() {\r\n+            this.bindEvents();\r\n+            this.initializeAdvancedSettings();\r\n+            this.initializeBlockedUsers();\r\n+        },\r\n+\r\n+        bindEvents: function() {\r\n+            var self = this;\r\n+            \r\n+            \u002F\u002F Settings form validation\r\n+            $mo(document).on('submit', '#mo_osp_settings_form', function(e) {\r\n+                var isValid = self.validateSettings();\r\n+                if (!isValid) {\r\n+                    e.preventDefault();\r\n+                }\r\n+            });\r\n+\r\n+            \u002F\u002F Advanced settings toggle\r\n+            $mo(document).on('click', '#mo-osp-toggle-advanced', function(e) {\r\n+                e.preventDefault();\r\n+                self.toggleAdvancedSettings();\r\n+            });\r\n+\r\n+            \u002F\u002F Real-time validation\r\n+            $mo(document).on('input', '.mo-form-input', function() {\r\n+                self.validateField($mo(this));\r\n+                \r\n+                \u002F\u002F Also validate cross-field relationships for relevant fields\r\n+                var fieldId = $mo(this).attr('id');\r\n+                if (fieldId === 'mo_osp_max_attempts' || fieldId === 'mo_osp_hourly_limit' || fieldId === 'mo_osp_daily_limit') {\r\n+                    \u002F\u002F Clear previous validation summary to avoid confusion\r\n+                    $mo('.mo-osp-validation-summary').remove();\r\n+                    \u002F\u002F Validate cross-field relationships\r\n+                    self.validateCrossFieldRelationships();\r\n+                }\r\n+            });\r\n+\r\n+            \u002F\u002F Clear auto-dismiss timer when user dismisses the notice (core adds .notice-dismiss after wp-notice-added).\r\n+            $mo(document).on('click', '#mo-osp-admin-notice-container .notice-dismiss', function() {\r\n+                if (self.noticeDismissTimer) {\r\n+                    clearTimeout(self.noticeDismissTimer);\r\n+                    self.noticeDismissTimer = null;\r\n+                }\r\n+            });\r\n+\r\n+            \u002F\u002F Blocked users refresh\r\n+            $mo(document).on('click', '#mo-osp-refresh-blocked-users', function() {\r\n+                self.loadBlockedUsers();\r\n+            });\r\n+\r\n+            \u002F\u002F Clear all blocked users \u002F limits \u002F puzzle flags\r\n+            $mo(document).on('click', '#mo-osp-clear-all-blocked-users', function() {\r\n+                self.clearAllBlockedUsers();\r\n+            });\r\n+\r\n+            \u002F\u002F Addon enable\u002Fdisable toggle\r\n+            $mo(document).on('change', '#mo_osp_enabled', function() {\r\n+                self.toggleAddonStatus($mo(this));\r\n+            });\r\n+\r\n+            \u002F\u002F Unblock user\r\n+            $mo(document).on('click', '.mo-osp-unblock-user', function() {\r\n+                var identifierHash = $mo(this).data('hash');\r\n+                self.unblockUser(identifierHash);\r\n+            });\r\n+\r\n+            \u002F\u002F Pagination\r\n+            $mo(document).on('click', '#mo-osp-prev-page', function() {\r\n+                if (self.currentPage > 0) {\r\n+                    self.currentPage--;\r\n+                    self.loadBlockedUsers();\r\n+                }\r\n+            });\r\n+\r\n+            $mo(document).on('click', '#mo-osp-next-page', function() {\r\n+                var maxPage = Math.ceil(self.totalUsers \u002F self.pageSize) - 1;\r\n+                if (self.currentPage \u003C maxPage) {\r\n+                    self.currentPage++;\r\n+                    self.loadBlockedUsers();\r\n+                }\r\n+            });\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Initialize advanced settings state\r\n+         *\u002F\r\n+        initializeAdvancedSettings: function() {\r\n+            \u002F\u002F Always start hidden by default, ignore localStorage for initial state\r\n+            this.hideAdvancedSettings();\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Toggle advanced settings visibility\r\n+         *\u002F\r\n+        toggleAdvancedSettings: function() {\r\n+            var $moadvancedSection = $mo('#mo-osp-advanced-settings');\r\n+            var $motoggleButton = $mo('#mo-osp-toggle-advanced');\r\n+            var $motoggleText = $mo('#mo-osp-toggle-text');\r\n+            var $motoggleIcon = $mo('#mo-osp-toggle-icon');\r\n+\r\n+            if ($moadvancedSection.hasClass('mo-osp-advanced-hidden')) {\r\n+                this.showAdvancedSettings();\r\n+            } else {\r\n+                this.hideAdvancedSettings();\r\n+            }\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Show advanced settings\r\n+         *\u002F\r\n+        showAdvancedSettings: function() {\r\n+            var $moadvancedSection = $mo('#mo-osp-advanced-settings');\r\n+            var $motoggleText = $mo('#mo-osp-toggle-text');\r\n+            var $motoggleIcon = $mo('#mo-osp-toggle-icon');\r\n+\r\n+            $moadvancedSection.removeClass('mo-osp-advanced-hidden').addClass('mo-osp-advanced-visible');\r\n+            $motoggleText.text('Hide Advanced');\r\n+            $motoggleIcon.addClass('rotate-180');\r\n+            \r\n+            localStorage.setItem('mo_osp_advanced_expanded', 'true');\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Hide advanced settings\r\n+         *\u002F\r\n+        hideAdvancedSettings: function() {\r\n+            var $moadvancedSection = $mo('#mo-osp-advanced-settings');\r\n+            var $motoggleText = $mo('#mo-osp-toggle-text');\r\n+            var $motoggleIcon = $mo('#mo-osp-toggle-icon');\r\n+\r\n+            $moadvancedSection.removeClass('mo-osp-advanced-visible').addClass('mo-osp-advanced-hidden');\r\n+            $motoggleText.text('Show Advanced');\r\n+            $motoggleIcon.removeClass('rotate-180');\r\n+            \r\n+            localStorage.setItem('mo_osp_advanced_expanded', 'false');\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Validate individual field\r\n+         *\u002F\r\n+        validateField: function($mofield) {\r\n+            var fieldId = $mofield.attr('id');\r\n+            var value = $mofield.val();\r\n+            var isValid = true;\r\n+            var errorMessage = '';\r\n+\r\n+            \u002F\u002F Remove existing error styling\r\n+            $mofield.removeClass('mo-osp-error-field');\r\n+            $mofield.siblings('.mo-osp-validation-error').remove();\r\n+\r\n+            switch (fieldId) {\r\n+                case 'mo_osp_cooldown_time':\r\n+                    var cooldownTime = parseInt(value);\r\n+                    if (isNaN(cooldownTime) || cooldownTime \u003C 0 || cooldownTime > 86400) {\r\n+                        isValid = false;\r\n+                        errorMessage = 'Wait time must be between 0 and 86400 seconds (24 hours)';\r\n+                    }\r\n+                    break;\r\n+\r\n+                case 'mo_osp_max_attempts':\r\n+                    var maxAttempts = parseInt(value);\r\n+                    if (isNaN(maxAttempts) || maxAttempts \u003C 3 || maxAttempts > 10) {\r\n+                        isValid = false;\r\n+                        errorMessage = 'Maximum attempts must be between 3 and 10';\r\n+                    }\r\n+                    break;\r\n+\r\n+                case 'mo_osp_block_time':\r\n+                    var blockTime = parseInt(value);\r\n+                    if (isNaN(blockTime) || blockTime \u003C 60 || blockTime > 604800) {\r\n+                        isValid = false;\r\n+                        errorMessage = 'Block time must be between 60 seconds and 604800 seconds (7 days)';\r\n+                    }\r\n+                    break;\r\n+\r\n+                case 'mo_osp_daily_limit':\r\n+                    var dailyLimit = parseInt(value);\r\n+                    if (isNaN(dailyLimit) || dailyLimit \u003C 1 || dailyLimit > 1000) {\r\n+                        isValid = false;\r\n+                        errorMessage = 'Daily limit must be between 1 and 1000';\r\n+                    }\r\n+                    break;\r\n+\r\n+                case 'mo_osp_hourly_limit':\r\n+                    var hourlyLimit = parseInt(value);\r\n+                    if (isNaN(hourlyLimit) || hourlyLimit \u003C 1 || hourlyLimit > 100) {\r\n+                        isValid = false;\r\n+                        errorMessage = 'Hourly limit must be between 1 and 100';\r\n+                    }\r\n+                    break;\r\n+            }\r\n+\r\n+            if (!isValid) {\r\n+                $mofield.addClass('mo-osp-error-field');\r\n+                $mofield.parent().append('\u003Cspan class=\"mo-osp-validation-error\">' + errorMessage + '\u003C\u002Fspan>');\r\n+            }\r\n+\r\n+            return isValid;\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Validate entire settings form\r\n+         *\u002F\r\n+        validateSettings: function() {\r\n+            var isValid = true;\r\n+            var errors = [];\r\n+\r\n+            \u002F\u002F Clear previous errors\r\n+            $mo('.mo-osp-error-field').removeClass('mo-osp-error-field');\r\n+            $mo('.mo-osp-validation-error').remove();\r\n+\r\n+            \u002F\u002F Validate all form fields\r\n+            var $mofields = $mo('#mo_osp_settings_form .mo-form-input');\r\n+            var self = this;\r\n+            $mofields.each(function() {\r\n+                if (!self.validateField($mo(this))) {\r\n+                    isValid = false;\r\n+                }\r\n+            });\r\n+\r\n+            \u002F\u002F Validate cross-field relationships\r\n+            if (isValid && !this.validateCrossFieldRelationships()) {\r\n+                isValid = false;\r\n+            }\r\n+\r\n+            \u002F\u002F Show summary if there are errors\r\n+            if (!isValid && errors.length === 0) {\r\n+                this.showValidationSummary('Please correct the highlighted fields before saving.');\r\n+            }\r\n+\r\n+            return isValid;\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Show validation summary\r\n+         *\u002F\r\n+        showValidationSummary: function(message) {\r\n+            \u002F\u002F Remove existing summary\r\n+            $mo('.mo-osp-validation-summary').remove();\r\n+            \r\n+            \u002F\u002F Add new summary\r\n+            var $mosummary = $mo('\u003Cdiv class=\"mo-osp-validation-error mo-osp-validation-summary\" style=\"margin-bottom: 20px; padding: 12px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 6px;\">' + message + '\u003C\u002Fdiv>');\r\n+            $mo('#mo_osp_settings_form').prepend($mosummary);\r\n+            \r\n+            \u002F\u002F Scroll to top\r\n+            $mo('html, body').animate({\r\n+                scrollTop: $mosummary.offset().top - 100\r\n+            }, 500);\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Validate cross-field relationships to ensure settings make logical sense\r\n+         *\u002F\r\n+        validateCrossFieldRelationships: function() {\r\n+            var maxAttempts = parseInt($mo('#mo_osp_max_attempts').val()) || 3;\r\n+            var hourlyLimit = parseInt($mo('#mo_osp_hourly_limit').val()) || 5;\r\n+            var dailyLimit = parseInt($mo('#mo_osp_daily_limit').val()) || 10;\r\n+            \r\n+            var errors = [];\r\n+            \r\n+            \u002F\u002F Hourly limit must be greater than max attempts per window\r\n+            if (hourlyLimit \u003C= maxAttempts) {\r\n+                errors.push('Hourly limit (' + hourlyLimit + ') must be greater than max attempts per window (' + maxAttempts + ')');\r\n+                $mo('#mo_osp_hourly_limit').addClass('mo-osp-error-field');\r\n+            } else {\r\n+                $mo('#mo_osp_hourly_limit').removeClass('mo-osp-error-field');\r\n+            }\r\n+            \r\n+            \u002F\u002F Daily limit must be greater than hourly limit\r\n+            if (dailyLimit \u003C= hourlyLimit) {\r\n+                errors.push('Daily limit (' + dailyLimit + ') must be greater than hourly limit (' + hourlyLimit + ')');\r\n+                $mo('#mo_osp_daily_limit').addClass('mo-osp-error-field');\r\n+            } else {\r\n+                $mo('#mo_osp_daily_limit').removeClass('mo-osp-error-field');\r\n+            }\r\n+            \r\n+            \u002F\u002F Show cross-field validation errors\r\n+            if (errors.length > 0) {\r\n+                this.showValidationSummary('Settings validation failed: ' + errors.join('; '));\r\n+                return false;\r\n+            }\r\n+            \r\n+            return true;\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Initialize blocked users section\r\n+         *\u002F\r\n+        initializeBlockedUsers: function() {\r\n+            this.loadBlockedUsers();\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Load blocked users list\r\n+         *\u002F\r\n+        loadBlockedUsers: function() {\r\n+            var self = this;\r\n+            var $mocontainer = $mo('#mo-osp-blocked-users-container');\r\n+            var $moloading = $mo('#mo-osp-blocked-users-loading');\r\n+            var $motbody = $mo('#mo-osp-blocked-users-tbody');\r\n+\r\n+            $moloading.show();\r\n+            $motbody.html('');\r\n+\r\n+            $mo.ajax({\r\n+                url: mo_osp_admin_ajax.ajax_url,\r\n+                type: 'POST',\r\n+                data: {\r\n+                    action: 'mo_osp_get_blocked_users',\r\n+                    security: mo_osp_admin_ajax.nonce,\r\n+                    limit: self.pageSize,\r\n+                    offset: self.currentPage * self.pageSize\r\n+                },\r\n+                success: function(response) {\r\n+                    $moloading.hide();\r\n+                    if (response.success && response.data && response.data.users) {\r\n+                        self.totalUsers = response.data.total || 0;\r\n+                        self.renderBlockedUsers(response.data.users);\r\n+                        self.updatePagination();\r\n+                    } else {\r\n+                        $motbody.html('\u003Ctr>\u003Ctd colspan=\"4\" class=\"mo-osp-no-data\">' + \r\n+                            (response.data && response.data.message ? response.data.message : 'No blocked users found.') + \r\n+                            '\u003C\u002Ftd>\u003C\u002Ftr>');\r\n+                    }\r\n+                },\r\n+                error: function() {\r\n+                    $moloading.hide();\r\n+                    $motbody.html('\u003Ctr>\u003Ctd colspan=\"4\" class=\"mo-osp-error\">Error loading blocked users. Please try again.\u003C\u002Ftd>\u003C\u002Ftr>');\r\n+                }\r\n+            });\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Render blocked users in table\r\n+         *\u002F\r\n+        renderBlockedUsers: function(users) {\r\n+            var $motbody = $mo('#mo-osp-blocked-users-tbody');\r\n+            $motbody.empty();\r\n+\r\n+            if (users.length === 0) {\r\n+                $motbody.html('\u003Ctr>\u003Ctd colspan=\"4\" class=\"mo-osp-no-data\">No blocked users found.\u003C\u002Ftd>\u003C\u002Ftr>');\r\n+                return;\r\n+            }\r\n+\r\n+            users.forEach(function(user) {\r\n+                var row = '\u003Ctr data-hash=\"' + user.identifier_hash + '\">' +\r\n+                    '\u003Ctd>\u003Cspan class=\"mo-osp-identifier-type\">' + user.identifier_type + '\u003C\u002Fspan> ' + \r\n+                    '\u003Cspan class=\"mo-osp-identifier-masked\">' + user.identifier_masked + '\u003C\u002Fspan>\u003C\u002Ftd>' +\r\n+                    '\u003Ctd>\u003Cspan class=\"mo-osp-block-reason\">' + user.block_reason_label + '\u003C\u002Fspan>\u003C\u002Ftd>' +\r\n+                    '\u003Ctd>\u003Cspan class=\"mo-osp-remaining-time\" data-remaining=\"' + user.remaining_time + '\">' + \r\n+                    user.remaining_time_formatted + '\u003C\u002Fspan>\u003C\u002Ftd>' +\r\n+                    '\u003Ctd>\u003Cbutton type=\"button\" class=\"mo-osp-unblock-user\" ' +\r\n+                    'data-hash=\"' + user.identifier_hash + '\">' +\r\n+                    '\u003Csvg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" style=\"vertical-align: middle;\">' +\r\n+                    '\u003Cpath d=\"M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z\" fill=\"currentColor\"\u002F>' +\r\n+                    '\u003C\u002Fsvg>' +\r\n+                    ' Unblock\u003C\u002Fbutton>\u003C\u002Ftd>' +\r\n+                    '\u003C\u002Ftr>';\r\n+                $motbody.append(row);\r\n+            });\r\n+\r\n+            \u002F\u002F Start countdown timers\r\n+            this.startCountdownTimers();\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Start countdown timers for remaining time\r\n+         *\u002F\r\n+        startCountdownTimers: function() {\r\n+            var self = this;\r\n+            $mo('.mo-osp-remaining-time').each(function() {\r\n+                var $motime = $mo(this);\r\n+                var remaining = parseInt($motime.data('remaining')) || 0;\r\n+                \r\n+                if (remaining > 0) {\r\n+                    var interval = setInterval(function() {\r\n+                        remaining--;\r\n+                        if (remaining \u003C= 0) {\r\n+                            clearInterval(interval);\r\n+                            $motime.text('Expired');\r\n+                            $motime.closest('tr').addClass('mo-osp-expired');\r\n+                        } else {\r\n+                            $motime.text(self.formatTime(remaining));\r\n+                            $motime.data('remaining', remaining);\r\n+                        }\r\n+                    }, 1000);\r\n+                }\r\n+            });\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Format time in seconds to human-readable format\r\n+         *\u002F\r\n+        formatTime: function(seconds) {\r\n+            if (seconds \u003C 60) {\r\n+                return seconds + 's';\r\n+            } else if (seconds \u003C 3600) {\r\n+                var minutes = Math.floor(seconds \u002F 60);\r\n+                var secs = seconds % 60;\r\n+                return minutes + 'm ' + (secs > 0 ? secs + 's' : '');\r\n+            } else {\r\n+                var hours = Math.floor(seconds \u002F 3600);\r\n+                var minutes = Math.floor((seconds % 3600) \u002F 60);\r\n+                return hours + 'h ' + (minutes > 0 ? minutes + 'm' : '');\r\n+            }\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Update pagination controls\r\n+         *\u002F\r\n+        updatePagination: function() {\r\n+            var $mopagination = $mo('#mo-osp-blocked-users-pagination');\r\n+            var $moprev = $mo('#mo-osp-prev-page');\r\n+            var $monext = $mo('#mo-osp-next-page');\r\n+            var $moinfo = $mo('#mo-osp-page-info');\r\n+\r\n+            if (this.totalUsers === 0) {\r\n+                $mopagination.hide();\r\n+                return;\r\n+            }\r\n+\r\n+            $mopagination.show();\r\n+            var maxPage = Math.ceil(this.totalUsers \u002F this.pageSize) - 1;\r\n+            var start = this.currentPage * this.pageSize + 1;\r\n+            var end = Math.min((this.currentPage + 1) * this.pageSize, this.totalUsers);\r\n+\r\n+            $moinfo.text('Showing ' + start + '-' + end + ' of ' + this.totalUsers);\r\n+            $moprev.prop('disabled', this.currentPage === 0);\r\n+            $monext.prop('disabled', this.currentPage >= maxPage);\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Unblock a user\r\n+         *\u002F\r\n+        \u002F**\r\n+         * Clear all block data (spam rows, rate limits, puzzle flags)\r\n+         *\u002F\r\n+        clearAllBlockedUsers: function() {\r\n+            var self = this;\r\n+            var $mobtn = $mo('#mo-osp-clear-all-blocked-users');\r\n+            var originalHtml = $mobtn.html();\r\n+\r\n+            if (!window.confirm('This will remove all blocked users from the list, reset hourly\u002Fdaily rate limits, and clear puzzle requirements stored by this addon. This cannot be undone. Continue?')) {\r\n+                return;\r\n+            }\r\n+\r\n+            $mobtn.prop('disabled', true);\r\n+\r\n+            $mo.ajax({\r\n+                url: mo_osp_admin_ajax.ajax_url,\r\n+                type: 'POST',\r\n+                data: {\r\n+                    action: 'mo_osp_clear_all_blocked_users',\r\n+                    security: mo_osp_admin_ajax.nonce\r\n+                },\r\n+                success: function(response) {\r\n+                    var msg = response.data && response.data.message ? response.data.message : '';\r\n+                    if (response.success && msg) {\r\n+                        self.currentPage = 0;\r\n+                        self.loadBlockedUsers();\r\n+                        self.showAdminNotice(msg, 'success');\r\n+                    } else if (msg) {\r\n+                        self.showAdminNotice(msg, 'error');\r\n+                    } else {\r\n+                        self.showAdminNotice('Failed to clear data.', 'error');\r\n+                    }\r\n+                },\r\n+                error: function(xhr) {\r\n+                    var errMsg = 'Error clearing data. Please try again.';\r\n+                    if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {\r\n+                        errMsg = xhr.responseJSON.data.message;\r\n+                    }\r\n+                    self.showAdminNotice(errMsg, 'error');\r\n+                },\r\n+                complete: function() {\r\n+                    $mobtn.prop('disabled', false).html(originalHtml);\r\n+                }\r\n+            });\r\n+        },\r\n+\r\n+        unblockUser: function(identifierHash) {\r\n+            var self = this;\r\n+            var $mobutton = $mo('.mo-osp-unblock-user[data-hash=\"' + identifierHash + '\"]');\r\n+            var originalText = $mobutton.text();\r\n+\r\n+            if (!confirm('Are you sure you want to unblock this user?')) {\r\n+                return;\r\n+            }\r\n+\r\n+            $mobutton.prop('disabled', true).text('Unblocking...');\r\n+\r\n+            $mo.ajax({\r\n+                url: mo_osp_admin_ajax.ajax_url,\r\n+                type: 'POST',\r\n+                data: {\r\n+                    action: 'mo_osp_unblock_user_by_hash',\r\n+                    security: mo_osp_admin_ajax.nonce,\r\n+                    identifier_hash: identifierHash\r\n+                },\r\n+                success: function(response) {\r\n+                    if (response.success) {\r\n+                        \u002F\u002F Remove row or reload list\r\n+                        $mobutton.closest('tr').fadeOut(300, function() {\r\n+                            $mo(this).remove();\r\n+                            self.loadBlockedUsers();\r\n+                        });\r\n+                    } else {\r\n+                        self.showAdminNotice(\r\n+                            response.data && response.data.message ? response.data.message : 'Failed to unblock user.',\r\n+                            'error'\r\n+                        );\r\n+                        $mobutton.prop('disabled', false).text(originalText);\r\n+                    }\r\n+                },\r\n+                error: function(xhr) {\r\n+                    var errMsg = 'Error unblocking user. Please try again.';\r\n+                    if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {\r\n+                        errMsg = xhr.responseJSON.data.message;\r\n+                    }\r\n+                    self.showAdminNotice(errMsg, 'error');\r\n+                    $mobutton.prop('disabled', false).text(originalText);\r\n+                }\r\n+            });\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Toggle addon enabled status via AJAX\r\n+         *\u002F\r\n+        toggleAddonStatus: function($mocheckbox) {\r\n+            var self = this;\r\n+            var enabled = $mocheckbox.is(':checked') ? 1 : 0;\r\n+            var previousState = !enabled;\r\n+\r\n+            $mocheckbox.prop('disabled', true);\r\n+            self.showAdminNotice('', '');\r\n+\r\n+            $mo.ajax({\r\n+                url: mo_osp_admin_ajax.ajax_url,\r\n+                type: 'POST',\r\n+                data: {\r\n+                    action: 'mo_osp_toggle_addon',\r\n+                    security: mo_osp_admin_ajax.nonce,\r\n+                    enabled: enabled\r\n+                },\r\n+                success: function(response) {\r\n+                    if (response.success) {\r\n+                        var messageType = enabled ? 'success' : 'error';\r\n+                        self.showAdminNotice(response.data && response.data.message ? response.data.message : 'Addon status updated.', messageType);\r\n+                    } else {\r\n+                        $mocheckbox.prop('checked', previousState);\r\n+                        self.showAdminNotice(response.data && response.data.message ? response.data.message : 'Failed to update addon status.', 'error');\r\n+                    }\r\n+                },\r\n+                error: function() {\r\n+                    $mocheckbox.prop('checked', previousState);\r\n+                    self.showAdminNotice('Error updating addon status. Please try again.', 'error');\r\n+                },\r\n+                complete: function() {\r\n+                    $mocheckbox.prop('disabled', false);\r\n+                }\r\n+            });\r\n+        },\r\n+\r\n+        \u002F**\r\n+         * Show admin notice message (auto-dismisses after noticeAutoDismissMs).\r\n+         *\u002F\r\n+        showAdminNotice: function(message, type) {\r\n+            var self = this;\r\n+            var $mocontainer = $mo('#mo-osp-admin-notice-container');\r\n+\r\n+            if (this.noticeDismissTimer) {\r\n+                clearTimeout(this.noticeDismissTimer);\r\n+                this.noticeDismissTimer = null;\r\n+            }\r\n+\r\n+            $mocontainer.empty();\r\n+\r\n+            if (!message) {\r\n+                return;\r\n+            }\r\n+\r\n+            var noticeClass = 'notice notice-warning';\r\n+            if (type === 'success') {\r\n+                noticeClass = 'notice notice-success mo-notice-success';\r\n+            } else if (type === 'error') {\r\n+                noticeClass = 'notice notice-error mo-notice-error';\r\n+            }\r\n+\r\n+            var $monotice = $mo(\r\n+                '\u003Cdiv class=\"' + noticeClass + ' is-dismissible mo-admin-notif\" style=\"margin-top:1%;\">' +\r\n+                '\u003Cp>' + message + '\u003C\u002Fp>' +\r\n+                '\u003C\u002Fdiv>'\r\n+            );\r\n+\r\n+            $mocontainer.append($monotice);\r\n+            $mo(document).trigger('wp-notice-added', [$monotice]);\r\n+\r\n+            this.noticeDismissTimer = setTimeout(function() {\r\n+                self.noticeDismissTimer = null;\r\n+                if (!$monotice.length || !$monotice[0].ownerDocument.documentElement.contains($monotice[0])) {\r\n+                    return;\r\n+                }\r\n+                $monotice.fadeOut(300, function() {\r\n+                    $mo(this).remove();\r\n+                });\r\n+            }, this.noticeAutoDismissMs);\r\n+        },\r\n+\r\n+    };\r\n+\r\n+    \u002F\u002F Initialize when document is ready\r\n+    $mo(document).ready(function() {\r\n+        if (typeof mo_osp_admin_ajax !== 'undefined') {\r\n+            MO_OSP_Admin.init();\r\n+        }\r\n+    });\r\n+\r\n+    \u002F\u002F Make it globally accessible for debugging\r\n+    window.MO_OSP_Admin = MO_OSP_Admin;\r\n+\r\n })(jQuery);\n\\ No newline at end of file\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fspam-preventer.js \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fspam-preventer.js\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fspam-preventer.js\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fincludes\u002Fjs\u002Fspam-preventer.js\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,1846 +1,1846 @@\n-\u002F**\n- * Fixed OTP Spam Preventer - Proper Integration with Existing OTP Flow\n- * Based on resendcontrol addon patterns\n- *\u002F\n-\n-(function($mo) {\n-    'use strict';\n-\n-    \u002F\u002F Global variables\n-    let activeTimers = [];\n-    let isSpamPreventersInitialized = false;\n-    let currentBrowserID = '';\n-\n-    \u002F\u002F Button selectors (matching resendcontrol patterns)\n-    const buttonSelectors = [\n-        'input[value*=\"Send OTP\"]',\n-        'input[value*=\"send otp\"]', \n-        'input[value*=\"SEND OTP\"]',\n-        'button:contains(\"Send OTP\")',\n-        'button:contains(\"send otp\")',\n-        'button:contains(\"SEND OTP\")',\n-        '#miniorange_otp_token_submit',\n-        'input[name=\"miniorange_otp_token_submit\"]',\n-        'input[id*=\"send_otp\"]',\n-        'input[class*=\"send_otp\"]',\n-        'button[id*=\"send_otp\"]',\n-        'button[class*=\"send_otp\"]',\n-        '#mo_wc_send_otp'\n-    ];\n-\n-    \u002F**\n-     * WooCommerce block checkout: popup \"send OTP\" button id contains \"send_otp\" so it matches\n-     * button[id*=\"send_otp\"]. Do not hide or disable it — hide() stuck the button; disable() stuck\n-     * it when AJAX errors \u002F validation responses did not run our restore paths (user fixes form\n-     * and cannot retry). Double-send is acceptable; server enforces limits.\n-     *\u002F\n-    function isWcBlockCheckoutPopupSendButton($btn) {\n-        if (!$btn || !$btn.length) {\n-            return false;\n-        }\n-        if ($btn.attr('id') === 'miniorange_wc_popup_send_otp_token') {\n-            return true;\n-        }\n-        return $btn.closest('#miniorange_wc_popup_send_otp_token').length > 0;\n-    }\n-\n-    function mospPrepareOtpButtonForRequest($mobutton) {\n-        if (!$mobutton || !$mobutton.length) {\n-            return;\n-        }\n-        if (isWcBlockCheckoutPopupSendButton($mobutton)) {\n-            const $wcPrep = $mobutton.closest('#miniorange_wc_popup_send_otp_token');\n-            ($wcPrep.length ? $wcPrep : $mobutton).data('mo-osp-waiting-for-response', true);\n-            return;\n-        }\n-        $mobutton.hide();\n-        $mobutton.data('mo-osp-waiting-for-response', true);\n-    }\n-\n-    function mospRestoreOtpButtonAfterRequest($btn) {\n-        if (!$btn || !$btn.length) {\n-            return;\n-        }\n-        if (isWcBlockCheckoutPopupSendButton($btn)) {\n-            const $wc = $btn.closest('#miniorange_wc_popup_send_otp_token');\n-            if ($wc.length) {\n-                $wc.prop('disabled', false).css('opacity', '').removeAttr('aria-busy').show();\n-                $wc.data('mo-osp-waiting-for-response', false);\n-            }\n-        } else {\n-            $btn.show();\n-            $btn.data('mo-osp-waiting-for-response', false);\n-        }\n-    }\n-\n-    \u002F**\n-     * Initialize spam preventer (following resendcontrol pattern)\n-     *\u002F\n-    function initializeSpamPreventer() {\n-        if (isSpamPreventersInitialized) {\n-            return;\n-        }\n-        \n-        \u002F\u002F Initialize browser ID\n-        initializeBrowserID();\n-        \n-        \u002F\u002F Wait for any OTP button to appear, then bind events\n-        waitForAnyElement(buttonSelectors, function(matchingSelector) {\n-            bindSpamPreventionEvents();\n-            \n-            \u002F\u002F Check if we should auto-trigger Send OTP after puzzle verification\n-            checkAndAutoTriggerSendOTP();\n-        });\n-\n-        isSpamPreventersInitialized = true;\n-    }\n-    \n-    \u002F**\n-     * Show message after puzzle completion and prompt user to resubmit\n-     *\u002F\n-    function checkAndAutoTriggerSendOTP() {\n-        const puzzleCompleted = sessionStorage.getItem('mo_osp_puzzle_completed');\n-        \n-        if (puzzleCompleted === 'true') {\n-            \n-            \u002F\u002F Clear the flag from sessionStorage\n-            sessionStorage.removeItem('mo_osp_puzzle_completed');\n-        }\n-    }\n-\n-    \u002F**\n-     * Initialize browser ID for tracking\n-     *\u002F\n-    function initializeBrowserID() {\n-        currentBrowserID = localStorage.getItem('mo_osp_browser_id');\n-        \n-        if (!currentBrowserID) {\n-            currentBrowserID = generateBrowserID();\n-            localStorage.setItem('mo_osp_browser_id', currentBrowserID);\n-        }\n-        \n-        \u002F\u002F Make globally available\n-        window.mo_osp_browser_id = currentBrowserID;\n-    }\n-\n-    \u002F**\n-     * Generate unique browser ID\n-     *\u002F\n-    function generateBrowserID() {\n-        const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n-        let result = '';\n-        for (let i = 0; i \u003C 8; i++) {\n-            result += chars.charAt(Math.floor(Math.random() * chars.length));\n-        }\n-        return result;\n-    }\n-\n-    \u002F**\n-     * Wait for any element to appear (resendcontrol pattern)\n-     *\u002F\n-    function waitForAnyElement(selectors, callback) {\n-        const interval = setInterval(() => {\n-            const matchingSelector = selectors.find(selector => $mo(selector).length > 0);\n-            if (matchingSelector) {\n-                clearInterval(interval);\n-                callback(matchingSelector);\n-            }\n-        }, 100);\n-\n-        setTimeout(() => {\n-            clearInterval(interval);\n-        }, 3000);\n-    }\n-\n-    \u002F**\n-     * Bind spam prevention events (following resendcontrol pattern)\n-     *\u002F\n-    function bindSpamPreventionEvents() {\n-        const messageSelector = 'div[id*=\"mo_message\"]';\n-        \n-        buttonSelectors.forEach(function(buttonSelector) {\n-            $mo(buttonSelector).each(function() {\n-                const $mobutton = $mo(this);\n-                \n-                \u002F\u002F Prevent multiple bindings\n-                if ($mobutton.data('spam-preventer-bound')) {\n-                    return;\n-                }\n-                $mobutton.data('spam-preventer-bound', true);\n-                \n-                $mobutton.on('click', function(e) {\n-                    \n-                    \u002F\u002F CRITICAL: Skip spam prevention checks for external popup buttons\n-                    \u002F\u002F External popup handles its own validation and error messages\n-                    \u002F\u002F Check at click time since external popup may be dynamically loaded\n-                    const isExternalPopupButton = ($mobutton.attr('id') === 'send_otp' && \n-                                                  $mo('#mo_site_otp_form').length > 0) ||\n-                                                 ($mo('#mo_site_otp_form').length > 0 && \n-                                                  $mo('.mo_customer_validation-modal').length > 0 &&\n-                                                  ($mobutton.closest('#mo_site_otp_form').length > 0 || \n-                                                   $mobutton.closest('.mo_customer_validation-modal').length > 0));\n-                    \n-                    if (isExternalPopupButton) {\n-                        \u002F\u002F Don't prevent default or stop propagation - let external popup handle it\n-                        \u002F\u002F Just return without doing anything\n-                        return;\n-                    }\n-                    \n-                    \u002F\u002F Check if puzzle was just completed (skip puzzle check, allow OTP to proceed)\n-                    if (window.mo_osp_puzzle_just_completed || window.mo_osp_puzzle_verified) {\n-                        window.mo_osp_puzzle_just_completed = false; \u002F\u002F Clear the flag\n-                        \u002F\u002F Check cooldown even after puzzle completion\n-                        checkCooldownBeforeOTPSend($mobutton, messageSelector, e);\n-                        return;\n-                    }\n-                    \n-                    \u002F\u002F IMPORTANT: Don't check puzzle requirement if we're currently verifying\n-                    \u002F\u002F This prevents redundant AJAX calls during puzzle verification flow\n-                    if (window.MO_OSP_Puzzle && window.MO_OSP_Puzzle.isShowing) {\n-                        \u002F\u002F Allow default behavior to continue\n-                        setupPostOTPHandling($mobutton, messageSelector);\n-                        return;\n-                    }\n-                    \n-                    \u002F\u002F First check cooldown, then check puzzle requirement\n-                    checkCooldownBeforeOTPSend($mobutton, messageSelector, e).then(function(cooldownResult) {\n-                        if (cooldownResult.onCooldown) {\n-                            \u002F\u002F Cooldown is active, show error message and prevent OTP send\n-                            e.preventDefault();\n-                            e.stopImmediatePropagation();\n-                            showCooldownError($mobutton, messageSelector, cooldownResult.remainingTime);\n-                            return false;\n-                        } else {\n-                            \u002F\u002F No cooldown, check if puzzle is required\n-                            checkPuzzleRequirement().then(function(result) {\n-                                if (result.puzzleRequired) {\n-                                    e.preventDefault();\n-                                    e.stopImmediatePropagation();\n-                                    showPuzzlePopup();\n-                                    return false;\n-                                } else {\n-                                    setupPostOTPHandling($mobutton, messageSelector);\n-                                }\n-                            }).catch(function(error) {\n-                                setupPostOTPHandling($mobutton, messageSelector);\n-                            });\n-                        }\n-                    }).catch(function(error) {\n-                        \u002F\u002F On error, proceed with normal flow\n-                        checkPuzzleRequirement().then(function(result) {\n-                            if (result.puzzleRequired) {\n-                                e.preventDefault();\n-                                e.stopImmediatePropagation();\n-                                showPuzzlePopup();\n-                                return false;\n-                            } else {\n-                                setupPostOTPHandling($mobutton, messageSelector);\n-                            }\n-                        }).catch(function(error2) {\n-                            setupPostOTPHandling($mobutton, messageSelector);\n-                        });\n-                    });\n-                });\n-            });\n-        });\n-    }\n-\n-    \u002F**\n-     * Check if cooldown is active before OTP send\n-     *\u002F\n-    function checkCooldownBeforeOTPSend($mobutton, messageSelector, e) {\n-        return new Promise((resolve, reject) => {\n-            if (typeof mo_osp_ajax === 'undefined') {\n-                resolve({ onCooldown: false });\n-                return;\n-            }\n-\n-            const email = getEmailFromForm();\n-            const phone = getPhoneFromForm();\n-\n-            $mo.ajax({\n-                url: mo_osp_ajax.ajax_url,\n-                type: 'POST',\n-                dataType: 'json',\n-                data: {\n-                    action: 'mo_osp_check_blocked',\n-                    nonce: mo_osp_ajax.nonce,\n-                    mo_osp_browser_id: currentBrowserID,\n-                    email: email,\n-                    phone: phone\n-                },\n-                success: function(response) {\n-                    \u002F\u002F Handle WordPress JSON success wrapper\n-                    if (response && response.data) {\n-                        response = response.data;\n-                    }\n-                    \n-                    if (response && response.cooldown && response.remaining_time > 0) {\n-                        resolve({ \n-                            onCooldown: true, \n-                            remainingTime: parseInt(response.remaining_time) \n-                        });\n-                    } else {\n-                        resolve({ onCooldown: false });\n-                    }\n-                },\n-                error: function(xhr, status, error) {\n-                    \u002F\u002F If response is HTML (error page), treat as no cooldown to allow OTP send\n-                    if (xhr.responseText && xhr.responseText.trim().startsWith('\u003C')) {\n-                        resolve({ onCooldown: false });\n-                        return;\n-                    }\n-                    resolve({ onCooldown: false });\n-                }\n-            });\n-        });\n-    }\n-\n-    \u002F**\n-     * Show cooldown error message with timer\n-     *\u002F\n-    function showCooldownError($mobutton, messageSelector, remainingTime) {\n-        \n-        \u002F\u002F Find or create message element\n-        let $momessageElem = $mo(messageSelector);\n-        if ($momessageElem.length === 0) {\n-            $momessageElem = $mo('div[id*=\"mo_message\"], #mo_message, .mo_message').first();\n-        }\n-        \n-        \u002F\u002F If still no message element, try to find the form and create one\n-        if ($momessageElem.length === 0) {\n-            const $form = $mobutton.closest('form');\n-            if ($form.length > 0) {\n-                \u002F\u002F Try to find existing message container or create one\n-                $momessageElem = $form.find('[id*=\"mo_message\"], .mo_message').first();\n-                if ($momessageElem.length === 0) {\n-                    \u002F\u002F Create message element\n-                    $momessageElem = $mo('\u003Cdiv id=\"mo_message\" style=\"display:block;\">\u003C\u002Fdiv>');\n-                    $mobutton.before($momessageElem);\n-                }\n-            }\n-        }\n-        \n-        if ($momessageElem.length === 0) {\n-            \u002F\u002F Last resort: create a message element at the button's location\n-            $momessageElem = $mo('\u003Cdiv id=\"mo_message\" style=\"display:block;margin:10px 0;\">\u003C\u002Fdiv>');\n-            $mobutton.before($momessageElem);\n-        }\n-        \n-        \n-        \u002F\u002F Format the error message with timer using USER_IS_BLOCKED_AJAX format\n-        \u002F\u002F Message: \"You have exceeded the limit to send OTP. Please wait for {minutes}:{seconds} minutes\"\n-        const minutes = Math.floor(remainingTime \u002F 60);\n-        const seconds = remainingTime % 60;\n-        const formattedMinutes = String(minutes).padStart(2, '0');\n-        const formattedSeconds = String(seconds).padStart(2, '0');\n-        \n-        \u002F\u002F Use the USER_IS_BLOCKED_AJAX message format\n-        const errorMessage = 'You have exceeded the limit to send OTP. Please wait for ' + \n-                            formattedMinutes + ':' + formattedSeconds + ' minutes';\n-        \n-        \n-        \u002F\u002F Display the error message\n-        $momessageElem.text(errorMessage);\n-        mospApplyMoMessageErrorStyles($momessageElem);\n-        if (!isWcCheckoutPopupMessageDisplay($momessageElem)) {\n-            $momessageElem.css({\n-                'padding': '10px',\n-                'border-radius': '4px',\n-                'margin': '10px 0'\n-            });\n-        }\n-        $momessageElem.show();\n-        \n-        \u002F\u002F Start the timer (use isBlocked=true for error messages)\n-        startBlockTimer(remainingTime, $mobutton, $momessageElem, errorMessage);\n-    }\n-\n-    \u002F**\n-     * Show puzzle popup for AJAX forms when puzzle_required response is received\n-     *\u002F\n-    function showPuzzleForAjaxForm(response) {\n-        \n-        \u002F\u002F Check if puzzle popup HTML exists (should be added by mosp_add_puzzle_popup_to_frontend)\n-        if ($mo('#mo-osp-puzzle-overlay').length === 0) {\n-            console.error('[OSP] Puzzle overlay not found! Make sure puzzle popup HTML is added to frontend.');\n-            \u002F\u002F Show error message to user\n-            const messageElement = findMessageElement();\n-            if (messageElement && messageElement.length > 0) {\n-                const $msgEl = $mo(messageElement);\n-                $msgEl.text('Puzzle verification required but puzzle system is not loaded. Please refresh the page.');\n-                mospApplyMoMessageErrorStyles($msgEl);\n-                $msgEl.show();\n-            }\n-            return;\n-        }\n-        \n-        \u002F\u002F CRITICAL: Ensure puzzle overlay has higher z-index than WooCommerce checkout popup\n-        \u002F\u002F WooCommerce checkout popup uses z-index: 100000, so puzzle needs to be higher\n-        var $puzzleOverlay = $mo('#mo-osp-puzzle-overlay');\n-        $puzzleOverlay.css('z-index', '100001');\n-        \n-        \u002F\u002F Show the puzzle popup\n-        $mo('#mo-osp-puzzle-popup-outer-div').show().css('z-index', '100002');\n-        $puzzleOverlay.removeClass('mo-osp-hidden');\n-        \n-        \u002F\u002F Set up callback for when puzzle is completed\n-        window.MO_OSP_Puzzle_onAjaxSuccess = function(verificationData) {\n-            \n-            \u002F\u002F PRIORITY 1: Try to resubmit stored AJAX request if available\n-            if (window.mo_osp_pending_ajax_request) {\n-                const originalRequest = window.mo_osp_pending_ajax_request;\n-                \n-                \u002F\u002F Add puzzle verification data to the request\n-                let requestData = originalRequest.data;\n-                \n-                \u002F\u002F Handle both string and object data formats\n-                if (typeof requestData === 'string') {\n-                    \u002F\u002F Parse query string and add puzzle data\n-                    const params = new URLSearchParams(requestData);\n-                    params.set('puzzle_verified', 'true');\n-                    if (verificationData && verificationData.puzzle_nonce) {\n-                        params.set('mo_osp_puzzle_nonce', verificationData.puzzle_nonce);\n-                    }\n-                    if (verificationData && verificationData.verification_token) {\n-                        params.set('verification_token', verificationData.verification_token);\n-                    }\n-                    requestData = params.toString();\n-                } else if (typeof requestData === 'object') {\n-                    \u002F\u002F Add puzzle data to object\n-                    requestData.puzzle_verified = 'true';\n-                    if (verificationData && verificationData.puzzle_nonce) {\n-                        requestData.mo_osp_puzzle_nonce = verificationData.puzzle_nonce;\n-                    }\n-                    if (verificationData && verificationData.verification_token) {\n-                        requestData.verification_token = verificationData.verification_token;\n-                    }\n-                }\n-                \n-                \n-                \u002F\u002F Resubmit the original AJAX request with puzzle verification data\n-                $mo.ajax({\n-                    url: originalRequest.url,\n-                    type: originalRequest.type,\n-                    data: requestData,\n-                    dataType: originalRequest.dataType,\n-                    crossDomain: originalRequest.crossDomain,\n-                    success: function(response) {\n-                        \u002F\u002F Call original success callback if it exists\n-                        if (originalRequest.originalSuccess) {\n-                            originalRequest.originalSuccess.call(this, response);\n-                        }\n-                    },\n-                    error: function(jqXHR, textStatus, errorThrown) {\n-                        console.error('[OSP] Resubmitted AJAX request failed:', textStatus, errorThrown);\n-                        \u002F\u002F Call original error callback if it exists\n-                        if (originalRequest.originalError) {\n-                            originalRequest.originalError.call(this, jqXHR, textStatus, errorThrown);\n-                        }\n-                    }\n-                });\n-                \n-                \u002F\u002F Clear stored request\n-                delete window.mo_osp_pending_ajax_request;\n-                return;\n-            }\n-            \n-            \u002F\u002F PRIORITY 2: Fallback to button click if no stored request\n-            const $button = $mo(buttonSelectors.join(',')).filter(':visible').first();\n-            if ($button.length > 0) {\n-                \u002F\u002F Trigger the button click again to resubmit OTP request\n-                \u002F\u002F The puzzle_verified flag will be added by puzzle-system.js\n-                $button.trigger('click');\n-            } else {\n-                console.error('[OSP] Could not find OTP button to resubmit request');\n-                console.error('[OSP] Available buttons:', $mo(buttonSelectors.join(',')).length);\n-                console.error('[OSP] Button selectors:', buttonSelectors);\n-                \n-                \u002F\u002F Last resort: Try to find any form and submit it\n-                const $forms = $mo('form').not('#mo_validate_form').not('#validation_goBack_form').not('#verification_resend_otp_form');\n-                if ($forms.length > 0) {\n-                    const $form = $forms.first();\n-                    \n-                    \u002F\u002F Add puzzle verification data\n-                    if (!$form.find('input[name=\"puzzle_verified\"]').length) {\n-                        $form.append('\u003Cinput type=\"hidden\" name=\"puzzle_verified\" value=\"true\">');\n-                    }\n-                    if (verificationData && verificationData.puzzle_nonce && !$form.find('input[name=\"mo_osp_puzzle_nonce\"]').length) {\n-                        $form.append('\u003Cinput type=\"hidden\" name=\"mo_osp_puzzle_nonce\" value=\"' + verificationData.puzzle_nonce + '\">');\n-                    }\n-                    if (verificationData && verificationData.verification_token && !$form.find('input[name=\"verification_token\"]').length) {\n-                        $form.append('\u003Cinput type=\"hidden\" name=\"verification_token\" value=\"' + verificationData.verification_token + '\">');\n-                    }\n-                    \n-                    $form.submit();\n-                } else {\n-                    console.error('[OSP] No form found either. User may need to manually resubmit.');\n-                }\n-            }\n-        };\n-        \n-        \u002F\u002F Initialize and show puzzle if system is available\n-        if (typeof window.MO_OSP_Puzzle !== 'undefined') {\n-            if (typeof window.MO_OSP_Puzzle.init === 'function' && !window.MO_OSP_Puzzle.initialized) {\n-                window.MO_OSP_Puzzle.init();\n-                window.MO_OSP_Puzzle.initialized = true;\n-            }\n-            window.MO_OSP_Puzzle.showPuzzle({});\n-        } else {\n-            console.error('[OSP] MO_OSP_Puzzle not available yet, waiting...');\n-            \u002F\u002F Wait for puzzle system to load\n-            setTimeout(function() {\n-                if (typeof window.MO_OSP_Puzzle !== 'undefined') {\n-                    if (typeof window.MO_OSP_Puzzle.init === 'function' && !window.MO_OSP_Puzzle.initialized) {\n-                        window.MO_OSP_Puzzle.init();\n-                        window.MO_OSP_Puzzle.initialized = true;\n-                    }\n-                    window.MO_OSP_Puzzle.showPuzzle({});\n-                } else {\n-                    console.error('[OSP] MO_OSP_Puzzle still not available after wait');\n-                    const messageElement = findMessageElement();\n-                    if (messageElement && messageElement.length > 0) {\n-                        const $msgEl = $mo(messageElement);\n-                        $msgEl.text('Puzzle verification required but puzzle system failed to load. Please refresh the page.');\n-                        mospApplyMoMessageErrorStyles($msgEl);\n-                        $msgEl.show();\n-                    }\n-                }\n-            }, 500);\n-        }\n-    }\n-\n-    \u002F**\n-     * Check if puzzle is required before OTP send\n-     *\u002F\n-    function checkPuzzleRequirement() {\n-        return new Promise((resolve, reject) => {\n-            if (typeof mo_osp_ajax === 'undefined') {\n-                resolve({ puzzleRequired: false });\n-                return;\n-            }\n-\n-            $mo.ajax({\n-                url: mo_osp_ajax.ajax_url,\n-                type: 'POST',\n-                data: {\n-                    action: 'mo_osp_check_puzzle_requirement',\n-                    nonce: mo_osp_ajax.nonce,\n-                    mo_osp_browser_id: currentBrowserID,\n-                    email: getEmailFromForm(),\n-                    phone: getPhoneFromForm()\n-                },\n-                success: function(response) {\n-                    if (response.success && response.data) {\n-                        const puzzleRequired = response.data.puzzle_required === true;\n-                        resolve({ puzzleRequired: puzzleRequired });\n-                    } else {\n-                        resolve({ puzzleRequired: false });\n-                    }\n-                },\n-                error: function() {\n-                    reject(new Error('Failed to check puzzle requirement'));\n-                }\n-            });\n-        });\n-    }\n-\n-    \u002F**\n-     * Setup post-OTP handling - intercept AJAX responses and add timers\n-     *\u002F\n-    function setupPostOTPHandling($mobutton, messageSelector) {\n-        mospPrepareOtpButtonForRequest($mobutton);\n-    }\n-\n-    \u002F**\n-     * Intercept AJAX responses to add timers to messages\n-     * This is called globally for all AJAX responses\n-     *\u002F\n-    function interceptAjaxResponse(response, messageElement) {\n-        \n-        if (!response) {\n-            return;\n-        }\n-\n-        \u002F\u002F PRIORITY 0: Handle puzzle_required response for AJAX forms\n-        if (response.result === 'puzzle_required' || response.puzzle_required === true || response.authType === 'PUZZLE_REQUIRED') {\n-            showPuzzleForAjaxForm(response);\n-            return;\n-        }\n-\n-        if (!response.message) {\n-            return;\n-        }\n-\n-        const messageText = response.message;\n-        const isSuccess = response.result === 'success' || response.result === 'SUCCESS';\n-        const isError = response.result === 'error' || response.result === 'ERROR';\n-        \n-        \u002F\u002F Find message element if not provided\n-        if (!messageElement) {\n-            messageElement = findMessageElement();\n-        }\n-        \n-        if (!messageElement || messageElement.length === 0) {\n-            \u002F\u002F Try again after a short delay\n-            setTimeout(function() {\n-                interceptAjaxResponse(response, null);\n-            }, 200);\n-            return;\n-        }\n-        \n-        \n-        \u002F\u002F PRIORITY 1: Handle error\u002Fblocked responses with timer (cooldown\u002Fblock)\n-        \u002F\u002F This should override any existing success messages\n-        \u002F\u002F Check for error response OR error message in text\n-        const isBlockedError = (isError && response.blocked === true && response.remaining_time > 0) ||\n-                               (messageText.includes('exceeded') && messageText.includes('limit') && messageText.match(\u002F\\d+:\\d+\u002F));\n-        \n-        if (isBlockedError) {\n-            const $messageElement = $mo(messageElement);\n-            \n-            \u002F\u002F Extract remaining time from response or message text\n-            let remainingTime = 0;\n-            if (response.remaining_time && response.remaining_time > 0) {\n-                remainingTime = response.remaining_time;\n-            } else {\n-                \u002F\u002F Try to extract from message text\n-                remainingTime = extractTimerFromMessage(messageText);\n-            }\n-            \n-            if (remainingTime \u003C= 0) {\n-                return;\n-            }\n-            \n-            \u002F\u002F Stop any existing timers for this element\n-            if ($messageElement.data('mo-osp-timer-active')) {\n-                \u002F\u002F Clear all active timers\n-                activeTimers.forEach(function(timer) {\n-                    clearInterval(timer);\n-                });\n-                activeTimers = [];\n-            }\n-            \n-            \u002F\u002F Clear any existing timer flags to allow replacement\n-            $messageElement.data('mo-osp-timer-added', false);\n-            $messageElement.data('mo-osp-timer-active', false);\n-            \n-            \u002F\u002F Format the error message with timer\n-            const minutes = Math.floor(remainingTime \u002F 60);\n-            const seconds = remainingTime % 60;\n-            const formattedMinutes = String(minutes).padStart(2, '0');\n-            const formattedSeconds = String(seconds).padStart(2, '0');\n-            \n-            \u002F\u002F Use the message from response, or format it if it has placeholders\n-            let errorMessage = messageText;\n-            if (errorMessage.includes('{minutes}') || errorMessage.includes('{seconds}')) {\n-                errorMessage = errorMessage.replace('{minutes}', formattedMinutes).replace('{seconds}', formattedSeconds);\n-            } else if (!errorMessage.includes(formattedMinutes + ':' + formattedSeconds)) {\n-                \u002F\u002F If message doesn't have timer format, format it\n-                if (errorMessage.includes('exceeded') && errorMessage.includes('limit')) {\n-                    \u002F\u002F Extract the base message (before the timer)\n-                    const baseMessage = errorMessage.replace(\u002F\\d+:\\d+\\s*(?:minute|min)s?\u002Fi, '').trim();\n-                    if (baseMessage.endsWith('Please wait for')) {\n-                        errorMessage = baseMessage.substring(0, baseMessage.lastIndexOf('Please wait for')).trim() + ' Please wait for ' + formattedMinutes + ':' + formattedSeconds + ' minutes';\n-                    } else {\n-                        errorMessage = errorMessage.replace(\u002F\\d+:\\d+\\s*(?:minute|min)s?\u002Fi, formattedMinutes + ':' + formattedSeconds + ' minutes');\n-                    }\n-                } else {\n-                    errorMessage = messageText;\n-                }\n-            }\n-            \n-            \u002F\u002F Replace the message content with error message (overwrite any success message)\n-            $messageElement.text(errorMessage);\n-            mospApplyMoMessageErrorStyles($messageElement);\n-            $messageElement.show();\n-            \n-            \u002F\u002F Set a flag to prevent success message from overwriting this error\n-            $messageElement.data('mo-osp-error-message', true);\n-            \n-            \u002F\u002F Store error response for potential restoration if overwritten\n-            window.mo_osp_last_error_response = {\n-                blocked: true,\n-                message: errorMessage,\n-                remaining_time: remainingTime,\n-                result: 'error'\n-            };\n-            \n-            \u002F\u002F Start the timer (this will check for active timer, but we've cleared it)\n-            const $button = findButtonForMessage(messageElement);\n-            startBlockTimer(remainingTime, $button, messageElement, errorMessage);\n-            return;\n-        }\n-        \n-        \u002F\u002F PRIORITY 2: Check if it's a blocked\u002Ferror message with timer in text\n-        if ((isError || messageText.includes('exceeded')) && messageText.includes('limit')) {\n-            const totalSeconds = extractTimerFromMessage(messageText);\n-            if (totalSeconds > 0) {\n-                const $messageElement = $mo(messageElement);\n-                \u002F\u002F Clear any existing timer flags\n-                $messageElement.data('mo-osp-timer-added', false);\n-                $messageElement.data('mo-osp-timer-active', false);\n-                \u002F\u002F Update message styling for error\n-                mospApplyMoMessageErrorStyles($messageElement);\n-                const $button = findButtonForMessage(messageElement);\n-                startBlockTimer(totalSeconds, $button, messageElement, messageText);\n-                return;\n-            }\n-        }\n-        \n-        \u002F\u002F Check if it's a success message (OTP actually sent — not mismatch\u002Fvalidation copy containing \"OTP\"\u002F\"sent\")\n-        \u002F\u002F But don't process if we have an active error message\n-        const $messageElement = $mo(messageElement);\n-        if (isSuccess && mospMessageMatchesOtpSentResendTimerAllowlist(messageText)) {\n-            \u002F\u002F Check if we have an active error message - if so, don't overwrite it\n-            if ($messageElement.data('mo-osp-error-message')) {\n-                return;\n-            }\n-\n-            if (isWcCheckoutPopupMessageDisplay($messageElement)) {\n-                neutralizeWcCheckoutPopupMessageStyle($messageElement);\n-            }\n-            \n-            \u002F\u002F CRITICAL: Skip cooldown check for external popup responses\n-            \u002F\u002F External popup handles its own success\u002Ferror messages and shouldn't be overwritten\n-            const isExternalPopup = ($messageElement.attr('id') === 'mo_message' && \n-                                    $mo('#mo_site_otp_form').length > 0) ||\n-                                   ($mo('#mo_site_otp_form').length > 0 && \n-                                    $mo('.mo_customer_validation-modal').length > 0);\n-            \n-            if (isExternalPopup) {\n-                return; \u002F\u002F Don't process external popup success messages - let external popup handle them\n-            }\n-            \n-            \u002F\u002F Get actual remaining cooldown time from backend\n-            const email = getEmailFromForm();\n-            const phone = getPhoneFromForm();\n-            \n-            if (typeof mo_osp_ajax !== 'undefined') {\n-                $mo.ajax({\n-                    url: mo_osp_ajax.ajax_url,\n-                    type: 'POST',\n-                    dataType: 'json',\n-                    data: {\n-                        action: 'mo_osp_check_blocked',\n-                        nonce: mo_osp_ajax.nonce,\n-                        mo_osp_browser_id: currentBrowserID,\n-                        email: email,\n-                        phone: phone\n-                    },\n-                    success: function(cooldownResponse) {\n-                        \u002F\u002F Handle WordPress JSON success wrapper\n-                        if (cooldownResponse && cooldownResponse.data) {\n-                            cooldownResponse = cooldownResponse.data;\n-                        }\n-                        \n-                        let cooldownTime = 0;\n-                        \n-                        \u002F\u002F Check if user is on cooldown\n-                        if (cooldownResponse && cooldownResponse.cooldown && cooldownResponse.remaining_time > 0) {\n-                            cooldownTime = parseInt(cooldownResponse.remaining_time);\n-                        } else {\n-                            \u002F\u002F If not on cooldown yet, use default cooldown time\n-                            cooldownTime = (typeof mo_osp_ajax !== 'undefined' && mo_osp_ajax.timer_time) \n-                                ? parseInt(mo_osp_ajax.timer_time) : 60;\n-                        }\n-                        \n-                        if (cooldownTime > 0) {\n-                            const $button = findButtonForMessage(messageElement);\n-                            startCooldownTimer(cooldownTime, $button, messageElement, messageText);\n-                        } else {\n-                            \u002F\u002F No cooldown (likely whitelisted IP), show success message without timer\n-                            const $button = findButtonForMessage(messageElement);\n-                            if ($button && $button.length > 0) {\n-                                $button.show();\n-                            }\n-                            \u002F\u002F Update message to show success without timer text\n-                            if (messageElement && window.verifyOTPmessage) {\n-                                const $message = $mo(messageElement);\n-                                $message.text(window.verifyOTPmessage);\n-                            }\n-                        }\n-                    },\n-                    error: function(xhr, status, error) {\n-                        \u002F\u002F If response is HTML (error page), treat as no cooldown to allow OTP send\n-                        if (xhr.responseText && xhr.responseText.trim().startsWith('\u003C')) {\n-                            \u002F\u002F HTML response - likely an error page, skip timer\n-                            const $button = findButtonForMessage(messageElement);\n-                            if ($button && $button.length > 0) {\n-                                $button.show();\n-                            }\n-                            return;\n-                        }\n-                        \u002F\u002F Fallback to default cooldown time on error\n-                        const defaultTime = (typeof mo_osp_ajax !== 'undefined' && mo_osp_ajax.timer_time) \n-                            ? parseInt(mo_osp_ajax.timer_time) : 60;\n-                        const $button = findButtonForMessage(messageElement);\n-                        startCooldownTimer(defaultTime, $button, messageElement, messageText);\n-                    }\n-                });\n-            } else {\n-                \u002F\u002F Fallback if mo_osp_ajax is not available\n-                const defaultTime = 60;\n-                const $button = findButtonForMessage(messageElement);\n-                startCooldownTimer(defaultTime, $button, messageElement, messageText);\n-            }\n-        }\n-    }\n-\n-    \u002F**\n-     * Find message element using various selectors\n-     *\u002F\n-    function findMessageElement() {\n-        \u002F\u002F Prefer WooCommerce checkout popup message so cooldown\u002Ftimer targets the same node the user sees.\n-        const $wcPopupMsg = $mo('#mo_message_wc_pop_up');\n-        if ($wcPopupMsg.length && $wcPopupMsg.is(':visible')) {\n-            return $wcPopupMsg.first();\n-        }\n-\n-        const selectors = [\n-            'div[id*=\"mo_message\"]',\n-            '#mo_message',\n-            '.mo_message',\n-            '[id*=\"mo_message\"]',\n-            '[class*=\"mo_message\"]'\n-        ];\n-\n-        for (let i = 0; i \u003C selectors.length; i++) {\n-            const $elem = $mo(selectors[i]);\n-            if ($elem.length > 0 && $elem.is(':visible')) {\n-                return $elem.first();\n-            }\n-        }\n-\n-        return null;\n-    }\n-\n-    \u002F**\n-     * Find button associated with message element\n-     *\u002F\n-    function findButtonForMessage($messageElement) {\n-        if (!$messageElement || $messageElement.length === 0) {\n-            return $mo();\n-        }\n-\n-        \u002F\u002F WC block checkout: message is #mo_message_wc_pop_up; do not use the first generic\n-        \u002F\u002F \"Send OTP\" control elsewhere in the checkout form (wrong target for hide\u002Fshow\u002Ftimer).\n-        if (isWcCheckoutPopupMessageDisplay($messageElement)) {\n-            const $wcBtn = $mo('button#miniorange_wc_popup_send_otp_token').first();\n-            if ($wcBtn.length > 0) {\n-                return $wcBtn;\n-            }\n-        }\n-\n-        \u002F\u002F Try to find button near the message element\n-        const $form = $messageElement.closest('form');\n-        if ($form.length > 0) {\n-            for (let i = 0; i \u003C buttonSelectors.length; i++) {\n-                const $button = $form.find(buttonSelectors[i]);\n-                if ($button.length > 0) {\n-                    return $button.first();\n-                }\n-            }\n-        }\n-        \n-        \u002F\u002F Fallback: find any button with waiting flag\n-        return $mo(buttonSelectors.join(', ')).filter(function() {\n-            return $mo(this).data('mo-osp-waiting-for-response') === true;\n-        }).first();\n-    }\n-\n-    \u002F**\n-     * Extract timer from message text\n-     *\u002F\n-    function extractTimerFromMessage(messageText) {\n-        let totalSeconds = 0;\n-        \n-        \u002F\u002F Pattern 1: MM:SS format (handle large numbers like 1430:57)\n-        const timerMatch = messageText.match(\u002F(\\d{1,4}):(\\d{2})\u002F);\n-        if (timerMatch) {\n-            const minutes = parseInt(timerMatch[1]);\n-            const seconds = parseInt(timerMatch[2]);\n-            totalSeconds = (minutes * 60) + seconds;\n-            return totalSeconds;\n-        }\n-        \n-        \u002F\u002F Pattern 2: \"X minutes\" format\n-        const altTimerMatch = messageText.match(\u002F(\\d+)\\s*minutes?\u002Fi);\n-        if (altTimerMatch) {\n-            totalSeconds = parseInt(altTimerMatch[1]) * 60;\n-            return totalSeconds;\n-        }\n-        \n-        \u002F\u002F Pattern 3: \"X seconds\" format\n-        const secTimerMatch = messageText.match(\u002F(\\d+)\\s*seconds?\u002Fi);\n-        if (secTimerMatch) {\n-            totalSeconds = parseInt(secTimerMatch[1]);\n-            return totalSeconds;\n-        }\n-        \n-        return 0;\n-    }\n-\n-    \u002F**\n-     * Start cooldown timer (resendcontrol pattern)\n-     *\u002F\n-    function startCooldownTimer(timeLeft, $mobutton, $momessageElem, message) {\n-        \n-        if ($momessageElem.length > 0) {\n-            $momessageElem.show();\n-            \n-            if (isWcCheckoutPopupMessageDisplay($momessageElem)) {\n-                neutralizeWcCheckoutPopupMessageStyle($momessageElem);\n-            } else {\n-                \u002F\u002F CRITICAL: Preserve success message styling (green background, dark text)\n-                const bgColor = $momessageElem.css('background-color');\n-                const isSuccessMessage = bgColor && (\n-                    bgColor === 'rgb(142, 237, 142)' ||\n-                    bgColor === '#8eed8e' ||\n-                    bgColor.indexOf('142, 237, 142') !== -1 ||\n-                    $momessageElem.css('background-color').indexOf('8eed8e') !== -1\n-                );\n-\n-                if (isSuccessMessage) {\n-                    $momessageElem.css({\n-                        'color': '#464646',\n-                        'background-color': '#8eed8e',\n-                    });\n-                }\n-            }\n-        }\n-        \n-        startTimer(timeLeft, $momessageElem[0], $mobutton, message, false);\n-    }\n-\n-    \u002F**\n-     * Start block timer (resendcontrol pattern)\n-     *\u002F\n-    function startBlockTimer(timeLeft, $mobutton, $momessageElem, message) {\n-        \n-        if ($momessageElem.length > 0) {\n-            $momessageElem.show();\n-        }\n-        \n-        startTimer(timeLeft, $momessageElem[0], $mobutton, message, true);\n-    }\n-\n-    function isWcCheckoutPopupMessageDisplay($display) {\n-        const $d = $mo($display);\n-        return $d.length && $d.attr('id') === 'mo_message_wc_pop_up';\n-    }\n-\n-    \u002F**\n-     * WC block\u002Fclassic checkout popup: success text should have no inline error\u002Fsuccess colors.\n-     *\u002F\n-    function neutralizeWcCheckoutPopupMessageStyle($display) {\n-        const $d = $mo($display);\n-        if (!isWcCheckoutPopupMessageDisplay($d)) {\n-            return;\n-        }\n-        $d.removeAttr('style');\n-        $d.removeData('mo-osp-error-message');\n-    }\n-\n-    \u002F**\n-     * Error styling: WC checkout popup has no pink background; other mo_message containers keep the alert bar.\n-     *\u002F\n-    function mospApplyMoMessageErrorStyles($el) {\n-        const $e = $mo($el);\n-        if (!$e.length) {\n-            return;\n-        }\n-        if (isWcCheckoutPopupMessageDisplay($e)) {\n-            $e.css({\n-                'background-color': 'transparent',\n-                'background': 'none',\n-                'color': '#ff5b5b'\n-            });\n-            return;\n-        }\n-        $e.css({\n-            'background-color': '#ffefef',\n-            'color': '#ff5b5b'\n-        });\n-    }\n-\n-    \u002F**\n-     * Check if message container is plugin-owned.\n-     *\u002F\n-    function isPluginMessageContainer($display) {\n-        if (!$display || $display.length === 0) {\n-            return false;\n-        }\n-        if ($display.is('#mo_message, #mo_message_wc_pop_up, .mo_message')) {\n-            return true;\n-        }\n-        const id = ($display.attr('id') || '').toLowerCase();\n-        if (id.indexOf('mo_message') !== -1) {\n-            return true;\n-        }\n-        const className = ($display.attr('class') || '').toLowerCase();\n-        return className.indexOf('mo_message') !== -1;\n-    }\n-\n-    \u002F**\n-     * Generic timer function (optimized)\n-     *\u002F\n-    function startTimer(duration, display, button, displayMessage, isBlocked) {\n-        if (!display) {\n-            return;\n-        }\n-        \n-        \u002F\u002F If duration is 0 or less, don't start timer (for whitelisted IPs or when no cooldown).\n-        if (duration \u003C= 0) {\n-            const $display = $mo(display);\n-            \u002F\u002F Just show the message without timer\n-            if (displayMessage && window.verifyOTPmessage) {\n-                $display.text(window.verifyOTPmessage);\n-            } else if (displayMessage) {\n-                $display.text(displayMessage);\n-            }\n-            neutralizeWcCheckoutPopupMessageStyle($display);\n-            return;\n-        }\n-        \n-        const $display = $mo(display);\n-        const shouldAppendTimer = isPluginMessageContainer($display);\n-        \n-        \u002F\u002F Check if timer is already active - prevent duplicate timers\n-        if ($display.data('mo-osp-timer-active')) {\n-            return;\n-        }\n-        \n-        \u002F\u002F Mark as having timer to prevent duplicates\n-        $display.data('mo-osp-timer-active', true);\n-        \n-        let timer = duration;\n-        \n-        \u002F\u002F Update immediately\n-        const minutes = String(Math.floor(timer \u002F 60)).padStart(2, '0');\n-        const seconds = String(timer % 60).padStart(2, '0');\n-        const formattedMessage = formatTimerMessage(displayMessage, minutes, seconds, timer, isBlocked, shouldAppendTimer);\n-        $display.text(formattedMessage);\n-        \n-        \u002F\u002F CRITICAL: Set success styling AFTER text update to override any inline styles\n-        if (!isBlocked) {\n-            if (isWcCheckoutPopupMessageDisplay($display)) {\n-                neutralizeWcCheckoutPopupMessageStyle($display);\n-            } else {\n-                const bgColor = $display.css('background-color');\n-                const isSuccessMessage = bgColor && (\n-                    bgColor === 'rgb(142, 237, 142)' ||\n-                    bgColor === '#8eed8e' ||\n-                    bgColor.indexOf('142, 237, 142') !== -1\n-                );\n-\n-                if (isSuccessMessage) {\n-                    \u002F\u002F Override inline styles to ensure correct success styling\n-                    $display.css({\n-                        'color': '#464646', \u002F\u002F Dark green text for better readability on green background\n-                        'background-color': '#8eed8e', \u002F\u002F Green background\n-                    });\n-                }\n-            }\n-        }\n-        \n-        $display.show();\n-\n-        const timerFunction = setInterval(() => {\n-            \u002F\u002F Check if error message flag is set - if so, this timer should stop (error message takes priority)\n-            if ($display.data('mo-osp-error-message') && !isBlocked) {\n-                clearInterval(timerFunction);\n-                const index = activeTimers.indexOf(timerFunction);\n-                if (index > -1) {\n-                    activeTimers.splice(index, 1);\n-                }\n-                return;\n-            }\n-\n-            \u002F\u002F If verification success\u002Ffailure text is now shown, stop resend timer so\n-            \u002F\u002F it cannot overwrite this message with old OTP-sent cooldown content.\n-            const liveText = mospNormalizeMessageText($display.text() || '');\n-            if (!isBlocked && liveText) {\n-                const liveBaseText = liveText.replace(\u002F\\s*you can send the next otp after\\s+\\d{1,2}:\\d{2}\\.?\u002Fgi, '').trim();\n-                const hasOutcomeMessage = mospIsOtpVerificationOutcomeMessage(liveBaseText) || mospIsNegativeOtpFeedbackMessage(liveBaseText);\n-                const isOtpSentCopy = mospMessageMatchesOtpSentResendTimerAllowlist(liveBaseText);\n-                if (hasOutcomeMessage && !isOtpSentCopy) {\n-                    clearInterval(timerFunction);\n-                    $display.data('mo-osp-timer-active', false);\n-                    $display.data('mo-osp-timer-added', false);\n-                    const activeIndex = activeTimers.indexOf(timerFunction);\n-                    if (activeIndex > -1) {\n-                        activeTimers.splice(activeIndex, 1);\n-                    }\n-                    return;\n-                }\n-            }\n-            \n-            timer--;\n-            \n-            if (timer \u003C 0) {\n-                clearInterval(timerFunction);\n-                \n-                \u002F\u002F Remove timer flag\n-                $display.data('mo-osp-timer-active', false);\n-                $display.data('mo-osp-timer-added', false);\n-                $display.data('mo-osp-error-message', false);\n-                \n-                neutralizeWcCheckoutPopupMessageStyle($display);\n-\n-                \u002F\u002F WC checkout popup + success resend cooldown: keep OTP-sent text visible (strip countdown only).\n-                const domEl = display && display.nodeType === 1 ? display : null;\n-                const isWcPopupTarget = !isBlocked && (\n-                    isWcCheckoutPopupMessageDisplay($display) ||\n-                    (domEl && domEl.id === 'mo_message_wc_pop_up')\n-                );\n-                if (isWcPopupTarget) {\n-                    const stripResendLine = function (t) {\n-                        if (!t || typeof t !== 'string') {\n-                            return '';\n-                        }\n-                        return t.replace(\u002F\\s*You can send the next OTP after\\s+\\d{1,2}:\\d{2}\\.?\u002Fgi, '').trim();\n-                    };\n-                    let baseMsg = stripResendLine(typeof displayMessage === 'string' ? mospStripHtml(displayMessage) : '');\n-                    if (!baseMsg && window.verifyOTPmessage) {\n-                        baseMsg = stripResendLine(mospStripHtml(String(window.verifyOTPmessage)));\n-                    }\n-                    if (!baseMsg) {\n-                        baseMsg = stripResendLine($display.text() || '');\n-                    }\n-                    $display.text(baseMsg);\n-                    $display.show();\n-                } else {\n-                    $display.hide();\n-                }\n-                if (button && button.length > 0) {\n-                    mospRestoreOtpButtonAfterRequest($mo(button));\n-                }\n-\n-                \u002F\u002F Remove from active timers\n-                const index = activeTimers.indexOf(timerFunction);\n-                if (index > -1) {\n-                    activeTimers.splice(index, 1);\n-                }\n-                return;\n-            }\n-\n-            const minutes = String(Math.floor(timer \u002F 60)).padStart(2, '0');\n-            const seconds = String(timer % 60).padStart(2, '0');\n-            const formattedMessage = formatTimerMessage(displayMessage, minutes, seconds, timer, isBlocked, shouldAppendTimer);\n-            \n-            \u002F\u002F Always update the message text\n-            \u002F\u002F For error messages (isBlocked=true), we need to update the countdown\n-            \u002F\u002F For success messages, we also need to update the countdown\n-            $display.text(formattedMessage);\n-            \n-            \u002F\u002F CRITICAL: Set success styling AFTER text update to override any inline styles\n-            if (!isBlocked) {\n-                if (isWcCheckoutPopupMessageDisplay($display)) {\n-                    neutralizeWcCheckoutPopupMessageStyle($display);\n-                } else {\n-                    const bgColor = $display.css('background-color');\n-                    const isSuccessMessage = bgColor && (\n-                        bgColor === 'rgb(142, 237, 142)' ||\n-                        bgColor === '#8eed8e' ||\n-                        bgColor.indexOf('142, 237, 142') !== -1\n-                    );\n-\n-                    if (isSuccessMessage) {\n-                        \u002F\u002F Override inline styles to ensure correct success styling\n-                        $display.css({\n-                            'color': '#464646', \u002F\u002F Dark green text for readability\n-                            'background-color': '#8eed8e', \u002F\u002F Green background\n-                        });\n-                    }\n-                }\n-            }\n-        }, 1000);\n-\n-        activeTimers.push(timerFunction);\n-    }\n-\n-    \u002F**\n-     * Plain text from HTML (e.g. verifyOTPmessage may contain markup).\n-     *\u002F\n-    function mospStripHtml(str) {\n-        if (!str || typeof str !== 'string') {\n-            return '';\n-        }\n-        const tmp = document.createElement('div');\n-        tmp.innerHTML = str;\n-        return (tmp.textContent || tmp.innerText || '').trim();\n-    }\n-\n-    \u002F**\n-     * Normalize text for message classification (NBSP, trim).\n-     *\u002F\n-    function mospNormalizeMessageText(text) {\n-        if (!text || typeof text !== 'string') {\n-            return '';\n-        }\n-        return text.replace(\u002F\\u00a0\u002Fg, ' ').replace(\u002F\\s+\u002Fg, ' ').trim();\n-    }\n-\n-    \u002F**\n-     * Validation \u002F mismatch \u002F failure copy — never append resend countdown to these.\n-     *\u002F\n-    function mospIsNegativeOtpFeedbackMessage(text) {\n-        if (!text || typeof text !== 'string') {\n-            return false;\n-        }\n-        const t = mospNormalizeMessageText(text).toLowerCase();\n-        \u002F\u002F Mismatch \u002F comparison errors often contain \"OTP\" and \"sent\" but are not send-success (e.g. WC phone mismatch).\n-        if (\u002F\\b(do\\s+not|does\\s+not|did\\s+not|don'?t)\\s+match\\b\u002F.test(t)) {\n-            return true;\n-        }\n-        if (\u002F\\bnot\\s+match\\b\u002F.test(t) && \u002F\\b(?:otp|phone|email|number|verification|code|contact|submission)\\b\u002F.test(t)) {\n-            return true;\n-        }\n-        if (\u002F\\botp\\b.*\\bsent\\b.*\\bnot\\s+match\\b\u002F.test(t) || \u002F\\bsent\\b.*\\botp\\b.*\\bnot\\s+match\\b\u002F.test(t)) {\n-            return true;\n-        }\n-        \u002F\u002F Normal send-success copy includes \"Please enter the OTP below\" — not a validation failure.\n-        if (\u002F\\b(?:otp|passcode|verification\\s+code|sms\\s+code|code)\\s+has\\s+been\\s+sent\\b\u002F.test(t)) {\n-            return false;\n-        }\n-        if (\u002F\\bhas\\s+been\\s+sent\\s+to\\b\u002F.test(t)) {\n-            return false;\n-        }\n-        return \u002F\\b(mismatch|invalid|incorrect|failed|failure|unsuccessful|wrong\\s+(?:otp|code|number)|expired|verification\\s+failed|not\\s+verified|unable\\s+to|could\\s+not|must\\s+enter|please\\s+enter|is\\s+required|are\\s+required|\\berror\\b|exceeded\\s+the\\s+limit|try\\s+again)\\b\u002Fi.test(t);\n-    }\n-\n-    \u002F**\n-     * OTP verification outcome copy (success\u002Ffailure). If this appears while resend timer\n-     * is running, timer must stop and preserve this message.\n-     *\u002F\n-    function mospIsOtpVerificationOutcomeMessage(text) {\n-        if (!text || typeof text !== 'string') {\n-            return false;\n-        }\n-        const t = mospNormalizeMessageText(text).toLowerCase();\n-        if (!t) {\n-            return false;\n-        }\n-        if (t === 'success' || t === 'otp verified' || t === 'otp verification successful') {\n-            return true;\n-        }\n-        return \u002F\\b(otp|one time passcode|verification\\s+code|code)\\b.*\\b(verified|validated|successful|successfully)\\b\u002F.test(t) ||\n-               \u002F\\b(verified|validated|successful|successfully)\\b.*\\b(otp|one time passcode|verification\\s+code|code)\\b\u002F.test(t);\n-    }\n-\n-    \u002F**\n-     * Messages that may receive the client line: \"You can send the next OTP after MM:SS.\"\n-     * Mirrors default English strings from MoMessages (OTP_SENT_PHONE, OTP_SENT_EMAIL, OTP_SENT, SMS_SENT_SUCCESS).\n-     * Customized admin messages that change wording will not match until they keep the same opening\u002Fclosing phrases.\n-     *\n-     * Not included: LIMIT_OTP_SENT \u002F USER_IS_BLOCKED_* (server already supplies cooldown text);\n-     * CHOOSE_METHOD \u002F mismatch \u002F error strings.\n-     *\u002F\n-    function mospMessageMatchesOtpSentResendTimerAllowlist(text) {\n-        if (!text || typeof text !== 'string') {\n-            return false;\n-        }\n-        if (mospIsNegativeOtpFeedbackMessage(text)) {\n-            return false;\n-        }\n-        const plain = mospStripHtml(String(text));\n-        let base = mospNormalizeMessageText(plain).replace(\u002F\\s+\u002Fg, ' ').trim().toLowerCase();\n-        base = base.replace(\u002F\\s*you can send the next otp after\\s+\\d{1,2}:\\d{2}\\.?\u002Fgi, '').trim();\n-\n-        if (\u002Fclick\\s+here\\s+to\\s+send\\s+otp|send\\s+otp\\s+to\\s+continue\u002Fi.test(base)) {\n-            return false;\n-        }\n-\n-        \u002F\u002F MoMessages::OTP_SENT_PHONE — \"A OTP (One Time Passcode) has been sent to … Please enter the OTP in the field below to verify your phone.\"\n-        if (base.indexOf('a otp (one time passcode) has been sent to ') === 0 &&\n-            base.indexOf('please enter the otp in the field below to verify your phone') !== -1) {\n-            return true;\n-        }\n-\n-        \u002F\u002F MoMessages::OTP_SENT_EMAIL — \"A One Time Passcode has been sent to … Please enter the OTP below to verify your Email Address\"\n-        if (base.indexOf('a one time passcode has been sent to ') === 0 &&\n-            base.indexOf('please enter the otp below to verify your email address') !== -1) {\n-            return true;\n-        }\n-\n-        \u002F\u002F MoMessages::OTP_SENT — \"A passcode has been sent to {{method}}. Please enter the otp below to verify your account.\"\n-        if (base.indexOf('a passcode has been sent to ') === 0 &&\n-            base.indexOf('please enter the otp below to verify your account') !== -1) {\n-            return true;\n-        }\n-\n-        \u002F\u002F MoMessages::SMS_SENT_SUCCESS\n-        if (base === 'sms was sent successfully.' || base === 'sms was sent successfully') {\n-            return true;\n-        }\n-\n-        return false;\n-    }\n-\n-    \u002F**\n-     * Format timer message with countdown (consolidated logic)\n-     *\u002F\n-    function formatTimerMessage(displayMessage, minutes, seconds, totalSeconds, isBlocked, shouldAppendTimer) {\n-        const strippedDisplay = typeof displayMessage === 'string'\n-            ? displayMessage.replace(\u002F\\s*You can send the next OTP after\\s+\\d{1,2}:\\d{2}\\.?\u002Fgi, '').trim()\n-            : displayMessage;\n-        let serverMessage = strippedDisplay || (isBlocked ? 'You are temporarily blocked.' : 'Please wait before requesting another OTP.');\n-\n-        \u002F\u002F Never attach or keep the resend line on validation \u002F mismatch messages.\n-        if (!isBlocked && mospIsNegativeOtpFeedbackMessage(serverMessage)) {\n-            return serverMessage;\n-        }\n-\n-        \u002F\u002F Check if message has placeholder patterns\n-        if (serverMessage.includes('{minutes}') && serverMessage.includes('{seconds}')) {\n-            return serverMessage.replace('{minutes}', minutes).replace('{seconds}', seconds);\n-        }\n-        \n-        \u002F\u002F Check if message already contains timer information (avoid duplication)\n-        if (serverMessage.match(\u002F\\d{1,4}:\\d{2}\\s*(minutes?|mins?)\u002Fi)) {\n-            return serverMessage.replace(\u002F\\d{1,4}:\\d{2}\\s*(minutes?|mins?)\u002Fgi, `${minutes}:${seconds} minutes`);\n-        }\n-        \n-        if (serverMessage.match(\u002F\\d+\\s*(minutes?|mins?)\u002Fi)) {\n-            const totalMinutes = Math.floor(totalSeconds \u002F 60);\n-            return serverMessage.replace(\u002F\\d+\\s*(minutes?|mins?)\u002Fgi, `${totalMinutes} minutes`);\n-        }\n-        \n-        if (serverMessage.match(\u002F\\d+\\s*(seconds?|secs?)\u002Fi)) {\n-            return serverMessage.replace(\u002F\\d+\\s*(seconds?|secs?)\u002Fgi, `${totalSeconds} seconds`);\n-        }\n-        \n-        \u002F\u002F If totalSeconds is 0, don't add timer text (for whitelisted IPs or when no cooldown).\n-        if (totalSeconds \u003C= 0) {\n-            if (!isBlocked && window.verifyOTPmessage) {\n-                return window.verifyOTPmessage;\n-            }\n-            return serverMessage;\n-        }\n-        \n-        \u002F\u002F For cooldown messages, try to use window.verifyOTPmessage if available and no server message\n-        if (!isBlocked && (!strippedDisplay || strippedDisplay.trim() === '') && window.verifyOTPmessage) {\n-            if (!shouldAppendTimer) {\n-                return window.verifyOTPmessage;\n-            }\n-            const verifyPlain = mospStripHtml(String(window.verifyOTPmessage));\n-            if (mospIsNegativeOtpFeedbackMessage(verifyPlain) || !mospMessageMatchesOtpSentResendTimerAllowlist(verifyPlain)) {\n-                return window.verifyOTPmessage;\n-            }\n-            return window.verifyOTPmessage + ` You can send the next OTP after ${minutes}:${seconds}.`;\n-        }\n-        \n-        \u002F\u002F Fallback: add countdown to server message (only for allowlisted OTP-sent copy from MoMessages)\n-        if (!isBlocked) {\n-            if (!shouldAppendTimer || mospIsNegativeOtpFeedbackMessage(serverMessage) || !mospMessageMatchesOtpSentResendTimerAllowlist(serverMessage)) {\n-                return serverMessage;\n-            }\n-            return `${serverMessage} You can send the next OTP after ${minutes}:${seconds}.`;\n-        }\n-        return `${serverMessage} (${minutes}:${seconds} remaining)`;\n-    }\n-\n-    \u002F**\n-     * Show puzzle popup\n-     *\u002F\n-    function showPuzzlePopup() {\n-        if (typeof MO_OSP_Puzzle !== 'undefined') {\n-            \u002F\u002F CRITICAL: Ensure puzzle overlay has higher z-index than WooCommerce checkout popup\n-            \u002F\u002F WooCommerce checkout popup uses z-index: 100000, so puzzle needs to be higher\n-            var $puzzleOverlay = $mo('#mo-osp-puzzle-overlay');\n-            if ($puzzleOverlay.length > 0) {\n-                $puzzleOverlay.css('z-index', '100001');\n-            }\n-            var $puzzlePopup = $mo('#mo-osp-puzzle-popup-outer-div');\n-            if ($puzzlePopup.length > 0) {\n-                $puzzlePopup.css('z-index', '100002');\n-            }\n-            MO_OSP_Puzzle.showPuzzle({});\n-        } else {\n-            alert('Security verification required. Please refresh the page.');\n-        }\n-    }\n-\n-    \u002F**\n-     * Clear all active timers\n-     *\u002F\n-    function clearAllTimers() {\n-        activeTimers.forEach(function(timer) {\n-            clearInterval(timer);\n-        });\n-        activeTimers = [];\n-    }\n-\n-    \u002F**\n-     * Get email from form fields (with phone fallback for consistency)\n-     *\u002F\n-    function getEmailFromForm() {\n-        let email = '';\n-        $mo('input[type=\"email\"], input[name*=\"email\"], input[id*=\"email\"]').each(function() {\n-            const $field = $mo(this);\n-            const type = ($field.attr('type') || '').toLowerCase();\n-            if (type === 'button' || type === 'submit' || type === 'reset') {\n-                return;\n-            }\n-            const value = $field.val();\n-            if (value && !\u002Fsend\\s+otp|verify\\s+otp\u002Fi.test(value)) {\n-                email = value;\n-                return false;\n-            }\n-        });\n-        \n-        \u002F\u002F If no email found, use phone number as email for consistency\n-        if (!email) {\n-            const phone = getPhoneFromForm();\n-            if (phone) {\n-                email = phone;\n-            }\n-        }\n-        return email;\n-    }\n-\n-    \u002F**\n-     * Get phone from form fields\n-     *\u002F\n-    function getPhoneFromForm() {\n-        let phone = '';\n-        $mo('input[type=\"tel\"], input[name*=\"phone\"], input[id*=\"phone\"], input[name*=\"mobile\"]').each(function() {\n-            const $field = $mo(this);\n-            const type = ($field.attr('type') || '').toLowerCase();\n-            if (type === 'button' || type === 'submit' || type === 'reset') {\n-                return;\n-            }\n-            const value = $field.val();\n-            if (!value || \u002Fsend\\s+otp|verify\\s+otp\u002Fi.test(value)) {\n-                return;\n-            }\n-            \u002F\u002F Normalize to digits\u002F+ and require a minimum length to avoid tokens like \"6ff2c895dc\".\n-            const normalized = String(value).replace(\u002F[^0-9+]\u002Fg, '');\n-            const digitCount = normalized.replace(\u002F\\D\u002Fg, '').length;\n-            if (digitCount >= 6) {\n-                phone = normalized;\n-                return false;\n-            }\n-        });\n-        return phone;\n-    }\n-\n-    \u002F**\n-     * Handle puzzle success (called by puzzle system)\n-     *\u002F\n-    window.MO_OSP_SpamPreventer_onPuzzleSuccess = function() {\n-        clearAllTimers();\n-        \n-        \u002F\u002F Set flag to allow OTP to proceed after puzzle completion\n-        window.mo_osp_skip_puzzle_check = true;\n-        setTimeout(function() {\n-            window.mo_osp_skip_puzzle_check = false;\n-        }, 5000);\n-        \n-        \u002F\u002F Clear any existing verifyOTPmessage\n-        if (window.verifyOTPmessage) {\n-            delete window.verifyOTPmessage;\n-        }\n-        \n-        \u002F\u002F Hide any existing messages (never hide WC checkout popup line — id contains substring \"mo_message\")\n-        $mo('div[id*=\"mo_message\"]').not('#mo_message_wc_pop_up').hide();\n-        \n-        \u002F\u002F Show all OTP buttons\n-        buttonSelectors.forEach(function(selector) {\n-            $mo(selector).each(function() {\n-                mospRestoreOtpButtonAfterRequest($mo(this));\n-            });\n-        });\n-    };\n-\n-    \u002F**\n-     * Setup global AJAX response interceptor and message monitor\n-     *\u002F\n-    function setupAjaxInterceptor() {\n-        \u002F\u002F Monitor for message elements that are added or updated\n-        const messageObserver = new MutationObserver(function(mutations) {\n-            mutations.forEach(function(mutation) {\n-                \u002F\u002F Check added nodes\n-                mutation.addedNodes.forEach(function(node) {\n-                    if (node.nodeType === 1) { \u002F\u002F Element node\n-                        const $node = $mo(node);\n-                        const $messageCandidate = $node.is('[id*=\"mo_message\"], .mo_message, [class*=\"mo_message\"]') \n-                            ? $node \n-                            : $node.find('[id*=\"mo_message\"], .mo_message, [class*=\"mo_message\"]').first();\n-                        if ($messageCandidate.length > 0) {\n-                            checkAndAddTimerToMessage($messageCandidate);\n-                        }\n-                    }\n-                });\n-                \n-                \u002F\u002F Check for text changes in existing message elements\n-                if (mutation.type === 'childList' || mutation.type === 'characterData') {\n-                    const target = mutation.target;\n-                    if (target.nodeType === 1) {\n-                        const $target = $mo(target);\n-                        if ($target.is('[id*=\"mo_message\"], .mo_message, [class*=\"mo_message\"]') || \n-                            $target.find('[id*=\"mo_message\"], .mo_message').length > 0) {\n-                            \u002F\u002F Use immediate check (no setTimeout) to catch error messages before they're overwritten\n-                            const $msgElem = $target.is('[id*=\"mo_message\"], .mo_message') ? $target : $target.find('[id*=\"mo_message\"], .mo_message').first();\n-                            if ($msgElem.length > 0) {\n-                                const currentText = $msgElem.text() || '';\n-                                \n-                                \u002F\u002F PRIORITY: Check if error message just appeared\n-                                \u002F\u002F But only process if we haven't already processed this exact message\n-                                if (currentText.includes('exceeded') && currentText.includes('limit')) {\n-                                    const lastProcessedError = $msgElem.data('mo-osp-last-processed-error');\n-                                    const timerActive = $msgElem.data('mo-osp-timer-active');\n-                                    if (lastProcessedError !== currentText || !timerActive) {\n-                                        checkAndAddTimerToMessage($msgElem);\n-                                        return;\n-                                    } else {\n-                                        \u002F\u002F Already processed this error message and timer is active, skip to prevent loop\n-                                        return;\n-                                    }\n-                                }\n-                                \n-                                \u002F\u002F Check if error message flag is set but message was overwritten with success\n-                                if ($msgElem.data('mo-osp-error-message')) {\n-                                    \u002F\u002F If error flag is set but message is success, restore error message\n-                                    if (currentText.includes('sent') && currentText.includes('OTP') && !currentText.includes('exceeded')) {\n-                                        \u002F\u002F The error should have been set by interceptAjaxResponse, but if it was overwritten,\n-                                        \u002F\u002F we need to check if we have the error response stored\n-                                        if (window.mo_osp_last_error_response && window.mo_osp_last_error_response.blocked) {\n-                                            const errorResponse = window.mo_osp_last_error_response;\n-                                            const minutes = Math.floor(errorResponse.remaining_time \u002F 60);\n-                                            const seconds = errorResponse.remaining_time % 60;\n-                                            const formattedMinutes = String(minutes).padStart(2, '0');\n-                                            const formattedSeconds = String(seconds).padStart(2, '0');\n-                                            let errorMessage = errorResponse.message || 'You have exceeded the limit to send OTP. Please wait for ' + \n-                                                formattedMinutes + ':' + formattedSeconds + ' minutes';\n-                                            if (errorMessage.includes('{minutes}') || errorMessage.includes('{seconds}')) {\n-                                                errorMessage = errorMessage.replace('{minutes}', formattedMinutes).replace('{seconds}', formattedSeconds);\n-                                            }\n-                                            $msgElem.text(errorMessage);\n-                                            mospApplyMoMessageErrorStyles($msgElem);\n-                                            \u002F\u002F Clear timer flags and restart timer\n-                                            $msgElem.data('mo-osp-timer-active', false);\n-                                            $msgElem.data('mo-osp-timer-added', false);\n-                                            const $button = findButtonForMessage($msgElem);\n-                                            startBlockTimer(errorResponse.remaining_time, $button, $msgElem, errorMessage);\n-                                            return;\n-                                        }\n-                                    }\n-                                }\n-                                \n-                                \u002F\u002F For other messages, use setTimeout to avoid too many checks\n-                                setTimeout(function() {\n-                                    checkAndAddTimerToMessage($msgElem);\n-                                }, 100);\n-                            }\n-                        }\n-                    }\n-                }\n-            });\n-        });\n-        \n-        \u002F\u002F Start observing the document body for changes\n-        if (document.body) {\n-            messageObserver.observe(document.body, {\n-                childList: true,\n-                subtree: true,\n-                characterData: true\n-            });\n-        }\n-        \n-        \u002F\u002F Also check existing messages periodically (fallback)\n-        setInterval(function() {\n-            const $messages = $mo('[id*=\"mo_message\"], .mo_message');\n-            $messages.each(function() {\n-                const $msg = $mo(this);\n-                if ($msg.is(':visible') && !$msg.data('mo-osp-timer-added')) {\n-                    checkAndAddTimerToMessage($msg);\n-                }\n-            });\n-        }, 500);\n-        \n-        \u002F\u002F Intercept jQuery AJAX responses and store request data for puzzle resubmission\n-        const originalAjax = $mo.ajax;\n-        $mo.ajax = function(options) {\n-            const originalSuccess = options.success;\n-            const originalError = options.error;\n-            \n-            \u002F\u002F Check if this is an OTP-related request by examining URL or data\n-            const isOtpRequest = (options.url && (\n-                options.url.indexOf('admin-ajax.php') !== -1 || \n-                options.url.indexOf('otp') !== -1 ||\n-                options.url.indexOf('miniorange') !== -1\n-            )) || (options.data && (\n-                (typeof options.data === 'string' && (options.data.indexOf('otp') !== -1 || options.data.indexOf('miniorange') !== -1)) ||\n-                (typeof options.data === 'object' && (options.data.action && (\n-                    options.data.action.indexOf('otp') !== -1 || \n-                    options.data.action.indexOf('miniorange') !== -1 ||\n-                    options.data.action === 'mo_external_popup_option'\n-                )))\n-            ));\n-            \n-            \u002F\u002F Wrap success callback to check for puzzle_required\n-            options.success = function(response, textStatus, jqXHR) {\n-                \u002F\u002F Check if this is an external popup request\n-                const isExternalPopupRequest = options.data && (\n-                    (typeof options.data === 'object' && options.data.action === 'mo_external_popup_option') ||\n-                    (typeof options.data === 'string' && options.data.indexOf('mo_external_popup_option') !== -1)\n-                );\n-                \n-                \u002F\u002F Check if puzzle is required - if so, store the request for resubmission\n-                if (isOtpRequest && response && (response.result === 'puzzle_required' || response.puzzle_required === true || response.authType === 'PUZZLE_REQUIRED')) {\n-                    window.mo_osp_pending_ajax_request = {\n-                        url: options.url,\n-                        type: options.type || 'POST',\n-                        data: typeof options.data === 'string' ? options.data : (options.data ? JSON.parse(JSON.stringify(options.data)) : {}),\n-                        dataType: options.dataType || 'json',\n-                        crossDomain: options.crossDomain || false,\n-                        originalSuccess: originalSuccess,\n-                        originalError: originalError\n-                    };\n-                }\n-                \n-                \u002F\u002F CRITICAL: Skip interceptAjaxResponse for external popup success responses\n-                \u002F\u002F External popup handles its own success\u002Ferror messages and shouldn't be overwritten\n-                if (isExternalPopupRequest && response && response.result === 'success') {\n-                    \u002F\u002F Call original success callback without intercepting\n-                    if (originalSuccess) {\n-                        originalSuccess.apply(this, arguments);\n-                    }\n-                    return;\n-                }\n-                \n-                \u002F\u002F Call interceptAjaxResponse if response has message\n-                if (response && (response.message || response.result)) {\n-                    interceptAjaxResponse(response, null);\n-                }\n-                \n-                \u002F\u002F Call original success callback\n-                if (originalSuccess) {\n-                    originalSuccess.apply(this, arguments);\n-                }\n-            };\n-            \n-            \u002F\u002F Wrap error callback\n-            options.error = function(jqXHR, textStatus, errorThrown) {\n-                \u002F\u002F Try to parse error response\n-                try {\n-                    const response = jqXHR.responseJSON || JSON.parse(jqXHR.responseText);\n-                    \n-                    \u002F\u002F Check if puzzle is required in error response\n-                    if (isOtpRequest && response && (response.result === 'puzzle_required' || response.puzzle_required === true || response.authType === 'PUZZLE_REQUIRED')) {\n-                        window.mo_osp_pending_ajax_request = {\n-                            url: options.url,\n-                            type: options.type || 'POST',\n-                            data: typeof options.data === 'string' ? options.data : (options.data ? JSON.parse(JSON.stringify(options.data)) : {}),\n-                            dataType: options.dataType || 'json',\n-                            crossDomain: options.crossDomain || false,\n-                            originalSuccess: originalSuccess,\n-                            originalError: originalError\n-                        };\n-                    }\n-                    \n-                    if (response && (response.message || response.result)) {\n-                        interceptAjaxResponse(response, null);\n-                    }\n-                } catch (e) {\n-                    \u002F\u002F Ignore parse errors\n-                }\n-                \n-                \u002F\u002F Call original error callback\n-                if (originalError) {\n-                    originalError.apply(this, arguments);\n-                }\n-            };\n-            \n-            \u002F\u002F Call original ajax\n-            return originalAjax.apply(this, arguments);\n-        };\n-    }\n-\n-    \u002F**\n-     * Check message element and add timer if needed\n-     *\u002F\n-    function checkAndAddTimerToMessage($messageElement) {\n-        if (!$messageElement || $messageElement.length === 0) {\n-            return;\n-        }\n-        \n-        const messageSelector = '[id*=\"mo_message\"], .mo_message, [class*=\"mo_message\"]';\n-        \u002F\u002F Only act on OTP message containers to avoid corrupting unrelated text\n-        if (!$messageElement.is(messageSelector)) {\n-            const $innerMessage = $messageElement.find(messageSelector).first();\n-            if ($innerMessage.length === 0) {\n-                return;\n-            }\n-            $messageElement = $innerMessage;\n-        }\n-        const messageText = $messageElement.text() || '';\n-        \n-        if (!messageText.trim()) {\n-            return;\n-        }\n-        \n-        \u002F\u002F PRIORITY: Check if it's a blocked\u002Ferror message with timer\n-        \u002F\u002F Error messages should always replace success messages, even if timer is already added\n-        if (messageText.includes('exceeded') && messageText.includes('limit')) {\n-            \n-            const timerActive = $messageElement.data('mo-osp-timer-active');\n-            \n-            \u002F\u002F Check if we've already processed this exact error message to prevent infinite loops\n-            const lastProcessedError = $messageElement.data('mo-osp-last-processed-error');\n-            if (lastProcessedError === messageText && timerActive) {\n-                return;\n-            }\n-            \n-            \u002F\u002F CRITICAL: Set error flag FIRST, then stop timer\n-            \u002F\u002F This ensures that if the timer interval callback is already queued, it will see the flag and stop\n-            $messageElement.data('mo-osp-error-message', true);\n-            $messageElement.data('mo-osp-last-processed-error', messageText);\n-            \n-            \u002F\u002F IMPORTANT: Set the error message text IMMEDIATELY\n-            \u002F\u002F This prevents the success timer (if still running) from overwriting it\n-            \u002F\u002F But only if the current text is different to avoid triggering unnecessary mutations\n-            const currentText = $messageElement.text() || '';\n-            if (currentText !== messageText) {\n-                $messageElement.text(messageText);\n-            }\n-            mospApplyMoMessageErrorStyles($messageElement);\n-            $messageElement.show();\n-            \n-            const totalSeconds = extractTimerFromMessage(messageText);\n-            if (totalSeconds > 0) {\n-                \u002F\u002F Check if interceptAjaxResponse already handled this error message\n-                \u002F\u002F If the timer is active and the error flag is set, interceptAjaxResponse likely already started it\n-                if (timerActive && $messageElement.data('mo-osp-error-message')) {\n-                    return;\n-                }\n-                \n-                \u002F\u002F CRITICAL: ALWAYS stop any active timer when error message is detected\n-                \u002F\u002F The success timer might be running and overwriting the error message\n-                \u002F\u002F We MUST stop it immediately, regardless of what the current text says\n-                if (timerActive) {\n-                    \u002F\u002F Clear all active timers (whether success or error)\n-                    activeTimers.forEach(function(timer) {\n-                        clearInterval(timer);\n-                    });\n-                    activeTimers = [];\n-                    \u002F\u002F Clear flags AFTER clearing timers to ensure clean state\n-                    $messageElement.data('mo-osp-timer-active', false);\n-                    $messageElement.data('mo-osp-timer-added', false);\n-                }\n-                \n-                \u002F\u002F Set timer-added flag (but NOT timer-active - let startTimer set that)\n-                $messageElement.data('mo-osp-timer-added', true);\n-                \n-                \u002F\u002F Store error response for potential restoration if overwritten\n-                window.mo_osp_last_error_response = {\n-                    blocked: true,\n-                    message: messageText,\n-                    remaining_time: totalSeconds,\n-                    result: 'error'\n-                };\n-                \n-                const $button = findButtonForMessage($messageElement);\n-                startBlockTimer(totalSeconds, $button, $messageElement, messageText);\n-                return;\n-            }\n-        }\n-        \n-        \u002F\u002F Success OTP-sent only (MutationObserver): same rules as interceptAjaxResponse — not generic \"sent\"\u002F\"OTP\"\n-        const looksLikeSendPrompt = \u002Fclick\\s+here\\s+to\\s+send\\s+otp|send\\s+otp\u002Fi.test(messageText);\n-        if (mospMessageMatchesOtpSentResendTimerAllowlist(messageText) && !looksLikeSendPrompt &&\n-            !messageText.match(\u002F\\d{1,2}:\\d{2}\\s*(remaining|minutes?|mins?)\u002Fi)) {\n-            \n-            \u002F\u002F Check if we have an active error message - if so, don't process success message\n-            if ($messageElement.data('mo-osp-error-message')) {\n-                return;\n-            }\n-\n-            if (isWcCheckoutPopupMessageDisplay($messageElement)) {\n-                neutralizeWcCheckoutPopupMessageStyle($messageElement);\n-            }\n-            \n-            \u002F\u002F Skip if timer already added (only for success messages)\n-            if ($messageElement.data('mo-osp-timer-added')) {\n-                return;\n-            }\n-            $messageElement.data('mo-osp-timer-added', true);\n-            \n-            \u002F\u002F Get actual remaining cooldown time from backend\n-            const email = getEmailFromForm();\n-            const phone = getPhoneFromForm();\n-            \n-            if (typeof mo_osp_ajax !== 'undefined') {\n-                $mo.ajax({\n-                    url: mo_osp_ajax.ajax_url,\n-                    type: 'POST',\n-                    dataType: 'json',\n-                    data: {\n-                        action: 'mo_osp_check_blocked',\n-                        nonce: mo_osp_ajax.nonce,\n-                        mo_osp_browser_id: currentBrowserID,\n-                        email: email,\n-                        phone: phone\n-                    },\n-                    success: function(cooldownResponse) {\n-                        \u002F\u002F Handle WordPress JSON success wrapper\n-                        if (cooldownResponse && cooldownResponse.data) {\n-                            cooldownResponse = cooldownResponse.data;\n-                        }\n-                        \n-                        let cooldownTime = 0;\n-                        if (cooldownResponse && cooldownResponse.cooldown && cooldownResponse.remaining_time > 0) {\n-                            cooldownTime = parseInt(cooldownResponse.remaining_time);\n-                        } else {\n-                            cooldownTime = (typeof mo_osp_ajax !== 'undefined' && mo_osp_ajax.timer_time) \n-                                ? parseInt(mo_osp_ajax.timer_time) : 60;\n-                        }\n-                        if (cooldownTime > 0) {\n-                            const $button = findButtonForMessage($messageElement);\n-                            startCooldownTimer(cooldownTime, $button, $messageElement, messageText);\n-                        } else {\n-                            \u002F\u002F No cooldown, show button\n-                            const $button = findButtonForMessage($messageElement);\n-                            if ($button && $button.length > 0) {\n-                                mospRestoreOtpButtonAfterRequest($button);\n-                            }\n-                            $messageElement.data('mo-osp-timer-added', false); \u002F\u002F Allow retry\n-                        }\n-                    },\n-                    error: function(xhr, status, error) {\n-                        \u002F\u002F If response is HTML (error page), treat as no cooldown to allow OTP send\n-                        if (xhr.responseText && xhr.responseText.trim().startsWith('\u003C')) {\n-                            \u002F\u002F HTML response - likely an error page, skip timer\n-                            const $button = findButtonForMessage($messageElement);\n-                            if ($button && $button.length > 0) {\n-                                mospRestoreOtpButtonAfterRequest($button);\n-                            }\n-                            $messageElement.data('mo-osp-timer-added', false);\n-                            return;\n-                        }\n-                        const defaultTime = (typeof mo_osp_ajax !== 'undefined' && mo_osp_ajax.timer_time) \n-                            ? parseInt(mo_osp_ajax.timer_time) : 60;\n-                        const $button = findButtonForMessage($messageElement);\n-                        startCooldownTimer(defaultTime, $button, $messageElement, messageText);\n-                    }\n-                });\n-            } else {\n-                \u002F\u002F Fallback if mo_osp_ajax is not available\n-                const defaultTime = 60;\n-                const $button = findButtonForMessage($messageElement);\n-                startCooldownTimer(defaultTime, $button, $messageElement, messageText);\n-            }\n-        }\n-    }\n-\n-    \u002F\u002F Initialize when document is ready (resendcontrol pattern)\n-    $mo(document).ready(function() {\n-        setTimeout(function() {\n-            initializeSpamPreventer();\n-            setupAjaxInterceptor();\n-        }, 100);\n-    });\n-\n-})(jQuery);\n+\u002F**\r\n+ * Fixed OTP Spam Preventer - Proper Integration with Existing OTP Flow\r\n+ * Based on resendcontrol addon patterns\r\n+ *\u002F\r\n+\r\n+(function($mo) {\r\n+    'use strict';\r\n+\r\n+    \u002F\u002F Global variables\r\n+    let activeTimers = [];\r\n+    let isSpamPreventersInitialized = false;\r\n+    let currentBrowserID = '';\r\n+\r\n+    \u002F\u002F Button selectors (matching resendcontrol patterns)\r\n+    const buttonSelectors = [\r\n+        'input[value*=\"Send OTP\"]',\r\n+        'input[value*=\"send otp\"]', \r\n+        'input[value*=\"SEND OTP\"]',\r\n+        'button:contains(\"Send OTP\")',\r\n+        'button:contains(\"send otp\")',\r\n+        'button:contains(\"SEND OTP\")',\r\n+        '#miniorange_otp_token_submit',\r\n+        'input[name=\"miniorange_otp_token_submit\"]',\r\n+        'input[id*=\"send_otp\"]',\r\n+        'input[class*=\"send_otp\"]',\r\n+        'button[id*=\"send_otp\"]',\r\n+        'button[class*=\"send_otp\"]',\r\n+        '#mo_wc_send_otp'\r\n+    ];\r\n+\r\n+    \u002F**\r\n+     * WooCommerce block checkout: popup \"send OTP\" button id contains \"send_otp\" so it matches\r\n+     * button[id*=\"send_otp\"]. Do not hide or disable it — hide() stuck the button; disable() stuck\r\n+     * it when AJAX errors \u002F validation responses did not run our restore paths (user fixes form\r\n+     * and cannot retry). Double-send is acceptable; server enforces limits.\r\n+     *\u002F\r\n+    function isWcBlockCheckoutPopupSendButton($btn) {\r\n+        if (!$btn || !$btn.length) {\r\n+            return false;\r\n+        }\r\n+        if ($btn.attr('id') === 'miniorange_wc_popup_send_otp_token') {\r\n+            return true;\r\n+        }\r\n+        return $btn.closest('#miniorange_wc_popup_send_otp_token').length > 0;\r\n+    }\r\n+\r\n+    function mospPrepareOtpButtonForRequest($mobutton) {\r\n+        if (!$mobutton || !$mobutton.length) {\r\n+            return;\r\n+        }\r\n+        if (isWcBlockCheckoutPopupSendButton($mobutton)) {\r\n+            const $wcPrep = $mobutton.closest('#miniorange_wc_popup_send_otp_token');\r\n+            ($wcPrep.length ? $wcPrep : $mobutton).data('mo-osp-waiting-for-response', true);\r\n+            return;\r\n+        }\r\n+        $mobutton.hide();\r\n+        $mobutton.data('mo-osp-waiting-for-response', true);\r\n+    }\r\n+\r\n+    function mospRestoreOtpButtonAfterRequest($btn) {\r\n+        if (!$btn || !$btn.length) {\r\n+            return;\r\n+        }\r\n+        if (isWcBlockCheckoutPopupSendButton($btn)) {\r\n+            const $wc = $btn.closest('#miniorange_wc_popup_send_otp_token');\r\n+            if ($wc.length) {\r\n+                $wc.prop('disabled', false).css('opacity', '').removeAttr('aria-busy').show();\r\n+                $wc.data('mo-osp-waiting-for-response', false);\r\n+            }\r\n+        } else {\r\n+            $btn.show();\r\n+            $btn.data('mo-osp-waiting-for-response', false);\r\n+        }\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Initialize spam preventer (following resendcontrol pattern)\r\n+     *\u002F\r\n+    function initializeSpamPreventer() {\r\n+        if (isSpamPreventersInitialized) {\r\n+            return;\r\n+        }\r\n+        \r\n+        \u002F\u002F Initialize browser ID\r\n+        initializeBrowserID();\r\n+        \r\n+        \u002F\u002F Wait for any OTP button to appear, then bind events\r\n+        waitForAnyElement(buttonSelectors, function(matchingSelector) {\r\n+            bindSpamPreventionEvents();\r\n+            \r\n+            \u002F\u002F Check if we should auto-trigger Send OTP after puzzle verification\r\n+            checkAndAutoTriggerSendOTP();\r\n+        });\r\n+\r\n+        isSpamPreventersInitialized = true;\r\n+    }\r\n+    \r\n+    \u002F**\r\n+     * Show message after puzzle completion and prompt user to resubmit\r\n+     *\u002F\r\n+    function checkAndAutoTriggerSendOTP() {\r\n+        const puzzleCompleted = sessionStorage.getItem('mo_osp_puzzle_completed');\r\n+        \r\n+        if (puzzleCompleted === 'true') {\r\n+            \r\n+            \u002F\u002F Clear the flag from sessionStorage\r\n+            sessionStorage.removeItem('mo_osp_puzzle_completed');\r\n+        }\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Initialize browser ID for tracking\r\n+     *\u002F\r\n+    function initializeBrowserID() {\r\n+        currentBrowserID = localStorage.getItem('mo_osp_browser_id');\r\n+        \r\n+        if (!currentBrowserID) {\r\n+            currentBrowserID = generateBrowserID();\r\n+            localStorage.setItem('mo_osp_browser_id', currentBrowserID);\r\n+        }\r\n+        \r\n+        \u002F\u002F Make globally available\r\n+        window.mo_osp_browser_id = currentBrowserID;\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Generate unique browser ID\r\n+     *\u002F\r\n+    function generateBrowserID() {\r\n+        const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\r\n+        let result = '';\r\n+        for (let i = 0; i \u003C 8; i++) {\r\n+            result += chars.charAt(Math.floor(Math.random() * chars.length));\r\n+        }\r\n+        return result;\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Wait for any element to appear (resendcontrol pattern)\r\n+     *\u002F\r\n+    function waitForAnyElement(selectors, callback) {\r\n+        const interval = setInterval(() => {\r\n+            const matchingSelector = selectors.find(selector => $mo(selector).length > 0);\r\n+            if (matchingSelector) {\r\n+                clearInterval(interval);\r\n+                callback(matchingSelector);\r\n+            }\r\n+        }, 100);\r\n+\r\n+        setTimeout(() => {\r\n+            clearInterval(interval);\r\n+        }, 3000);\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Bind spam prevention events (following resendcontrol pattern)\r\n+     *\u002F\r\n+    function bindSpamPreventionEvents() {\r\n+        const messageSelector = 'div[id*=\"mo_message\"]';\r\n+        \r\n+        buttonSelectors.forEach(function(buttonSelector) {\r\n+            $mo(buttonSelector).each(function() {\r\n+                const $mobutton = $mo(this);\r\n+                \r\n+                \u002F\u002F Prevent multiple bindings\r\n+                if ($mobutton.data('spam-preventer-bound')) {\r\n+                    return;\r\n+                }\r\n+                $mobutton.data('spam-preventer-bound', true);\r\n+                \r\n+                $mobutton.on('click', function(e) {\r\n+                    \r\n+                    \u002F\u002F CRITICAL: Skip spam prevention checks for external popup buttons\r\n+                    \u002F\u002F External popup handles its own validation and error messages\r\n+                    \u002F\u002F Check at click time since external popup may be dynamically loaded\r\n+                    const isExternalPopupButton = ($mobutton.attr('id') === 'send_otp' && \r\n+                                                  $mo('#mo_site_otp_form').length > 0) ||\r\n+                                                 ($mo('#mo_site_otp_form').length > 0 && \r\n+                                                  $mo('.mo_customer_validation-modal').length > 0 &&\r\n+                                                  ($mobutton.closest('#mo_site_otp_form').length > 0 || \r\n+                                                   $mobutton.closest('.mo_customer_validation-modal').length > 0));\r\n+                    \r\n+                    if (isExternalPopupButton) {\r\n+                        \u002F\u002F Don't prevent default or stop propagation - let external popup handle it\r\n+                        \u002F\u002F Just return without doing anything\r\n+                        return;\r\n+                    }\r\n+                    \r\n+                    \u002F\u002F Check if puzzle was just completed (skip puzzle check, allow OTP to proceed)\r\n+                    if (window.mo_osp_puzzle_just_completed || window.mo_osp_puzzle_verified) {\r\n+                        window.mo_osp_puzzle_just_completed = false; \u002F\u002F Clear the flag\r\n+                        \u002F\u002F Check cooldown even after puzzle completion\r\n+                        checkCooldownBeforeOTPSend($mobutton, messageSelector, e);\r\n+                        return;\r\n+                    }\r\n+                    \r\n+                    \u002F\u002F IMPORTANT: Don't check puzzle requirement if we're currently verifying\r\n+                    \u002F\u002F This prevents redundant AJAX calls during puzzle verification flow\r\n+                    if (window.MO_OSP_Puzzle && window.MO_OSP_Puzzle.isShowing) {\r\n+                        \u002F\u002F Allow default behavior to continue\r\n+                        setupPostOTPHandling($mobutton, messageSelector);\r\n+                        return;\r\n+                    }\r\n+                    \r\n+                    \u002F\u002F First check cooldown, then check puzzle requirement\r\n+                    checkCooldownBeforeOTPSend($mobutton, messageSelector, e).then(function(cooldownResult) {\r\n+                        if (cooldownResult.onCooldown) {\r\n+                            \u002F\u002F Cooldown is active, show error message and prevent OTP send\r\n+                            e.preventDefault();\r\n+                            e.stopImmediatePropagation();\r\n+                            showCooldownError($mobutton, messageSelector, cooldownResult.remainingTime);\r\n+                            return false;\r\n+                        } else {\r\n+                            \u002F\u002F No cooldown, check if puzzle is required\r\n+                            checkPuzzleRequirement().then(function(result) {\r\n+                                if (result.puzzleRequired) {\r\n+                                    e.preventDefault();\r\n+                                    e.stopImmediatePropagation();\r\n+                                    showPuzzlePopup();\r\n+                                    return false;\r\n+                                } else {\r\n+                                    setupPostOTPHandling($mobutton, messageSelector);\r\n+                                }\r\n+                            }).catch(function(error) {\r\n+                                setupPostOTPHandling($mobutton, messageSelector);\r\n+                            });\r\n+                        }\r\n+                    }).catch(function(error) {\r\n+                        \u002F\u002F On error, proceed with normal flow\r\n+                        checkPuzzleRequirement().then(function(result) {\r\n+                            if (result.puzzleRequired) {\r\n+                                e.preventDefault();\r\n+                                e.stopImmediatePropagation();\r\n+                                showPuzzlePopup();\r\n+                                return false;\r\n+                            } else {\r\n+                                setupPostOTPHandling($mobutton, messageSelector);\r\n+                            }\r\n+                        }).catch(function(error2) {\r\n+                            setupPostOTPHandling($mobutton, messageSelector);\r\n+                        });\r\n+                    });\r\n+                });\r\n+            });\r\n+        });\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Check if cooldown is active before OTP send\r\n+     *\u002F\r\n+    function checkCooldownBeforeOTPSend($mobutton, messageSelector, e) {\r\n+        return new Promise((resolve, reject) => {\r\n+            if (typeof mo_osp_ajax === 'undefined') {\r\n+                resolve({ onCooldown: false });\r\n+                return;\r\n+            }\r\n+\r\n+            const email = getEmailFromForm();\r\n+            const phone = getPhoneFromForm();\r\n+\r\n+            $mo.ajax({\r\n+                url: mo_osp_ajax.ajax_url,\r\n+                type: 'POST',\r\n+                dataType: 'json',\r\n+                data: {\r\n+                    action: 'mo_osp_check_blocked',\r\n+                    nonce: mo_osp_ajax.nonce,\r\n+                    mo_osp_browser_id: currentBrowserID,\r\n+                    email: email,\r\n+                    phone: phone\r\n+                },\r\n+                success: function(response) {\r\n+                    \u002F\u002F Handle WordPress JSON success wrapper\r\n+                    if (response && response.data) {\r\n+                        response = response.data;\r\n+                    }\r\n+                    \r\n+                    if (response && response.cooldown && response.remaining_time > 0) {\r\n+                        resolve({ \r\n+                            onCooldown: true, \r\n+                            remainingTime: parseInt(response.remaining_time) \r\n+                        });\r\n+                    } else {\r\n+                        resolve({ onCooldown: false });\r\n+                    }\r\n+                },\r\n+                error: function(xhr, status, error) {\r\n+                    \u002F\u002F If response is HTML (error page), treat as no cooldown to allow OTP send\r\n+                    if (xhr.responseText && xhr.responseText.trim().startsWith('\u003C')) {\r\n+                        resolve({ onCooldown: false });\r\n+                        return;\r\n+                    }\r\n+                    resolve({ onCooldown: false });\r\n+                }\r\n+            });\r\n+        });\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Show cooldown error message with timer\r\n+     *\u002F\r\n+    function showCooldownError($mobutton, messageSelector, remainingTime) {\r\n+        \r\n+        \u002F\u002F Find or create message element\r\n+        let $momessageElem = $mo(messageSelector);\r\n+        if ($momessageElem.length === 0) {\r\n+            $momessageElem = $mo('div[id*=\"mo_message\"], #mo_message, .mo_message').first();\r\n+        }\r\n+        \r\n+        \u002F\u002F If still no message element, try to find the form and create one\r\n+        if ($momessageElem.length === 0) {\r\n+            const $form = $mobutton.closest('form');\r\n+            if ($form.length > 0) {\r\n+                \u002F\u002F Try to find existing message container or create one\r\n+                $momessageElem = $form.find('[id*=\"mo_message\"], .mo_message').first();\r\n+                if ($momessageElem.length === 0) {\r\n+                    \u002F\u002F Create message element\r\n+                    $momessageElem = $mo('\u003Cdiv id=\"mo_message\" style=\"display:block;\">\u003C\u002Fdiv>');\r\n+                    $mobutton.before($momessageElem);\r\n+                }\r\n+            }\r\n+        }\r\n+        \r\n+        if ($momessageElem.length === 0) {\r\n+            \u002F\u002F Last resort: create a message element at the button's location\r\n+            $momessageElem = $mo('\u003Cdiv id=\"mo_message\" style=\"display:block;margin:10px 0;\">\u003C\u002Fdiv>');\r\n+            $mobutton.before($momessageElem);\r\n+        }\r\n+        \r\n+        \r\n+        \u002F\u002F Format the error message with timer using USER_IS_BLOCKED_AJAX format\r\n+        \u002F\u002F Message: \"You have exceeded the limit to send OTP. Please wait for {minutes}:{seconds} minutes\"\r\n+        const minutes = Math.floor(remainingTime \u002F 60);\r\n+        const seconds = remainingTime % 60;\r\n+        const formattedMinutes = String(minutes).padStart(2, '0');\r\n+        const formattedSeconds = String(seconds).padStart(2, '0');\r\n+        \r\n+        \u002F\u002F Use the USER_IS_BLOCKED_AJAX message format\r\n+        const errorMessage = 'You have exceeded the limit to send OTP. Please wait for ' + \r\n+                            formattedMinutes + ':' + formattedSeconds + ' minutes';\r\n+        \r\n+        \r\n+        \u002F\u002F Display the error message\r\n+        $momessageElem.text(errorMessage);\r\n+        mospApplyMoMessageErrorStyles($momessageElem);\r\n+        if (!isWcCheckoutPopupMessageDisplay($momessageElem)) {\r\n+            $momessageElem.css({\r\n+                'padding': '10px',\r\n+                'border-radius': '4px',\r\n+                'margin': '10px 0'\r\n+            });\r\n+        }\r\n+        $momessageElem.show();\r\n+        \r\n+        \u002F\u002F Start the timer (use isBlocked=true for error messages)\r\n+        startBlockTimer(remainingTime, $mobutton, $momessageElem, errorMessage);\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Show puzzle popup for AJAX forms when puzzle_required response is received\r\n+     *\u002F\r\n+    function showPuzzleForAjaxForm(response) {\r\n+        \r\n+        \u002F\u002F Check if puzzle popup HTML exists (should be added by mosp_add_puzzle_popup_to_frontend)\r\n+        if ($mo('#mo-osp-puzzle-overlay').length === 0) {\r\n+            console.error('[OSP] Puzzle overlay not found! Make sure puzzle popup HTML is added to frontend.');\r\n+            \u002F\u002F Show error message to user\r\n+            const messageElement = findMessageElement();\r\n+            if (messageElement && messageElement.length > 0) {\r\n+                const $msgEl = $mo(messageElement);\r\n+                $msgEl.text('Puzzle verification required but puzzle system is not loaded. Please refresh the page.');\r\n+                mospApplyMoMessageErrorStyles($msgEl);\r\n+                $msgEl.show();\r\n+            }\r\n+            return;\r\n+        }\r\n+        \r\n+        \u002F\u002F CRITICAL: Ensure puzzle overlay has higher z-index than WooCommerce checkout popup\r\n+        \u002F\u002F WooCommerce checkout popup uses z-index: 100000, so puzzle needs to be higher\r\n+        var $puzzleOverlay = $mo('#mo-osp-puzzle-overlay');\r\n+        $puzzleOverlay.css('z-index', '100001');\r\n+        \r\n+        \u002F\u002F Show the puzzle popup\r\n+        $mo('#mo-osp-puzzle-popup-outer-div').show().css('z-index', '100002');\r\n+        $puzzleOverlay.removeClass('mo-osp-hidden');\r\n+        \r\n+        \u002F\u002F Set up callback for when puzzle is completed\r\n+        window.MO_OSP_Puzzle_onAjaxSuccess = function(verificationData) {\r\n+            \r\n+            \u002F\u002F PRIORITY 1: Try to resubmit stored AJAX request if available\r\n+            if (window.mo_osp_pending_ajax_request) {\r\n+                const originalRequest = window.mo_osp_pending_ajax_request;\r\n+                \r\n+                \u002F\u002F Add puzzle verification data to the request\r\n+                let requestData = originalRequest.data;\r\n+                \r\n+                \u002F\u002F Handle both string and object data formats\r\n+                if (typeof requestData === 'string') {\r\n+                    \u002F\u002F Parse query string and add puzzle data\r\n+                    const params = new URLSearchParams(requestData);\r\n+                    params.set('puzzle_verified', 'true');\r\n+                    if (verificationData && verificationData.puzzle_nonce) {\r\n+                        params.set('mo_osp_puzzle_nonce', verificationData.puzzle_nonce);\r\n+                    }\r\n+                    if (verificationData && verificationData.verification_token) {\r\n+                        params.set('verification_token', verificationData.verification_token);\r\n+                    }\r\n+                    requestData = params.toString();\r\n+                } else if (typeof requestData === 'object') {\r\n+                    \u002F\u002F Add puzzle data to object\r\n+                    requestData.puzzle_verified = 'true';\r\n+                    if (verificationData && verificationData.puzzle_nonce) {\r\n+                        requestData.mo_osp_puzzle_nonce = verificationData.puzzle_nonce;\r\n+                    }\r\n+                    if (verificationData && verificationData.verification_token) {\r\n+                        requestData.verification_token = verificationData.verification_token;\r\n+                    }\r\n+                }\r\n+                \r\n+                \r\n+                \u002F\u002F Resubmit the original AJAX request with puzzle verification data\r\n+                $mo.ajax({\r\n+                    url: originalRequest.url,\r\n+                    type: originalRequest.type,\r\n+                    data: requestData,\r\n+                    dataType: originalRequest.dataType,\r\n+                    crossDomain: originalRequest.crossDomain,\r\n+                    success: function(response) {\r\n+                        \u002F\u002F Call original success callback if it exists\r\n+                        if (originalRequest.originalSuccess) {\r\n+                            originalRequest.originalSuccess.call(this, response);\r\n+                        }\r\n+                    },\r\n+                    error: function(jqXHR, textStatus, errorThrown) {\r\n+                        console.error('[OSP] Resubmitted AJAX request failed:', textStatus, errorThrown);\r\n+                        \u002F\u002F Call original error callback if it exists\r\n+                        if (originalRequest.originalError) {\r\n+                            originalRequest.originalError.call(this, jqXHR, textStatus, errorThrown);\r\n+                        }\r\n+                    }\r\n+                });\r\n+                \r\n+                \u002F\u002F Clear stored request\r\n+                delete window.mo_osp_pending_ajax_request;\r\n+                return;\r\n+            }\r\n+            \r\n+            \u002F\u002F PRIORITY 2: Fallback to button click if no stored request\r\n+            const $button = $mo(buttonSelectors.join(',')).filter(':visible').first();\r\n+            if ($button.length > 0) {\r\n+                \u002F\u002F Trigger the button click again to resubmit OTP request\r\n+                \u002F\u002F The puzzle_verified flag will be added by puzzle-system.js\r\n+                $button.trigger('click');\r\n+            } else {\r\n+                console.error('[OSP] Could not find OTP button to resubmit request');\r\n+                console.error('[OSP] Available buttons:', $mo(buttonSelectors.join(',')).length);\r\n+                console.error('[OSP] Button selectors:', buttonSelectors);\r\n+                \r\n+                \u002F\u002F Last resort: Try to find any form and submit it\r\n+                const $forms = $mo('form').not('#mo_validate_form').not('#validation_goBack_form').not('#verification_resend_otp_form');\r\n+                if ($forms.length > 0) {\r\n+                    const $form = $forms.first();\r\n+                    \r\n+                    \u002F\u002F Add puzzle verification data\r\n+                    if (!$form.find('input[name=\"puzzle_verified\"]').length) {\r\n+                        $form.append('\u003Cinput type=\"hidden\" name=\"puzzle_verified\" value=\"true\">');\r\n+                    }\r\n+                    if (verificationData && verificationData.puzzle_nonce && !$form.find('input[name=\"mo_osp_puzzle_nonce\"]').length) {\r\n+                        $form.append('\u003Cinput type=\"hidden\" name=\"mo_osp_puzzle_nonce\" value=\"' + verificationData.puzzle_nonce + '\">');\r\n+                    }\r\n+                    if (verificationData && verificationData.verification_token && !$form.find('input[name=\"verification_token\"]').length) {\r\n+                        $form.append('\u003Cinput type=\"hidden\" name=\"verification_token\" value=\"' + verificationData.verification_token + '\">');\r\n+                    }\r\n+                    \r\n+                    $form.submit();\r\n+                } else {\r\n+                    console.error('[OSP] No form found either. User may need to manually resubmit.');\r\n+                }\r\n+            }\r\n+        };\r\n+        \r\n+        \u002F\u002F Initialize and show puzzle if system is available\r\n+        if (typeof window.MO_OSP_Puzzle !== 'undefined') {\r\n+            if (typeof window.MO_OSP_Puzzle.init === 'function' && !window.MO_OSP_Puzzle.initialized) {\r\n+                window.MO_OSP_Puzzle.init();\r\n+                window.MO_OSP_Puzzle.initialized = true;\r\n+            }\r\n+            window.MO_OSP_Puzzle.showPuzzle({});\r\n+        } else {\r\n+            console.error('[OSP] MO_OSP_Puzzle not available yet, waiting...');\r\n+            \u002F\u002F Wait for puzzle system to load\r\n+            setTimeout(function() {\r\n+                if (typeof window.MO_OSP_Puzzle !== 'undefined') {\r\n+                    if (typeof window.MO_OSP_Puzzle.init === 'function' && !window.MO_OSP_Puzzle.initialized) {\r\n+                        window.MO_OSP_Puzzle.init();\r\n+                        window.MO_OSP_Puzzle.initialized = true;\r\n+                    }\r\n+                    window.MO_OSP_Puzzle.showPuzzle({});\r\n+                } else {\r\n+                    console.error('[OSP] MO_OSP_Puzzle still not available after wait');\r\n+                    const messageElement = findMessageElement();\r\n+                    if (messageElement && messageElement.length > 0) {\r\n+                        const $msgEl = $mo(messageElement);\r\n+                        $msgEl.text('Puzzle verification required but puzzle system failed to load. Please refresh the page.');\r\n+                        mospApplyMoMessageErrorStyles($msgEl);\r\n+                        $msgEl.show();\r\n+                    }\r\n+                }\r\n+            }, 500);\r\n+        }\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Check if puzzle is required before OTP send\r\n+     *\u002F\r\n+    function checkPuzzleRequirement() {\r\n+        return new Promise((resolve, reject) => {\r\n+            if (typeof mo_osp_ajax === 'undefined') {\r\n+                resolve({ puzzleRequired: false });\r\n+                return;\r\n+            }\r\n+\r\n+            $mo.ajax({\r\n+                url: mo_osp_ajax.ajax_url,\r\n+                type: 'POST',\r\n+                data: {\r\n+                    action: 'mo_osp_check_puzzle_requirement',\r\n+                    nonce: mo_osp_ajax.nonce,\r\n+                    mo_osp_browser_id: currentBrowserID,\r\n+                    email: getEmailFromForm(),\r\n+                    phone: getPhoneFromForm()\r\n+                },\r\n+                success: function(response) {\r\n+                    if (response.success && response.data) {\r\n+                        const puzzleRequired = response.data.puzzle_required === true;\r\n+                        resolve({ puzzleRequired: puzzleRequired });\r\n+                    } else {\r\n+                        resolve({ puzzleRequired: false });\r\n+                    }\r\n+                },\r\n+                error: function() {\r\n+                    reject(new Error('Failed to check puzzle requirement'));\r\n+                }\r\n+            });\r\n+        });\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Setup post-OTP handling - intercept AJAX responses and add timers\r\n+     *\u002F\r\n+    function setupPostOTPHandling($mobutton, messageSelector) {\r\n+        mospPrepareOtpButtonForRequest($mobutton);\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Intercept AJAX responses to add timers to messages\r\n+     * This is called globally for all AJAX responses\r\n+     *\u002F\r\n+    function interceptAjaxResponse(response, messageElement) {\r\n+        \r\n+        if (!response) {\r\n+            return;\r\n+        }\r\n+\r\n+        \u002F\u002F PRIORITY 0: Handle puzzle_required response for AJAX forms\r\n+        if (response.result === 'puzzle_required' || response.puzzle_required === true || response.authType === 'PUZZLE_REQUIRED') {\r\n+            showPuzzleForAjaxForm(response);\r\n+            return;\r\n+        }\r\n+\r\n+        if (!response.message) {\r\n+            return;\r\n+        }\r\n+\r\n+        const messageText = response.message;\r\n+        const isSuccess = response.result === 'success' || response.result === 'SUCCESS';\r\n+        const isError = response.result === 'error' || response.result === 'ERROR';\r\n+        \r\n+        \u002F\u002F Find message element if not provided\r\n+        if (!messageElement) {\r\n+            messageElement = findMessageElement();\r\n+        }\r\n+        \r\n+        if (!messageElement || messageElement.length === 0) {\r\n+            \u002F\u002F Try again after a short delay\r\n+            setTimeout(function() {\r\n+                interceptAjaxResponse(response, null);\r\n+            }, 200);\r\n+            return;\r\n+        }\r\n+        \r\n+        \r\n+        \u002F\u002F PRIORITY 1: Handle error\u002Fblocked responses with timer (cooldown\u002Fblock)\r\n+        \u002F\u002F This should override any existing success messages\r\n+        \u002F\u002F Check for error response OR error message in text\r\n+        const isBlockedError = (isError && response.blocked === true && response.remaining_time > 0) ||\r\n+                               (messageText.includes('exceeded') && messageText.includes('limit') && messageText.match(\u002F\\d+:\\d+\u002F));\r\n+        \r\n+        if (isBlockedError) {\r\n+            const $messageElement = $mo(messageElement);\r\n+            \r\n+            \u002F\u002F Extract remaining time from response or message text\r\n+            let remainingTime = 0;\r\n+            if (response.remaining_time && response.remaining_time > 0) {\r\n+                remainingTime = response.remaining_time;\r\n+            } else {\r\n+                \u002F\u002F Try to extract from message text\r\n+                remainingTime = extractTimerFromMessage(messageText);\r\n+            }\r\n+            \r\n+            if (remainingTime \u003C= 0) {\r\n+                return;\r\n+            }\r\n+            \r\n+            \u002F\u002F Stop any existing timers for this element\r\n+            if ($messageElement.data('mo-osp-timer-active')) {\r\n+                \u002F\u002F Clear all active timers\r\n+                activeTimers.forEach(function(timer) {\r\n+                    clearInterval(timer);\r\n+                });\r\n+                activeTimers = [];\r\n+            }\r\n+            \r\n+            \u002F\u002F Clear any existing timer flags to allow replacement\r\n+            $messageElement.data('mo-osp-timer-added', false);\r\n+            $messageElement.data('mo-osp-timer-active', false);\r\n+            \r\n+            \u002F\u002F Format the error message with timer\r\n+            const minutes = Math.floor(remainingTime \u002F 60);\r\n+            const seconds = remainingTime % 60;\r\n+            const formattedMinutes = String(minutes).padStart(2, '0');\r\n+            const formattedSeconds = String(seconds).padStart(2, '0');\r\n+            \r\n+            \u002F\u002F Use the message from response, or format it if it has placeholders\r\n+            let errorMessage = messageText;\r\n+            if (errorMessage.includes('{minutes}') || errorMessage.includes('{seconds}')) {\r\n+                errorMessage = errorMessage.replace('{minutes}', formattedMinutes).replace('{seconds}', formattedSeconds);\r\n+            } else if (!errorMessage.includes(formattedMinutes + ':' + formattedSeconds)) {\r\n+                \u002F\u002F If message doesn't have timer format, format it\r\n+                if (errorMessage.includes('exceeded') && errorMessage.includes('limit')) {\r\n+                    \u002F\u002F Extract the base message (before the timer)\r\n+                    const baseMessage = errorMessage.replace(\u002F\\d+:\\d+\\s*(?:minute|min)s?\u002Fi, '').trim();\r\n+                    if (baseMessage.endsWith('Please wait for')) {\r\n+                        errorMessage = baseMessage.substring(0, baseMessage.lastIndexOf('Please wait for')).trim() + ' Please wait for ' + formattedMinutes + ':' + formattedSeconds + ' minutes';\r\n+                    } else {\r\n+                        errorMessage = errorMessage.replace(\u002F\\d+:\\d+\\s*(?:minute|min)s?\u002Fi, formattedMinutes + ':' + formattedSeconds + ' minutes');\r\n+                    }\r\n+                } else {\r\n+                    errorMessage = messageText;\r\n+                }\r\n+            }\r\n+            \r\n+            \u002F\u002F Replace the message content with error message (overwrite any success message)\r\n+            $messageElement.text(errorMessage);\r\n+            mospApplyMoMessageErrorStyles($messageElement);\r\n+            $messageElement.show();\r\n+            \r\n+            \u002F\u002F Set a flag to prevent success message from overwriting this error\r\n+            $messageElement.data('mo-osp-error-message', true);\r\n+            \r\n+            \u002F\u002F Store error response for potential restoration if overwritten\r\n+            window.mo_osp_last_error_response = {\r\n+                blocked: true,\r\n+                message: errorMessage,\r\n+                remaining_time: remainingTime,\r\n+                result: 'error'\r\n+            };\r\n+            \r\n+            \u002F\u002F Start the timer (this will check for active timer, but we've cleared it)\r\n+            const $button = findButtonForMessage(messageElement);\r\n+            startBlockTimer(remainingTime, $button, messageElement, errorMessage);\r\n+            return;\r\n+        }\r\n+        \r\n+        \u002F\u002F PRIORITY 2: Check if it's a blocked\u002Ferror message with timer in text\r\n+        if ((isError || messageText.includes('exceeded')) && messageText.includes('limit')) {\r\n+            const totalSeconds = extractTimerFromMessage(messageText);\r\n+            if (totalSeconds > 0) {\r\n+                const $messageElement = $mo(messageElement);\r\n+                \u002F\u002F Clear any existing timer flags\r\n+                $messageElement.data('mo-osp-timer-added', false);\r\n+                $messageElement.data('mo-osp-timer-active', false);\r\n+                \u002F\u002F Update message styling for error\r\n+                mospApplyMoMessageErrorStyles($messageElement);\r\n+                const $button = findButtonForMessage(messageElement);\r\n+                startBlockTimer(totalSeconds, $button, messageElement, messageText);\r\n+                return;\r\n+            }\r\n+        }\r\n+        \r\n+        \u002F\u002F Check if it's a success message (OTP actually sent — not mismatch\u002Fvalidation copy containing \"OTP\"\u002F\"sent\")\r\n+        \u002F\u002F But don't process if we have an active error message\r\n+        const $messageElement = $mo(messageElement);\r\n+        if (isSuccess && mospMessageMatchesOtpSentResendTimerAllowlist(messageText)) {\r\n+            \u002F\u002F Check if we have an active error message - if so, don't overwrite it\r\n+            if ($messageElement.data('mo-osp-error-message')) {\r\n+                return;\r\n+            }\r\n+\r\n+            if (isWcCheckoutPopupMessageDisplay($messageElement)) {\r\n+                neutralizeWcCheckoutPopupMessageStyle($messageElement);\r\n+            }\r\n+            \r\n+            \u002F\u002F CRITICAL: Skip cooldown check for external popup responses\r\n+            \u002F\u002F External popup handles its own success\u002Ferror messages and shouldn't be overwritten\r\n+            const isExternalPopup = ($messageElement.attr('id') === 'mo_message' && \r\n+                                    $mo('#mo_site_otp_form').length > 0) ||\r\n+                                   ($mo('#mo_site_otp_form').length > 0 && \r\n+                                    $mo('.mo_customer_validation-modal').length > 0);\r\n+            \r\n+            if (isExternalPopup) {\r\n+                return; \u002F\u002F Don't process external popup success messages - let external popup handle them\r\n+            }\r\n+            \r\n+            \u002F\u002F Get actual remaining cooldown time from backend\r\n+            const email = getEmailFromForm();\r\n+            const phone = getPhoneFromForm();\r\n+            \r\n+            if (typeof mo_osp_ajax !== 'undefined') {\r\n+                $mo.ajax({\r\n+                    url: mo_osp_ajax.ajax_url,\r\n+                    type: 'POST',\r\n+                    dataType: 'json',\r\n+                    data: {\r\n+                        action: 'mo_osp_check_blocked',\r\n+                        nonce: mo_osp_ajax.nonce,\r\n+                        mo_osp_browser_id: currentBrowserID,\r\n+                        email: email,\r\n+                        phone: phone\r\n+                    },\r\n+                    success: function(cooldownResponse) {\r\n+                        \u002F\u002F Handle WordPress JSON success wrapper\r\n+                        if (cooldownResponse && cooldownResponse.data) {\r\n+                            cooldownResponse = cooldownResponse.data;\r\n+                        }\r\n+                        \r\n+                        let cooldownTime = 0;\r\n+                        \r\n+                        \u002F\u002F Check if user is on cooldown\r\n+                        if (cooldownResponse && cooldownResponse.cooldown && cooldownResponse.remaining_time > 0) {\r\n+                            cooldownTime = parseInt(cooldownResponse.remaining_time);\r\n+                        } else {\r\n+                            \u002F\u002F If not on cooldown yet, use default cooldown time\r\n+                            cooldownTime = (typeof mo_osp_ajax !== 'undefined' && mo_osp_ajax.timer_time) \r\n+                                ? parseInt(mo_osp_ajax.timer_time) : 60;\r\n+                        }\r\n+                        \r\n+                        if (cooldownTime > 0) {\r\n+                            const $button = findButtonForMessage(messageElement);\r\n+                            startCooldownTimer(cooldownTime, $button, messageElement, messageText);\r\n+                        } else {\r\n+                            \u002F\u002F No cooldown (likely whitelisted IP), show success message without timer\r\n+                            const $button = findButtonForMessage(messageElement);\r\n+                            if ($button && $button.length > 0) {\r\n+                                $button.show();\r\n+                            }\r\n+                            \u002F\u002F Update message to show success without timer text\r\n+                            if (messageElement && window.verifyOTPmessage) {\r\n+                                const $message = $mo(messageElement);\r\n+                                $message.text(window.verifyOTPmessage);\r\n+                            }\r\n+                        }\r\n+                    },\r\n+                    error: function(xhr, status, error) {\r\n+                        \u002F\u002F If response is HTML (error page), treat as no cooldown to allow OTP send\r\n+                        if (xhr.responseText && xhr.responseText.trim().startsWith('\u003C')) {\r\n+                            \u002F\u002F HTML response - likely an error page, skip timer\r\n+                            const $button = findButtonForMessage(messageElement);\r\n+                            if ($button && $button.length > 0) {\r\n+                                $button.show();\r\n+                            }\r\n+                            return;\r\n+                        }\r\n+                        \u002F\u002F Fallback to default cooldown time on error\r\n+                        const defaultTime = (typeof mo_osp_ajax !== 'undefined' && mo_osp_ajax.timer_time) \r\n+                            ? parseInt(mo_osp_ajax.timer_time) : 60;\r\n+                        const $button = findButtonForMessage(messageElement);\r\n+                        startCooldownTimer(defaultTime, $button, messageElement, messageText);\r\n+                    }\r\n+                });\r\n+            } else {\r\n+                \u002F\u002F Fallback if mo_osp_ajax is not available\r\n+                const defaultTime = 60;\r\n+                const $button = findButtonForMessage(messageElement);\r\n+                startCooldownTimer(defaultTime, $button, messageElement, messageText);\r\n+            }\r\n+        }\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Find message element using various selectors\r\n+     *\u002F\r\n+    function findMessageElement() {\r\n+        \u002F\u002F Prefer WooCommerce checkout popup message so cooldown\u002Ftimer targets the same node the user sees.\r\n+        const $wcPopupMsg = $mo('#mo_message_wc_pop_up');\r\n+        if ($wcPopupMsg.length && $wcPopupMsg.is(':visible')) {\r\n+            return $wcPopupMsg.first();\r\n+        }\r\n+\r\n+        const selectors = [\r\n+            'div[id*=\"mo_message\"]',\r\n+            '#mo_message',\r\n+            '.mo_message',\r\n+            '[id*=\"mo_message\"]',\r\n+            '[class*=\"mo_message\"]'\r\n+        ];\r\n+\r\n+        for (let i = 0; i \u003C selectors.length; i++) {\r\n+            const $elem = $mo(selectors[i]);\r\n+            if ($elem.length > 0 && $elem.is(':visible')) {\r\n+                return $elem.first();\r\n+            }\r\n+        }\r\n+\r\n+        return null;\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Find button associated with message element\r\n+     *\u002F\r\n+    function findButtonForMessage($messageElement) {\r\n+        if (!$messageElement || $messageElement.length === 0) {\r\n+            return $mo();\r\n+        }\r\n+\r\n+        \u002F\u002F WC block checkout: message is #mo_message_wc_pop_up; do not use the first generic\r\n+        \u002F\u002F \"Send OTP\" control elsewhere in the checkout form (wrong target for hide\u002Fshow\u002Ftimer).\r\n+        if (isWcCheckoutPopupMessageDisplay($messageElement)) {\r\n+            const $wcBtn = $mo('button#miniorange_wc_popup_send_otp_token').first();\r\n+            if ($wcBtn.length > 0) {\r\n+                return $wcBtn;\r\n+            }\r\n+        }\r\n+\r\n+        \u002F\u002F Try to find button near the message element\r\n+        const $form = $messageElement.closest('form');\r\n+        if ($form.length > 0) {\r\n+            for (let i = 0; i \u003C buttonSelectors.length; i++) {\r\n+                const $button = $form.find(buttonSelectors[i]);\r\n+                if ($button.length > 0) {\r\n+                    return $button.first();\r\n+                }\r\n+            }\r\n+        }\r\n+        \r\n+        \u002F\u002F Fallback: find any button with waiting flag\r\n+        return $mo(buttonSelectors.join(', ')).filter(function() {\r\n+            return $mo(this).data('mo-osp-waiting-for-response') === true;\r\n+        }).first();\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Extract timer from message text\r\n+     *\u002F\r\n+    function extractTimerFromMessage(messageText) {\r\n+        let totalSeconds = 0;\r\n+        \r\n+        \u002F\u002F Pattern 1: MM:SS format (handle large numbers like 1430:57)\r\n+        const timerMatch = messageText.match(\u002F(\\d{1,4}):(\\d{2})\u002F);\r\n+        if (timerMatch) {\r\n+            const minutes = parseInt(timerMatch[1]);\r\n+            const seconds = parseInt(timerMatch[2]);\r\n+            totalSeconds = (minutes * 60) + seconds;\r\n+            return totalSeconds;\r\n+        }\r\n+        \r\n+        \u002F\u002F Pattern 2: \"X minutes\" format\r\n+        const altTimerMatch = messageText.match(\u002F(\\d+)\\s*minutes?\u002Fi);\r\n+        if (altTimerMatch) {\r\n+            totalSeconds = parseInt(altTimerMatch[1]) * 60;\r\n+            return totalSeconds;\r\n+        }\r\n+        \r\n+        \u002F\u002F Pattern 3: \"X seconds\" format\r\n+        const secTimerMatch = messageText.match(\u002F(\\d+)\\s*seconds?\u002Fi);\r\n+        if (secTimerMatch) {\r\n+            totalSeconds = parseInt(secTimerMatch[1]);\r\n+            return totalSeconds;\r\n+        }\r\n+        \r\n+        return 0;\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Start cooldown timer (resendcontrol pattern)\r\n+     *\u002F\r\n+    function startCooldownTimer(timeLeft, $mobutton, $momessageElem, message) {\r\n+        \r\n+        if ($momessageElem.length > 0) {\r\n+            $momessageElem.show();\r\n+            \r\n+            if (isWcCheckoutPopupMessageDisplay($momessageElem)) {\r\n+                neutralizeWcCheckoutPopupMessageStyle($momessageElem);\r\n+            } else {\r\n+                \u002F\u002F CRITICAL: Preserve success message styling (green background, dark text)\r\n+                const bgColor = $momessageElem.css('background-color');\r\n+                const isSuccessMessage = bgColor && (\r\n+                    bgColor === 'rgb(142, 237, 142)' ||\r\n+                    bgColor === '#8eed8e' ||\r\n+                    bgColor.indexOf('142, 237, 142') !== -1 ||\r\n+                    $momessageElem.css('background-color').indexOf('8eed8e') !== -1\r\n+                );\r\n+\r\n+                if (isSuccessMessage) {\r\n+                    $momessageElem.css({\r\n+                        'color': '#464646',\r\n+                        'background-color': '#8eed8e',\r\n+                    });\r\n+                }\r\n+            }\r\n+        }\r\n+        \r\n+        startTimer(timeLeft, $momessageElem[0], $mobutton, message, false);\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Start block timer (resendcontrol pattern)\r\n+     *\u002F\r\n+    function startBlockTimer(timeLeft, $mobutton, $momessageElem, message) {\r\n+        \r\n+        if ($momessageElem.length > 0) {\r\n+            $momessageElem.show();\r\n+        }\r\n+        \r\n+        startTimer(timeLeft, $momessageElem[0], $mobutton, message, true);\r\n+    }\r\n+\r\n+    function isWcCheckoutPopupMessageDisplay($display) {\r\n+        const $d = $mo($display);\r\n+        return $d.length && $d.attr('id') === 'mo_message_wc_pop_up';\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * WC block\u002Fclassic checkout popup: success text should have no inline error\u002Fsuccess colors.\r\n+     *\u002F\r\n+    function neutralizeWcCheckoutPopupMessageStyle($display) {\r\n+        const $d = $mo($display);\r\n+        if (!isWcCheckoutPopupMessageDisplay($d)) {\r\n+            return;\r\n+        }\r\n+        $d.removeAttr('style');\r\n+        $d.removeData('mo-osp-error-message');\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Error styling: WC checkout popup has no pink background; other mo_message containers keep the alert bar.\r\n+     *\u002F\r\n+    function mospApplyMoMessageErrorStyles($el) {\r\n+        const $e = $mo($el);\r\n+        if (!$e.length) {\r\n+            return;\r\n+        }\r\n+        if (isWcCheckoutPopupMessageDisplay($e)) {\r\n+            $e.css({\r\n+                'background-color': 'transparent',\r\n+                'background': 'none',\r\n+                'color': '#ff5b5b'\r\n+            });\r\n+            return;\r\n+        }\r\n+        $e.css({\r\n+            'background-color': '#ffefef',\r\n+            'color': '#ff5b5b'\r\n+        });\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Check if message container is plugin-owned.\r\n+     *\u002F\r\n+    function isPluginMessageContainer($display) {\r\n+        if (!$display || $display.length === 0) {\r\n+            return false;\r\n+        }\r\n+        if ($display.is('#mo_message, #mo_message_wc_pop_up, .mo_message')) {\r\n+            return true;\r\n+        }\r\n+        const id = ($display.attr('id') || '').toLowerCase();\r\n+        if (id.indexOf('mo_message') !== -1) {\r\n+            return true;\r\n+        }\r\n+        const className = ($display.attr('class') || '').toLowerCase();\r\n+        return className.indexOf('mo_message') !== -1;\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Generic timer function (optimized)\r\n+     *\u002F\r\n+    function startTimer(duration, display, button, displayMessage, isBlocked) {\r\n+        if (!display) {\r\n+            return;\r\n+        }\r\n+        \r\n+        \u002F\u002F If duration is 0 or less, don't start timer (for whitelisted IPs or when no cooldown).\r\n+        if (duration \u003C= 0) {\r\n+            const $display = $mo(display);\r\n+            \u002F\u002F Just show the message without timer\r\n+            if (displayMessage && window.verifyOTPmessage) {\r\n+                $display.text(window.verifyOTPmessage);\r\n+            } else if (displayMessage) {\r\n+                $display.text(displayMessage);\r\n+            }\r\n+            neutralizeWcCheckoutPopupMessageStyle($display);\r\n+            return;\r\n+        }\r\n+        \r\n+        const $display = $mo(display);\r\n+        const shouldAppendTimer = isPluginMessageContainer($display);\r\n+        \r\n+        \u002F\u002F Check if timer is already active - prevent duplicate timers\r\n+        if ($display.data('mo-osp-timer-active')) {\r\n+            return;\r\n+        }\r\n+        \r\n+        \u002F\u002F Mark as having timer to prevent duplicates\r\n+        $display.data('mo-osp-timer-active', true);\r\n+        \r\n+        let timer = duration;\r\n+        \r\n+        \u002F\u002F Update immediately\r\n+        const minutes = String(Math.floor(timer \u002F 60)).padStart(2, '0');\r\n+        const seconds = String(timer % 60).padStart(2, '0');\r\n+        const formattedMessage = formatTimerMessage(displayMessage, minutes, seconds, timer, isBlocked, shouldAppendTimer);\r\n+        $display.text(formattedMessage);\r\n+        \r\n+        \u002F\u002F CRITICAL: Set success styling AFTER text update to override any inline styles\r\n+        if (!isBlocked) {\r\n+            if (isWcCheckoutPopupMessageDisplay($display)) {\r\n+                neutralizeWcCheckoutPopupMessageStyle($display);\r\n+            } else {\r\n+                const bgColor = $display.css('background-color');\r\n+                const isSuccessMessage = bgColor && (\r\n+                    bgColor === 'rgb(142, 237, 142)' ||\r\n+                    bgColor === '#8eed8e' ||\r\n+                    bgColor.indexOf('142, 237, 142') !== -1\r\n+                );\r\n+\r\n+                if (isSuccessMessage) {\r\n+                    \u002F\u002F Override inline styles to ensure correct success styling\r\n+                    $display.css({\r\n+                        'color': '#464646', \u002F\u002F Dark green text for better readability on green background\r\n+                        'background-color': '#8eed8e', \u002F\u002F Green background\r\n+                    });\r\n+                }\r\n+            }\r\n+        }\r\n+        \r\n+        $display.show();\r\n+\r\n+        const timerFunction = setInterval(() => {\r\n+            \u002F\u002F Check if error message flag is set - if so, this timer should stop (error message takes priority)\r\n+            if ($display.data('mo-osp-error-message') && !isBlocked) {\r\n+                clearInterval(timerFunction);\r\n+                const index = activeTimers.indexOf(timerFunction);\r\n+                if (index > -1) {\r\n+                    activeTimers.splice(index, 1);\r\n+                }\r\n+                return;\r\n+            }\r\n+\r\n+            \u002F\u002F If verification success\u002Ffailure text is now shown, stop resend timer so\r\n+            \u002F\u002F it cannot overwrite this message with old OTP-sent cooldown content.\r\n+            const liveText = mospNormalizeMessageText($display.text() || '');\r\n+            if (!isBlocked && liveText) {\r\n+                const liveBaseText = liveText.replace(\u002F\\s*you can send the next otp after\\s+\\d{1,2}:\\d{2}\\.?\u002Fgi, '').trim();\r\n+                const hasOutcomeMessage = mospIsOtpVerificationOutcomeMessage(liveBaseText) || mospIsNegativeOtpFeedbackMessage(liveBaseText);\r\n+                const isOtpSentCopy = mospMessageMatchesOtpSentResendTimerAllowlist(liveBaseText);\r\n+                if (hasOutcomeMessage && !isOtpSentCopy) {\r\n+                    clearInterval(timerFunction);\r\n+                    $display.data('mo-osp-timer-active', false);\r\n+                    $display.data('mo-osp-timer-added', false);\r\n+                    const activeIndex = activeTimers.indexOf(timerFunction);\r\n+                    if (activeIndex > -1) {\r\n+                        activeTimers.splice(activeIndex, 1);\r\n+                    }\r\n+                    return;\r\n+                }\r\n+            }\r\n+            \r\n+            timer--;\r\n+            \r\n+            if (timer \u003C 0) {\r\n+                clearInterval(timerFunction);\r\n+                \r\n+                \u002F\u002F Remove timer flag\r\n+                $display.data('mo-osp-timer-active', false);\r\n+                $display.data('mo-osp-timer-added', false);\r\n+                $display.data('mo-osp-error-message', false);\r\n+                \r\n+                neutralizeWcCheckoutPopupMessageStyle($display);\r\n+\r\n+                \u002F\u002F WC checkout popup + success resend cooldown: keep OTP-sent text visible (strip countdown only).\r\n+                const domEl = display && display.nodeType === 1 ? display : null;\r\n+                const isWcPopupTarget = !isBlocked && (\r\n+                    isWcCheckoutPopupMessageDisplay($display) ||\r\n+                    (domEl && domEl.id === 'mo_message_wc_pop_up')\r\n+                );\r\n+                if (isWcPopupTarget) {\r\n+                    const stripResendLine = function (t) {\r\n+                        if (!t || typeof t !== 'string') {\r\n+                            return '';\r\n+                        }\r\n+                        return t.replace(\u002F\\s*You can send the next OTP after\\s+\\d{1,2}:\\d{2}\\.?\u002Fgi, '').trim();\r\n+                    };\r\n+                    let baseMsg = stripResendLine(typeof displayMessage === 'string' ? mospStripHtml(displayMessage) : '');\r\n+                    if (!baseMsg && window.verifyOTPmessage) {\r\n+                        baseMsg = stripResendLine(mospStripHtml(String(window.verifyOTPmessage)));\r\n+                    }\r\n+                    if (!baseMsg) {\r\n+                        baseMsg = stripResendLine($display.text() || '');\r\n+                    }\r\n+                    $display.text(baseMsg);\r\n+                    $display.show();\r\n+                } else {\r\n+                    $display.hide();\r\n+                }\r\n+                if (button && button.length > 0) {\r\n+                    mospRestoreOtpButtonAfterRequest($mo(button));\r\n+                }\r\n+\r\n+                \u002F\u002F Remove from active timers\r\n+                const index = activeTimers.indexOf(timerFunction);\r\n+                if (index > -1) {\r\n+                    activeTimers.splice(index, 1);\r\n+                }\r\n+                return;\r\n+            }\r\n+\r\n+            const minutes = String(Math.floor(timer \u002F 60)).padStart(2, '0');\r\n+            const seconds = String(timer % 60).padStart(2, '0');\r\n+            const formattedMessage = formatTimerMessage(displayMessage, minutes, seconds, timer, isBlocked, shouldAppendTimer);\r\n+            \r\n+            \u002F\u002F Always update the message text\r\n+            \u002F\u002F For error messages (isBlocked=true), we need to update the countdown\r\n+            \u002F\u002F For success messages, we also need to update the countdown\r\n+            $display.text(formattedMessage);\r\n+            \r\n+            \u002F\u002F CRITICAL: Set success styling AFTER text update to override any inline styles\r\n+            if (!isBlocked) {\r\n+                if (isWcCheckoutPopupMessageDisplay($display)) {\r\n+                    neutralizeWcCheckoutPopupMessageStyle($display);\r\n+                } else {\r\n+                    const bgColor = $display.css('background-color');\r\n+                    const isSuccessMessage = bgColor && (\r\n+                        bgColor === 'rgb(142, 237, 142)' ||\r\n+                        bgColor === '#8eed8e' ||\r\n+                        bgColor.indexOf('142, 237, 142') !== -1\r\n+                    );\r\n+\r\n+                    if (isSuccessMessage) {\r\n+                        \u002F\u002F Override inline styles to ensure correct success styling\r\n+                        $display.css({\r\n+                            'color': '#464646', \u002F\u002F Dark green text for readability\r\n+                            'background-color': '#8eed8e', \u002F\u002F Green background\r\n+                        });\r\n+                    }\r\n+                }\r\n+            }\r\n+        }, 1000);\r\n+\r\n+        activeTimers.push(timerFunction);\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Plain text from HTML (e.g. verifyOTPmessage may contain markup).\r\n+     *\u002F\r\n+    function mospStripHtml(str) {\r\n+        if (!str || typeof str !== 'string') {\r\n+            return '';\r\n+        }\r\n+        const tmp = document.createElement('div');\r\n+        tmp.innerHTML = str;\r\n+        return (tmp.textContent || tmp.innerText || '').trim();\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Normalize text for message classification (NBSP, trim).\r\n+     *\u002F\r\n+    function mospNormalizeMessageText(text) {\r\n+        if (!text || typeof text !== 'string') {\r\n+            return '';\r\n+        }\r\n+        return text.replace(\u002F\\u00a0\u002Fg, ' ').replace(\u002F\\s+\u002Fg, ' ').trim();\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Validation \u002F mismatch \u002F failure copy — never append resend countdown to these.\r\n+     *\u002F\r\n+    function mospIsNegativeOtpFeedbackMessage(text) {\r\n+        if (!text || typeof text !== 'string') {\r\n+            return false;\r\n+        }\r\n+        const t = mospNormalizeMessageText(text).toLowerCase();\r\n+        \u002F\u002F Mismatch \u002F comparison errors often contain \"OTP\" and \"sent\" but are not send-success (e.g. WC phone mismatch).\r\n+        if (\u002F\\b(do\\s+not|does\\s+not|did\\s+not|don'?t)\\s+match\\b\u002F.test(t)) {\r\n+            return true;\r\n+        }\r\n+        if (\u002F\\bnot\\s+match\\b\u002F.test(t) && \u002F\\b(?:otp|phone|email|number|verification|code|contact|submission)\\b\u002F.test(t)) {\r\n+            return true;\r\n+        }\r\n+        if (\u002F\\botp\\b.*\\bsent\\b.*\\bnot\\s+match\\b\u002F.test(t) || \u002F\\bsent\\b.*\\botp\\b.*\\bnot\\s+match\\b\u002F.test(t)) {\r\n+            return true;\r\n+        }\r\n+        \u002F\u002F Normal send-success copy includes \"Please enter the OTP below\" — not a validation failure.\r\n+        if (\u002F\\b(?:otp|passcode|verification\\s+code|sms\\s+code|code)\\s+has\\s+been\\s+sent\\b\u002F.test(t)) {\r\n+            return false;\r\n+        }\r\n+        if (\u002F\\bhas\\s+been\\s+sent\\s+to\\b\u002F.test(t)) {\r\n+            return false;\r\n+        }\r\n+        return \u002F\\b(mismatch|invalid|incorrect|failed|failure|unsuccessful|wrong\\s+(?:otp|code|number)|expired|verification\\s+failed|not\\s+verified|unable\\s+to|could\\s+not|must\\s+enter|please\\s+enter|is\\s+required|are\\s+required|\\berror\\b|exceeded\\s+the\\s+limit|try\\s+again)\\b\u002Fi.test(t);\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * OTP verification outcome copy (success\u002Ffailure). If this appears while resend timer\r\n+     * is running, timer must stop and preserve this message.\r\n+     *\u002F\r\n+    function mospIsOtpVerificationOutcomeMessage(text) {\r\n+        if (!text || typeof text !== 'string') {\r\n+            return false;\r\n+        }\r\n+        const t = mospNormalizeMessageText(text).toLowerCase();\r\n+        if (!t) {\r\n+            return false;\r\n+        }\r\n+        if (t === 'success' || t === 'otp verified' || t === 'otp verification successful') {\r\n+            return true;\r\n+        }\r\n+        return \u002F\\b(otp|one time passcode|verification\\s+code|code)\\b.*\\b(verified|validated|successful|successfully)\\b\u002F.test(t) ||\r\n+               \u002F\\b(verified|validated|successful|successfully)\\b.*\\b(otp|one time passcode|verification\\s+code|code)\\b\u002F.test(t);\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Messages that may receive the client line: \"You can send the next OTP after MM:SS.\"\r\n+     * Mirrors default English strings from MoMessages (OTP_SENT_PHONE, OTP_SENT_EMAIL, OTP_SENT, SMS_SENT_SUCCESS).\r\n+     * Customized admin messages that change wording will not match until they keep the same opening\u002Fclosing phrases.\r\n+     *\r\n+     * Not included: LIMIT_OTP_SENT \u002F USER_IS_BLOCKED_* (server already supplies cooldown text);\r\n+     * CHOOSE_METHOD \u002F mismatch \u002F error strings.\r\n+     *\u002F\r\n+    function mospMessageMatchesOtpSentResendTimerAllowlist(text) {\r\n+        if (!text || typeof text !== 'string') {\r\n+            return false;\r\n+        }\r\n+        if (mospIsNegativeOtpFeedbackMessage(text)) {\r\n+            return false;\r\n+        }\r\n+        const plain = mospStripHtml(String(text));\r\n+        let base = mospNormalizeMessageText(plain).replace(\u002F\\s+\u002Fg, ' ').trim().toLowerCase();\r\n+        base = base.replace(\u002F\\s*you can send the next otp after\\s+\\d{1,2}:\\d{2}\\.?\u002Fgi, '').trim();\r\n+\r\n+        if (\u002Fclick\\s+here\\s+to\\s+send\\s+otp|send\\s+otp\\s+to\\s+continue\u002Fi.test(base)) {\r\n+            return false;\r\n+        }\r\n+\r\n+        \u002F\u002F MoMessages::OTP_SENT_PHONE — \"A OTP (One Time Passcode) has been sent to … Please enter the OTP in the field below to verify your phone.\"\r\n+        if (base.indexOf('a otp (one time passcode) has been sent to ') === 0 &&\r\n+            base.indexOf('please enter the otp in the field below to verify your phone') !== -1) {\r\n+            return true;\r\n+        }\r\n+\r\n+        \u002F\u002F MoMessages::OTP_SENT_EMAIL — \"A One Time Passcode has been sent to … Please enter the OTP below to verify your Email Address\"\r\n+        if (base.indexOf('a one time passcode has been sent to ') === 0 &&\r\n+            base.indexOf('please enter the otp below to verify your email address') !== -1) {\r\n+            return true;\r\n+        }\r\n+\r\n+        \u002F\u002F MoMessages::OTP_SENT — \"A passcode has been sent to {{method}}. Please enter the otp below to verify your account.\"\r\n+        if (base.indexOf('a passcode has been sent to ') === 0 &&\r\n+            base.indexOf('please enter the otp below to verify your account') !== -1) {\r\n+            return true;\r\n+        }\r\n+\r\n+        \u002F\u002F MoMessages::SMS_SENT_SUCCESS\r\n+        if (base === 'sms was sent successfully.' || base === 'sms was sent successfully') {\r\n+            return true;\r\n+        }\r\n+\r\n+        return false;\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Format timer message with countdown (consolidated logic)\r\n+     *\u002F\r\n+    function formatTimerMessage(displayMessage, minutes, seconds, totalSeconds, isBlocked, shouldAppendTimer) {\r\n+        const strippedDisplay = typeof displayMessage === 'string'\r\n+            ? displayMessage.replace(\u002F\\s*You can send the next OTP after\\s+\\d{1,2}:\\d{2}\\.?\u002Fgi, '').trim()\r\n+            : displayMessage;\r\n+        let serverMessage = strippedDisplay || (isBlocked ? 'You are temporarily blocked.' : 'Please wait before requesting another OTP.');\r\n+\r\n+        \u002F\u002F Never attach or keep the resend line on validation \u002F mismatch messages.\r\n+        if (!isBlocked && mospIsNegativeOtpFeedbackMessage(serverMessage)) {\r\n+            return serverMessage;\r\n+        }\r\n+\r\n+        \u002F\u002F Check if message has placeholder patterns\r\n+        if (serverMessage.includes('{minutes}') && serverMessage.includes('{seconds}')) {\r\n+            return serverMessage.replace('{minutes}', minutes).replace('{seconds}', seconds);\r\n+        }\r\n+        \r\n+        \u002F\u002F Check if message already contains timer information (avoid duplication)\r\n+        if (serverMessage.match(\u002F\\d{1,4}:\\d{2}\\s*(minutes?|mins?)\u002Fi)) {\r\n+            return serverMessage.replace(\u002F\\d{1,4}:\\d{2}\\s*(minutes?|mins?)\u002Fgi, `${minutes}:${seconds} minutes`);\r\n+        }\r\n+        \r\n+        if (serverMessage.match(\u002F\\d+\\s*(minutes?|mins?)\u002Fi)) {\r\n+            const totalMinutes = Math.floor(totalSeconds \u002F 60);\r\n+            return serverMessage.replace(\u002F\\d+\\s*(minutes?|mins?)\u002Fgi, `${totalMinutes} minutes`);\r\n+        }\r\n+        \r\n+        if (serverMessage.match(\u002F\\d+\\s*(seconds?|secs?)\u002Fi)) {\r\n+            return serverMessage.replace(\u002F\\d+\\s*(seconds?|secs?)\u002Fgi, `${totalSeconds} seconds`);\r\n+        }\r\n+        \r\n+        \u002F\u002F If totalSeconds is 0, don't add timer text (for whitelisted IPs or when no cooldown).\r\n+        if (totalSeconds \u003C= 0) {\r\n+            if (!isBlocked && window.verifyOTPmessage) {\r\n+                return window.verifyOTPmessage;\r\n+            }\r\n+            return serverMessage;\r\n+        }\r\n+        \r\n+        \u002F\u002F For cooldown messages, try to use window.verifyOTPmessage if available and no server message\r\n+        if (!isBlocked && (!strippedDisplay || strippedDisplay.trim() === '') && window.verifyOTPmessage) {\r\n+            if (!shouldAppendTimer) {\r\n+                return window.verifyOTPmessage;\r\n+            }\r\n+            const verifyPlain = mospStripHtml(String(window.verifyOTPmessage));\r\n+            if (mospIsNegativeOtpFeedbackMessage(verifyPlain) || !mospMessageMatchesOtpSentResendTimerAllowlist(verifyPlain)) {\r\n+                return window.verifyOTPmessage;\r\n+            }\r\n+            return window.verifyOTPmessage + ` You can send the next OTP after ${minutes}:${seconds}.`;\r\n+        }\r\n+        \r\n+        \u002F\u002F Fallback: add countdown to server message (only for allowlisted OTP-sent copy from MoMessages)\r\n+        if (!isBlocked) {\r\n+            if (!shouldAppendTimer || mospIsNegativeOtpFeedbackMessage(serverMessage) || !mospMessageMatchesOtpSentResendTimerAllowlist(serverMessage)) {\r\n+                return serverMessage;\r\n+            }\r\n+            return `${serverMessage} You can send the next OTP after ${minutes}:${seconds}.`;\r\n+        }\r\n+        return `${serverMessage} (${minutes}:${seconds} remaining)`;\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Show puzzle popup\r\n+     *\u002F\r\n+    function showPuzzlePopup() {\r\n+        if (typeof MO_OSP_Puzzle !== 'undefined') {\r\n+            \u002F\u002F CRITICAL: Ensure puzzle overlay has higher z-index than WooCommerce checkout popup\r\n+            \u002F\u002F WooCommerce checkout popup uses z-index: 100000, so puzzle needs to be higher\r\n+            var $puzzleOverlay = $mo('#mo-osp-puzzle-overlay');\r\n+            if ($puzzleOverlay.length > 0) {\r\n+                $puzzleOverlay.css('z-index', '100001');\r\n+            }\r\n+            var $puzzlePopup = $mo('#mo-osp-puzzle-popup-outer-div');\r\n+            if ($puzzlePopup.length > 0) {\r\n+                $puzzlePopup.css('z-index', '100002');\r\n+            }\r\n+            MO_OSP_Puzzle.showPuzzle({});\r\n+        } else {\r\n+            alert('Security verification required. Please refresh the page.');\r\n+        }\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Clear all active timers\r\n+     *\u002F\r\n+    function clearAllTimers() {\r\n+        activeTimers.forEach(function(timer) {\r\n+            clearInterval(timer);\r\n+        });\r\n+        activeTimers = [];\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Get email from form fields (with phone fallback for consistency)\r\n+     *\u002F\r\n+    function getEmailFromForm() {\r\n+        let email = '';\r\n+        $mo('input[type=\"email\"], input[name*=\"email\"], input[id*=\"email\"]').each(function() {\r\n+            const $field = $mo(this);\r\n+            const type = ($field.attr('type') || '').toLowerCase();\r\n+            if (type === 'button' || type === 'submit' || type === 'reset') {\r\n+                return;\r\n+            }\r\n+            const value = $field.val();\r\n+            if (value && !\u002Fsend\\s+otp|verify\\s+otp\u002Fi.test(value)) {\r\n+                email = value;\r\n+                return false;\r\n+            }\r\n+        });\r\n+        \r\n+        \u002F\u002F If no email found, use phone number as email for consistency\r\n+        if (!email) {\r\n+            const phone = getPhoneFromForm();\r\n+            if (phone) {\r\n+                email = phone;\r\n+            }\r\n+        }\r\n+        return email;\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Get phone from form fields\r\n+     *\u002F\r\n+    function getPhoneFromForm() {\r\n+        let phone = '';\r\n+        $mo('input[type=\"tel\"], input[name*=\"phone\"], input[id*=\"phone\"], input[name*=\"mobile\"]').each(function() {\r\n+            const $field = $mo(this);\r\n+            const type = ($field.attr('type') || '').toLowerCase();\r\n+            if (type === 'button' || type === 'submit' || type === 'reset') {\r\n+                return;\r\n+            }\r\n+            const value = $field.val();\r\n+            if (!value || \u002Fsend\\s+otp|verify\\s+otp\u002Fi.test(value)) {\r\n+                return;\r\n+            }\r\n+            \u002F\u002F Normalize to digits\u002F+ and require a minimum length to avoid tokens like \"6ff2c895dc\".\r\n+            const normalized = String(value).replace(\u002F[^0-9+]\u002Fg, '');\r\n+            const digitCount = normalized.replace(\u002F\\D\u002Fg, '').length;\r\n+            if (digitCount >= 6) {\r\n+                phone = normalized;\r\n+                return false;\r\n+            }\r\n+        });\r\n+        return phone;\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Handle puzzle success (called by puzzle system)\r\n+     *\u002F\r\n+    window.MO_OSP_SpamPreventer_onPuzzleSuccess = function() {\r\n+        clearAllTimers();\r\n+        \r\n+        \u002F\u002F Set flag to allow OTP to proceed after puzzle completion\r\n+        window.mo_osp_skip_puzzle_check = true;\r\n+        setTimeout(function() {\r\n+            window.mo_osp_skip_puzzle_check = false;\r\n+        }, 5000);\r\n+        \r\n+        \u002F\u002F Clear any existing verifyOTPmessage\r\n+        if (window.verifyOTPmessage) {\r\n+            delete window.verifyOTPmessage;\r\n+        }\r\n+        \r\n+        \u002F\u002F Hide any existing messages (never hide WC checkout popup line — id contains substring \"mo_message\")\r\n+        $mo('div[id*=\"mo_message\"]').not('#mo_message_wc_pop_up').hide();\r\n+        \r\n+        \u002F\u002F Show all OTP buttons\r\n+        buttonSelectors.forEach(function(selector) {\r\n+            $mo(selector).each(function() {\r\n+                mospRestoreOtpButtonAfterRequest($mo(this));\r\n+            });\r\n+        });\r\n+    };\r\n+\r\n+    \u002F**\r\n+     * Setup global AJAX response interceptor and message monitor\r\n+     *\u002F\r\n+    function setupAjaxInterceptor() {\r\n+        \u002F\u002F Monitor for message elements that are added or updated\r\n+        const messageObserver = new MutationObserver(function(mutations) {\r\n+            mutations.forEach(function(mutation) {\r\n+                \u002F\u002F Check added nodes\r\n+                mutation.addedNodes.forEach(function(node) {\r\n+                    if (node.nodeType === 1) { \u002F\u002F Element node\r\n+                        const $node = $mo(node);\r\n+                        const $messageCandidate = $node.is('[id*=\"mo_message\"], .mo_message, [class*=\"mo_message\"]') \r\n+                            ? $node \r\n+                            : $node.find('[id*=\"mo_message\"], .mo_message, [class*=\"mo_message\"]').first();\r\n+                        if ($messageCandidate.length > 0) {\r\n+                            checkAndAddTimerToMessage($messageCandidate);\r\n+                        }\r\n+                    }\r\n+                });\r\n+                \r\n+                \u002F\u002F Check for text changes in existing message elements\r\n+                if (mutation.type === 'childList' || mutation.type === 'characterData') {\r\n+                    const target = mutation.target;\r\n+                    if (target.nodeType === 1) {\r\n+                        const $target = $mo(target);\r\n+                        if ($target.is('[id*=\"mo_message\"], .mo_message, [class*=\"mo_message\"]') || \r\n+                            $target.find('[id*=\"mo_message\"], .mo_message').length > 0) {\r\n+                            \u002F\u002F Use immediate check (no setTimeout) to catch error messages before they're overwritten\r\n+                            const $msgElem = $target.is('[id*=\"mo_message\"], .mo_message') ? $target : $target.find('[id*=\"mo_message\"], .mo_message').first();\r\n+                            if ($msgElem.length > 0) {\r\n+                                const currentText = $msgElem.text() || '';\r\n+                                \r\n+                                \u002F\u002F PRIORITY: Check if error message just appeared\r\n+                                \u002F\u002F But only process if we haven't already processed this exact message\r\n+                                if (currentText.includes('exceeded') && currentText.includes('limit')) {\r\n+                                    const lastProcessedError = $msgElem.data('mo-osp-last-processed-error');\r\n+                                    const timerActive = $msgElem.data('mo-osp-timer-active');\r\n+                                    if (lastProcessedError !== currentText || !timerActive) {\r\n+                                        checkAndAddTimerToMessage($msgElem);\r\n+                                        return;\r\n+                                    } else {\r\n+                                        \u002F\u002F Already processed this error message and timer is active, skip to prevent loop\r\n+                                        return;\r\n+                                    }\r\n+                                }\r\n+                                \r\n+                                \u002F\u002F Check if error message flag is set but message was overwritten with success\r\n+                                if ($msgElem.data('mo-osp-error-message')) {\r\n+                                    \u002F\u002F If error flag is set but message is success, restore error message\r\n+                                    if (currentText.includes('sent') && currentText.includes('OTP') && !currentText.includes('exceeded')) {\r\n+                                        \u002F\u002F The error should have been set by interceptAjaxResponse, but if it was overwritten,\r\n+                                        \u002F\u002F we need to check if we have the error response stored\r\n+                                        if (window.mo_osp_last_error_response && window.mo_osp_last_error_response.blocked) {\r\n+                                            const errorResponse = window.mo_osp_last_error_response;\r\n+                                            const minutes = Math.floor(errorResponse.remaining_time \u002F 60);\r\n+                                            const seconds = errorResponse.remaining_time % 60;\r\n+                                            const formattedMinutes = String(minutes).padStart(2, '0');\r\n+                                            const formattedSeconds = String(seconds).padStart(2, '0');\r\n+                                            let errorMessage = errorResponse.message || 'You have exceeded the limit to send OTP. Please wait for ' + \r\n+                                                formattedMinutes + ':' + formattedSeconds + ' minutes';\r\n+                                            if (errorMessage.includes('{minutes}') || errorMessage.includes('{seconds}')) {\r\n+                                                errorMessage = errorMessage.replace('{minutes}', formattedMinutes).replace('{seconds}', formattedSeconds);\r\n+                                            }\r\n+                                            $msgElem.text(errorMessage);\r\n+                                            mospApplyMoMessageErrorStyles($msgElem);\r\n+                                            \u002F\u002F Clear timer flags and restart timer\r\n+                                            $msgElem.data('mo-osp-timer-active', false);\r\n+                                            $msgElem.data('mo-osp-timer-added', false);\r\n+                                            const $button = findButtonForMessage($msgElem);\r\n+                                            startBlockTimer(errorResponse.remaining_time, $button, $msgElem, errorMessage);\r\n+                                            return;\r\n+                                        }\r\n+                                    }\r\n+                                }\r\n+                                \r\n+                                \u002F\u002F For other messages, use setTimeout to avoid too many checks\r\n+                                setTimeout(function() {\r\n+                                    checkAndAddTimerToMessage($msgElem);\r\n+                                }, 100);\r\n+                            }\r\n+                        }\r\n+                    }\r\n+                }\r\n+            });\r\n+        });\r\n+        \r\n+        \u002F\u002F Start observing the document body for changes\r\n+        if (document.body) {\r\n+            messageObserver.observe(document.body, {\r\n+                childList: true,\r\n+                subtree: true,\r\n+                characterData: true\r\n+            });\r\n+        }\r\n+        \r\n+        \u002F\u002F Also check existing messages periodically (fallback)\r\n+        setInterval(function() {\r\n+            const $messages = $mo('[id*=\"mo_message\"], .mo_message');\r\n+            $messages.each(function() {\r\n+                const $msg = $mo(this);\r\n+                if ($msg.is(':visible') && !$msg.data('mo-osp-timer-added')) {\r\n+                    checkAndAddTimerToMessage($msg);\r\n+                }\r\n+            });\r\n+        }, 500);\r\n+        \r\n+        \u002F\u002F Intercept jQuery AJAX responses and store request data for puzzle resubmission\r\n+        const originalAjax = $mo.ajax;\r\n+        $mo.ajax = function(options) {\r\n+            const originalSuccess = options.success;\r\n+            const originalError = options.error;\r\n+            \r\n+            \u002F\u002F Check if this is an OTP-related request by examining URL or data\r\n+            const isOtpRequest = (options.url && (\r\n+                options.url.indexOf('admin-ajax.php') !== -1 || \r\n+                options.url.indexOf('otp') !== -1 ||\r\n+                options.url.indexOf('miniorange') !== -1\r\n+            )) || (options.data && (\r\n+                (typeof options.data === 'string' && (options.data.indexOf('otp') !== -1 || options.data.indexOf('miniorange') !== -1)) ||\r\n+                (typeof options.data === 'object' && (options.data.action && (\r\n+                    options.data.action.indexOf('otp') !== -1 || \r\n+                    options.data.action.indexOf('miniorange') !== -1 ||\r\n+                    options.data.action === 'mo_external_popup_option'\r\n+                )))\r\n+            ));\r\n+            \r\n+            \u002F\u002F Wrap success callback to check for puzzle_required\r\n+            options.success = function(response, textStatus, jqXHR) {\r\n+                \u002F\u002F Check if this is an external popup request\r\n+                const isExternalPopupRequest = options.data && (\r\n+                    (typeof options.data === 'object' && options.data.action === 'mo_external_popup_option') ||\r\n+                    (typeof options.data === 'string' && options.data.indexOf('mo_external_popup_option') !== -1)\r\n+                );\r\n+                \r\n+                \u002F\u002F Check if puzzle is required - if so, store the request for resubmission\r\n+                if (isOtpRequest && response && (response.result === 'puzzle_required' || response.puzzle_required === true || response.authType === 'PUZZLE_REQUIRED')) {\r\n+                    window.mo_osp_pending_ajax_request = {\r\n+                        url: options.url,\r\n+                        type: options.type || 'POST',\r\n+                        data: typeof options.data === 'string' ? options.data : (options.data ? JSON.parse(JSON.stringify(options.data)) : {}),\r\n+                        dataType: options.dataType || 'json',\r\n+                        crossDomain: options.crossDomain || false,\r\n+                        originalSuccess: originalSuccess,\r\n+                        originalError: originalError\r\n+                    };\r\n+                }\r\n+                \r\n+                \u002F\u002F CRITICAL: Skip interceptAjaxResponse for external popup success responses\r\n+                \u002F\u002F External popup handles its own success\u002Ferror messages and shouldn't be overwritten\r\n+                if (isExternalPopupRequest && response && response.result === 'success') {\r\n+                    \u002F\u002F Call original success callback without intercepting\r\n+                    if (originalSuccess) {\r\n+                        originalSuccess.apply(this, arguments);\r\n+                    }\r\n+                    return;\r\n+                }\r\n+                \r\n+                \u002F\u002F Call interceptAjaxResponse if response has message\r\n+                if (response && (response.message || response.result)) {\r\n+                    interceptAjaxResponse(response, null);\r\n+                }\r\n+                \r\n+                \u002F\u002F Call original success callback\r\n+                if (originalSuccess) {\r\n+                    originalSuccess.apply(this, arguments);\r\n+                }\r\n+            };\r\n+            \r\n+            \u002F\u002F Wrap error callback\r\n+            options.error = function(jqXHR, textStatus, errorThrown) {\r\n+                \u002F\u002F Try to parse error response\r\n+                try {\r\n+                    const response = jqXHR.responseJSON || JSON.parse(jqXHR.responseText);\r\n+                    \r\n+                    \u002F\u002F Check if puzzle is required in error response\r\n+                    if (isOtpRequest && response && (response.result === 'puzzle_required' || response.puzzle_required === true || response.authType === 'PUZZLE_REQUIRED')) {\r\n+                        window.mo_osp_pending_ajax_request = {\r\n+                            url: options.url,\r\n+                            type: options.type || 'POST',\r\n+                            data: typeof options.data === 'string' ? options.data : (options.data ? JSON.parse(JSON.stringify(options.data)) : {}),\r\n+                            dataType: options.dataType || 'json',\r\n+                            crossDomain: options.crossDomain || false,\r\n+                            originalSuccess: originalSuccess,\r\n+                            originalError: originalError\r\n+                        };\r\n+                    }\r\n+                    \r\n+                    if (response && (response.message || response.result)) {\r\n+                        interceptAjaxResponse(response, null);\r\n+                    }\r\n+                } catch (e) {\r\n+                    \u002F\u002F Ignore parse errors\r\n+                }\r\n+                \r\n+                \u002F\u002F Call original error callback\r\n+                if (originalError) {\r\n+                    originalError.apply(this, arguments);\r\n+                }\r\n+            };\r\n+            \r\n+            \u002F\u002F Call original ajax\r\n+            return originalAjax.apply(this, arguments);\r\n+        };\r\n+    }\r\n+\r\n+    \u002F**\r\n+     * Check message element and add timer if needed\r\n+     *\u002F\r\n+    function checkAndAddTimerToMessage($messageElement) {\r\n+        if (!$messageElement || $messageElement.length === 0) {\r\n+            return;\r\n+        }\r\n+        \r\n+        const messageSelector = '[id*=\"mo_message\"], .mo_message, [class*=\"mo_message\"]';\r\n+        \u002F\u002F Only act on OTP message containers to avoid corrupting unrelated text\r\n+        if (!$messageElement.is(messageSelector)) {\r\n+            const $innerMessage = $messageElement.find(messageSelector).first();\r\n+            if ($innerMessage.length === 0) {\r\n+                return;\r\n+            }\r\n+            $messageElement = $innerMessage;\r\n+        }\r\n+        const messageText = $messageElement.text() || '';\r\n+        \r\n+        if (!messageText.trim()) {\r\n+            return;\r\n+        }\r\n+        \r\n+        \u002F\u002F PRIORITY: Check if it's a blocked\u002Ferror message with timer\r\n+        \u002F\u002F Error messages should always replace success messages, even if timer is already added\r\n+        if (messageText.includes('exceeded') && messageText.includes('limit')) {\r\n+            \r\n+            const timerActive = $messageElement.data('mo-osp-timer-active');\r\n+            \r\n+            \u002F\u002F Check if we've already processed this exact error message to prevent infinite loops\r\n+            const lastProcessedError = $messageElement.data('mo-osp-last-processed-error');\r\n+            if (lastProcessedError === messageText && timerActive) {\r\n+                return;\r\n+            }\r\n+            \r\n+            \u002F\u002F CRITICAL: Set error flag FIRST, then stop timer\r\n+            \u002F\u002F This ensures that if the timer interval callback is already queued, it will see the flag and stop\r\n+            $messageElement.data('mo-osp-error-message', true);\r\n+            $messageElement.data('mo-osp-last-processed-error', messageText);\r\n+            \r\n+            \u002F\u002F IMPORTANT: Set the error message text IMMEDIATELY\r\n+            \u002F\u002F This prevents the success timer (if still running) from overwriting it\r\n+            \u002F\u002F But only if the current text is different to avoid triggering unnecessary mutations\r\n+            const currentText = $messageElement.text() || '';\r\n+            if (currentText !== messageText) {\r\n+                $messageElement.text(messageText);\r\n+            }\r\n+            mospApplyMoMessageErrorStyles($messageElement);\r\n+            $messageElement.show();\r\n+            \r\n+            const totalSeconds = extractTimerFromMessage(messageText);\r\n+            if (totalSeconds > 0) {\r\n+                \u002F\u002F Check if interceptAjaxResponse already handled this error message\r\n+                \u002F\u002F If the timer is active and the error flag is set, interceptAjaxResponse likely already started it\r\n+                if (timerActive && $messageElement.data('mo-osp-error-message')) {\r\n+                    return;\r\n+                }\r\n+                \r\n+                \u002F\u002F CRITICAL: ALWAYS stop any active timer when error message is detected\r\n+                \u002F\u002F The success timer might be running and overwriting the error message\r\n+                \u002F\u002F We MUST stop it immediately, regardless of what the current text says\r\n+                if (timerActive) {\r\n+                    \u002F\u002F Clear all active timers (whether success or error)\r\n+                    activeTimers.forEach(function(timer) {\r\n+                        clearInterval(timer);\r\n+                    });\r\n+                    activeTimers = [];\r\n+                    \u002F\u002F Clear flags AFTER clearing timers to ensure clean state\r\n+                    $messageElement.data('mo-osp-timer-active', false);\r\n+                    $messageElement.data('mo-osp-timer-added', false);\r\n+                }\r\n+                \r\n+                \u002F\u002F Set timer-added flag (but NOT timer-active - let startTimer set that)\r\n+                $messageElement.data('mo-osp-timer-added', true);\r\n+                \r\n+                \u002F\u002F Store error response for potential restoration if overwritten\r\n+                window.mo_osp_last_error_response = {\r\n+                    blocked: true,\r\n+                    message: messageText,\r\n+                    remaining_time: totalSeconds,\r\n+                    result: 'error'\r\n+                };\r\n+                \r\n+                const $button = findButtonForMessage($messageElement);\r\n+                startBlockTimer(totalSeconds, $button, $messageElement, messageText);\r\n+                return;\r\n+            }\r\n+        }\r\n+        \r\n+        \u002F\u002F Success OTP-sent only (MutationObserver): same rules as interceptAjaxResponse — not generic \"sent\"\u002F\"OTP\"\r\n+        const looksLikeSendPrompt = \u002Fclick\\s+here\\s+to\\s+send\\s+otp|send\\s+otp\u002Fi.test(messageText);\r\n+        if (mospMessageMatchesOtpSentResendTimerAllowlist(messageText) && !looksLikeSendPrompt &&\r\n+            !messageText.match(\u002F\\d{1,2}:\\d{2}\\s*(remaining|minutes?|mins?)\u002Fi)) {\r\n+            \r\n+            \u002F\u002F Check if we have an active error message - if so, don't process success message\r\n+            if ($messageElement.data('mo-osp-error-message')) {\r\n+                return;\r\n+            }\r\n+\r\n+            if (isWcCheckoutPopupMessageDisplay($messageElement)) {\r\n+                neutralizeWcCheckoutPopupMessageStyle($messageElement);\r\n+            }\r\n+            \r\n+            \u002F\u002F Skip if timer already added (only for success messages)\r\n+            if ($messageElement.data('mo-osp-timer-added')) {\r\n+                return;\r\n+            }\r\n+            $messageElement.data('mo-osp-timer-added', true);\r\n+            \r\n+            \u002F\u002F Get actual remaining cooldown time from backend\r\n+            const email = getEmailFromForm();\r\n+            const phone = getPhoneFromForm();\r\n+            \r\n+            if (typeof mo_osp_ajax !== 'undefined') {\r\n+                $mo.ajax({\r\n+                    url: mo_osp_ajax.ajax_url,\r\n+                    type: 'POST',\r\n+                    dataType: 'json',\r\n+                    data: {\r\n+                        action: 'mo_osp_check_blocked',\r\n+                        nonce: mo_osp_ajax.nonce,\r\n+                        mo_osp_browser_id: currentBrowserID,\r\n+                        email: email,\r\n+                        phone: phone\r\n+                    },\r\n+                    success: function(cooldownResponse) {\r\n+                        \u002F\u002F Handle WordPress JSON success wrapper\r\n+                        if (cooldownResponse && cooldownResponse.data) {\r\n+                            cooldownResponse = cooldownResponse.data;\r\n+                        }\r\n+                        \r\n+                        let cooldownTime = 0;\r\n+                        if (cooldownResponse && cooldownResponse.cooldown && cooldownResponse.remaining_time > 0) {\r\n+                            cooldownTime = parseInt(cooldownResponse.remaining_time);\r\n+                        } else {\r\n+                            cooldownTime = (typeof mo_osp_ajax !== 'undefined' && mo_osp_ajax.timer_time) \r\n+                                ? parseInt(mo_osp_ajax.timer_time) : 60;\r\n+                        }\r\n+                        if (cooldownTime > 0) {\r\n+                            const $button = findButtonForMessage($messageElement);\r\n+                            startCooldownTimer(cooldownTime, $button, $messageElement, messageText);\r\n+                        } else {\r\n+                            \u002F\u002F No cooldown, show button\r\n+                            const $button = findButtonForMessage($messageElement);\r\n+                            if ($button && $button.length > 0) {\r\n+                                mospRestoreOtpButtonAfterRequest($button);\r\n+                            }\r\n+                            $messageElement.data('mo-osp-timer-added', false); \u002F\u002F Allow retry\r\n+                        }\r\n+                    },\r\n+                    error: function(xhr, status, error) {\r\n+                        \u002F\u002F If response is HTML (error page), treat as no cooldown to allow OTP send\r\n+                        if (xhr.responseText && xhr.responseText.trim().startsWith('\u003C')) {\r\n+                            \u002F\u002F HTML response - likely an error page, skip timer\r\n+                            const $button = findButtonForMessage($messageElement);\r\n+                            if ($button && $button.length > 0) {\r\n+                                mospRestoreOtpButtonAfterRequest($button);\r\n+                            }\r\n+                            $messageElement.data('mo-osp-timer-added', false);\r\n+                            return;\r\n+                        }\r\n+                        const defaultTime = (typeof mo_osp_ajax !== 'undefined' && mo_osp_ajax.timer_time) \r\n+                            ? parseInt(mo_osp_ajax.timer_time) : 60;\r\n+                        const $button = findButtonForMessage($messageElement);\r\n+                        startCooldownTimer(defaultTime, $button, $messageElement, messageText);\r\n+                    }\r\n+                });\r\n+            } else {\r\n+                \u002F\u002F Fallback if mo_osp_ajax is not available\r\n+                const defaultTime = 60;\r\n+                const $button = findButtonForMessage($messageElement);\r\n+                startCooldownTimer(defaultTime, $button, $messageElement, messageText);\r\n+            }\r\n+        }\r\n+    }\r\n+\r\n+    \u002F\u002F Initialize when document is ready (resendcontrol pattern)\r\n+    $mo(document).ready(function() {\r\n+        setTimeout(function() {\r\n+            initializeSpamPreventer();\r\n+            setupAjaxInterceptor();\r\n+        }, 100);\r\n+    });\r\n+\r\n+})(jQuery);\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fviews\u002Fspampreventer.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fviews\u002Fspampreventer.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Faddons\u002Fotpspampreventer\u002Fviews\u002Fspampreventer.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Faddons\u002Fotpspampreventer\u002Fviews\u002Fspampreventer.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,243 +1,243 @@\n-\u003C?php\n-\u002F**\n- * OTP Spam Preventer View\n- *\n- * @package otpspampreventer\u002Fviews\n- *\u002F\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-?>\n-\n-\u003Cdiv class=\"mo-osp-container\">\n-\t\u003Cform method=\"post\" action=\"\" id=\"mo_osp_settings_form\">\n-\t\t\u003C?php wp_nonce_field( 'mo_osp_settings_save' ); ?>\n-\t\t\u003Cinput type=\"hidden\" name=\"option\" value=\"mo_osp_settings_save\" \u002F>\n-\n-\t\t\u003Cdiv class=\"mo-header\">\n-\t\t\t\u003Cp class=\"mo-heading flex-1\">\n-\t\t\t\t\u003C?php echo esc_html( __( 'OTP Spam Protection', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\u003C\u002Fp>\n-\t\t\t\u003Cinput type=\"submit\" name=\"save\" id=\"save\" class=\"mo-button inverted\" value=\"\u003C?php echo esc_attr( __( 'Save Settings', 'miniorange-otp-verification' ) ); ?>\">\n-\t\t\u003C\u002Fdiv>\n-\n-\t\t\u003Cdiv id=\"mo-osp-admin-notice-container\">\u003C\u002Fdiv>\n-\n-\t\t\u003Cdiv class=\"mo-osp-addon-toggle-row\">\n-\t\t\t\u003Clabel class=\"mo-osp-addon-toggle mo-osp-addon-toggle-emphasis\" for=\"mo_osp_enabled\">\n-\t\t\t\t\u003Cinput type=\"checkbox\" id=\"mo_osp_enabled\" name=\"mo_osp_enabled\" value=\"1\" \u003C?php checked( ! empty( $settings['enabled'] ) ); ?> \u002F>\n-\t\t\t\t\u003Cspan>\u003C?php echo esc_html( __( 'Enable Addon', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fspan>\n-\t\t\t\u003C\u002Flabel>\n-\t\t\u003C\u002Fdiv>\n-\n-\t\t\u003Cdiv class=\"mo-osp-card\">\n-\t\t\t\u003Cdiv class=\"mo-osp-card-header\">\n-\t\t\t\t\u003Ch3 class=\"mo-osp-section-title\">\n-\t\t\t\t\t\u003Csvg class=\"mo-osp-section-icon\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\u003Cpath d=\"M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 4L13.5 7H7V9H13.5L15 12L21 9ZM4 15.5C4 17.43 5.57 19 7.5 19S11 17.43 11 15.5 9.43 12 7.5 12 4 13.57 4 15.5ZM7.5 17C6.67 17 6 16.33 6 15.5S6.67 14 7.5 14 9 14.67 9 15.5 8.33 17 7.5 17Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\u003C?php echo esc_html( __( 'Basic Protection Settings', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\u003C\u002Fh3>\n-\t\t\t\t\u003Cp class=\"mo-osp-section-desc\">\u003C?php echo esc_html( __( 'Configure how long users must wait between OTP requests and how many attempts are allowed.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\u003Cdiv class=\"mo-osp-card-body\">\n-\t\t\t\t\u003Cdiv class=\"mo-osp-fields-grid\">\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group\">\n-\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\n-\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\n-\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2ZM17 13H11V7H12.5V11.5H17V13Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Wait Time Between Requests', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\t\u003C\u002Flabel>\n-\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo_osp_cooldown_time\" name=\"mo_osp_cooldown_time\" value=\"\u003C?php echo esc_attr( $settings['cooldown_time'] ); ?>\" min=\"0\" max=\"86400\" class=\"mo-form-input w-full\" \u002F>\n-\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'Seconds users must wait before requesting another OTP (e.g., 60 = 1 minute).', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group\">\n-\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\n-\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\n-\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M12 2C10.1 2 8.5 3.6 8.5 5.5S10.1 9 12 9 15.5 7.4 15.5 5.5 13.9 2 12 2ZM12 7C11.2 7 10.5 6.3 10.5 5.5S11.2 4 12 4 13.5 4.7 13.5 5.5 12.8 7 12 7ZM5.5 8C3.6 8 2 9.6 2 11.5S3.6 15 5.5 15 9 13.4 9 11.5 7.4 8 5.5 8ZM18.5 8C16.6 8 15 9.6 15 11.5S16.6 15 18.5 15 22 13.4 22 11.5 20.4 8 18.5 8ZM12 10.5C10.1 10.5 8.5 12.1 8.5 14S10.1 17.5 12 17.5 15.5 15.9 15.5 14 13.9 10.5 12 10.5ZM5.5 16C3.6 16 2 17.6 2 19.5S3.6 23 5.5 23 9 21.4 9 19.5 7.4 16 5.5 16ZM18.5 16C16.6 16 15 17.6 15 19.5S16.6 23 18.5 23 22 21.4 22 19.5 20.4 16 18.5 16Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Maximum Attempts Allowed', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\t\u003C\u002Flabel>\n-\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo_osp_max_attempts\" name=\"mo_osp_max_attempts\" value=\"\u003C?php echo esc_attr( $settings['max_attempts'] ); ?>\" min=\"3\" max=\"10\" class=\"mo-form-input w-full\" \u002F>\n-\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'How many OTP requests are allowed before blocking (between 1-10).', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group\">\n-\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\n-\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\n-\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M18 8H17V6C17 3.24 14.76 1 12 1S7 3.24 7 6V8H6C4.9 8 4 8.9 4 10V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V10C20 8.9 19.1 8 18 8ZM12 17C10.9 17 10 16.1 10 15S10.9 13 12 13 14 13.9 14 15 13.1 17 12 17ZM15.1 8H8.9V6C8.9 4.29 10.29 2.9 12 2.9S15.1 4.29 15.1 6V8Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Block Duration', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\t\u003C\u002Flabel>\n-\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo_osp_block_time\" name=\"mo_osp_block_time\" value=\"\u003C?php echo esc_attr( $settings['block_time'] ); ?>\" min=\"60\" max=\"604800\" class=\"mo-form-input w-full\" \u002F>\n-\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'How long to block users after too many attempts in seconds (3600 = 1 hour).', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\u003C\u002Fdiv>\n-\t\t\u003C\u002Fdiv>\n-\n-\t\t\u003Cdiv class=\"mo-osp-card\">\n-\t\t\t\u003Cbutton type=\"button\" id=\"mo-osp-toggle-advanced\" class=\"mo-osp-toggle-btn\">\n-\t\t\t\t\u003Cdiv class=\"mo-osp-toggle-content\">\n-\t\t\t\t\t\u003Ch3 class=\"mo-osp-section-title\">\n-\t\t\t\t\t\t\u003Csvg class=\"mo-osp-section-icon\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\u003Cpath d=\"M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5A3.5 3.5 0 0 1 15.5 12A3.5 3.5 0 0 1 12 15.5M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12S19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.54 5.05 19.27 4.97 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.72 4.96 4.46 5.05 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12S4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.21 15.05 2.34 15.27L4.34 18.73C4.46 18.95 4.73 19.03 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.28 19.04 19.54 18.95 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Advanced Settings', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\u003C\u002Fh3>\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-toggle-indicator\">\n-\t\t\t\t\t\t\u003Cspan id=\"mo-osp-toggle-text\" class=\"mo-osp-toggle-text\">\u003C?php echo esc_html( __( 'Show Advanced', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fspan>\n-\t\t\t\t\t\t\u003Cspan id=\"mo-osp-toggle-icon\" class=\"mo-osp-toggle-icon\">▼\u003C\u002Fspan>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\u003C\u002Fbutton>\n-\n-\t\t\t\u003Cdiv id=\"mo-osp-advanced-settings\" class=\"mo-osp-advanced-hidden\">\n-\t\t\t\t\u003Cdiv class=\"mo-osp-advanced-content\">\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-subsection\">\n-\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-subsection-header\">\n-\t\t\t\t\t\t\t\u003Ch4 class=\"mo-osp-subsection-title\">\n-\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-subsection-icon\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M11.99 2C6.47 2 2 6.48 2 12S6.47 22 11.99 22C17.52 22 22 17.52 22 12S17.52 2 11.99 2ZM12 20C7.58 20 4 16.42 4 12S7.58 4 12 4S20 7.58 20 12S16.42 20 12 20ZM12.5 7H11V13L16.25 16.15L17 14.92L12.5 12.25V7Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Daily & Hourly Limits', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\t\u003C\u002Fh4>\n-\t\t\t\t\t\t\t\u003Cp class=\"mo-osp-section-desc\">\u003C?php echo esc_html( __( 'Set maximum OTP requests per user per day and per hour to prevent abuse.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-fields-grid\">\n-\t\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group\">\n-\t\t\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\n-\t\t\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\n-\t\t\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M19 3H18V1H16V3H8V1H6V3H5C3.89 3 3.01 3.9 3.01 5L3 19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM19 19H5V8H19V19ZM7 10H12V15H7V10Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Daily Limit Per User', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\t\t\t\u003C\u002Flabel>\n-\t\t\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo_osp_daily_limit\" name=\"mo_osp_daily_limit\" value=\"\u003C?php echo esc_attr( $settings['daily_limit'] ); ?>\" min=\"1\" max=\"1000\" class=\"mo-form-input w-full\" \u002F>\n-\t\t\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'Maximum OTP requests one user can make in a single day.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\t\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group\">\n-\t\t\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\n-\t\t\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\n-\t\t\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M11.99 2C6.47 2 2 6.48 2 12S6.47 22 11.99 22C17.52 22 22 17.52 22 12S17.52 2 11.99 2ZM12 20C7.58 20 4 16.42 4 12S7.58 4 12 4S20 7.58 20 12S16.42 20 12 20ZM12.5 7H11V13L16.25 16.15L17 14.92L12.5 12.25V7Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Hourly Limit Per User', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\t\t\t\u003C\u002Flabel>\n-\t\t\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo_osp_hourly_limit\" name=\"mo_osp_hourly_limit\" value=\"\u003C?php echo esc_attr( $settings['hourly_limit'] ); ?>\" min=\"1\" max=\"100\" class=\"mo-form-input w-full\" \u002F>\n-\t\t\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'Maximum OTP requests one user can make in a single hour.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-subsection\">\n-\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-subsection-header\">\n-\t\t\t\t\t\t\t\u003Ch4 class=\"mo-osp-subsection-title\">\n-\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-subsection-icon\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M12 2C13.5 2 15 2.19 16.43 2.56L15.65 4.93C14.46 4.33 13.24 4 12 4C8.27 4 4.94 5.66 3 8.5C4.94 11.34 8.27 13 12 13S19.06 11.34 21 8.5C20.72 7.93 20.39 7.4 20.03 6.93L21.42 5.54C22.41 6.69 23.06 7.79 23.06 8.5C23.06 9.21 22.41 10.31 21.42 11.46C19.94 13.34 16.06 15 12 15S4.06 13.34 2.58 11.46C1.59 10.31 0.94 9.21 0.94 8.5C0.94 7.79 1.59 6.69 2.58 5.54C4.06 3.66 7.94 2 12 2ZM12 6.5C13.38 6.5 14.5 7.62 14.5 9S13.38 11.5 12 11.5 9.5 10.38 9.5 9 10.62 6.5 12 6.5ZM18.5 1L17 2.5L18.5 4L20 2.5L18.5 1Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Trusted IP Addresses', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\t\u003C\u002Fh4>\n-\t\t\t\t\t\t\t\u003Cp class=\"mo-osp-section-desc\">\u003C?php echo esc_html( __( 'IP addresses that should never be blocked, even if they exceed limits.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group mo-osp-field-full\">\n-\t\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\n-\t\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\n-\t\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M4.93 4.93L3.51 6.34C2.52 7.33 2 8.61 2 10S2.52 12.67 3.51 13.66L6.34 16.49C7.33 17.48 8.61 18 10 18S12.67 17.48 13.66 16.49L16.49 13.66C17.48 12.67 18 11.39 18 10S17.48 7.33 16.49 6.34L13.66 3.51C12.67 2.52 11.39 2 10 2S7.33 2.52 6.34 3.51L4.93 4.93ZM15.07 9.07L13.66 10.49L12.24 9.07L10.83 10.49L12.24 11.9L10.83 13.32L12.24 14.73L13.66 13.32L15.07 14.73L16.49 13.32L15.07 11.9L16.49 10.49L15.07 9.07ZM8.41 8.41L9.83 7L11.24 8.41L12.66 7L14.07 8.41L12.66 9.83L11.24 8.41L9.83 9.83L8.41 8.41Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Whitelist IP Addresses', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\t\t\u003C\u002Flabel>\n-\t\t\t\t\t\t\t\t\u003Ctextarea id=\"mo_osp_whitelist_ips\" name=\"mo_osp_whitelist_ips\" rows=\"6\" class=\"mo-form-textarea w-full\">\n-\t\t\t\t\t\t\t\t\u003C?php\n-\t\t\t\t\t\t\t\t\t$whitelist_display = isset( $settings['whitelist_ips'] ) && is_array( $settings['whitelist_ips'] )\n-\t\t\t\t\t\t\t\t\t\t? $settings['whitelist_ips']\n-\t\t\t\t\t\t\t\t\t\t: ( is_string( $settings['whitelist_ips'] )\n-\t\t\t\t\t\t\t\t\t\t\t? array_filter( array_map( 'trim', explode( \"\\n\", $settings['whitelist_ips'] ) ) )\n-\t\t\t\t\t\t\t\t\t\t\t: array() );\n-\t\t\t\t\t\t\t\t\techo esc_textarea( implode( \"\\n\", $whitelist_display ) );\n-\t\t\t\t\t\t\t\t\t?>\n-\t\t\t\t\t\t\t\t\u003C\u002Ftextarea>\n-\t\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'Enter one IP address per line (e.g., 192.168.1.1). These IPs will bypass all protection.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\u003C\u002Fdiv>\n-\t\t\u003C\u002Fdiv>\n-\n-\t\t\u003Cdiv class=\"mo-osp-card\">\n-\t\t\t\u003Cdiv class=\"mo-osp-card-header\">\n-\t\t\t\t\u003Ch3 class=\"mo-osp-section-title\">\n-\t\t\t\t\t\u003Csvg class=\"mo-osp-section-icon\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\u003Cpath d=\"M12 2C6.48 2 2 6.48 2 12S6.48 22 12 22 22 17.52 22 12 17.52 2 12 2ZM13 17H11V15H13V17ZM13 13H11V7H13V13Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\u003C?php echo esc_html( __( 'Blocked Users', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\u003C\u002Fh3>\n-\t\t\t\t\u003Cp class=\"mo-osp-section-desc\">\u003C?php echo esc_html( __( 'View and manage users who are currently blocked due to rate limits or excessive attempts.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\u003C\u002Fdiv>\n-\n-\t\t\t\u003Cdiv class=\"mo-osp-card-body\">\n-\t\t\t\t\u003Cdiv id=\"mo-osp-blocked-users-container\">\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-loading\" id=\"mo-osp-blocked-users-loading\" style=\"display: none;\">\n-\t\t\t\t\t\t\u003Cp>\u003C?php echo esc_html( __( 'Loading blocked users...', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\u003Cdiv id=\"mo-osp-blocked-users-table-container\">\n-\t\t\t\t\t\t\u003Ctable class=\"mo-osp-blocked-users-table\" id=\"mo-osp-blocked-users-table\">\n-\t\t\t\t\t\t\t\u003Cthead>\n-\t\t\t\t\t\t\t\t\u003Ctr>\n-\t\t\t\t\t\t\t\t\t\u003Cth>\u003C?php echo esc_html( __( 'User Identifier', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fth>\n-\t\t\t\t\t\t\t\t\t\u003Cth>\u003C?php echo esc_html( __( 'Block Reason', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fth>\n-\t\t\t\t\t\t\t\t\t\u003Cth>\u003C?php echo esc_html( __( 'Remaining Time', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fth>\n-\t\t\t\t\t\t\t\t\t\u003Cth>\u003C?php echo esc_html( __( 'Actions', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fth>\n-\t\t\t\t\t\t\t\t\u003C\u002Ftr>\n-\t\t\t\t\t\t\t\u003C\u002Fthead>\n-\t\t\t\t\t\t\t\u003Ctbody id=\"mo-osp-blocked-users-tbody\">\n-\t\t\t\t\t\t\t\t\u003Ctr>\n-\t\t\t\t\t\t\t\t\t\u003Ctd colspan=\"4\" class=\"mo-osp-no-data\">\n-\t\t\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'No blocked users found.', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\t\t\t\u003C\u002Ftd>\n-\t\t\t\t\t\t\t\t\u003C\u002Ftr>\n-\t\t\t\t\t\t\t\u003C\u002Ftbody>\n-\t\t\t\t\t\t\u003C\u002Ftable>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-blocked-users-pagination\" id=\"mo-osp-blocked-users-pagination\" style=\"display: none;\">\n-\t\t\t\t\t\t\u003Cbutton type=\"button\" class=\"mo-button mo-button-secondary\" id=\"mo-osp-prev-page\" disabled>\u003C?php echo esc_html( __( 'Previous', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fbutton>\n-\t\t\t\t\t\t\u003Cspan id=\"mo-osp-page-info\">\u003C\u002Fspan>\n-\t\t\t\t\t\t\u003Cbutton type=\"button\" class=\"mo-button mo-button-secondary\" id=\"mo-osp-next-page\" disabled>\u003C?php echo esc_html( __( 'Next', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fbutton>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\t\u003Cdiv class=\"mo-osp-blocked-users-actions\">\n-\t\t\t\t\t\t\u003Cbutton type=\"button\" class=\"mo-button mo-button-secondary\" id=\"mo-osp-clear-all-blocked-users\">\n-\t\t\t\t\t\t\t\u003Csvg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\u003Cpath d=\"M6 19C6 20.1 6.9 21 8 21H16C17.1 21 18 20.1 18 19V7H6V19ZM8 9H16V19H8V9ZM15.5 4L14.5 3H9.5L8.5 4H5V6H19V4H15.5Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Clear All', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\u003C\u002Fbutton>\n-\t\t\t\t\t\t\u003Cbutton type=\"button\" class=\"mo-button mo-button-secondary\" id=\"mo-osp-refresh-blocked-users\">\n-\t\t\t\t\t\t\t\u003Csvg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\n-\t\t\t\t\t\t\t\t\u003Cpath d=\"M17.65 6.35C16.2 4.9 14.21 4 12 4C7.58 4 4 7.58 4 12S7.58 20 12 20C15.73 20 18.84 17.45 19.73 14H17.65C16.83 16.33 14.61 18 12 18C8.69 18 6 15.31 6 12S8.69 6 12 6C13.66 6 15.14 6.69 16.22 7.78L13 11H20V4L17.65 6.35Z\" fill=\"currentColor\"\u002F>\n-\t\t\t\t\t\t\t\u003C\u002Fsvg>\n-\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Refresh List', 'miniorange-otp-verification' ) ); ?>\n-\t\t\t\t\t\t\u003C\u002Fbutton>\n-\t\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\t\u003C\u002Fdiv>\n-\t\t\t\u003C\u002Fdiv>\n-\t\t\u003C\u002Fdiv>\n-\t\u003C\u002Fform>\n-\u003C\u002Fdiv>\n+\u003C?php\r\n+\u002F**\r\n+ * OTP Spam Preventer View\r\n+ *\r\n+ * @package otpspampreventer\u002Fviews\r\n+ *\u002F\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+?>\r\n+\r\n+\u003Cdiv class=\"mo-osp-container\">\r\n+\t\u003Cform method=\"post\" action=\"\" id=\"mo_osp_settings_form\">\r\n+\t\t\u003C?php wp_nonce_field( 'mo_osp_settings_save' ); ?>\r\n+\t\t\u003Cinput type=\"hidden\" name=\"option\" value=\"mo_osp_settings_save\" \u002F>\r\n+\r\n+\t\t\u003Cdiv class=\"mo-header\">\r\n+\t\t\t\u003Cp class=\"mo-heading flex-1\">\r\n+\t\t\t\t\u003C?php echo esc_html( __( 'OTP Spam Protection', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\u003C\u002Fp>\r\n+\t\t\t\u003Cinput type=\"submit\" name=\"save\" id=\"save\" class=\"mo-button inverted\" value=\"\u003C?php echo esc_attr( __( 'Save Settings', 'miniorange-otp-verification' ) ); ?>\">\r\n+\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\u003Cdiv id=\"mo-osp-admin-notice-container\">\u003C\u002Fdiv>\r\n+\r\n+\t\t\u003Cdiv class=\"mo-osp-addon-toggle-row\">\r\n+\t\t\t\u003Clabel class=\"mo-osp-addon-toggle mo-osp-addon-toggle-emphasis\" for=\"mo_osp_enabled\">\r\n+\t\t\t\t\u003Cinput type=\"checkbox\" id=\"mo_osp_enabled\" name=\"mo_osp_enabled\" value=\"1\" \u003C?php checked( ! empty( $settings['enabled'] ) ); ?> \u002F>\r\n+\t\t\t\t\u003Cspan>\u003C?php echo esc_html( __( 'Enable Addon', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fspan>\r\n+\t\t\t\u003C\u002Flabel>\r\n+\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\u003Cdiv class=\"mo-osp-card\">\r\n+\t\t\t\u003Cdiv class=\"mo-osp-card-header\">\r\n+\t\t\t\t\u003Ch3 class=\"mo-osp-section-title\">\r\n+\t\t\t\t\t\u003Csvg class=\"mo-osp-section-icon\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\u003Cpath d=\"M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 4L13.5 7H7V9H13.5L15 12L21 9ZM4 15.5C4 17.43 5.57 19 7.5 19S11 17.43 11 15.5 9.43 12 7.5 12 4 13.57 4 15.5ZM7.5 17C6.67 17 6 16.33 6 15.5S6.67 14 7.5 14 9 14.67 9 15.5 8.33 17 7.5 17Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\u003C?php echo esc_html( __( 'Basic Protection Settings', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\u003C\u002Fh3>\r\n+\t\t\t\t\u003Cp class=\"mo-osp-section-desc\">\u003C?php echo esc_html( __( 'Configure how long users must wait between OTP requests and how many attempts are allowed.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\u003Cdiv class=\"mo-osp-card-body\">\r\n+\t\t\t\t\u003Cdiv class=\"mo-osp-fields-grid\">\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group\">\r\n+\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\r\n+\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\r\n+\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2ZM17 13H11V7H12.5V11.5H17V13Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Wait Time Between Requests', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\t\u003C\u002Flabel>\r\n+\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo_osp_cooldown_time\" name=\"mo_osp_cooldown_time\" value=\"\u003C?php echo esc_attr( $settings['cooldown_time'] ); ?>\" min=\"0\" max=\"86400\" class=\"mo-form-input w-full\" \u002F>\r\n+\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'Seconds users must wait before requesting another OTP (e.g., 60 = 1 minute).', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group\">\r\n+\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\r\n+\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\r\n+\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M12 2C10.1 2 8.5 3.6 8.5 5.5S10.1 9 12 9 15.5 7.4 15.5 5.5 13.9 2 12 2ZM12 7C11.2 7 10.5 6.3 10.5 5.5S11.2 4 12 4 13.5 4.7 13.5 5.5 12.8 7 12 7ZM5.5 8C3.6 8 2 9.6 2 11.5S3.6 15 5.5 15 9 13.4 9 11.5 7.4 8 5.5 8ZM18.5 8C16.6 8 15 9.6 15 11.5S16.6 15 18.5 15 22 13.4 22 11.5 20.4 8 18.5 8ZM12 10.5C10.1 10.5 8.5 12.1 8.5 14S10.1 17.5 12 17.5 15.5 15.9 15.5 14 13.9 10.5 12 10.5ZM5.5 16C3.6 16 2 17.6 2 19.5S3.6 23 5.5 23 9 21.4 9 19.5 7.4 16 5.5 16ZM18.5 16C16.6 16 15 17.6 15 19.5S16.6 23 18.5 23 22 21.4 22 19.5 20.4 16 18.5 16Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Maximum Attempts Allowed', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\t\u003C\u002Flabel>\r\n+\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo_osp_max_attempts\" name=\"mo_osp_max_attempts\" value=\"\u003C?php echo esc_attr( $settings['max_attempts'] ); ?>\" min=\"3\" max=\"10\" class=\"mo-form-input w-full\" \u002F>\r\n+\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'How many OTP requests are allowed before blocking (between 1-10).', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group\">\r\n+\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\r\n+\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\r\n+\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M18 8H17V6C17 3.24 14.76 1 12 1S7 3.24 7 6V8H6C4.9 8 4 8.9 4 10V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V10C20 8.9 19.1 8 18 8ZM12 17C10.9 17 10 16.1 10 15S10.9 13 12 13 14 13.9 14 15 13.1 17 12 17ZM15.1 8H8.9V6C8.9 4.29 10.29 2.9 12 2.9S15.1 4.29 15.1 6V8Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Block Duration', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\t\u003C\u002Flabel>\r\n+\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo_osp_block_time\" name=\"mo_osp_block_time\" value=\"\u003C?php echo esc_attr( $settings['block_time'] ); ?>\" min=\"60\" max=\"604800\" class=\"mo-form-input w-full\" \u002F>\r\n+\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'How long to block users after too many attempts in seconds (3600 = 1 hour).', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\u003C\u002Fdiv>\r\n+\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\u003Cdiv class=\"mo-osp-card\">\r\n+\t\t\t\u003Cbutton type=\"button\" id=\"mo-osp-toggle-advanced\" class=\"mo-osp-toggle-btn\">\r\n+\t\t\t\t\u003Cdiv class=\"mo-osp-toggle-content\">\r\n+\t\t\t\t\t\u003Ch3 class=\"mo-osp-section-title\">\r\n+\t\t\t\t\t\t\u003Csvg class=\"mo-osp-section-icon\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\u003Cpath d=\"M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5A3.5 3.5 0 0 1 15.5 12A3.5 3.5 0 0 1 12 15.5M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12S19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.54 5.05 19.27 4.97 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.72 4.96 4.46 5.05 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12S4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.21 15.05 2.34 15.27L4.34 18.73C4.46 18.95 4.73 19.03 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.28 19.04 19.54 18.95 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Advanced Settings', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\u003C\u002Fh3>\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-toggle-indicator\">\r\n+\t\t\t\t\t\t\u003Cspan id=\"mo-osp-toggle-text\" class=\"mo-osp-toggle-text\">\u003C?php echo esc_html( __( 'Show Advanced', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fspan>\r\n+\t\t\t\t\t\t\u003Cspan id=\"mo-osp-toggle-icon\" class=\"mo-osp-toggle-icon\">▼\u003C\u002Fspan>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\u003C\u002Fbutton>\r\n+\r\n+\t\t\t\u003Cdiv id=\"mo-osp-advanced-settings\" class=\"mo-osp-advanced-hidden\">\r\n+\t\t\t\t\u003Cdiv class=\"mo-osp-advanced-content\">\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-subsection\">\r\n+\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-subsection-header\">\r\n+\t\t\t\t\t\t\t\u003Ch4 class=\"mo-osp-subsection-title\">\r\n+\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-subsection-icon\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M11.99 2C6.47 2 2 6.48 2 12S6.47 22 11.99 22C17.52 22 22 17.52 22 12S17.52 2 11.99 2ZM12 20C7.58 20 4 16.42 4 12S7.58 4 12 4S20 7.58 20 12S16.42 20 12 20ZM12.5 7H11V13L16.25 16.15L17 14.92L12.5 12.25V7Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Daily & Hourly Limits', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\t\u003C\u002Fh4>\r\n+\t\t\t\t\t\t\t\u003Cp class=\"mo-osp-section-desc\">\u003C?php echo esc_html( __( 'Set maximum OTP requests per user per day and per hour to prevent abuse.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-fields-grid\">\r\n+\t\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group\">\r\n+\t\t\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\r\n+\t\t\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\r\n+\t\t\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M19 3H18V1H16V3H8V1H6V3H5C3.89 3 3.01 3.9 3.01 5L3 19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM19 19H5V8H19V19ZM7 10H12V15H7V10Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Daily Limit Per User', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\t\t\t\u003C\u002Flabel>\r\n+\t\t\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo_osp_daily_limit\" name=\"mo_osp_daily_limit\" value=\"\u003C?php echo esc_attr( $settings['daily_limit'] ); ?>\" min=\"1\" max=\"1000\" class=\"mo-form-input w-full\" \u002F>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'Maximum OTP requests one user can make in a single day.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group\">\r\n+\t\t\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\r\n+\t\t\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\r\n+\t\t\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M11.99 2C6.47 2 2 6.48 2 12S6.47 22 11.99 22C17.52 22 22 17.52 22 12S17.52 2 11.99 2ZM12 20C7.58 20 4 16.42 4 12S7.58 4 12 4S20 7.58 20 12S16.42 20 12 20ZM12.5 7H11V13L16.25 16.15L17 14.92L12.5 12.25V7Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Hourly Limit Per User', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\t\t\t\u003C\u002Flabel>\r\n+\t\t\t\t\t\t\t\t\t\u003Cinput type=\"number\" id=\"mo_osp_hourly_limit\" name=\"mo_osp_hourly_limit\" value=\"\u003C?php echo esc_attr( $settings['hourly_limit'] ); ?>\" min=\"1\" max=\"100\" class=\"mo-form-input w-full\" \u002F>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'Maximum OTP requests one user can make in a single hour.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-subsection\">\r\n+\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-subsection-header\">\r\n+\t\t\t\t\t\t\t\u003Ch4 class=\"mo-osp-subsection-title\">\r\n+\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-subsection-icon\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M12 2C13.5 2 15 2.19 16.43 2.56L15.65 4.93C14.46 4.33 13.24 4 12 4C8.27 4 4.94 5.66 3 8.5C4.94 11.34 8.27 13 12 13S19.06 11.34 21 8.5C20.72 7.93 20.39 7.4 20.03 6.93L21.42 5.54C22.41 6.69 23.06 7.79 23.06 8.5C23.06 9.21 22.41 10.31 21.42 11.46C19.94 13.34 16.06 15 12 15S4.06 13.34 2.58 11.46C1.59 10.31 0.94 9.21 0.94 8.5C0.94 7.79 1.59 6.69 2.58 5.54C4.06 3.66 7.94 2 12 2ZM12 6.5C13.38 6.5 14.5 7.62 14.5 9S13.38 11.5 12 11.5 9.5 10.38 9.5 9 10.62 6.5 12 6.5ZM18.5 1L17 2.5L18.5 4L20 2.5L18.5 1Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Trusted IP Addresses', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\t\u003C\u002Fh4>\r\n+\t\t\t\t\t\t\t\u003Cp class=\"mo-osp-section-desc\">\u003C?php echo esc_html( __( 'IP addresses that should never be blocked, even if they exceed limits.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\t\t\t\u003Cdiv class=\"mo-osp-field-group mo-osp-field-full\">\r\n+\t\t\t\t\t\t\t\u003Cdiv class=\"mo-input-wrapper group\">\r\n+\t\t\t\t\t\t\t\t\u003Clabel class=\"mo-input-label\">\r\n+\t\t\t\t\t\t\t\t\t\u003Csvg class=\"mo-osp-field-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\t\t\u003Cpath d=\"M4.93 4.93L3.51 6.34C2.52 7.33 2 8.61 2 10S2.52 12.67 3.51 13.66L6.34 16.49C7.33 17.48 8.61 18 10 18S12.67 17.48 13.66 16.49L16.49 13.66C17.48 12.67 18 11.39 18 10S17.48 7.33 16.49 6.34L13.66 3.51C12.67 2.52 11.39 2 10 2S7.33 2.52 6.34 3.51L4.93 4.93ZM15.07 9.07L13.66 10.49L12.24 9.07L10.83 10.49L12.24 11.9L10.83 13.32L12.24 14.73L13.66 13.32L15.07 14.73L16.49 13.32L15.07 11.9L16.49 10.49L15.07 9.07ZM8.41 8.41L9.83 7L11.24 8.41L12.66 7L14.07 8.41L12.66 9.83L11.24 8.41L9.83 9.83L8.41 8.41Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Whitelist IP Addresses', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Flabel>\r\n+\t\t\t\t\t\t\t\t\u003Ctextarea id=\"mo_osp_whitelist_ips\" name=\"mo_osp_whitelist_ips\" rows=\"6\" class=\"mo-form-textarea w-full\">\r\n+\t\t\t\t\t\t\t\t\u003C?php\r\n+\t\t\t\t\t\t\t\t\t$whitelist_display = isset( $settings['whitelist_ips'] ) && is_array( $settings['whitelist_ips'] )\r\n+\t\t\t\t\t\t\t\t\t\t? $settings['whitelist_ips']\r\n+\t\t\t\t\t\t\t\t\t\t: ( is_string( $settings['whitelist_ips'] )\r\n+\t\t\t\t\t\t\t\t\t\t\t? array_filter( array_map( 'trim', explode( \"\\n\", $settings['whitelist_ips'] ) ) )\r\n+\t\t\t\t\t\t\t\t\t\t\t: array() );\r\n+\t\t\t\t\t\t\t\t\techo esc_textarea( implode( \"\\n\", $whitelist_display ) );\r\n+\t\t\t\t\t\t\t\t\t?>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Ftextarea>\r\n+\t\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\t\t\u003Cp class=\"mo-osp-field-desc\">\u003C?php echo esc_html( __( 'Enter one IP address per line (e.g., 192.168.1.1). These IPs will bypass all protection.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\u003C\u002Fdiv>\r\n+\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\u003Cdiv class=\"mo-osp-card\">\r\n+\t\t\t\u003Cdiv class=\"mo-osp-card-header\">\r\n+\t\t\t\t\u003Ch3 class=\"mo-osp-section-title\">\r\n+\t\t\t\t\t\u003Csvg class=\"mo-osp-section-icon\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\u003Cpath d=\"M12 2C6.48 2 2 6.48 2 12S6.48 22 12 22 22 17.52 22 12 17.52 2 12 2ZM13 17H11V15H13V17ZM13 13H11V7H13V13Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\u003C?php echo esc_html( __( 'Blocked Users', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\u003C\u002Fh3>\r\n+\t\t\t\t\u003Cp class=\"mo-osp-section-desc\">\u003C?php echo esc_html( __( 'View and manage users who are currently blocked due to rate limits or excessive attempts.', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\u003C\u002Fdiv>\r\n+\r\n+\t\t\t\u003Cdiv class=\"mo-osp-card-body\">\r\n+\t\t\t\t\u003Cdiv id=\"mo-osp-blocked-users-container\">\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-loading\" id=\"mo-osp-blocked-users-loading\" style=\"display: none;\">\r\n+\t\t\t\t\t\t\u003Cp>\u003C?php echo esc_html( __( 'Loading blocked users...', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fp>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\u003Cdiv id=\"mo-osp-blocked-users-table-container\">\r\n+\t\t\t\t\t\t\u003Ctable class=\"mo-osp-blocked-users-table\" id=\"mo-osp-blocked-users-table\">\r\n+\t\t\t\t\t\t\t\u003Cthead>\r\n+\t\t\t\t\t\t\t\t\u003Ctr>\r\n+\t\t\t\t\t\t\t\t\t\u003Cth>\u003C?php echo esc_html( __( 'User Identifier', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fth>\r\n+\t\t\t\t\t\t\t\t\t\u003Cth>\u003C?php echo esc_html( __( 'Block Reason', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fth>\r\n+\t\t\t\t\t\t\t\t\t\u003Cth>\u003C?php echo esc_html( __( 'Remaining Time', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fth>\r\n+\t\t\t\t\t\t\t\t\t\u003Cth>\u003C?php echo esc_html( __( 'Actions', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fth>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Ftr>\r\n+\t\t\t\t\t\t\t\u003C\u002Fthead>\r\n+\t\t\t\t\t\t\t\u003Ctbody id=\"mo-osp-blocked-users-tbody\">\r\n+\t\t\t\t\t\t\t\t\u003Ctr>\r\n+\t\t\t\t\t\t\t\t\t\u003Ctd colspan=\"4\" class=\"mo-osp-no-data\">\r\n+\t\t\t\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'No blocked users found.', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\t\t\t\u003C\u002Ftd>\r\n+\t\t\t\t\t\t\t\t\u003C\u002Ftr>\r\n+\t\t\t\t\t\t\t\u003C\u002Ftbody>\r\n+\t\t\t\t\t\t\u003C\u002Ftable>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-blocked-users-pagination\" id=\"mo-osp-blocked-users-pagination\" style=\"display: none;\">\r\n+\t\t\t\t\t\t\u003Cbutton type=\"button\" class=\"mo-button mo-button-secondary\" id=\"mo-osp-prev-page\" disabled>\u003C?php echo esc_html( __( 'Previous', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fbutton>\r\n+\t\t\t\t\t\t\u003Cspan id=\"mo-osp-page-info\">\u003C\u002Fspan>\r\n+\t\t\t\t\t\t\u003Cbutton type=\"button\" class=\"mo-button mo-button-secondary\" id=\"mo-osp-next-page\" disabled>\u003C?php echo esc_html( __( 'Next', 'miniorange-otp-verification' ) ); ?>\u003C\u002Fbutton>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\t\u003Cdiv class=\"mo-osp-blocked-users-actions\">\r\n+\t\t\t\t\t\t\u003Cbutton type=\"button\" class=\"mo-button mo-button-secondary\" id=\"mo-osp-clear-all-blocked-users\">\r\n+\t\t\t\t\t\t\t\u003Csvg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\u003Cpath d=\"M6 19C6 20.1 6.9 21 8 21H16C17.1 21 18 20.1 18 19V7H6V19ZM8 9H16V19H8V9ZM15.5 4L14.5 3H9.5L8.5 4H5V6H19V4H15.5Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Clear All', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\u003C\u002Fbutton>\r\n+\t\t\t\t\t\t\u003Cbutton type=\"button\" class=\"mo-button mo-button-secondary\" id=\"mo-osp-refresh-blocked-users\">\r\n+\t\t\t\t\t\t\t\u003Csvg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\">\r\n+\t\t\t\t\t\t\t\t\u003Cpath d=\"M17.65 6.35C16.2 4.9 14.21 4 12 4C7.58 4 4 7.58 4 12S7.58 20 12 20C15.73 20 18.84 17.45 19.73 14H17.65C16.83 16.33 14.61 18 12 18C8.69 18 6 15.31 6 12S8.69 6 12 6C13.66 6 15.14 6.69 16.22 7.78L13 11H20V4L17.65 6.35Z\" fill=\"currentColor\"\u002F>\r\n+\t\t\t\t\t\t\t\u003C\u002Fsvg>\r\n+\t\t\t\t\t\t\t\u003C?php echo esc_html( __( 'Refresh List', 'miniorange-otp-verification' ) ); ?>\r\n+\t\t\t\t\t\t\u003C\u002Fbutton>\r\n+\t\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\t\u003C\u002Fdiv>\r\n+\t\t\t\u003C\u002Fdiv>\r\n+\t\t\u003C\u002Fdiv>\r\n+\t\u003C\u002Fform>\r\n+\u003C\u002Fdiv>\r\nOnly in \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2: api\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fautoload.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fautoload.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fautoload.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fautoload.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,272 +1,272 @@\n-\u003C?php\n-\u002F**\n- * Initializes plugin data.\n- * Contains defination of common functions.\n- *\n- * @package miniorange-otp-verification\n- *\u002F\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-use OTP\\Helper\\FormList;\n-use OTP\\Helper\\FormSessionData;\n-use OTP\\Helper\\MoUtility;\n-use OTP\\Objects\\FormHandler;\n-use OTP\\Objects\\IFormHandler;\n-use OTP\\MoOTPSplClassLoader;\n-use OTP\\LicenseLibrary\\Classes\\Mo_License_Library;\n-use OTP\\Helper\\MoConstants;\n-\n-define( 'MOV_DIR', plugin_dir_path( __FILE__ ) );\n-define( 'MOV_URL', plugin_dir_url( __FILE__ ) );\n-\n-$package_data = json_decode( initialize_package_json() );\n-\n-define( 'MOV_VERSION', $package_data->version );\n-define( 'MOV_TYPE', $package_data->type );\n-define( 'MOV_HOST', $package_data->hostname );\n-define( 'MOV_PORTAL', $package_data->portal );\n-define( 'MOV_DEFAULT_CUSTOMERKEY', $package_data->dcustomerkey );\n-define( 'MOV_DEFAULT_APIKEY', $package_data->dapikey );\n-define( 'MOV_SSL_VERIFY', $package_data->sslverify );\n-define( 'MOV_CSS_URL', MOV_URL . 'includes\u002Fcss\u002Fmo_customer_validation_style.css?version=' . MOV_VERSION );\n-define( 'MOV_FORM_CSS', MOV_URL . 'includes\u002Fcss\u002Fmo_forms_css.css?version=' . MOV_VERSION );\n-define( 'MO_INTTELINPUT_CSS', MOV_URL . 'includes\u002Fcss\u002FintlTelInput.min.css?version=' . MOV_VERSION );\n-define( 'MOV_JS_URL', MOV_URL . 'includes\u002Fjs\u002Fsettings.js?version=' . MOV_VERSION );\n-define( 'MOV_FEEDBACK_JS', MOV_URL . 'includes\u002Fjs\u002Fmo_feedback.js?version=' . MOV_VERSION );\n-define( 'VALIDATION_JS_URL', MOV_URL . 'includes\u002Fjs\u002FformValidation.js?version=' . MOV_VERSION );\n-define( 'MO_INTTELINPUT_JS', MOV_URL . 'includes\u002Fjs\u002FintlTelInput.min.js?version=' . MOV_VERSION );\n-define( 'MO_DROPDOWN_JS', MOV_URL . 'includes\u002Fjs\u002Fdropdown.js?version=' . MOV_VERSION );\n-define( 'MOV_LOADER_URL', MOV_URL . 'includes\u002Fimages\u002Floader.gif' );\n-define( 'MOV_DONATE', MOV_URL . 'includes\u002Fimages\u002Fdonate.png' );\n-define( 'MOV_PAYPAL', MOV_URL . 'includes\u002Fimages\u002Fpaypal.png' );\n-define( 'MOV_WHATSAPP', MOV_URL . 'includes\u002Fimages\u002FtourIcons\u002FwhatsApp.svg' );\n-define( 'MOV_NETBANK', MOV_URL . 'includes\u002Fimages\u002Fnetbanking.png' );\n-define( 'MOV_CARD', MOV_URL . 'includes\u002Fimages\u002Fcard.png' );\n-define( 'MOV_LOGO_URL', MOV_URL . 'includes\u002Fimages\u002Flogo.png' );\n-define( 'MOV_ICON', MOV_URL . 'includes\u002Fimages\u002Fminiorange_icon.png' );\n-define( 'MOV_ICON_GIF', MOV_URL . 'includes\u002Fimages\u002Fmo_icon.gif' );\n-define( 'MO_CUSTOM_FORM', MOV_URL . 'includes\u002Fjs\u002FcustomForm.js?version=' . MOV_VERSION );\n-define( 'MOV_ADDON_DIR', MOV_DIR . 'addons\u002F' );\n-define( 'MOV_USE_POLYLANG', true );\n-define( 'MO_TEST_MODE', $package_data->testmode );\n-define( 'MO_FAIL_MODE', $package_data->failmode );\n-define( 'MOV_SESSION_TYPE', $package_data->session );\n-define( 'MOV_MAIL_LOGO', MOV_URL . 'includes\u002Fimages\u002Fmo_support_icon.png' );\n-define( 'MOV_OFFERS_LOGO', MOV_URL . 'includes\u002Fimages\u002Fmo_sale_icon.png' );\n-define( 'MOV_FEATURES_GRAPHIC', MOV_URL . 'includes\u002Fimages\u002Fmo_features_graphic.png' );\n-define( 'MOV_TYPE_PLAN', $package_data->typeplan );\n-define( 'MOV_LICENSE_NAME', $package_data->licensename );\n-define( 'MOV_CSS', MOV_URL . 'includes\u002Fcss\u002Fmo_feedback_notice.css?version=' . MOV_VERSION );\n-define( 'MOV_MAIN_CSS', MOV_URL . 'includes\u002Fcss\u002Fmo-main.css?version=' . MOV_VERSION );\n-\n-$target = realpath( __DIR__ . '\u002Fclass-mootpsplclassloader.php' );\n-if ( false !== $target && 0 === strpos( $target, realpath( MOV_DIR ) ) ) {\n-\trequire $target;\n-}\n-\n-$idp_class_loader = new MoOTPSplClassLoader( 'OTP', realpath( __DIR__ . DIRECTORY_SEPARATOR . '..' ) );\n-$idp_class_loader->register();\n-$common_elements_path = realpath( __DIR__ . '\u002Fviews\u002Fcommon-elements.php' );\n-if ( false !== $common_elements_path && 0 === strpos( $common_elements_path, realpath( MOV_DIR ) ) ) {\n-\trequire $common_elements_path;\n-}\n-\n-if ( file_exists( MOV_DIR . MoConstants::LICENCE_SERVICE_FILE ) ) {\n-\tnew Mo_License_Library();\n-}\n-\n-\n-\u002F**\n- * Initializes handlers of forms.\n- *\n- * @return void\n- *\u002F\n-function mo_initialize_forms() {\n-\t$forms_dir  = MOV_DIR . 'handler\u002Fforms';\n-\t$forms_path = realpath( $forms_dir );\n-\n-\tif ( false === $forms_path || 0 !== strpos( $forms_path, realpath( MOV_DIR ) ) ) {\n-\t\treturn;\n-\t}\n-\n-\t$iterator = new RecursiveIteratorIterator(\n-\t\tnew RecursiveDirectoryIterator( $forms_path, RecursiveDirectoryIterator::SKIP_DOTS ),\n-\t\tRecursiveIteratorIterator::LEAVES_ONLY\n-\t);\n-\n-\t$handler_list      = FormList::instance();\n-\t$is_block_checkout = mo_is_block_based_checkout();\n-\n-\tforeach ( $iterator as $it ) {\n-\t\t$filename = $it->getFilename();\n-\t\t$filename = sanitize_file_name( str_replace( 'class-', '', $filename ) );\n-\n-\t\tif ( 'mowccheckoutnew.php' === $filename || 'woocommercecheckoutform.php' === $filename ) {\n-\t\t\t\u002F\u002F Load only one WooCommerce checkout handler based on whether block-based checkout is enabled.\n-\t\t\t$selected_file = $is_block_checkout ? 'mowccheckoutnew.php' : 'woocommercecheckoutform.php';\n-\n-\t\t\t\u002F\u002F If the current file is not the selected handler, skip it to avoid duplicate registration.\n-\t\t\tif ( $filename !== $selected_file ) {\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\n-\t\t\t$class_name = 'OTP\\\\Handler\\\\Forms\\\\' . str_replace( '.php', '', $selected_file );\n-\t\t} else {\n-\t\t\t$class_name = 'OTP\\\\Handler\\\\Forms\\\\' . str_replace( '.php', '', $filename );\n-\t\t}\n-\n-\t\tif ( class_exists( $class_name ) && method_exists( $class_name, 'instance' ) ) {\n-\t\t\t$form_handler = $class_name::instance();\n-\t\t\t$handler_list->add( $form_handler->get_form_key(), $form_handler );\n-\t\t}\n-\t}\n-}\n-\n-\u002F**\n- * Returns if block checkout is enabled.\n- *\n- * This version does not rely on WooCommerce Blocks PHP classes being loaded.\n- * Instead, it inspects the WooCommerce Checkout page content for the\n- * `woocommerce\u002Fcheckout` block, so it can be safely called early (e.g. in\n- * constructors) without depending on plugin load order.\n- *\n- * @return bool\n- *\u002F\n-function mo_is_block_based_checkout() {\n-\tif ( ! function_exists( 'is_plugin_active' ) ) {\n-\t\tinclude_once ABSPATH . 'wp-admin\u002Fincludes\u002Fplugin.php';\n-\t}\n-\n-\tif ( ! is_plugin_active( 'woocommerce\u002Fwoocommerce.php' ) ) {\n-\t\treturn false;\n-\t}\n-\n-\t\u002F\u002F Get the Checkout page ID directly from WooCommerce options.\n-\t$checkout_page_id = (int) get_option( 'woocommerce_checkout_page_id' );\n-\tif ( $checkout_page_id \u003C= 0 ) {\n-\t\treturn false;\n-\t}\n-\n-\t$checkout_page = get_post( $checkout_page_id );\n-\tif ( ! $checkout_page || empty( $checkout_page->post_content ) ) {\n-\t\treturn false;\n-\t}\n-\n-\t\u002F\u002F Prefer core has_block() if available (WP 5+).\n-\tif ( function_exists( 'has_block' ) ) {\n-\t\treturn has_block( 'woocommerce\u002Fcheckout', $checkout_page );\n-\t}\n-\n-\t\u002F\u002F Fallback: simple string search for the block name in post content.\n-\treturn ( false !== strpos( $checkout_page->post_content, 'woocommerce\u002Fcheckout' ) );\n-}\n-\n-\u002F**\n- * Returns admin post url.\n- *\n- * @return string\n- *\u002F\n-function admin_post_url() {\n-\treturn admin_url( 'admin-post.php' ); }\n-\n-\u002F**\n- * Returns wp ajax url.\n- *\n- * @return string\n- *\u002F\n-function wp_ajax_url() {\n-\treturn admin_url( 'admin-ajax.php' ); }\n-\n-\u002F**\n- * Escapes a string based on the type.\n- *\n- * @param string $string_value - string to be escaped.\n- * @param string $type - type of escaping to apply.\n- * @return string\n- *\u002F\n-function mo_esc_string( $string_value, $type ) {\n-\tif ( 'attr' === $type ) {\n-\t\treturn esc_attr( $string_value );\n-\t} elseif ( 'url' === $type ) {\n-\t\treturn esc_url( $string_value );\n-\t}\n-\n-\treturn esc_attr( $string_value );\n-}\n-\n-\u002F**\n- * Retrieves the value of the option from the wp_option table.\n- *\n- * @param string $string_value - option name to be retrieved.\n- * @param string $prefix - prefix of the option.\n- * @return mixed\n- *\u002F\n-function get_mo_option( $string_value, $prefix = null ) {\n-\t$string_value = ( null === $prefix ? 'mo_customer_validation_' : $prefix ) . $string_value;\n-\treturn apply_filters( 'get_mo_option', get_site_option( $string_value ) );\n-}\n-\n-\u002F**\n- * Updates the option set in the wp_option table.\n- *\n- * @param string $string_value - option name to be deleted.\n- * @param string $value - value of the option.\n- * @param string $prefix - prefix of the option.\n- *\u002F\n-function update_mo_option( $string_value, $value, $prefix = null ) {\n-\t$string_value = ( null === $prefix ? 'mo_customer_validation_' : $prefix ) . $string_value;\n-\tupdate_site_option( $string_value, apply_filters( 'update_mo_option', $value, $string_value ) );\n-}\n-\n-\u002F**\n- * Deletes the option set in the wp_option table.\n- *\n- * @param string $string_value - option name to be deleted.\n- * @param string $prefix - prefix of the option.\n- *\u002F\n-function delete_mo_option( $string_value, $prefix = null ) {\n-\t$string_value = ( null === $prefix ? 'mo_customer_validation_' : $prefix ) . $string_value;\n-\tdelete_site_option( $string_value );\n-}\n-\n-\u002F**\n- * Returns the class name without namespace.\n- *\n- * @param object $obj - object of the class.\n- * @return string\n- *\u002F\n-function get_mo_class( $obj ) {\n-\t$namespace_class = get_class( $obj );\n-\treturn substr( $namespace_class, strrpos( $namespace_class, '\\\\' ) + 1 );\n-}\n-\n-\u002F**\n- * To check if package.json file can be found through WP site URL or not.\n- * BuildScript.php updates the package.json file content in the below function instead of package.json to be used further in autoload.php\n- * example package.json string [\"name\"=>\"miniorange-otp-verification\",\"version\"=>\"5.5.1\",\"type\"=>\"MiniOrangeGateway\",\"testMode\"=>false,\"failMode\"=>false,\"hostname\"=>\"https:\\\u002F\\\u002Flogin.xecurify.com\",\"dCustomerKey\"=>\"16555\",\"dApiKey\"=>\"fFd2XcvTGDemZvbw1bcUesNJWEqKbbUq\",\"sslVerify\"=>true,\"session\"=>\"AUTO\"]\n- *\n- * @return string\n- *\u002F\n-function initialize_package_json() {\n-\t$package = wp_json_encode(\n-\t\tarray(\n-\t\t\t'name'         => 'miniorange-otp-verification',\n-\t\t\t'version'      => '5.5.1',\n-\t\t\t'type'         => 'MiniOrangeGateway',\n-\t\t\t'testmode'     => false,\n-\t\t\t'failmode'     => false,\n-\t\t\t'hostname'     => 'https:\u002F\u002Flogin.xecurify.com',\n-\t\t\t'portal'       => 'https:\u002F\u002Fportal.miniorange.com',\n-\t\t\t'dcustomerkey' => '16555',\n-\t\t\t'dapikey'      => 'fFd2XcvTGDemZvbw1bcUesNJWEqKbbUq',\n-\t\t\t'sslverify'    => true,\n-\t\t\t'session'      => 'AUTO',\n-\t\t\t'typeplan'     => 'wp_otp_verification_basic_plan',\n-\t\t\t'licensename'  => 'WP_OTP_VERIFICATION_PLUGIN',\n-\t\t)\n-\t);\n-\treturn $package;\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * Initializes plugin data.\r\n+ * Contains defination of common functions.\r\n+ *\r\n+ * @package miniorange-otp-verification\r\n+ *\u002F\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+use OTP\\Helper\\FormList;\r\n+use OTP\\Helper\\FormSessionData;\r\n+use OTP\\Helper\\MoUtility;\r\n+use OTP\\Objects\\FormHandler;\r\n+use OTP\\Objects\\IFormHandler;\r\n+use OTP\\MoOTPSplClassLoader;\r\n+use OTP\\LicenseLibrary\\Classes\\Mo_License_Library;\r\n+use OTP\\Helper\\MoConstants;\r\n+\r\n+define( 'MOV_DIR', plugin_dir_path( __FILE__ ) );\r\n+define( 'MOV_URL', plugin_dir_url( __FILE__ ) );\r\n+\r\n+$package_data = json_decode( initialize_package_json() );\r\n+\r\n+define( 'MOV_VERSION', $package_data->version );\r\n+define( 'MOV_TYPE', $package_data->type );\r\n+define( 'MOV_HOST', $package_data->hostname );\r\n+define( 'MOV_PORTAL', $package_data->portal );\r\n+define( 'MOV_DEFAULT_CUSTOMERKEY', $package_data->dcustomerkey );\r\n+define( 'MOV_DEFAULT_APIKEY', $package_data->dapikey );\r\n+define( 'MOV_SSL_VERIFY', $package_data->sslverify );\r\n+define( 'MOV_CSS_URL', MOV_URL . 'includes\u002Fcss\u002Fmo_customer_validation_style.css?version=' . MOV_VERSION );\r\n+define( 'MOV_FORM_CSS', MOV_URL . 'includes\u002Fcss\u002Fmo_forms_css.css?version=' . MOV_VERSION );\r\n+define( 'MO_INTTELINPUT_CSS', MOV_URL . 'includes\u002Fcss\u002FintlTelInput.min.css?version=' . MOV_VERSION );\r\n+define( 'MOV_JS_URL', MOV_URL . 'includes\u002Fjs\u002Fsettings.js?version=' . MOV_VERSION );\r\n+define( 'MOV_FEEDBACK_JS', MOV_URL . 'includes\u002Fjs\u002Fmo_feedback.js?version=' . MOV_VERSION );\r\n+define( 'VALIDATION_JS_URL', MOV_URL . 'includes\u002Fjs\u002FformValidation.js?version=' . MOV_VERSION );\r\n+define( 'MO_INTTELINPUT_JS', MOV_URL . 'includes\u002Fjs\u002FintlTelInput.min.js?version=' . MOV_VERSION );\r\n+define( 'MO_DROPDOWN_JS', MOV_URL . 'includes\u002Fjs\u002Fdropdown.js?version=' . MOV_VERSION );\r\n+define( 'MOV_LOADER_URL', MOV_URL . 'includes\u002Fimages\u002Floader.gif' );\r\n+define( 'MOV_DONATE', MOV_URL . 'includes\u002Fimages\u002Fdonate.png' );\r\n+define( 'MOV_PAYPAL', MOV_URL . 'includes\u002Fimages\u002Fpaypal.png' );\r\n+define( 'MOV_WHATSAPP', MOV_URL . 'includes\u002Fimages\u002FtourIcons\u002FwhatsApp.svg' );\r\n+define( 'MOV_NETBANK', MOV_URL . 'includes\u002Fimages\u002Fnetbanking.png' );\r\n+define( 'MOV_CARD', MOV_URL . 'includes\u002Fimages\u002Fcard.png' );\r\n+define( 'MOV_LOGO_URL', MOV_URL . 'includes\u002Fimages\u002Flogo.png' );\r\n+define( 'MOV_ICON', MOV_URL . 'includes\u002Fimages\u002Fminiorange_icon.png' );\r\n+define( 'MOV_ICON_GIF', MOV_URL . 'includes\u002Fimages\u002Fmo_icon.gif' );\r\n+define( 'MO_CUSTOM_FORM', MOV_URL . 'includes\u002Fjs\u002FcustomForm.js?version=' . MOV_VERSION );\r\n+define( 'MOV_ADDON_DIR', MOV_DIR . 'addons\u002F' );\r\n+define( 'MOV_USE_POLYLANG', true );\r\n+define( 'MO_TEST_MODE', $package_data->testmode );\r\n+define( 'MO_FAIL_MODE', $package_data->failmode );\r\n+define( 'MOV_SESSION_TYPE', $package_data->session );\r\n+define( 'MOV_MAIL_LOGO', MOV_URL . 'includes\u002Fimages\u002Fmo_support_icon.png' );\r\n+define( 'MOV_OFFERS_LOGO', MOV_URL . 'includes\u002Fimages\u002Fmo_sale_icon.png' );\r\n+define( 'MOV_FEATURES_GRAPHIC', MOV_URL . 'includes\u002Fimages\u002Fmo_features_graphic.png' );\r\n+define( 'MOV_TYPE_PLAN', $package_data->typeplan );\r\n+define( 'MOV_LICENSE_NAME', $package_data->licensename );\r\n+define( 'MOV_CSS', MOV_URL . 'includes\u002Fcss\u002Fmo_feedback_notice.css?version=' . MOV_VERSION );\r\n+define( 'MOV_MAIN_CSS', MOV_URL . 'includes\u002Fcss\u002Fmo-main.css?version=' . MOV_VERSION );\r\n+\r\n+$target = realpath( __DIR__ . '\u002Fclass-mootpsplclassloader.php' );\r\n+if ( false !== $target && 0 === strpos( $target, realpath( MOV_DIR ) ) ) {\r\n+\trequire $target;\r\n+}\r\n+\r\n+$idp_class_loader = new MoOTPSplClassLoader( 'OTP', realpath( __DIR__ . DIRECTORY_SEPARATOR . '..' ) );\r\n+$idp_class_loader->register();\r\n+$common_elements_path = realpath( __DIR__ . '\u002Fviews\u002Fcommon-elements.php' );\r\n+if ( false !== $common_elements_path && 0 === strpos( $common_elements_path, realpath( MOV_DIR ) ) ) {\r\n+\trequire $common_elements_path;\r\n+}\r\n+\r\n+if ( file_exists( MOV_DIR . MoConstants::LICENCE_SERVICE_FILE ) ) {\r\n+\tnew Mo_License_Library();\r\n+}\r\n+\r\n+\r\n+\u002F**\r\n+ * Initializes handlers of forms.\r\n+ *\r\n+ * @return void\r\n+ *\u002F\r\n+function mo_initialize_forms() {\r\n+\t$forms_dir  = MOV_DIR . 'handler\u002Fforms';\r\n+\t$forms_path = realpath( $forms_dir );\r\n+\r\n+\tif ( false === $forms_path || 0 !== strpos( $forms_path, realpath( MOV_DIR ) ) ) {\r\n+\t\treturn;\r\n+\t}\r\n+\r\n+\t$iterator = new RecursiveIteratorIterator(\r\n+\t\tnew RecursiveDirectoryIterator( $forms_path, RecursiveDirectoryIterator::SKIP_DOTS ),\r\n+\t\tRecursiveIteratorIterator::LEAVES_ONLY\r\n+\t);\r\n+\r\n+\t$handler_list      = FormList::instance();\r\n+\t$is_block_checkout = mo_is_block_based_checkout();\r\n+\r\n+\tforeach ( $iterator as $it ) {\r\n+\t\t$filename = $it->getFilename();\r\n+\t\t$filename = sanitize_file_name( str_replace( 'class-', '', $filename ) );\r\n+\r\n+\t\tif ( 'mowccheckoutnew.php' === $filename || 'woocommercecheckoutform.php' === $filename ) {\r\n+\t\t\t\u002F\u002F Load only one WooCommerce checkout handler based on whether block-based checkout is enabled.\r\n+\t\t\t$selected_file = $is_block_checkout ? 'mowccheckoutnew.php' : 'woocommercecheckoutform.php';\r\n+\r\n+\t\t\t\u002F\u002F If the current file is not the selected handler, skip it to avoid duplicate registration.\r\n+\t\t\tif ( $filename !== $selected_file ) {\r\n+\t\t\t\tcontinue;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$class_name = 'OTP\\\\Handler\\\\Forms\\\\' . str_replace( '.php', '', $selected_file );\r\n+\t\t} else {\r\n+\t\t\t$class_name = 'OTP\\\\Handler\\\\Forms\\\\' . str_replace( '.php', '', $filename );\r\n+\t\t}\r\n+\r\n+\t\tif ( class_exists( $class_name ) && method_exists( $class_name, 'instance' ) ) {\r\n+\t\t\t$form_handler = $class_name::instance();\r\n+\t\t\t$handler_list->add( $form_handler->get_form_key(), $form_handler );\r\n+\t\t}\r\n+\t}\r\n+}\r\n+\r\n+\u002F**\r\n+ * Returns if block checkout is enabled.\r\n+ *\r\n+ * This version does not rely on WooCommerce Blocks PHP classes being loaded.\r\n+ * Instead, it inspects the WooCommerce Checkout page content for the\r\n+ * `woocommerce\u002Fcheckout` block, so it can be safely called early (e.g. in\r\n+ * constructors) without depending on plugin load order.\r\n+ *\r\n+ * @return bool\r\n+ *\u002F\r\n+function mo_is_block_based_checkout() {\r\n+\tif ( ! function_exists( 'is_plugin_active' ) ) {\r\n+\t\tinclude_once ABSPATH . 'wp-admin\u002Fincludes\u002Fplugin.php';\r\n+\t}\r\n+\r\n+\tif ( ! is_plugin_active( 'woocommerce\u002Fwoocommerce.php' ) ) {\r\n+\t\treturn false;\r\n+\t}\r\n+\r\n+\t\u002F\u002F Get the Checkout page ID directly from WooCommerce options.\r\n+\t$checkout_page_id = (int) get_option( 'woocommerce_checkout_page_id' );\r\n+\tif ( $checkout_page_id \u003C= 0 ) {\r\n+\t\treturn false;\r\n+\t}\r\n+\r\n+\t$checkout_page = get_post( $checkout_page_id );\r\n+\tif ( ! $checkout_page || empty( $checkout_page->post_content ) ) {\r\n+\t\treturn false;\r\n+\t}\r\n+\r\n+\t\u002F\u002F Prefer core has_block() if available (WP 5+).\r\n+\tif ( function_exists( 'has_block' ) ) {\r\n+\t\treturn has_block( 'woocommerce\u002Fcheckout', $checkout_page );\r\n+\t}\r\n+\r\n+\t\u002F\u002F Fallback: simple string search for the block name in post content.\r\n+\treturn ( false !== strpos( $checkout_page->post_content, 'woocommerce\u002Fcheckout' ) );\r\n+}\r\n+\r\n+\u002F**\r\n+ * Returns admin post url.\r\n+ *\r\n+ * @return string\r\n+ *\u002F\r\n+function admin_post_url() {\r\n+\treturn admin_url( 'admin-post.php' ); }\r\n+\r\n+\u002F**\r\n+ * Returns wp ajax url.\r\n+ *\r\n+ * @return string\r\n+ *\u002F\r\n+function wp_ajax_url() {\r\n+\treturn admin_url( 'admin-ajax.php' ); }\r\n+\r\n+\u002F**\r\n+ * Escapes a string based on the type.\r\n+ *\r\n+ * @param string $string_value - string to be escaped.\r\n+ * @param string $type - type of escaping to apply.\r\n+ * @return string\r\n+ *\u002F\r\n+function mo_esc_string( $string_value, $type ) {\r\n+\tif ( 'attr' === $type ) {\r\n+\t\treturn esc_attr( $string_value );\r\n+\t} elseif ( 'url' === $type ) {\r\n+\t\treturn esc_url( $string_value );\r\n+\t}\r\n+\r\n+\treturn esc_attr( $string_value );\r\n+}\r\n+\r\n+\u002F**\r\n+ * Retrieves the value of the option from the wp_option table.\r\n+ *\r\n+ * @param string $string_value - option name to be retrieved.\r\n+ * @param string $prefix - prefix of the option.\r\n+ * @return mixed\r\n+ *\u002F\r\n+function get_mo_option( $string_value, $prefix = null ) {\r\n+\t$string_value = ( null === $prefix ? 'mo_customer_validation_' : $prefix ) . $string_value;\r\n+\treturn apply_filters( 'get_mo_option', get_site_option( $string_value ) );\r\n+}\r\n+\r\n+\u002F**\r\n+ * Updates the option set in the wp_option table.\r\n+ *\r\n+ * @param string $string_value - option name to be deleted.\r\n+ * @param string $value - value of the option.\r\n+ * @param string $prefix - prefix of the option.\r\n+ *\u002F\r\n+function update_mo_option( $string_value, $value, $prefix = null ) {\r\n+\t$string_value = ( null === $prefix ? 'mo_customer_validation_' : $prefix ) . $string_value;\r\n+\tupdate_site_option( $string_value, apply_filters( 'update_mo_option', $value, $string_value ) );\r\n+}\r\n+\r\n+\u002F**\r\n+ * Deletes the option set in the wp_option table.\r\n+ *\r\n+ * @param string $string_value - option name to be deleted.\r\n+ * @param string $prefix - prefix of the option.\r\n+ *\u002F\r\n+function delete_mo_option( $string_value, $prefix = null ) {\r\n+\t$string_value = ( null === $prefix ? 'mo_customer_validation_' : $prefix ) . $string_value;\r\n+\tdelete_site_option( $string_value );\r\n+}\r\n+\r\n+\u002F**\r\n+ * Returns the class name without namespace.\r\n+ *\r\n+ * @param object $obj - object of the class.\r\n+ * @return string\r\n+ *\u002F\r\n+function get_mo_class( $obj ) {\r\n+\t$namespace_class = get_class( $obj );\r\n+\treturn substr( $namespace_class, strrpos( $namespace_class, '\\\\' ) + 1 );\r\n+}\r\n+\r\n+\u002F**\r\n+ * To check if package.json file can be found through WP site URL or not.\r\n+ * BuildScript.php updates the package.json file content in the below function instead of package.json to be used further in autoload.php\r\n+ * example package.json string [\"name\"=>\"miniorange-otp-verification\",\"version\"=>\"5.5.2\",\"type\"=>\"MiniOrangeGateway\",\"testMode\"=>false,\"failMode\"=>false,\"hostname\"=>\"https:\\\u002F\\\u002Flogin.xecurify.com\",\"dCustomerKey\"=>\"16555\",\"dApiKey\"=>\"fFd2XcvTGDemZvbw1bcUesNJWEqKbbUq\",\"sslVerify\"=>true,\"session\"=>\"AUTO\"]\r\n+ *\r\n+ * @return string\r\n+ *\u002F\r\n+function initialize_package_json() {\r\n+\t$package = wp_json_encode(\r\n+\t\tarray(\r\n+\t\t\t'name'         => 'miniorange-otp-verification',\r\n+\t\t\t'version'      => '5.5.2',\r\n+\t\t\t'type'         => 'MiniOrangeGateway',\r\n+\t\t\t'testmode'     => false,\r\n+\t\t\t'failmode'     => false,\r\n+\t\t\t'hostname'     => 'https:\u002F\u002Flogin.xecurify.com',\r\n+\t\t\t'portal'       => 'https:\u002F\u002Fportal.miniorange.com',\r\n+\t\t\t'dcustomerkey' => '16555',\r\n+\t\t\t'dapikey'      => 'fFd2XcvTGDemZvbw1bcUesNJWEqKbbUq',\r\n+\t\t\t'sslverify'    => true,\r\n+\t\t\t'session'      => 'AUTO',\r\n+\t\t\t'typeplan'     => 'wp_otp_verification_basic_plan',\r\n+\t\t\t'licensename'  => 'WP_OTP_VERIFICATION_PLUGIN',\r\n+\t\t)\r\n+\t);\r\n+\treturn $package;\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fclass-moinit.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fclass-moinit.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fclass-moinit.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fclass-moinit.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,489 +1,491 @@\n-\u003C?php\n-\u002F**\n- * Main File MoInit\n- *\n- * @package miniorange-otp-verification\n- *\u002F\n-\n-namespace OTP;\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-use OTP\\Handler\\EmailVerificationLogic;\n-use OTP\\Handler\\FormActionHandler;\n-use OTP\\Handler\\MoActionHandlerHandler;\n-use OTP\\Handler\\MoRegistrationHandler;\n-use OTP\\Handler\\PhoneVerificationLogic;\n-use OTP\\Helper\\CountryList;\n-use OTP\\Helper\\GatewayFunctions;\n-use OTP\\Helper\\MenuItems;\n-use OTP\\Helper\\MoConstants;\n-use OTP\\Helper\\MoDisplayMessages;\n-use OTP\\Helper\\MoMessages;\n-use OTP\\Helper\\MoUtility;\n-use OTP\\Helper\\MOVisualTour;\n-use OTP\\Helper\\Templates\\DefaultPopup;\n-use OTP\\Helper\\Templates\\ErrorPopup;\n-use OTP\\Helper\\Templates\\ExternalPopup;\n-use OTP\\Helper\\Templates\\UserChoicePopup;\n-use OTP\\Objects\\PluginPageDetails;\n-use OTP\\Objects\\TabDetails;\n-use OTP\\Objects\\Tabs;\n-use OTP\\Traits\\Instance;\n-use OTP\\Helper\\MocURLCall;\n-use OTP\\Objects\\BaseMessages;\n-use OTP\\Helper\\MoVersionUpdate;\n-use OTP\\Helper\\MoAlphaNumeric;\n-use OTP\\Helper\\MoSMSBackupGateway;\n-use OTP\\Helper\\MoGloballyBannedPhone;\n-use OTP\\Helper\\MoWhatsApp;\n-use OTP\\Helper\\MoMasterCode;\n-use OTP\\Helper\\PopupTemplateChange;\n-use OTP\\Helper\\MoReporting;\n-use OTP\\Helper\\TransactionCost;\n-use OTP\\Helper\\MoAutofill;\n-use OTP\\Helper\\MoPHPSessions;\n-\n-if ( ! class_exists( 'MoInit' ) ) {\n-\t\u002F**\n-\t * Final class that runs base functionalities of the plugin.\n-\t * It initializes some of the common helper and handler for the plugin\n-\t * classes.\n-\t *\u002F\n-\tfinal class MoInit {\n-\n-\t\tuse Instance;\n-\n-\t\t\u002F**\n-\t\t * Constructor\n-\t\t *\u002F\n-\t\tprivate function __construct() {\n-\t\t\tMoPHPSessions::bootstrap();\n-\t\t\tmo_initialize_forms();\n-\t\t\t$this->initialize_hooks();\n-\t\t\t$this->initialize_globals();\n-\t\t\t$this->initialize_helpers();\n-\t\t\t$this->initialize_handlers();\n-\t\t\t$this->register_addons();\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Initialize all the main hooks needed for the plugin\n-\t\t *\u002F\n-\t\tprivate function initialize_hooks() {\n-\t\t\tadd_action( 'plugins_loaded', array( $this, 'otp_load_textdomain' ), 1 );\n-\t\t\tadd_action( 'admin_menu', array( $this, 'miniorange_customer_validation_menu' ) );\n-\t\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'mo_registration_plugin_settings_style' ) );\n-\t\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'mo_registration_plugin_settings_script' ) );\n-\t\t\tadd_action( 'wp_enqueue_scripts', array( $this, 'mo_registration_plugin_frontend_scripts' ), 99 );\n-\t\t\tadd_action( 'login_enqueue_scripts', array( $this, 'mo_registration_plugin_frontend_scripts' ), 99 );\n-\t\t\tadd_action( 'mo_registration_show_message', array( $this, 'mo_show_otp_message' ), 1, 2 );\n-\t\t\tadd_action( 'hourly_sync', array( $this, 'hourly_sync' ) );\n-\t\t\tadd_action( 'admin_footer', array( $this, 'feedback_request' ) );\n-\t\t\tadd_filter( 'wp_mail_from_name', array( $this, 'custom_wp_mail_from_name' ) );\n-\t\t\tadd_filter( 'plugin_row_meta', array( $this, 'mo_meta_links' ), 10, 2 );\n-\t\t\tadd_action( 'wp_enqueue_scripts', array( $this, 'load_jquery_on_forms' ) );\n-\t\t\tadd_action( 'plugin_action_links_' . MOV_PLUGIN_NAME, array( $this, 'plugin_action_links' ), 10, 1 );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Function to check if jQuery library is included, if not present then insert it.\n-\t\t * This was added to avoid conflicts with other scripts in WordPress all the while\n-\t\t * making sure our plugin is working as intended.\n-\t\t *\u002F\n-\t\tpublic function load_jquery_on_forms() {\n-\t\t\tif ( ! wp_script_is( 'jquery', 'enqueued' ) ) {\n-\t\t\t\twp_enqueue_script( 'jquery' );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Initialize all the helper classes with proper file validation\n-\t\t *\u002F\n-\t\tprivate function initialize_helpers() {\n-\t\t\tMoMessages::instance();\n-\t\t\tMOVisualTour::instance();\n-\t\t\tTransactionCost::instance();\n-\n-\t\t\t\u002F\u002F Initialize helper singletons using fully-qualified class names.\n-\t\t\t$helper_classes = array(\n-\t\t\t\tMoVersionUpdate::class,\n-\t\t\t\tMoAlphaNumeric::class,\n-\t\t\t\tMoSMSBackupGateway::class,\n-\t\t\t\tMoGloballyBannedPhone::class,\n-\t\t\t\tMoWhatsApp::class,\n-\t\t\t\tMoMasterCode::class,\n-\t\t\t\tMoReporting::class,\n-\t\t\t\tPopupTemplateChange::class,\n-\t\t\t\tMoAutofill::class,\n-\t\t\t);\n-\n-\t\t\tforeach ( $helper_classes as $helper_class ) {\n-\t\t\t\ttry {\n-\t\t\t\t\t\u002F\u002F Derive the expected helper file path (e.g. helper\u002Fclass-moreporting.php).\n-\t\t\t\t\t$short_name       = substr( $helper_class, strrpos( $helper_class, '\\\\' ) + 1 );\n-\t\t\t\t\t$file_name        = 'class-' . strtolower( $short_name ) . '.php';\n-\t\t\t\t\t$helper_dir       = MOV_DIR . 'helper' . DIRECTORY_SEPARATOR;\n-\t\t\t\t\t$helper_file_path = $helper_dir . $file_name;\n-\n-\t\t\t\t\t$real_helper_file = realpath( $helper_file_path );\n-\t\t\t\t\t$real_helper_dir  = realpath( $helper_dir );\n-\n-\t\t\t\t\t\u002F\u002F Only load the file if it exists inside the expected helper directory.\n-\t\t\t\t\tif ( $real_helper_file && $real_helper_dir && 0 === strpos( $real_helper_file, $real_helper_dir ) && file_exists( $real_helper_file ) ) {\n-\t\t\t\t\t\trequire_once $real_helper_file;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tif ( class_exists( $helper_class, false ) && method_exists( $helper_class, 'instance' ) ) {\n-\t\t\t\t\t\t$helper_class::instance();\n-\t\t\t\t\t}\n-\t\t\t\t} catch ( \\Exception $e ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t} catch ( \\Error $e ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Initialize all the Template Handlers\n-\t\t *\u002F\n-\t\tprivate function initialize_handlers() {\n-\t\t\tFormActionHandler::instance();\n-\t\t\tMoActionHandlerHandler::instance();\n-\t\t\tDefaultPopup::instance();\n-\t\t\tErrorPopup::instance();\n-\t\t\tExternalPopup::instance();\n-\t\t\tUserChoicePopup::instance();\n-\t\t\tMoRegistrationHandler::instance();\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Initialize all the global variables.\n-\t\t *\u002F\n-\t\tprivate function initialize_globals() {\n-\t\t\tglobal $phone_logic, $email_logic;\n-\t\t\t$phone_logic = PhoneVerificationLogic::instance();\n-\t\t\t$email_logic = EmailVerificationLogic::instance();\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * This function hooks into the admin_menu WordPress hook to generate\n-\t\t * WordPress menu items. You define all the options and links you want\n-\t\t * to show to the admin in the WordPress sidebar.\n-\t\t *\u002F\n-\t\tpublic function miniorange_customer_validation_menu() {\n-\t\t\tMenuItems::instance();\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * The main callback function for each of the menu links. This function\n-\t\t * is called when user visits any one of the menu URLs.\n-\t\t *\u002F\n-\t\tpublic function mo_customer_validation_options() {\n-\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\t$controller_file = realpath( MOV_DIR . 'controllers\u002Fmain-controller.php' );\n-\t\t\t$base_dir        = realpath( MOV_DIR . 'controllers\u002F' );\n-\t\t\tif ( MoUtility::mo_require_file( $controller_file, $base_dir ) ) {\n-\t\t\t\trequire $controller_file;\n-\t\t\t} else {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * This function checks the current page to load the main scripts and styles on admin dashboard only\n-\t\t *\u002F\n-\t\tpublic function check_current_page() {\n-\n-\t\t\t\u002F\u002F Only load scripts on OTP plugin pages.\n-\t\t\t$current_screen = get_current_screen();\n-\t\t\tif ( ! $current_screen ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\t\t\t$otp_plugin_pages = array(\n-\t\t\t\t'toplevel_page_mosettings',\n-\t\t\t\t'otp-verification_page_monotifications',\n-\t\t\t\t'otp-verification_page_otpsettings',\n-\t\t\t\t'otp-verification_page_mogateway',\n-\t\t\t\t'otp-verification_page_moreporting',\n-\t\t\t\t'otp-verification_page_mowhatsapp',\n-\t\t\t\t'otp-verification_page_addon',\n-\t\t\t\t'otp-verification_page_otpaccount',\n-\t\t\t\t'otp-verification_page_mootppricing',\n-\t\t\t);\n-\n-\t\t\t\u002F\u002F Also check by page parameter for additional safety.\n-\t\t\t$page           = MoUtility::get_current_page_parameter_value( 'page', '' );\n-\t\t\t$otp_page_slugs = array(\n-\t\t\t\t'mosettings',\n-\t\t\t\t'monotifications',\n-\t\t\t\t'otpsettings',\n-\t\t\t\t'mogateway',\n-\t\t\t\t'moreporting',\n-\t\t\t\t'mowhatsapp',\n-\t\t\t\t'addon',\n-\t\t\t\t'otpaccount',\n-\t\t\t\t'mootppricing',\n-\t\t\t);\n-\n-\t\t\t\u002F\u002F Only load scripts if we're on an OTP plugin page.\n-\t\t\tif ( ! in_array( $current_screen->id, $otp_plugin_pages, true ) && ! in_array( $page, $otp_page_slugs, true ) ) {\n-\t\t\t\treturn true;\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * This function is called to append our CSS file\n-\t\t * in the backend and frontend. Uses the admin_enqueue_scripts\n-\t\t * and enqueue_scripts WordPress hook.\n-\t\t *\u002F\n-\t\tpublic function mo_registration_plugin_settings_style() {\n-\t\t\t\u002F\u002F Load feedback styles on all admin pages since feedback form appears on all pages.\n-\t\t\twp_enqueue_style( 'mo_customer_validation_feedback_style', MOV_CSS, array(), MOV_VERSION );\n-\n-\t\t\tif ( $this->check_current_page() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\twp_enqueue_style( 'mo_customer_validation_admin_settings_style', MOV_CSS_URL, array(), MOV_VERSION );\n-\t\t\twp_enqueue_style( 'mo_customer_validation_form_main_css', MOV_FORM_CSS, array(), MOV_VERSION );\n-\t\t\twp_enqueue_style( 'mo_customer_validation_inttelinput_style', MO_INTTELINPUT_CSS, array(), MOV_VERSION );\n-\t\t\twp_enqueue_style( 'mo_main_style', MOV_MAIN_CSS, array(), MOV_VERSION );\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * This function is called to append our CSS file\n-\t\t * in the backend and frontend. Uses the admin_enqueue_scripts\n-\t\t * and enqueue_scripts WordPress hook.\n-\t\t *\u002F\n-\t\tpublic function mo_registration_plugin_settings_script() {\n-\t\t\t\u002F\u002F Load feedback script on all admin pages since feedback form appears on all pages.\n-\t\t\twp_enqueue_script( 'mo_customer_validation_feedback_script', MOV_FEEDBACK_JS, array( 'jquery' ), MOV_VERSION, false );\n-\n-\t\t\tif ( $this->check_current_page() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\t$country_val      = array();\n-\t\t\t$whatsapp_enabled = get_mo_option( 'mo_whatsapp_enable' );\n-\t\t\t$request_uri      = remove_query_arg( array( 'mosettings', 'form', 'subpage' ), isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '' );\n-\t\t\t$whatsapp_tab_url = add_query_arg( array( 'page' => 'mowhatsapp' ), $request_uri );\n-\t\t\t$whatsapp_file    = file_exists( MOV_DIR . 'helper' . DIRECTORY_SEPARATOR . 'class-mowhatsapp.php' );\n-\n-\t\t\twp_enqueue_script( 'mo_customer_validation_admin_settings_script', MOV_JS_URL, array( 'jquery' ), MOV_VERSION, false );\n-\t\t\twp_localize_script(\n-\t\t\t\t'mo_customer_validation_admin_settings_script',\n-\t\t\t\t'moadminsettings',\n-\t\t\t\tarray(\n-\t\t\t\t\t'iswhatsappenable'       => $whatsapp_enabled,\n-\t\t\t\t\t'whatsapp_tab'           => $whatsapp_tab_url,\n-\t\t\t\t\t'whatsapp_file'          => $whatsapp_file,\n-\t\t\t\t\t'whatsapp_enabled_text'  => esc_html__( 'OTP Over WhatsApp Enabled', 'miniorange-otp-verification' ),\n-\t\t\t\t\t'whatsapp_disabled_text' => esc_html__( 'Enable OTP Over WhatsApp?', 'miniorange-otp-verification' ),\n-\t\t\t\t\t'form_is_not_found'      => MoMessages::showMessage( MoMessages::FORM_IS_NOT_FOUND ),\n-\t\t\t\t\t'ajaxUrl'                => admin_url( 'admin-ajax.php' ),\n-\t\t\t\t\t'security'               => wp_create_nonce( 'mo_admin_actions' ),\n-\t\t\t\t\t'mo_twilio_setupguide'   => MoConstants::MO_TWILIO_SETUP_GUIDE,\n-\t\t\t\t\t'mo_gateway_setupguide'  => MoConstants::MO_GATEWAY_SETUP_GUIDE,\n-\n-\t\t\t\t)\n-\t\t\t);\n-\t\t\twp_enqueue_script( 'mo_customer_validation_form_validation_script', VALIDATION_JS_URL, array( 'jquery' ), MOV_VERSION, false );\n-\t\t\twp_register_script( 'mo_customer_validation_inttelinput_script', MO_INTTELINPUT_JS, array( 'jquery' ), MOV_VERSION, false );\n-\t\t\t$countriesavail = CountryList::get_countrycode_list();\n-\t\t\t$countriesavail = apply_filters( 'selected_countries', $countriesavail );\n-\t\t\tforeach ( $countriesavail as $key => $value ) {\n-\t\t\t\tarray_push( $country_val, $value );\n-\t\t\t}\n-\t\t\twp_localize_script(\n-\t\t\t\t'mo_customer_validation_inttelinput_script',\n-\t\t\t\t'moselecteddropdown',\n-\t\t\t\tarray(\n-\t\t\t\t\t'selecteddropdown' => $country_val,\n-\t\t\t\t)\n-\t\t\t);\n-\t\t\twp_enqueue_script( 'mo_customer_validation_inttelinput_script' );\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * This function is called to append certain javascripts\n-\t\t * to the frontend. Mostly used for the appending a country\n-\t\t * code dropdown to the phone number field.\n-\t\t *\u002F\n-\t\tpublic function mo_registration_plugin_frontend_scripts() {\n-\t\t\twp_enqueue_style( 'mo_customer_validation_form_main_css', MOV_FORM_CSS, array(), MOV_VERSION );\n-\t\t\t$country_val = array();\n-\t\t\tif ( ! get_mo_option( 'show_dropdown_on_form' ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\t$selector = apply_filters( 'mo_phone_dropdown_selector', array() );\n-\t\t\tif ( MoUtility::is_blank( $selector ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\t$selector       = array_unique( $selector );\n-\t\t\t$countriesavail = CountryList::get_countrycode_list();\n-\t\t\t$countriesavail = apply_filters( 'selected_countries', $countriesavail );\n-\t\t\tforeach ( $countriesavail as $key => $value ) {\n-\t\t\t\tarray_push( $country_val, $value );\n-\t\t\t}\n-\t\t\t$default_country = CountryList::get_default_country_iso_code();\n-\t\t\t$get_ip_country  = apply_filters( 'mo_get_default_country', $default_country );\n-\t\t\twp_register_script( 'mo_customer_validation_inttelinput_script', MO_INTTELINPUT_JS, array( 'jquery' ), MOV_VERSION, false );\n-\t\t\twp_localize_script(\n-\t\t\t\t'mo_customer_validation_inttelinput_script',\n-\t\t\t\t'moselecteddropdown',\n-\t\t\t\tarray(\n-\t\t\t\t\t'selecteddropdown' => $country_val,\n-\n-\t\t\t\t)\n-\t\t\t);\n-\t\t\twp_enqueue_script( 'mo_customer_validation_inttelinput_script' );\n-\n-\t\t\twp_enqueue_style( 'mo_customer_validation_inttelinput_style', MO_INTTELINPUT_CSS, array(), MOV_VERSION );\n-\t\t\twp_register_script( 'mo_customer_validation_dropdown_script', MO_DROPDOWN_JS, array( 'jquery' ), MOV_VERSION, true );\n-\t\t\twp_localize_script(\n-\t\t\t\t'mo_customer_validation_dropdown_script',\n-\t\t\t\t'modropdownvars',\n-\t\t\t\tarray(\n-\t\t\t\t\t'selector'       => wp_json_encode( $selector ),\n-\t\t\t\t\t'defaultCountry' => $get_ip_country,\n-\t\t\t\t\t'onlyCountries'  => CountryList::get_only_country_list(),\n-\t\t\t\t)\n-\t\t\t);\n-\t\t\twp_enqueue_script( 'mo_customer_validation_dropdown_script' );\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * This function runs when mo_registration_show_message hook\n-\t\t * is initiated. The hook runs to show a plugin generated\n-\t\t * message to the user in the admin dashboard.\n-\t\t *\n-\t\t * @param string $content refers to the message content.\n-\t\t * @param string $type refers to the type of message.\n-\t\t *\u002F\n-\t\tpublic function mo_show_otp_message( $content, $type ) {\n-\t\t\tnew MoDisplayMessages( $content, $type );\n-\t\t}\n-\n-\n-\n-\t\t\u002F**\n-\t\t * Function tells where to look for translations.\n-\t\t * \u003Cb>PLEASE NOTE:\u003C\u002Fb> Dont be clever and try to replace the Text domain 'miniorange-otp-verification'\n-\t\t * with a constant value. Its kept as string for a reason. Its so that other automated\n-\t\t * tools can read it and use it for automatic translation.\n-\t\t *\u002F\n-\t\tpublic function otp_load_textdomain() {\n-\t\t\tload_plugin_textdomain( 'miniorange-otp-verification', false, dirname( plugin_basename( __FILE__ ) ) . '\u002Flang\u002F' );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Function initializes all the AddOns associated with the plugin.\n-\t\t *\n-\t\t * We can use reflection and automate the instantiation process\n-\t\t * but that will be a little costly and would affect performance\n-\t\t * hence decided not to.\n-\t\t *\u002F\n-\t\tprivate function register_addons() {\n-\n-\t\t\t$gateway = GatewayFunctions::instance();\n-\t\t\t$gateway->register_addons();\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Function hooks into the admin_footer hook to append the feedback form in the\n-\t\t * footer section of the page.\n-\t\t *\u002F\n-\t\tpublic function feedback_request() {\n-\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\t$feedback_file = realpath( MOV_DIR . 'controllers\u002Ffeedback.php' );\n-\t\t\t$base_dir      = realpath( MOV_DIR . 'controllers\u002F' );\n-\t\t\tif ( MoUtility::mo_require_file( $feedback_file, $base_dir ) ) {\n-\t\t\t\trequire $feedback_file;\n-\t\t\t} else {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Function hooks into the plugin_row_meta link to add custom\n-\t\t * links to the plugin's page.\n-\t\t *\n-\t\t * @param object $meta_fields .\n-\t\t * @param object $file .\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tpublic function mo_meta_links( $meta_fields, $file ) {\n-\t\t\tif ( MOV_PLUGIN_NAME === $file ) {\n-\t\t\t\t$meta_fields[] = \"\u003Cspan class='dashicons dashicons-sticky'>\u003C\u002Fspan>\n-            \u003Ca href='\" . MoConstants::FAQ_URL . \"' target='_blank'>\" . esc_html__( 'FAQs', 'miniorange-otp-verification' ) . '\u003C\u002Fa>';\n-\t\t\t}\n-\t\t\treturn $meta_fields;\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Add action links to the plugin list page for easy navigation\n-\t\t * after plugin activation.\n-\t\t *\n-\t\t * @param string $links .\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tpublic function plugin_action_links( $links ) {\n-\n-\t\t\t$tab_details = TabDetails::instance();\n-\n-\t\t\t$form_settings_tab = $tab_details->tab_details[ Tabs::FORMS ];\n-\t\t\tif ( ! function_exists( 'is_plugin_active' ) ) {\n-\t\t\t\tinclude_once ABSPATH . 'wp-admin\u002Fincludes\u002Fplugin.php';\n-\t\t\t}\n-\t\t\tif ( is_plugin_active( MOV_PLUGIN_NAME ) ) {\n-\t\t\t\t$links = array_merge(\n-\t\t\t\t\tarray(\n-\t\t\t\t\t\t'\u003Ca href=\"' . esc_url( admin_url( 'admin.php?page=' . $form_settings_tab->menu_slug ) ) . '\">' .\n-\t\t\t\t\t\t\tesc_html__( 'Settings', 'miniorange-otp-verification' )\n-\t\t\t\t\t\t. '\u003C\u002Fa>',\n-\t\t\t\t\t),\n-\t\t\t\t\t$links\n-\t\t\t\t);\n-\t\t\t}\n-\t\t\treturn $links;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Daily sync to do a license check and update the email and\n-\t\t * SMS Transaction.\n-\t\t *\n-\t\t * @note - this might say hourlySync but it's actually a daily sync\n-\t\t *\u002F\n-\t\tpublic function hourly_sync() {\n-\t\t\t$gateway = GatewayFunctions::instance();\n-\t\t\t$gateway->hourly_sync();\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Change the from name going out in the email\n-\t\t * via WP_MAIL of WordPress.\n-\t\t *\n-\t\t * @param  String $original_email_from The Original From Email Address passed by the hook.\n-\t\t * @return String From Email Address for the email going out\n-\t\t *\u002F\n-\t\tpublic function custom_wp_mail_from_name( $original_email_from ) {\n-\t\t\tif ( is_admin() && ! current_user_can( 'manage_options' ) ) {\n-\t\t\t\treturn $original_email_from;\n-\t\t\t}\n-\t\t\t$gateway = GatewayFunctions::instance();\n-\t\t\treturn $gateway->custom_wp_mail_from_name( $original_email_from );\n-\t\t}\n-\t}\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * Main File MoInit\r\n+ *\r\n+ * @package miniorange-otp-verification\r\n+ *\u002F\r\n+\r\n+namespace OTP;\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+use OTP\\API\\MoAbilitiesApi;\r\n+use OTP\\Handler\\EmailVerificationLogic;\r\n+use OTP\\Handler\\FormActionHandler;\r\n+use OTP\\Handler\\MoActionHandlerHandler;\r\n+use OTP\\Handler\\MoRegistrationHandler;\r\n+use OTP\\Handler\\PhoneVerificationLogic;\r\n+use OTP\\Helper\\CountryList;\r\n+use OTP\\Helper\\GatewayFunctions;\r\n+use OTP\\Helper\\MenuItems;\r\n+use OTP\\Helper\\MoConstants;\r\n+use OTP\\Helper\\MoDisplayMessages;\r\n+use OTP\\Helper\\MoMessages;\r\n+use OTP\\Helper\\MoUtility;\r\n+use OTP\\Helper\\MOVisualTour;\r\n+use OTP\\Helper\\Templates\\DefaultPopup;\r\n+use OTP\\Helper\\Templates\\ErrorPopup;\r\n+use OTP\\Helper\\Templates\\ExternalPopup;\r\n+use OTP\\Helper\\Templates\\UserChoicePopup;\r\n+use OTP\\Objects\\PluginPageDetails;\r\n+use OTP\\Objects\\TabDetails;\r\n+use OTP\\Objects\\Tabs;\r\n+use OTP\\Traits\\Instance;\r\n+use OTP\\Helper\\MocURLCall;\r\n+use OTP\\Objects\\BaseMessages;\r\n+use OTP\\Helper\\MoVersionUpdate;\r\n+use OTP\\Helper\\MoAlphaNumeric;\r\n+use OTP\\Helper\\MoSMSBackupGateway;\r\n+use OTP\\Helper\\MoGloballyBannedPhone;\r\n+use OTP\\Helper\\MoWhatsApp;\r\n+use OTP\\Helper\\MoMasterCode;\r\n+use OTP\\Helper\\PopupTemplateChange;\r\n+use OTP\\Helper\\MoReporting;\r\n+use OTP\\Helper\\TransactionCost;\r\n+use OTP\\Helper\\MoAutofill;\r\n+use OTP\\Helper\\MoPHPSessions;\r\n+\r\n+if ( ! class_exists( 'MoInit' ) ) {\r\n+\t\u002F**\r\n+\t * Final class that runs base functionalities of the plugin.\r\n+\t * It initializes some of the common helper and handler for the plugin\r\n+\t * classes.\r\n+\t *\u002F\r\n+\tfinal class MoInit {\r\n+\r\n+\t\tuse Instance;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Constructor\r\n+\t\t *\u002F\r\n+\t\tprivate function __construct() {\r\n+\t\t\tMoPHPSessions::bootstrap();\r\n+\t\t\tmo_initialize_forms();\r\n+\t\t\t$this->initialize_hooks();\r\n+\t\t\t$this->initialize_globals();\r\n+\t\t\t$this->initialize_helpers();\r\n+\t\t\t$this->initialize_handlers();\r\n+\t\t\t$this->register_addons();\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Initialize all the main hooks needed for the plugin\r\n+\t\t *\u002F\r\n+\t\tprivate function initialize_hooks() {\r\n+\t\t\tadd_action( 'plugins_loaded', array( $this, 'otp_load_textdomain' ), 1 );\r\n+\t\t\tadd_action( 'admin_menu', array( $this, 'miniorange_customer_validation_menu' ) );\r\n+\t\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'mo_registration_plugin_settings_style' ) );\r\n+\t\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'mo_registration_plugin_settings_script' ) );\r\n+\t\t\tadd_action( 'wp_enqueue_scripts', array( $this, 'mo_registration_plugin_frontend_scripts' ), 99 );\r\n+\t\t\tadd_action( 'login_enqueue_scripts', array( $this, 'mo_registration_plugin_frontend_scripts' ), 99 );\r\n+\t\t\tadd_action( 'mo_registration_show_message', array( $this, 'mo_show_otp_message' ), 1, 2 );\r\n+\t\t\tadd_action( 'hourly_sync', array( $this, 'hourly_sync' ) );\r\n+\t\t\tadd_action( 'admin_footer', array( $this, 'feedback_request' ) );\r\n+\t\t\tadd_filter( 'wp_mail_from_name', array( $this, 'custom_wp_mail_from_name' ) );\r\n+\t\t\tadd_filter( 'plugin_row_meta', array( $this, 'mo_meta_links' ), 10, 2 );\r\n+\t\t\tadd_action( 'wp_enqueue_scripts', array( $this, 'load_jquery_on_forms' ) );\r\n+\t\t\tadd_action( 'plugin_action_links_' . MOV_PLUGIN_NAME, array( $this, 'plugin_action_links' ), 10, 1 );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Function to check if jQuery library is included, if not present then insert it.\r\n+\t\t * This was added to avoid conflicts with other scripts in WordPress all the while\r\n+\t\t * making sure our plugin is working as intended.\r\n+\t\t *\u002F\r\n+\t\tpublic function load_jquery_on_forms() {\r\n+\t\t\tif ( ! wp_script_is( 'jquery', 'enqueued' ) ) {\r\n+\t\t\t\twp_enqueue_script( 'jquery' );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Initialize all the helper classes with proper file validation\r\n+\t\t *\u002F\r\n+\t\tprivate function initialize_helpers() {\r\n+\t\t\tMoMessages::instance();\r\n+\t\t\tMOVisualTour::instance();\r\n+\t\t\tTransactionCost::instance();\r\n+\r\n+\t\t\t\u002F\u002F Initialize helper singletons using fully-qualified class names.\r\n+\t\t\t$helper_classes = array(\r\n+\t\t\t\tMoVersionUpdate::class,\r\n+\t\t\t\tMoAlphaNumeric::class,\r\n+\t\t\t\tMoSMSBackupGateway::class,\r\n+\t\t\t\tMoGloballyBannedPhone::class,\r\n+\t\t\t\tMoWhatsApp::class,\r\n+\t\t\t\tMoMasterCode::class,\r\n+\t\t\t\tMoReporting::class,\r\n+\t\t\t\tPopupTemplateChange::class,\r\n+\t\t\t\tMoAutofill::class,\r\n+\t\t\t);\r\n+\r\n+\t\t\tforeach ( $helper_classes as $helper_class ) {\r\n+\t\t\t\ttry {\r\n+\t\t\t\t\t\u002F\u002F Derive the expected helper file path (e.g. helper\u002Fclass-moreporting.php).\r\n+\t\t\t\t\t$short_name       = substr( $helper_class, strrpos( $helper_class, '\\\\' ) + 1 );\r\n+\t\t\t\t\t$file_name        = 'class-' . strtolower( $short_name ) . '.php';\r\n+\t\t\t\t\t$helper_dir       = MOV_DIR . 'helper' . DIRECTORY_SEPARATOR;\r\n+\t\t\t\t\t$helper_file_path = $helper_dir . $file_name;\r\n+\r\n+\t\t\t\t\t$real_helper_file = realpath( $helper_file_path );\r\n+\t\t\t\t\t$real_helper_dir  = realpath( $helper_dir );\r\n+\r\n+\t\t\t\t\t\u002F\u002F Only load the file if it exists inside the expected helper directory.\r\n+\t\t\t\t\tif ( $real_helper_file && $real_helper_dir && 0 === strpos( $real_helper_file, $real_helper_dir ) && file_exists( $real_helper_file ) ) {\r\n+\t\t\t\t\t\trequire_once $real_helper_file;\r\n+\t\t\t\t\t}\r\n+\r\n+\t\t\t\t\tif ( class_exists( $helper_class, false ) && method_exists( $helper_class, 'instance' ) ) {\r\n+\t\t\t\t\t\t$helper_class::instance();\r\n+\t\t\t\t\t}\r\n+\t\t\t\t} catch ( \\Exception $e ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t} catch ( \\Error $e ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Initialize all the Template Handlers\r\n+\t\t *\u002F\r\n+\t\tprivate function initialize_handlers() {\r\n+\t\t\tFormActionHandler::instance();\r\n+\t\t\tMoActionHandlerHandler::instance();\r\n+\t\t\tDefaultPopup::instance();\r\n+\t\t\tErrorPopup::instance();\r\n+\t\t\tExternalPopup::instance();\r\n+\t\t\tUserChoicePopup::instance();\r\n+\t\t\tMoRegistrationHandler::instance();\r\n+\t\t\tMoAbilitiesApi::instance();\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Initialize all the global variables.\r\n+\t\t *\u002F\r\n+\t\tprivate function initialize_globals() {\r\n+\t\t\tglobal $phone_logic, $email_logic;\r\n+\t\t\t$phone_logic = PhoneVerificationLogic::instance();\r\n+\t\t\t$email_logic = EmailVerificationLogic::instance();\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function hooks into the admin_menu WordPress hook to generate\r\n+\t\t * WordPress menu items. You define all the options and links you want\r\n+\t\t * to show to the admin in the WordPress sidebar.\r\n+\t\t *\u002F\r\n+\t\tpublic function miniorange_customer_validation_menu() {\r\n+\t\t\tMenuItems::instance();\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * The main callback function for each of the menu links. This function\r\n+\t\t * is called when user visits any one of the menu URLs.\r\n+\t\t *\u002F\r\n+\t\tpublic function mo_customer_validation_options() {\r\n+\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t\t$controller_file = realpath( MOV_DIR . 'controllers\u002Fmain-controller.php' );\r\n+\t\t\t$base_dir        = realpath( MOV_DIR . 'controllers\u002F' );\r\n+\t\t\tif ( MoUtility::mo_require_file( $controller_file, $base_dir ) ) {\r\n+\t\t\t\trequire $controller_file;\r\n+\t\t\t} else {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function checks the current page to load the main scripts and styles on admin dashboard only\r\n+\t\t *\u002F\r\n+\t\tpublic function check_current_page() {\r\n+\r\n+\t\t\t\u002F\u002F Only load scripts on OTP plugin pages.\r\n+\t\t\t$current_screen = get_current_screen();\r\n+\t\t\tif ( ! $current_screen ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t\t$otp_plugin_pages = array(\r\n+\t\t\t\t'toplevel_page_mosettings',\r\n+\t\t\t\t'otp-verification_page_monotifications',\r\n+\t\t\t\t'otp-verification_page_otpsettings',\r\n+\t\t\t\t'otp-verification_page_mogateway',\r\n+\t\t\t\t'otp-verification_page_moreporting',\r\n+\t\t\t\t'otp-verification_page_mowhatsapp',\r\n+\t\t\t\t'otp-verification_page_addon',\r\n+\t\t\t\t'otp-verification_page_otpaccount',\r\n+\t\t\t\t'otp-verification_page_mootppricing',\r\n+\t\t\t);\r\n+\r\n+\t\t\t\u002F\u002F Also check by page parameter for additional safety.\r\n+\t\t\t$page           = MoUtility::get_current_page_parameter_value( 'page', '' );\r\n+\t\t\t$otp_page_slugs = array(\r\n+\t\t\t\t'mosettings',\r\n+\t\t\t\t'monotifications',\r\n+\t\t\t\t'otpsettings',\r\n+\t\t\t\t'mogateway',\r\n+\t\t\t\t'moreporting',\r\n+\t\t\t\t'mowhatsapp',\r\n+\t\t\t\t'addon',\r\n+\t\t\t\t'otpaccount',\r\n+\t\t\t\t'mootppricing',\r\n+\t\t\t);\r\n+\r\n+\t\t\t\u002F\u002F Only load scripts if we're on an OTP plugin page.\r\n+\t\t\tif ( ! in_array( $current_screen->id, $otp_plugin_pages, true ) && ! in_array( $page, $otp_page_slugs, true ) ) {\r\n+\t\t\t\treturn true;\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function is called to append our CSS file\r\n+\t\t * in the backend and frontend. Uses the admin_enqueue_scripts\r\n+\t\t * and enqueue_scripts WordPress hook.\r\n+\t\t *\u002F\r\n+\t\tpublic function mo_registration_plugin_settings_style() {\r\n+\t\t\t\u002F\u002F Load feedback styles on all admin pages since feedback form appears on all pages.\r\n+\t\t\twp_enqueue_style( 'mo_customer_validation_feedback_style', MOV_CSS, array(), MOV_VERSION );\r\n+\r\n+\t\t\tif ( $this->check_current_page() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t\twp_enqueue_style( 'mo_customer_validation_admin_settings_style', MOV_CSS_URL, array(), MOV_VERSION );\r\n+\t\t\twp_enqueue_style( 'mo_customer_validation_form_main_css', MOV_FORM_CSS, array(), MOV_VERSION );\r\n+\t\t\twp_enqueue_style( 'mo_customer_validation_inttelinput_style', MO_INTTELINPUT_CSS, array(), MOV_VERSION );\r\n+\t\t\twp_enqueue_style( 'mo_main_style', MOV_MAIN_CSS, array(), MOV_VERSION );\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function is called to append our CSS file\r\n+\t\t * in the backend and frontend. Uses the admin_enqueue_scripts\r\n+\t\t * and enqueue_scripts WordPress hook.\r\n+\t\t *\u002F\r\n+\t\tpublic function mo_registration_plugin_settings_script() {\r\n+\t\t\t\u002F\u002F Load feedback script on all admin pages since feedback form appears on all pages.\r\n+\t\t\twp_enqueue_script( 'mo_customer_validation_feedback_script', MOV_FEEDBACK_JS, array( 'jquery' ), MOV_VERSION, false );\r\n+\r\n+\t\t\tif ( $this->check_current_page() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t\t$country_val      = array();\r\n+\t\t\t$whatsapp_enabled = get_mo_option( 'mo_whatsapp_enable' );\r\n+\t\t\t$request_uri      = remove_query_arg( array( 'mosettings', 'form', 'subpage' ), isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '' );\r\n+\t\t\t$whatsapp_tab_url = add_query_arg( array( 'page' => 'mowhatsapp' ), $request_uri );\r\n+\t\t\t$whatsapp_file    = file_exists( MOV_DIR . 'helper' . DIRECTORY_SEPARATOR . 'class-mowhatsapp.php' );\r\n+\r\n+\t\t\twp_enqueue_script( 'mo_customer_validation_admin_settings_script', MOV_JS_URL, array( 'jquery' ), MOV_VERSION, false );\r\n+\t\t\twp_localize_script(\r\n+\t\t\t\t'mo_customer_validation_admin_settings_script',\r\n+\t\t\t\t'moadminsettings',\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'iswhatsappenable'       => $whatsapp_enabled,\r\n+\t\t\t\t\t'whatsapp_tab'           => $whatsapp_tab_url,\r\n+\t\t\t\t\t'whatsapp_file'          => $whatsapp_file,\r\n+\t\t\t\t\t'whatsapp_enabled_text'  => esc_html__( 'OTP Over WhatsApp Enabled', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t'whatsapp_disabled_text' => esc_html__( 'Enable OTP Over WhatsApp?', 'miniorange-otp-verification' ),\r\n+\t\t\t\t\t'form_is_not_found'      => MoMessages::showMessage( MoMessages::FORM_IS_NOT_FOUND ),\r\n+\t\t\t\t\t'ajaxUrl'                => admin_url( 'admin-ajax.php' ),\r\n+\t\t\t\t\t'security'               => wp_create_nonce( 'mo_admin_actions' ),\r\n+\t\t\t\t\t'mo_twilio_setupguide'   => MoConstants::MO_TWILIO_SETUP_GUIDE,\r\n+\t\t\t\t\t'mo_gateway_setupguide'  => MoConstants::MO_GATEWAY_SETUP_GUIDE,\r\n+\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t\twp_enqueue_script( 'mo_customer_validation_form_validation_script', VALIDATION_JS_URL, array( 'jquery' ), MOV_VERSION, false );\r\n+\t\t\twp_register_script( 'mo_customer_validation_inttelinput_script', MO_INTTELINPUT_JS, array( 'jquery' ), MOV_VERSION, false );\r\n+\t\t\t$countriesavail = CountryList::get_countrycode_list();\r\n+\t\t\t$countriesavail = apply_filters( 'selected_countries', $countriesavail );\r\n+\t\t\tforeach ( $countriesavail as $key => $value ) {\r\n+\t\t\t\tarray_push( $country_val, $value );\r\n+\t\t\t}\r\n+\t\t\twp_localize_script(\r\n+\t\t\t\t'mo_customer_validation_inttelinput_script',\r\n+\t\t\t\t'moselecteddropdown',\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'selecteddropdown' => $country_val,\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t\twp_enqueue_script( 'mo_customer_validation_inttelinput_script' );\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function is called to append certain javascripts\r\n+\t\t * to the frontend. Mostly used for the appending a country\r\n+\t\t * code dropdown to the phone number field.\r\n+\t\t *\u002F\r\n+\t\tpublic function mo_registration_plugin_frontend_scripts() {\r\n+\t\t\twp_enqueue_style( 'mo_customer_validation_form_main_css', MOV_FORM_CSS, array(), MOV_VERSION );\r\n+\t\t\t$country_val = array();\r\n+\t\t\tif ( ! get_mo_option( 'show_dropdown_on_form' ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t\t$selector = apply_filters( 'mo_phone_dropdown_selector', array() );\r\n+\t\t\tif ( MoUtility::is_blank( $selector ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t\t$selector       = array_unique( $selector );\r\n+\t\t\t$countriesavail = CountryList::get_countrycode_list();\r\n+\t\t\t$countriesavail = apply_filters( 'selected_countries', $countriesavail );\r\n+\t\t\tforeach ( $countriesavail as $key => $value ) {\r\n+\t\t\t\tarray_push( $country_val, $value );\r\n+\t\t\t}\r\n+\t\t\t$default_country = CountryList::get_default_country_iso_code();\r\n+\t\t\t$get_ip_country  = apply_filters( 'mo_get_default_country', $default_country );\r\n+\t\t\twp_register_script( 'mo_customer_validation_inttelinput_script', MO_INTTELINPUT_JS, array( 'jquery' ), MOV_VERSION, false );\r\n+\t\t\twp_localize_script(\r\n+\t\t\t\t'mo_customer_validation_inttelinput_script',\r\n+\t\t\t\t'moselecteddropdown',\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'selecteddropdown' => $country_val,\r\n+\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t\twp_enqueue_script( 'mo_customer_validation_inttelinput_script' );\r\n+\r\n+\t\t\twp_enqueue_style( 'mo_customer_validation_inttelinput_style', MO_INTTELINPUT_CSS, array(), MOV_VERSION );\r\n+\t\t\twp_register_script( 'mo_customer_validation_dropdown_script', MO_DROPDOWN_JS, array( 'jquery' ), MOV_VERSION, true );\r\n+\t\t\twp_localize_script(\r\n+\t\t\t\t'mo_customer_validation_dropdown_script',\r\n+\t\t\t\t'modropdownvars',\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'selector'       => wp_json_encode( $selector ),\r\n+\t\t\t\t\t'defaultCountry' => $get_ip_country,\r\n+\t\t\t\t\t'onlyCountries'  => CountryList::get_only_country_list(),\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\t\t\twp_enqueue_script( 'mo_customer_validation_dropdown_script' );\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function runs when mo_registration_show_message hook\r\n+\t\t * is initiated. The hook runs to show a plugin generated\r\n+\t\t * message to the user in the admin dashboard.\r\n+\t\t *\r\n+\t\t * @param string $content refers to the message content.\r\n+\t\t * @param string $type refers to the type of message.\r\n+\t\t *\u002F\r\n+\t\tpublic function mo_show_otp_message( $content, $type ) {\r\n+\t\t\tnew MoDisplayMessages( $content, $type );\r\n+\t\t}\r\n+\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Function tells where to look for translations.\r\n+\t\t * \u003Cb>PLEASE NOTE:\u003C\u002Fb> Dont be clever and try to replace the Text domain 'miniorange-otp-verification'\r\n+\t\t * with a constant value. Its kept as string for a reason. Its so that other automated\r\n+\t\t * tools can read it and use it for automatic translation.\r\n+\t\t *\u002F\r\n+\t\tpublic function otp_load_textdomain() {\r\n+\t\t\tload_plugin_textdomain( 'miniorange-otp-verification', false, dirname( plugin_basename( __FILE__ ) ) . '\u002Flang\u002F' );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Function initializes all the AddOns associated with the plugin.\r\n+\t\t *\r\n+\t\t * We can use reflection and automate the instantiation process\r\n+\t\t * but that will be a little costly and would affect performance\r\n+\t\t * hence decided not to.\r\n+\t\t *\u002F\r\n+\t\tprivate function register_addons() {\r\n+\r\n+\t\t\t$gateway = GatewayFunctions::instance();\r\n+\t\t\t$gateway->register_addons();\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Function hooks into the admin_footer hook to append the feedback form in the\r\n+\t\t * footer section of the page.\r\n+\t\t *\u002F\r\n+\t\tpublic function feedback_request() {\r\n+\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t\t$feedback_file = realpath( MOV_DIR . 'controllers\u002Ffeedback.php' );\r\n+\t\t\t$base_dir      = realpath( MOV_DIR . 'controllers\u002F' );\r\n+\t\t\tif ( MoUtility::mo_require_file( $feedback_file, $base_dir ) ) {\r\n+\t\t\t\trequire $feedback_file;\r\n+\t\t\t} else {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Function hooks into the plugin_row_meta link to add custom\r\n+\t\t * links to the plugin's page.\r\n+\t\t *\r\n+\t\t * @param object $meta_fields .\r\n+\t\t * @param object $file .\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tpublic function mo_meta_links( $meta_fields, $file ) {\r\n+\t\t\tif ( MOV_PLUGIN_NAME === $file ) {\r\n+\t\t\t\t$meta_fields[] = \"\u003Cspan class='dashicons dashicons-sticky'>\u003C\u002Fspan>\r\n+            \u003Ca href='\" . MoConstants::FAQ_URL . \"' target='_blank'>\" . esc_html__( 'FAQs', 'miniorange-otp-verification' ) . '\u003C\u002Fa>';\r\n+\t\t\t}\r\n+\t\t\treturn $meta_fields;\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Add action links to the plugin list page for easy navigation\r\n+\t\t * after plugin activation.\r\n+\t\t *\r\n+\t\t * @param string $links .\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tpublic function plugin_action_links( $links ) {\r\n+\r\n+\t\t\t$tab_details = TabDetails::instance();\r\n+\r\n+\t\t\t$form_settings_tab = $tab_details->tab_details[ Tabs::FORMS ];\r\n+\t\t\tif ( ! function_exists( 'is_plugin_active' ) ) {\r\n+\t\t\t\tinclude_once ABSPATH . 'wp-admin\u002Fincludes\u002Fplugin.php';\r\n+\t\t\t}\r\n+\t\t\tif ( is_plugin_active( MOV_PLUGIN_NAME ) ) {\r\n+\t\t\t\t$links = array_merge(\r\n+\t\t\t\t\tarray(\r\n+\t\t\t\t\t\t'\u003Ca href=\"' . esc_url( admin_url( 'admin.php?page=' . $form_settings_tab->menu_slug ) ) . '\">' .\r\n+\t\t\t\t\t\t\tesc_html__( 'Settings', 'miniorange-otp-verification' )\r\n+\t\t\t\t\t\t. '\u003C\u002Fa>',\r\n+\t\t\t\t\t),\r\n+\t\t\t\t\t$links\r\n+\t\t\t\t);\r\n+\t\t\t}\r\n+\t\t\treturn $links;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Daily sync to do a license check and update the email and\r\n+\t\t * SMS Transaction.\r\n+\t\t *\r\n+\t\t * @note - this might say hourlySync but it's actually a daily sync\r\n+\t\t *\u002F\r\n+\t\tpublic function hourly_sync() {\r\n+\t\t\t$gateway = GatewayFunctions::instance();\r\n+\t\t\t$gateway->hourly_sync();\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Change the from name going out in the email\r\n+\t\t * via WP_MAIL of WordPress.\r\n+\t\t *\r\n+\t\t * @param  String $original_email_from The Original From Email Address passed by the hook.\r\n+\t\t * @return String From Email Address for the email going out\r\n+\t\t *\u002F\r\n+\t\tpublic function custom_wp_mail_from_name( $original_email_from ) {\r\n+\t\t\tif ( is_admin() && ! current_user_can( 'manage_options' ) ) {\r\n+\t\t\t\treturn $original_email_from;\r\n+\t\t\t}\r\n+\t\t\t$gateway = GatewayFunctions::instance();\r\n+\t\t\treturn $gateway->custom_wp_mail_from_name( $original_email_from );\r\n+\t\t}\r\n+\t}\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-fluentform.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-fluentform.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-fluentform.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-fluentform.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -101,7 +101,11 @@\n \t\t * @param String $form - form values.\r\n \t\t *\u002F\r\n \t\tpublic function check_form_submit( $insert_data, $data, $form ) {\r\n-\t\t\t$form_id = null === $form['form_id'] ? $form['attributes']['id'] : $form['form_id'];\r\n+\t\t\tif ( is_array( $form ) ) {\r\n+\t\t\t\t$form_id = isset( $form['form_id'] ) ? $form['form_id'] : ( $form['id'] ?? null );\r\n+\t\t\t} else {\r\n+\t\t\t\t$form_id = $form->id ?? null;\r\n+\t\t\t}\r\n \t\t\tif ( ! array_key_exists( $form_id, $this->form_details ) ) {\r\n \t\t\t\treturn;\r\n \t\t\t}\r\n@@ -190,7 +194,7 @@\n \t\t\t}\r\n \t\t\t$post_data = MoUtility::mo_sanitize_array( $_POST );\r\n \t\t\tMoUtility::initialize_transaction( $this->form_session_var );\r\n-\t\t\tif ( $post_data['otpType'] === $this->type_phone_tag ) {\r\n+\t\t\tif ( $post_data['otptype'] === $this->type_phone_tag ) {\r\n \t\t\t\t$this->process_phone_and_send_otp( $post_data );\r\n \t\t\t} else {\r\n \t\t\t\t$this->process_email_and_send_otp( $post_data );\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-forminatorform.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-forminatorform.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-forminatorform.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-forminatorform.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -220,9 +220,9 @@\n \t\tprivate function check_integrity_and_validate_otp( $data ) {\r\n \r\n \t\t\t$this->check_integrity( $data );\r\n-\t\t\t$this->validate_challenge( sanitize_text_field( $data['otpType'] ), null, sanitize_text_field( $data['otp_token'] ) );\r\n+\t\t\t$this->validate_challenge( sanitize_text_field( $data['otptype'] ), null, sanitize_text_field( $data['otp_token'] ) );\r\n \r\n-\t\t\tif ( SessionUtils::is_status_match( $this->form_session_var, self::VALIDATED, sanitize_text_field( $data['otpType'] ) ) ) {\r\n+\t\t\tif ( SessionUtils::is_status_match( $this->form_session_var, self::VALIDATED, sanitize_text_field( $data['otptype'] ) ) ) {\r\n \t\t\t\twp_send_json(\r\n \t\t\t\t\tMoUtility::create_json(\r\n \t\t\t\t\t\tMoConstants::SUCCESS_JSON_TYPE,\r\n@@ -249,7 +249,7 @@\n \t\t * @param array $data - this is the get \u002F post data from the ajax call containing email or phone.\r\n \t\t *\u002F\r\n \t\tprivate function check_integrity( $data ) {\r\n-\t\t\tif ( 'phone' === $data['otpType'] ) {\r\n+\t\t\tif ( 'phone' === $data['otptype'] ) {\r\n \t\t\t\t$phone = MoUtility::process_phone_number( sanitize_text_field( $data['user_phone'] ) );\r\n \t\t\t\tif ( ! SessionUtils::is_phone_verified_match( $this->form_session_var, $phone ) ) {\r\n \t\t\t\t\twp_send_json(\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-moumpasswordreset.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-moumpasswordreset.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-moumpasswordreset.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-moumpasswordreset.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -364,8 +364,22 @@\n \t\t\tif ( ! isset( $_POST['security'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['security'] ) ), 'form_nonce' ) ) {\r\n \t\t\t\treturn;\r\n \t\t\t}\r\n-\t\t\t$user    = MoUtility::sanitize_check( 'username_b', $_POST );\r\n-\t\t\t$user    = $this->get_user( trim( $user ) );\r\n+\r\n+\t\t\t$otp_ver_type = $this->get_verification_type();\r\n+\t\t\tif ( ! SessionUtils::is_otp_initialized( $this->form_session_var )\r\n+\t\t\t\t|| ! SessionUtils::is_status_match( $this->form_session_var, self::VALIDATED, $otp_ver_type ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$posted_username  = MoUtility::sanitize_check( 'username_b', $_POST );\r\n+\t\t\t$session_username = SessionUtils::get_user_submitted( $this->form_session_var );\r\n+\t\t\tif ( MoUtility::is_blank( $session_username ) || $session_username !== trim( $posted_username ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$this->unset_otp_session_variables();\r\n+\r\n+\t\t\t$user    = $this->get_user( trim( $posted_username ) );\r\n \t\t\t$pwd_obj = $this->get_um_pwd_obj();\r\n \t\t\t\t\t\tum_fetch_user( $user->ID );\r\n \t\t\t$this->get_um_user_obj()->password_reset();\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-mowccheckoutnew.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-mowccheckoutnew.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-mowccheckoutnew.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-mowccheckoutnew.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -225,7 +225,7 @@\n \t\t\t\t);\r\n \t\t\t}\r\n \t\t\t$data = MoUtility::mo_sanitize_array( wp_unslash( $_POST ) );\r\n-\t\t\t$otp_type = MoUtility::sanitize_check( 'otpType', $data );\r\n+\t\t\t$otp_type = MoUtility::sanitize_check( 'otptype', $data );\r\n \t\t\tif ( VerificationType::EMAIL === $otp_type ) {\r\n \t\t\t\t$data['user_phone'] = '';\r\n \t\t\t} elseif ( VerificationType::PHONE === $otp_type ) {\r\n@@ -380,9 +380,9 @@\n \t\t *\u002F\r\n \t\tprivate function check_integrity_and_validate_otp( $data ) {\r\n \t\t\t$this->check_integrity( $data );\r\n-\t\t\t$this->validate_challenge( sanitize_text_field( $data['otpType'] ), null, sanitize_text_field( $data['otp_token'] ) );\r\n-\t\t\tif ( SessionUtils::is_status_match( $this->form_session_var, self::VALIDATED, $data['otpType'] ) ) {\r\n-\t\t\t\tMoPHPSessions::add_session_var( 'is_otp_verified_' . $data['otpType'], true );\r\n+\t\t\t$this->validate_challenge( sanitize_text_field( $data['otptype'] ), null, sanitize_text_field( $data['otp_token'] ) );\r\n+\t\t\tif ( SessionUtils::is_status_match( $this->form_session_var, self::VALIDATED, $data['otptype'] ) ) {\r\n+\t\t\t\tMoPHPSessions::add_session_var( 'is_otp_verified_' . $data['otptype'], true );\r\n \t\t\t\twp_send_json(\r\n \t\t\t\t\tMoUtility::create_json(\r\n \t\t\t\t\t\tMoConstants::SUCCESS_JSON_TYPE,\r\n@@ -405,7 +405,7 @@\n \t\t * @param array $data - post data submitted on validate OTP button.\r\n \t\t *\u002F\r\n \t\tprivate function check_integrity( $data ) {\r\n-\t\t\tif ( VerificationType::PHONE === $data['otpType'] ) {\r\n+\t\t\tif ( VerificationType::PHONE === $data['otptype'] ) {\r\n \t\t\t\t$phone = MoUtility::process_phone_number( sanitize_text_field( $data['user_phone'] ) );\r\n \t\t\t\tif ( ! SessionUtils::is_phone_verified_match( $this->form_session_var, $phone ) ) {\r\n \t\t\t\t\twp_send_json(\r\n@@ -416,7 +416,7 @@\n \t\t\t\t\t);\r\n \t\t\t\t}\r\n \t\t\t}\r\n-\t\t\tif ( VerificationType::EMAIL === $data['otpType'] ) {\r\n+\t\t\tif ( VerificationType::EMAIL === $data['otptype'] ) {\r\n \t\t\t\tif ( ! SessionUtils::is_email_verified_match( $this->form_session_var, sanitize_email( $data['user_email'] ) ) ) {\r\n \t\t\t\t\twp_send_json(\r\n \t\t\t\t\t\tMoUtility::create_json(\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-woocommercecheckoutform.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-woocommercecheckoutform.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-woocommercecheckoutform.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-woocommercecheckoutform.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -577,7 +577,7 @@\n \t\t\tif ( empty( $otp_token ) && isset( $data['order_verify'] ) ) {\r\n \t\t\t\t$otp_token = $data['order_verify'];\r\n \t\t\t}\r\n-\t\t\t$this->validate_challenge( $data['otpType'], null, $otp_token );\r\n+\t\t\t$this->validate_challenge( $data['otptype'], null, $otp_token );\r\n \t\t}\r\n \r\n \t\t\u002F**\r\n@@ -586,7 +586,7 @@\n \t\t * @param array $data - this is the get \u002F post data from the ajax call containing email or phone.\r\n \t\t *\u002F\r\n \t\tprivate function checkIntegrity( $data ) {\r\n-\t\t\tif ( 'phone' === $data['otpType'] ) {\r\n+\t\t\tif ( 'phone' === $data['otptype'] ) {\r\n \t\t\t\tif ( ! SessionUtils::is_phone_verified_match( $this->form_session_var, MoUtility::process_phone_number( $data['user_phone'] ) ) ) {\r\n \t\t\t\t\twp_send_json(\r\n \t\t\t\t\t\tMoUtility::create_json(\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-wpformsplugin.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-wpformsplugin.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-wpformsplugin.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-wpformsplugin.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -136,7 +136,7 @@\n \t\t\t$post_data = MoUtility::mo_sanitize_array( $_POST );\r\n \t\t\tMoUtility::initialize_transaction( $this->form_session_var );\r\n \r\n-\t\t\tif ( isset( $post_data['otpType'] ) && 'mo_wpform_' . $post_data['otpType'] . '_enable' === $this->type_phone_tag ) {\r\n+\t\t\tif ( isset( $post_data['otptype'] ) && 'mo_wpform_' . $post_data['otptype'] . '_enable' === $this->type_phone_tag ) {\r\n \t\t\t\t$this->process_phone_and_send_otp( $post_data );\r\n \t\t\t} else {\r\n \t\t\t\t$this->process_email_and_send_otp( $post_data );\r\n@@ -225,9 +225,9 @@\n \t\tprivate function check_integrity_and_validate_otp( $data ) {\r\n \r\n \t\t\t$this->check_integrity( $data );\r\n-\t\t\t$this->validate_challenge( $data['otpType'], null, $data['otp_token'] );\r\n+\t\t\t$this->validate_challenge( $data['otptype'], null, $data['otp_token'] );\r\n \r\n-\t\t\tif ( SessionUtils::is_status_match( $this->form_session_var, self::VALIDATED, $data['otpType'] ) ) {\r\n+\t\t\tif ( SessionUtils::is_status_match( $this->form_session_var, self::VALIDATED, $data['otptype'] ) ) {\r\n \t\t\t\twp_send_json(\r\n \t\t\t\t\tMoUtility::create_json(\r\n \t\t\t\t\t\tMoConstants::SUCCESS_JSON_TYPE,\r\n@@ -250,7 +250,7 @@\n \t\t * @param array $data - this is the get \u002F post data from the ajax call containing email or phone.\r\n \t\t *\u002F\r\n \t\tprivate function check_integrity( $data ) {\r\n-\t\t\tif ( 'phone' === $data['otpType'] ) {\r\n+\t\t\tif ( 'phone' === $data['otptype'] ) {\r\n \t\t\t\t$phone = MoUtility::process_phone_number( $data['user_phone'] );\r\n \t\t\t\tif ( ! SessionUtils::is_phone_verified_match( $this->form_session_var, $phone ) ) {\r\n \t\t\t\t\twp_send_json(\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-wploginform.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-wploginform.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhandler\u002Fforms\u002Fclass-wploginform.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhandler\u002Fforms\u002Fclass-wploginform.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -511,15 +511,16 @@\n \t\t\tif ( $skip_otp_process || $this->mo_delay_otp_process( $user->data->ID ) ) {\r\n \t\t\t\treturn true;\r\n \t\t\t}\r\n-\t\t\tif (\r\n-\t\t\t\t$this->by_pass_admin\r\n-\t\t\t\t&& $this->skip_password_check\r\n-\t\t\t\t&& ! $this->skip_pass_fallback\r\n-\t\t\t\t&& 'password' === $this->mo_get_wp_login_intent()\r\n-\t\t\t) {\r\n+\t\t\tif ( $this->by_pass_admin ) {\r\n \t\t\t\t$user_meta = get_userdata( $user->data->ID );\r\n-\t\t\t\t$user_role = $user_meta->roles;\r\n-\t\t\t\treturn in_array( 'administrator', $user_role, true );\r\n+\t\t\t\tif ( in_array( 'administrator', $user_meta->roles, true ) ) {\r\n+\t\t\t\t\t\u002F\u002F OTP-only mode: admin must use the password-intent link from the OTP popup.\r\n+\t\t\t\t\tif ( $this->skip_password_check && ! $this->skip_pass_fallback ) {\r\n+\t\t\t\t\t\treturn 'password' === $this->mo_get_wp_login_intent();\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t\u002F\u002F 2FA mode: password already verified — bypass OTP for admins.\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n \t\t\t}\r\n \t\t\treturn false;\r\n \t\t}\r\n@@ -1257,6 +1258,9 @@\n \t\t\t}\r\n \r\n \t\t\tif ( ! $this->mo_save_phone_numbers() ) {\r\n+\t\t\t\tif ( ! empty( $this->form_name ) ) {\r\n+\t\t\t\t\tMoPHPSessions::add_session_var( 'current_form_name', $this->form_name );\r\n+\t\t\t\t}\r\n \t\t\t\tminiorange_site_otp_validation_form(\r\n \t\t\t\t\tnull,\r\n \t\t\t\t\tnull,\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhelper\u002Fclass-mophpsessions.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhelper\u002Fclass-mophpsessions.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhelper\u002Fclass-mophpsessions.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhelper\u002Fclass-mophpsessions.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,611 +1,611 @@\n-\u003C?php\n-\u002F**\n- * Load administrator changes for MoPHPSessions\n- *\n- * @package miniorange-otp-verification\u002Fhelper\n- *\u002F\n-\n-namespace OTP\\Helper;\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-use OTP\\Objects\\IMoSessions;\n-\n-\u002F** TODO: Need to move each session type to different files *\u002F\n-if ( ! class_exists( 'MoPHPSessions' ) ) {\n-\t\u002F**\n-\t * Class for managing different types of session storage mechanisms.\n-\t *\n-\t * Implements the IMoSessions interface to provide consistent session handling\n-\t * across different storage types: PHP sessions, cookies, cache and transients.\n-\t *\n-\t * AUTO mode (recommended) picks SESSION or TRANSIENT per browser and falls back\n-\t * automatically when the primary mechanism cannot read\u002Fwrite OTP state.\n-\t *\u002F\n-\tclass MoPHPSessions implements IMoSessions {\n-\n-\t\tconst STORAGE_MODE_COOKIE = 'mo_otp_storage_mode';\n-\n-\t\t\u002F**\n-\t\t * Resolved storage type for the current request (SESSION or TRANSIENT when using AUTO\u002Ffallback).\n-\t\t *\n-\t\t * @var string|null\n-\t\t *\u002F\n-\t\tprivate static $resolved_type = null;\n-\n-\t\t\u002F**\n-\t\t * Whether PHP session viability was probed this request.\n-\t\t *\n-\t\t * @var bool\n-\t\t *\u002F\n-\t\tprivate static $session_viability_checked = false;\n-\n-\t\t\u002F**\n-\t\t * Cached result of PHP session viability probe.\n-\t\t *\n-\t\t * @var bool\n-\t\t *\u002F\n-\t\tprivate static $session_viable = false;\n-\n-\t\t\u002F**\n-\t\t * Whether shutdown\u002FREST hooks were registered to close PHP sessions.\n-\t\t *\n-\t\t * @var bool\n-\t\t *\u002F\n-\t\tprivate static $close_hooks_registered = false;\n-\n-\t\t\u002F**\n-\t\t * Bootstraps storage early so cookies can be set before output (fixes TRANSIENT on cached themes).\n-\t\t *\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic static function bootstrap() {\n-\t\t\tif ( ! defined( 'MOV_SESSION_TYPE' ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tself::register_session_close_hooks();\n-\n-\t\t\tif ( self::should_cross_fallback() ) {\n-\t\t\t\tself::ensure_transient_cookie();\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Registers hooks so PHP sessions are closed before REST\u002Floopback HTTP requests (Site Health).\n-\t\t *\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tprivate static function register_session_close_hooks() {\n-\t\t\tif ( self::$close_hooks_registered || ! \\function_exists( 'add_action' ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tself::$close_hooks_registered = true;\n-\t\t\t\\add_action( 'rest_api_init', array( __CLASS__, 'close_php_session' ), 0 );\n-\t\t\t\\add_filter( 'pre_http_request', array( __CLASS__, 'close_php_session_before_http' ), 1, 3 );\n-\t\t\t\\add_action( 'shutdown', array( __CLASS__, 'close_php_session' ), 0 );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Closes PHP session before WordPress loopback\u002FREST HTTP requests (Site Health).\n-\t\t *\n-\t\t * @param false|array|\\WP_Error $preempt Whether to preempt an HTTP request's return value.\n-\t\t * @param array                 $args    HTTP request arguments.\n-\t\t * @param string                $url     The request URL.\n-\t\t * @return false|array|\\WP_Error\n-\t\t *\u002F\n-\t\tpublic static function close_php_session_before_http( $preempt, $args, $url ) {\n-\t\t\tunset( $args, $url );\n-\t\t\tself::close_php_session();\n-\t\t\treturn $preempt;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Sets session values based on the configured session type.\n-\t\t *\n-\t\t * @param string $key Key to store data under.\n-\t\t * @param mixed  $val Value to store.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic static function add_session_var( $key, $val ) {\n-\t\t\tif ( empty( $key ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\t$storage_type = self::get_configured_storage_type();\n-\t\t\tif ( self::should_cross_fallback() ) {\n-\t\t\t\t$primary = self::get_effective_storage_type();\n-\t\t\t\tif ( ! self::write_by_type( $primary, $key, $val ) ) {\n-\t\t\t\t\t$alternate = self::get_alternate_storage_type( $primary );\n-\t\t\t\t\tif ( self::write_by_type( $alternate, $key, $val ) ) {\n-\t\t\t\t\t\tself::persist_storage_preference( $alternate );\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tself::write_by_type( $storage_type, $key, $val );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Retrieves a value stored in session by key.\n-\t\t *\n-\t\t * @param string $key Key used to store the value.\n-\t\t * @return mixed Value stored under the key, or null if not found.\n-\t\t *\u002F\n-\t\tpublic static function get_session_var( $key ) {\n-\t\t\tif ( empty( $key ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tif ( self::should_cross_fallback() ) {\n-\t\t\t\t$primary = self::get_effective_storage_type();\n-\t\t\t\t$value   = self::read_by_type( $primary, $key );\n-\t\t\t\tif ( null !== $value ) {\n-\t\t\t\t\treturn $value;\n-\t\t\t\t}\n-\t\t\t\treturn self::read_by_type( self::get_alternate_storage_type( $primary ), $key );\n-\t\t\t}\n-\n-\t\t\treturn self::read_by_type( self::get_configured_storage_type(), $key );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Unsets session values for the specified key.\n-\t\t *\n-\t\t * @param string $key Key to unset from the session.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic static function unset_session( $key ) {\n-\t\t\tif ( self::should_cross_fallback() ) {\n-\t\t\t\tself::unset_by_type( 'SESSION', $key );\n-\t\t\t\tself::unset_by_type( 'TRANSIENT', $key );\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tself::unset_by_type( self::get_configured_storage_type(), $key );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Legacy no-op: PHP sessions are opened and closed per operation in this class.\n-\t\t *\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic static function check_session() {\n-\t\t\t\u002F\u002F Intentionally empty. Use open\u002Fclose helpers inside MoPHPSessions only.\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Closes an active PHP session so REST API and loopback requests are not blocked.\n-\t\t *\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic static function close_php_session() {\n-\t\t\tif ( \\function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === \\session_status() ) {\n-\t\t\t\t\\session_write_close();\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Opens PHP session for reading and releases the lock when possible.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tprivate static function open_php_session_for_read() {\n-\t\t\tself::register_session_close_hooks();\n-\n-\t\t\tif ( PHP_SESSION_DISABLED === \\session_status() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( PHP_SESSION_ACTIVE === \\session_status() ) {\n-\t\t\t\treturn isset( $_SESSION );\n-\t\t\t}\n-\n-\t\t\tif ( \\headers_sent() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( \\PHP_VERSION_ID >= 70000 ) {\n-\t\t\t\treturn @\\session_start( array( 'read_and_close' => true ) ); \u002F\u002F phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged\n-\t\t\t}\n-\n-\t\t\tif ( @\\session_start() ) { \u002F\u002F phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged\n-\t\t\t\treturn isset( $_SESSION );\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Opens PHP session for writing.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tprivate static function open_php_session_for_write() {\n-\t\t\tself::register_session_close_hooks();\n-\n-\t\t\tif ( PHP_SESSION_DISABLED === \\session_status() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( PHP_SESSION_ACTIVE === \\session_status() ) {\n-\t\t\t\treturn isset( $_SESSION );\n-\t\t\t}\n-\n-\t\t\tif ( \\headers_sent() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\treturn @\\session_start(); \u002F\u002F phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Whether the current request uses native PHP $_SESSION storage.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function uses_php_session() {\n-\t\t\tif ( ! self::should_cross_fallback() ) {\n-\t\t\t\treturn 'SESSION' === self::get_configured_storage_type();\n-\t\t\t}\n-\t\t\treturn 'SESSION' === self::get_effective_storage_type();\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Returns configured MOV_SESSION_TYPE (may be AUTO).\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tprivate static function get_configured_storage_type() {\n-\t\t\treturn defined( 'MOV_SESSION_TYPE' ) ? strtoupper( (string) MOV_SESSION_TYPE ) : 'SESSION';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * SESSION \u002F TRANSIENT \u002F AUTO use adaptive storage with cross-fallback.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tprivate static function should_cross_fallback() {\n-\t\t\treturn in_array( self::get_configured_storage_type(), array( 'SESSION', 'TRANSIENT', 'AUTO' ), true );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Resolves effective storage (SESSION or TRANSIENT) for this request.\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tprivate static function get_effective_storage_type() {\n-\t\t\tif ( null !== self::$resolved_type ) {\n-\t\t\t\treturn self::$resolved_type;\n-\t\t\t}\n-\n-\t\t\t$configured = self::get_configured_storage_type();\n-\t\t\t$sticky     = self::get_sticky_storage_mode();\n-\n-\t\t\tif ( 'AUTO' === $configured ) {\n-\t\t\t\tself::$resolved_type = $sticky ? $sticky : self::detect_auto_storage_type();\n-\t\t\t} elseif ( $sticky ) {\n-\t\t\t\tself::$resolved_type = $sticky;\n-\t\t\t} else {\n-\t\t\t\tself::$resolved_type = in_array( $configured, array( 'SESSION', 'TRANSIENT' ), true ) ? $configured : 'TRANSIENT';\n-\t\t\t}\n-\n-\t\t\t\u002F**\n-\t\t\t * Filter the storage backend used for OTP session data.\n-\t\t\t *\n-\t\t\t * @param string $storage_type SESSION or TRANSIENT.\n-\t\t\t *\u002F\n-\t\t\treturn (string) \\apply_filters( 'mo_otp_storage_type', self::$resolved_type );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Reads sticky per-browser storage preference set after a fallback.\n-\t\t *\n-\t\t * @return string|null SESSION or TRANSIENT.\n-\t\t *\u002F\n-\t\tprivate static function get_sticky_storage_mode() {\n-\t\t\tif ( empty( $_COOKIE[ self::STORAGE_MODE_COOKIE ] ) ) {\n-\t\t\t\treturn null;\n-\t\t\t}\n-\t\t\t$mode = strtoupper( \\sanitize_text_field( \\wp_unslash( $_COOKIE[ self::STORAGE_MODE_COOKIE ] ) ) );\n-\t\t\treturn in_array( $mode, array( 'SESSION', 'TRANSIENT' ), true ) ? $mode : null;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Persists which storage type worked for this browser (12 hours).\n-\t\t *\n-\t\t * @param string $storage_type SESSION or TRANSIENT.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tprivate static function persist_storage_preference( $storage_type ) {\n-\t\t\t$storage_type = strtoupper( (string) $storage_type );\n-\t\t\tif ( ! in_array( $storage_type, array( 'SESSION', 'TRANSIENT' ), true ) || \\headers_sent() || ! self::can_set_cookie() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tself::$resolved_type = $storage_type;\n-\t\t\t\\setcookie( self::STORAGE_MODE_COOKIE, $storage_type, time() + ( 12 * \\HOUR_IN_SECONDS ), \\COOKIEPATH, \\COOKIE_DOMAIN, \\is_ssl(), true );\n-\t\t\t$_COOKIE[ self::STORAGE_MODE_COOKIE ] = $storage_type;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Picks a default storage type when MOV_SESSION_TYPE is AUTO.\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tprivate static function detect_auto_storage_type() {\n-\t\t\t$session_ok   = self::can_use_session_storage();\n-\t\t\t$transient_ok = self::can_use_transient_storage();\n-\n-\t\t\tif ( $session_ok && ! $transient_ok ) {\n-\t\t\t\treturn 'SESSION';\n-\t\t\t}\n-\t\t\tif ( $transient_ok && ! $session_ok ) {\n-\t\t\t\treturn 'TRANSIENT';\n-\t\t\t}\n-\t\t\tif ( $session_ok && $transient_ok ) {\n-\t\t\t\t\u002F**\n-\t\t\t\t * Preferred storage when both SESSION and TRANSIENT are viable (AUTO mode).\n-\t\t\t\t *\n-\t\t\t\t * @param string $preferred Default TRANSIENT for WordPress hosting compatibility.\n-\t\t\t\t *\u002F\n-\t\t\t\treturn (string) \\apply_filters( 'mo_otp_preferred_storage_type', 'TRANSIENT' );\n-\t\t\t}\n-\n-\t\t\treturn 'TRANSIENT';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Returns the other OTP storage backend.\n-\t\t *\n-\t\t * @param string $primary SESSION or TRANSIENT.\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tprivate static function get_alternate_storage_type( $primary ) {\n-\t\t\treturn 'SESSION' === $primary ? 'TRANSIENT' : 'SESSION';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Whether native PHP sessions can be used on this request.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tprivate static function can_use_session_storage() {\n-\t\t\tif ( self::$session_viability_checked ) {\n-\t\t\t\treturn self::$session_viable;\n-\t\t\t}\n-\n-\t\t\tself::$session_viability_checked = true;\n-\t\t\tself::$session_viable            = false;\n-\n-\t\t\tif ( PHP_SESSION_DISABLED === session_status() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( \\headers_sent() && PHP_SESSION_ACTIVE !== session_status() && '' === session_id() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( PHP_SESSION_ACTIVE === session_status() || '' !== session_id() ) {\n-\t\t\t\tself::$session_viable = isset( $_SESSION );\n-\t\t\t\treturn self::$session_viable;\n-\t\t\t}\n-\n-\t\t\tif ( \\headers_sent() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( self::open_php_session_for_write() ) {\n-\t\t\t\t$probe_key              = 'mo_otp_storage_probe';\n-\t\t\t\t$_SESSION[ $probe_key ] = '1';\n-\t\t\t\tself::$session_viable   = isset( $_SESSION[ $probe_key ] );\n-\t\t\t\tunset( $_SESSION[ $probe_key ] );\n-\t\t\t\tself::close_php_session();\n-\t\t\t}\n-\n-\t\t\treturn self::$session_viable;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Whether WordPress transients (with cookie key) can be used on this request.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tprivate static function can_use_transient_storage() {\n-\t\t\tif ( ! empty( $_COOKIE['transient_key'] ) ) {\n-\t\t\t\treturn true;\n-\t\t\t}\n-\n-\t\t\treturn ! \\headers_sent() && self::can_set_cookie();\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Ensures transient_key cookie exists for the current request.\n-\t\t *\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tprivate static function ensure_transient_cookie() {\n-\t\t\tself::get_transient_key();\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Returns the transient storage cookie value, creating it when possible.\n-\t\t *\n-\t\t * @return string|null\n-\t\t *\u002F\n-\t\tprivate static function get_transient_key() {\n-\t\t\tif ( ! empty( $_COOKIE['transient_key'] ) ) {\n-\t\t\t\treturn \\sanitize_text_field( \\wp_unslash( $_COOKIE['transient_key'] ) );\n-\t\t\t}\n-\n-\t\t\tif ( \\headers_sent() || ! self::can_set_cookie() ) {\n-\t\t\t\treturn null;\n-\t\t\t}\n-\n-\t\t\t$transient_key = self::generate_transient_key();\n-\t\t\tif ( ! $transient_key ) {\n-\t\t\t\treturn null;\n-\t\t\t}\n-\n-\t\t\t$_COOKIE['transient_key'] = $transient_key;\n-\t\t\t\\setcookie( 'transient_key', $transient_key, time() + ( 12 * \\HOUR_IN_SECONDS ), \\COOKIEPATH, \\COOKIE_DOMAIN, \\is_ssl(), true );\n-\n-\t\t\treturn $transient_key;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Whether WordPress cookie constants are available for setcookie().\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tprivate static function can_set_cookie() {\n-\t\t\treturn defined( 'COOKIEPATH' ) && defined( 'COOKIE_DOMAIN' );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Generates a random transient cookie key (safe before pluggable.php is loaded).\n-\t\t *\n-\t\t * @return string|null\n-\t\t *\u002F\n-\t\tprivate static function generate_transient_key() {\n-\t\t\tif ( \\function_exists( 'wp_generate_password' ) ) {\n-\t\t\t\treturn \\wp_generate_password( 32, false );\n-\t\t\t}\n-\n-\t\t\tif ( \\function_exists( 'random_bytes' ) ) {\n-\t\t\t\treturn \\bin2hex( \\random_bytes( 16 ) );\n-\t\t\t}\n-\n-\t\t\treturn \\md5( \\uniqid( 'mo_otp', true ) );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Writes OTP state to a specific storage backend.\n-\t\t *\n-\t\t * @param string $storage_type Storage backend.\n-\t\t * @param string $key          Session key.\n-\t\t * @param mixed  $val          Value to store.\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tprivate static function write_by_type( $storage_type, $key, $val ) {\n-\t\t\tswitch ( $storage_type ) {\n-\t\t\t\tcase 'SESSION':\n-\t\t\t\t\tif ( ! self::open_php_session_for_write() || ! isset( $_SESSION ) ) {\n-\t\t\t\t\t\treturn false;\n-\t\t\t\t\t}\n-\t\t\t\t\t$_SESSION[ $key ] = \\maybe_serialize( $val );\n-\t\t\t\t\t$written          = \\array_key_exists( $key, $_SESSION );\n-\t\t\t\t\tself::close_php_session();\n-\t\t\t\t\treturn $written;\n-\t\t\t\tcase 'TRANSIENT':\n-\t\t\t\t\t$transient_key = self::get_transient_key();\n-\t\t\t\t\tif ( ! $transient_key ) {\n-\t\t\t\t\t\treturn false;\n-\t\t\t\t\t}\n-\t\t\t\t\treturn false !== \\set_site_transient( 'mo_otp_' . $transient_key . $key, $val, 12 * \\HOUR_IN_SECONDS );\n-\t\t\t\tcase 'COOKIE':\n-\t\t\t\t\tif ( \\headers_sent() ) {\n-\t\t\t\t\t\treturn false;\n-\t\t\t\t\t}\n-\t\t\t\t\t$cookie_val = \\wp_json_encode( $val, JSON_UNESCAPED_SLASHES );\n-\t\t\t\t\t\\setcookie( $key, $cookie_val, time() + ( 12 * \\HOUR_IN_SECONDS ), \\COOKIEPATH, \\COOKIE_DOMAIN, \\is_ssl(), true );\n-\t\t\t\t\t$_COOKIE[ $key ] = $cookie_val;\n-\t\t\t\t\treturn true;\n-\t\t\t\tcase 'CACHE':\n-\t\t\t\t\tif ( ! \\wp_cache_add( $key, \\maybe_serialize( $val ) ) ) {\n-\t\t\t\t\t\t\\wp_cache_replace( $key, \\maybe_serialize( $val ) );\n-\t\t\t\t\t}\n-\t\t\t\t\treturn false !== \\wp_cache_get( $key );\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Reads OTP state from a specific storage backend.\n-\t\t *\n-\t\t * @param string $storage_type Storage backend.\n-\t\t * @param string $key          Session key.\n-\t\t * @return mixed|null\n-\t\t *\u002F\n-\t\tprivate static function read_by_type( $storage_type, $key ) {\n-\t\t\tswitch ( $storage_type ) {\n-\t\t\t\tcase 'SESSION':\n-\t\t\t\t\tif ( ! self::open_php_session_for_read() || ! isset( $_SESSION ) ) {\n-\t\t\t\t\t\treturn null;\n-\t\t\t\t\t}\n-\t\t\t\t\t\u002F\u002F phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Internal session payload written by this plugin.\n-\t\t\t\t\t$raw = isset( $_SESSION[ $key ] ) ? $_SESSION[ $key ] : null;\n-\t\t\t\t\tif ( \\function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === \\session_status() ) {\n-\t\t\t\t\t\tself::close_php_session();\n-\t\t\t\t\t}\n-\t\t\t\t\tif ( null === $raw ) {\n-\t\t\t\t\t\treturn null;\n-\t\t\t\t\t}\n-\t\t\t\t\treturn \\maybe_unserialize( $raw );\n-\t\t\t\tcase 'TRANSIENT':\n-\t\t\t\t\tif ( empty( $_COOKIE['transient_key'] ) ) {\n-\t\t\t\t\t\treturn null;\n-\t\t\t\t\t}\n-\t\t\t\t\t$transient_key = \\sanitize_text_field( \\wp_unslash( $_COOKIE['transient_key'] ) );\n-\t\t\t\t\t$value         = \\get_site_transient( 'mo_otp_' . $transient_key . $key );\n-\t\t\t\t\treturn ( false === $value ) ? null : $value;\n-\t\t\t\tcase 'COOKIE':\n-\t\t\t\t\t$raw = isset( $_COOKIE[ $key ] ) ? \\sanitize_text_field( \\wp_unslash( $_COOKIE[ $key ] ) ) : null;\n-\t\t\t\t\tif ( null === $raw ) {\n-\t\t\t\t\t\treturn null;\n-\t\t\t\t\t}\n-\t\t\t\t\t$decoded = json_decode( $raw, true );\n-\t\t\t\t\tif ( null === $decoded && JSON_ERROR_NONE !== json_last_error() ) {\n-\t\t\t\t\t\treturn null;\n-\t\t\t\t\t}\n-\t\t\t\t\treturn $decoded;\n-\t\t\t\tcase 'CACHE':\n-\t\t\t\t\t$raw = \\wp_cache_get( $key );\n-\t\t\t\t\tif ( null === $raw ) {\n-\t\t\t\t\t\treturn null;\n-\t\t\t\t\t}\n-\t\t\t\t\treturn \\maybe_unserialize( $raw );\n-\t\t\t}\n-\n-\t\t\treturn null;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Removes OTP state from a specific storage backend.\n-\t\t *\n-\t\t * @param string $storage_type Storage backend.\n-\t\t * @param string $key          Session key.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tprivate static function unset_by_type( $storage_type, $key ) {\n-\t\t\tswitch ( $storage_type ) {\n-\t\t\t\tcase 'SESSION':\n-\t\t\t\t\tif ( self::open_php_session_for_write() && isset( $_SESSION[ $key ] ) ) {\n-\t\t\t\t\t\tunset( $_SESSION[ $key ] );\n-\t\t\t\t\t}\n-\t\t\t\t\tself::close_php_session();\n-\t\t\t\t\tbreak;\n-\t\t\t\tcase 'TRANSIENT':\n-\t\t\t\t\tif ( ! empty( $_COOKIE['transient_key'] ) ) {\n-\t\t\t\t\t\t$transient_key = \\sanitize_text_field( \\wp_unslash( $_COOKIE['transient_key'] ) );\n-\t\t\t\t\t\t\\delete_site_transient( 'mo_otp_' . $transient_key . $key );\n-\t\t\t\t\t}\n-\t\t\t\t\tbreak;\n-\t\t\t\tcase 'COOKIE':\n-\t\t\t\t\tunset( $_COOKIE[ $key ] );\n-\t\t\t\t\tif ( ! \\headers_sent() ) {\n-\t\t\t\t\t\t\\setcookie( $key, '', time() - ( 15 * 60 ), \\COOKIEPATH, \\COOKIE_DOMAIN, \\is_ssl(), true );\n-\t\t\t\t\t}\n-\t\t\t\t\tbreak;\n-\t\t\t\tcase 'CACHE':\n-\t\t\t\t\t\\wp_cache_delete( $key );\n-\t\t\t\t\tbreak;\n-\t\t\t}\n-\t\t}\n-\t}\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * Load administrator changes for MoPHPSessions\r\n+ *\r\n+ * @package miniorange-otp-verification\u002Fhelper\r\n+ *\u002F\r\n+\r\n+namespace OTP\\Helper;\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+use OTP\\Objects\\IMoSessions;\r\n+\r\n+\u002F** TODO: Need to move each session type to different files *\u002F\r\n+if ( ! class_exists( 'MoPHPSessions' ) ) {\r\n+\t\u002F**\r\n+\t * Class for managing different types of session storage mechanisms.\r\n+\t *\r\n+\t * Implements the IMoSessions interface to provide consistent session handling\r\n+\t * across different storage types: PHP sessions, cookies, cache and transients.\r\n+\t *\r\n+\t * AUTO mode (recommended) picks SESSION or TRANSIENT per browser and falls back\r\n+\t * automatically when the primary mechanism cannot read\u002Fwrite OTP state.\r\n+\t *\u002F\r\n+\tclass MoPHPSessions implements IMoSessions {\r\n+\r\n+\t\tconst STORAGE_MODE_COOKIE = 'mo_otp_storage_mode';\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Resolved storage type for the current request (SESSION or TRANSIENT when using AUTO\u002Ffallback).\r\n+\t\t *\r\n+\t\t * @var string|null\r\n+\t\t *\u002F\r\n+\t\tprivate static $resolved_type = null;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Whether PHP session viability was probed this request.\r\n+\t\t *\r\n+\t\t * @var bool\r\n+\t\t *\u002F\r\n+\t\tprivate static $session_viability_checked = false;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Cached result of PHP session viability probe.\r\n+\t\t *\r\n+\t\t * @var bool\r\n+\t\t *\u002F\r\n+\t\tprivate static $session_viable = false;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Whether shutdown\u002FREST hooks were registered to close PHP sessions.\r\n+\t\t *\r\n+\t\t * @var bool\r\n+\t\t *\u002F\r\n+\t\tprivate static $close_hooks_registered = false;\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Bootstraps storage early so cookies can be set before output (fixes TRANSIENT on cached themes).\r\n+\t\t *\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic static function bootstrap() {\r\n+\t\t\tif ( ! defined( 'MOV_SESSION_TYPE' ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tself::register_session_close_hooks();\r\n+\r\n+\t\t\tif ( self::should_cross_fallback() ) {\r\n+\t\t\t\tself::ensure_transient_cookie();\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Registers hooks so PHP sessions are closed before REST\u002Floopback HTTP requests (Site Health).\r\n+\t\t *\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tprivate static function register_session_close_hooks() {\r\n+\t\t\tif ( self::$close_hooks_registered || ! \\function_exists( 'add_action' ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tself::$close_hooks_registered = true;\r\n+\t\t\t\\add_action( 'rest_api_init', array( __CLASS__, 'close_php_session' ), 0 );\r\n+\t\t\t\\add_filter( 'pre_http_request', array( __CLASS__, 'close_php_session_before_http' ), 1, 3 );\r\n+\t\t\t\\add_action( 'shutdown', array( __CLASS__, 'close_php_session' ), 0 );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Closes PHP session before WordPress loopback\u002FREST HTTP requests (Site Health).\r\n+\t\t *\r\n+\t\t * @param false|array|\\WP_Error $preempt Whether to preempt an HTTP request's return value.\r\n+\t\t * @param array                 $args    HTTP request arguments.\r\n+\t\t * @param string                $url     The request URL.\r\n+\t\t * @return false|array|\\WP_Error\r\n+\t\t *\u002F\r\n+\t\tpublic static function close_php_session_before_http( $preempt, $args, $url ) {\r\n+\t\t\tunset( $args, $url );\r\n+\t\t\tself::close_php_session();\r\n+\t\t\treturn $preempt;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Sets session values based on the configured session type.\r\n+\t\t *\r\n+\t\t * @param string $key Key to store data under.\r\n+\t\t * @param mixed  $val Value to store.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic static function add_session_var( $key, $val ) {\r\n+\t\t\tif ( empty( $key ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$storage_type = self::get_configured_storage_type();\r\n+\t\t\tif ( self::should_cross_fallback() ) {\r\n+\t\t\t\t$primary = self::get_effective_storage_type();\r\n+\t\t\t\tif ( ! self::write_by_type( $primary, $key, $val ) ) {\r\n+\t\t\t\t\t$alternate = self::get_alternate_storage_type( $primary );\r\n+\t\t\t\t\tif ( self::write_by_type( $alternate, $key, $val ) ) {\r\n+\t\t\t\t\t\tself::persist_storage_preference( $alternate );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tself::write_by_type( $storage_type, $key, $val );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Retrieves a value stored in session by key.\r\n+\t\t *\r\n+\t\t * @param string $key Key used to store the value.\r\n+\t\t * @return mixed Value stored under the key, or null if not found.\r\n+\t\t *\u002F\r\n+\t\tpublic static function get_session_var( $key ) {\r\n+\t\t\tif ( empty( $key ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( self::should_cross_fallback() ) {\r\n+\t\t\t\t$primary = self::get_effective_storage_type();\r\n+\t\t\t\t$value   = self::read_by_type( $primary, $key );\r\n+\t\t\t\tif ( null !== $value ) {\r\n+\t\t\t\t\treturn $value;\r\n+\t\t\t\t}\r\n+\t\t\t\treturn self::read_by_type( self::get_alternate_storage_type( $primary ), $key );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn self::read_by_type( self::get_configured_storage_type(), $key );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Unsets session values for the specified key.\r\n+\t\t *\r\n+\t\t * @param string $key Key to unset from the session.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic static function unset_session( $key ) {\r\n+\t\t\tif ( self::should_cross_fallback() ) {\r\n+\t\t\t\tself::unset_by_type( 'SESSION', $key );\r\n+\t\t\t\tself::unset_by_type( 'TRANSIENT', $key );\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tself::unset_by_type( self::get_configured_storage_type(), $key );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Legacy no-op: PHP sessions are opened and closed per operation in this class.\r\n+\t\t *\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic static function check_session() {\r\n+\t\t\t\u002F\u002F Intentionally empty. Use open\u002Fclose helpers inside MoPHPSessions only.\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Closes an active PHP session so REST API and loopback requests are not blocked.\r\n+\t\t *\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic static function close_php_session() {\r\n+\t\t\tif ( \\function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === \\session_status() ) {\r\n+\t\t\t\t\\session_write_close();\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Opens PHP session for reading and releases the lock when possible.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tprivate static function open_php_session_for_read() {\r\n+\t\t\tself::register_session_close_hooks();\r\n+\r\n+\t\t\tif ( PHP_SESSION_DISABLED === \\session_status() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( PHP_SESSION_ACTIVE === \\session_status() ) {\r\n+\t\t\t\treturn isset( $_SESSION );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( \\headers_sent() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( \\PHP_VERSION_ID >= 70000 ) {\r\n+\t\t\t\treturn @\\session_start( array( 'read_and_close' => true ) ); \u002F\u002F phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( @\\session_start() ) { \u002F\u002F phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged\r\n+\t\t\t\treturn isset( $_SESSION );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Opens PHP session for writing.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tprivate static function open_php_session_for_write() {\r\n+\t\t\tself::register_session_close_hooks();\r\n+\r\n+\t\t\tif ( PHP_SESSION_DISABLED === \\session_status() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( PHP_SESSION_ACTIVE === \\session_status() ) {\r\n+\t\t\t\treturn isset( $_SESSION );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( \\headers_sent() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn @\\session_start(); \u002F\u002F phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Whether the current request uses native PHP $_SESSION storage.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function uses_php_session() {\r\n+\t\t\tif ( ! self::should_cross_fallback() ) {\r\n+\t\t\t\treturn 'SESSION' === self::get_configured_storage_type();\r\n+\t\t\t}\r\n+\t\t\treturn 'SESSION' === self::get_effective_storage_type();\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Returns configured MOV_SESSION_TYPE (may be AUTO).\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tprivate static function get_configured_storage_type() {\r\n+\t\t\treturn defined( 'MOV_SESSION_TYPE' ) ? strtoupper( (string) MOV_SESSION_TYPE ) : 'SESSION';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * SESSION \u002F TRANSIENT \u002F AUTO use adaptive storage with cross-fallback.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tprivate static function should_cross_fallback() {\r\n+\t\t\treturn in_array( self::get_configured_storage_type(), array( 'SESSION', 'TRANSIENT', 'AUTO' ), true );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Resolves effective storage (SESSION or TRANSIENT) for this request.\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tprivate static function get_effective_storage_type() {\r\n+\t\t\tif ( null !== self::$resolved_type ) {\r\n+\t\t\t\treturn self::$resolved_type;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$configured = self::get_configured_storage_type();\r\n+\t\t\t$sticky     = self::get_sticky_storage_mode();\r\n+\r\n+\t\t\tif ( 'AUTO' === $configured ) {\r\n+\t\t\t\tself::$resolved_type = $sticky ? $sticky : self::detect_auto_storage_type();\r\n+\t\t\t} elseif ( $sticky ) {\r\n+\t\t\t\tself::$resolved_type = $sticky;\r\n+\t\t\t} else {\r\n+\t\t\t\tself::$resolved_type = in_array( $configured, array( 'SESSION', 'TRANSIENT' ), true ) ? $configured : 'TRANSIENT';\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F**\r\n+\t\t\t * Filter the storage backend used for OTP session data.\r\n+\t\t\t *\r\n+\t\t\t * @param string $storage_type SESSION or TRANSIENT.\r\n+\t\t\t *\u002F\r\n+\t\t\treturn (string) \\apply_filters( 'mo_otp_storage_type', self::$resolved_type );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Reads sticky per-browser storage preference set after a fallback.\r\n+\t\t *\r\n+\t\t * @return string|null SESSION or TRANSIENT.\r\n+\t\t *\u002F\r\n+\t\tprivate static function get_sticky_storage_mode() {\r\n+\t\t\tif ( empty( $_COOKIE[ self::STORAGE_MODE_COOKIE ] ) ) {\r\n+\t\t\t\treturn null;\r\n+\t\t\t}\r\n+\t\t\t$mode = strtoupper( \\sanitize_text_field( \\wp_unslash( $_COOKIE[ self::STORAGE_MODE_COOKIE ] ) ) );\r\n+\t\t\treturn in_array( $mode, array( 'SESSION', 'TRANSIENT' ), true ) ? $mode : null;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Persists which storage type worked for this browser (12 hours).\r\n+\t\t *\r\n+\t\t * @param string $storage_type SESSION or TRANSIENT.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tprivate static function persist_storage_preference( $storage_type ) {\r\n+\t\t\t$storage_type = strtoupper( (string) $storage_type );\r\n+\t\t\tif ( ! in_array( $storage_type, array( 'SESSION', 'TRANSIENT' ), true ) || \\headers_sent() || ! self::can_set_cookie() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tself::$resolved_type = $storage_type;\r\n+\t\t\t\\setcookie( self::STORAGE_MODE_COOKIE, $storage_type, time() + ( 12 * \\HOUR_IN_SECONDS ), \\COOKIEPATH, \\COOKIE_DOMAIN, \\is_ssl(), true );\r\n+\t\t\t$_COOKIE[ self::STORAGE_MODE_COOKIE ] = $storage_type;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Picks a default storage type when MOV_SESSION_TYPE is AUTO.\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tprivate static function detect_auto_storage_type() {\r\n+\t\t\t$session_ok   = self::can_use_session_storage();\r\n+\t\t\t$transient_ok = self::can_use_transient_storage();\r\n+\r\n+\t\t\tif ( $session_ok && ! $transient_ok ) {\r\n+\t\t\t\treturn 'SESSION';\r\n+\t\t\t}\r\n+\t\t\tif ( $transient_ok && ! $session_ok ) {\r\n+\t\t\t\treturn 'TRANSIENT';\r\n+\t\t\t}\r\n+\t\t\tif ( $session_ok && $transient_ok ) {\r\n+\t\t\t\t\u002F**\r\n+\t\t\t\t * Preferred storage when both SESSION and TRANSIENT are viable (AUTO mode).\r\n+\t\t\t\t *\r\n+\t\t\t\t * @param string $preferred Default TRANSIENT for WordPress hosting compatibility.\r\n+\t\t\t\t *\u002F\r\n+\t\t\t\treturn (string) \\apply_filters( 'mo_otp_preferred_storage_type', 'TRANSIENT' );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn 'TRANSIENT';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Returns the other OTP storage backend.\r\n+\t\t *\r\n+\t\t * @param string $primary SESSION or TRANSIENT.\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tprivate static function get_alternate_storage_type( $primary ) {\r\n+\t\t\treturn 'SESSION' === $primary ? 'TRANSIENT' : 'SESSION';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Whether native PHP sessions can be used on this request.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tprivate static function can_use_session_storage() {\r\n+\t\t\tif ( self::$session_viability_checked ) {\r\n+\t\t\t\treturn self::$session_viable;\r\n+\t\t\t}\r\n+\r\n+\t\t\tself::$session_viability_checked = true;\r\n+\t\t\tself::$session_viable            = false;\r\n+\r\n+\t\t\tif ( PHP_SESSION_DISABLED === session_status() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( \\headers_sent() && PHP_SESSION_ACTIVE !== session_status() && '' === session_id() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( PHP_SESSION_ACTIVE === session_status() || '' !== session_id() ) {\r\n+\t\t\t\tself::$session_viable = isset( $_SESSION );\r\n+\t\t\t\treturn self::$session_viable;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( \\headers_sent() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( self::open_php_session_for_write() ) {\r\n+\t\t\t\t$probe_key              = 'mo_otp_storage_probe';\r\n+\t\t\t\t$_SESSION[ $probe_key ] = '1';\r\n+\t\t\t\tself::$session_viable   = isset( $_SESSION[ $probe_key ] );\r\n+\t\t\t\tunset( $_SESSION[ $probe_key ] );\r\n+\t\t\t\tself::close_php_session();\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn self::$session_viable;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Whether WordPress transients (with cookie key) can be used on this request.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tprivate static function can_use_transient_storage() {\r\n+\t\t\tif ( ! empty( $_COOKIE['transient_key'] ) ) {\r\n+\t\t\t\treturn true;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn ! \\headers_sent() && self::can_set_cookie();\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Ensures transient_key cookie exists for the current request.\r\n+\t\t *\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tprivate static function ensure_transient_cookie() {\r\n+\t\t\tself::get_transient_key();\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Returns the transient storage cookie value, creating it when possible.\r\n+\t\t *\r\n+\t\t * @return string|null\r\n+\t\t *\u002F\r\n+\t\tprivate static function get_transient_key() {\r\n+\t\t\tif ( ! empty( $_COOKIE['transient_key'] ) ) {\r\n+\t\t\t\treturn \\sanitize_text_field( \\wp_unslash( $_COOKIE['transient_key'] ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( \\headers_sent() || ! self::can_set_cookie() ) {\r\n+\t\t\t\treturn null;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$transient_key = self::generate_transient_key();\r\n+\t\t\tif ( ! $transient_key ) {\r\n+\t\t\t\treturn null;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$_COOKIE['transient_key'] = $transient_key;\r\n+\t\t\t\\setcookie( 'transient_key', $transient_key, time() + ( 12 * \\HOUR_IN_SECONDS ), \\COOKIEPATH, \\COOKIE_DOMAIN, \\is_ssl(), true );\r\n+\r\n+\t\t\treturn $transient_key;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Whether WordPress cookie constants are available for setcookie().\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tprivate static function can_set_cookie() {\r\n+\t\t\treturn defined( 'COOKIEPATH' ) && defined( 'COOKIE_DOMAIN' );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Generates a random transient cookie key (safe before pluggable.php is loaded).\r\n+\t\t *\r\n+\t\t * @return string|null\r\n+\t\t *\u002F\r\n+\t\tprivate static function generate_transient_key() {\r\n+\t\t\tif ( \\function_exists( 'wp_generate_password' ) ) {\r\n+\t\t\t\treturn \\wp_generate_password( 32, false );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( \\function_exists( 'random_bytes' ) ) {\r\n+\t\t\t\treturn \\bin2hex( \\random_bytes( 16 ) );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn \\md5( \\uniqid( 'mo_otp', true ) );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Writes OTP state to a specific storage backend.\r\n+\t\t *\r\n+\t\t * @param string $storage_type Storage backend.\r\n+\t\t * @param string $key          Session key.\r\n+\t\t * @param mixed  $val          Value to store.\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tprivate static function write_by_type( $storage_type, $key, $val ) {\r\n+\t\t\tswitch ( $storage_type ) {\r\n+\t\t\t\tcase 'SESSION':\r\n+\t\t\t\t\tif ( ! self::open_php_session_for_write() || ! isset( $_SESSION ) ) {\r\n+\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t$_SESSION[ $key ] = \\maybe_serialize( $val );\r\n+\t\t\t\t\t$written          = \\array_key_exists( $key, $_SESSION );\r\n+\t\t\t\t\tself::close_php_session();\r\n+\t\t\t\t\treturn $written;\r\n+\t\t\t\tcase 'TRANSIENT':\r\n+\t\t\t\t\t$transient_key = self::get_transient_key();\r\n+\t\t\t\t\tif ( ! $transient_key ) {\r\n+\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\treturn false !== \\set_site_transient( 'mo_otp_' . $transient_key . $key, $val, 12 * \\HOUR_IN_SECONDS );\r\n+\t\t\t\tcase 'COOKIE':\r\n+\t\t\t\t\tif ( \\headers_sent() ) {\r\n+\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t$cookie_val = \\wp_json_encode( $val, JSON_UNESCAPED_SLASHES );\r\n+\t\t\t\t\t\\setcookie( $key, $cookie_val, time() + ( 12 * \\HOUR_IN_SECONDS ), \\COOKIEPATH, \\COOKIE_DOMAIN, \\is_ssl(), true );\r\n+\t\t\t\t\t$_COOKIE[ $key ] = $cookie_val;\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\tcase 'CACHE':\r\n+\t\t\t\t\tif ( ! \\wp_cache_add( $key, \\maybe_serialize( $val ) ) ) {\r\n+\t\t\t\t\t\t\\wp_cache_replace( $key, \\maybe_serialize( $val ) );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\treturn false !== \\wp_cache_get( $key );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Reads OTP state from a specific storage backend.\r\n+\t\t *\r\n+\t\t * @param string $storage_type Storage backend.\r\n+\t\t * @param string $key          Session key.\r\n+\t\t * @return mixed|null\r\n+\t\t *\u002F\r\n+\t\tprivate static function read_by_type( $storage_type, $key ) {\r\n+\t\t\tswitch ( $storage_type ) {\r\n+\t\t\t\tcase 'SESSION':\r\n+\t\t\t\t\tif ( ! self::open_php_session_for_read() || ! isset( $_SESSION ) ) {\r\n+\t\t\t\t\t\treturn null;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t\u002F\u002F phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Internal session payload written by this plugin.\r\n+\t\t\t\t\t$raw = isset( $_SESSION[ $key ] ) ? $_SESSION[ $key ] : null;\r\n+\t\t\t\t\tif ( \\function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === \\session_status() ) {\r\n+\t\t\t\t\t\tself::close_php_session();\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tif ( null === $raw ) {\r\n+\t\t\t\t\t\treturn null;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\treturn \\maybe_unserialize( $raw );\r\n+\t\t\t\tcase 'TRANSIENT':\r\n+\t\t\t\t\tif ( empty( $_COOKIE['transient_key'] ) ) {\r\n+\t\t\t\t\t\treturn null;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t$transient_key = \\sanitize_text_field( \\wp_unslash( $_COOKIE['transient_key'] ) );\r\n+\t\t\t\t\t$value         = \\get_site_transient( 'mo_otp_' . $transient_key . $key );\r\n+\t\t\t\t\treturn ( false === $value ) ? null : $value;\r\n+\t\t\t\tcase 'COOKIE':\r\n+\t\t\t\t\t$raw = isset( $_COOKIE[ $key ] ) ? \\sanitize_text_field( \\wp_unslash( $_COOKIE[ $key ] ) ) : null;\r\n+\t\t\t\t\tif ( null === $raw ) {\r\n+\t\t\t\t\t\treturn null;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\t$decoded = json_decode( $raw, true );\r\n+\t\t\t\t\tif ( null === $decoded && JSON_ERROR_NONE !== json_last_error() ) {\r\n+\t\t\t\t\t\treturn null;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\treturn $decoded;\r\n+\t\t\t\tcase 'CACHE':\r\n+\t\t\t\t\t$raw = \\wp_cache_get( $key );\r\n+\t\t\t\t\tif ( null === $raw ) {\r\n+\t\t\t\t\t\treturn null;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\treturn \\maybe_unserialize( $raw );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn null;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Removes OTP state from a specific storage backend.\r\n+\t\t *\r\n+\t\t * @param string $storage_type Storage backend.\r\n+\t\t * @param string $key          Session key.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tprivate static function unset_by_type( $storage_type, $key ) {\r\n+\t\t\tswitch ( $storage_type ) {\r\n+\t\t\t\tcase 'SESSION':\r\n+\t\t\t\t\tif ( self::open_php_session_for_write() && isset( $_SESSION[ $key ] ) ) {\r\n+\t\t\t\t\t\tunset( $_SESSION[ $key ] );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tself::close_php_session();\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\tcase 'TRANSIENT':\r\n+\t\t\t\t\tif ( ! empty( $_COOKIE['transient_key'] ) ) {\r\n+\t\t\t\t\t\t$transient_key = \\sanitize_text_field( \\wp_unslash( $_COOKIE['transient_key'] ) );\r\n+\t\t\t\t\t\t\\delete_site_transient( 'mo_otp_' . $transient_key . $key );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\tcase 'COOKIE':\r\n+\t\t\t\t\tunset( $_COOKIE[ $key ] );\r\n+\t\t\t\t\tif ( ! \\headers_sent() ) {\r\n+\t\t\t\t\t\t\\setcookie( $key, '', time() - ( 15 * 60 ), \\COOKIEPATH, \\COOKIE_DOMAIN, \\is_ssl(), true );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\tcase 'CACHE':\r\n+\t\t\t\t\t\\wp_cache_delete( $key );\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t}\r\n+\t\t}\r\n+\t}\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhelper\u002Fclass-moutility.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhelper\u002Fclass-moutility.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhelper\u002Fclass-moutility.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhelper\u002Fclass-moutility.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,1630 +1,1635 @@\n-\u003C?php\n-\u002F**\n- * Load administrator changes for MoUtility\n- *\n- * @package miniorange-otp-verification\u002Fhelper\n- *\u002F\n-\n-namespace OTP\\Helper;\n-\n-if ( ! defined( 'ABSPATH' ) ) {\n-\texit;\n-}\n-\n-use OTP\\Objects\\NotificationSettings;\n-use OTP\\Objects\\TabDetails;\n-use OTP\\Objects\\Tabs;\n-use OTP\\Objects\\VerificationType;\n-use ReflectionClass;\n-use ReflectionException;\n-use stdClass;\n-use OTP\\LicenseLibrary\\Mo_License_Service;\n-use OTP\\Helper\\MoConstants;\n-use OTP\\Helper\\CountryList;\n-\n-\u002F**\n- * This is the main Utility class of the plugin.\n- * Lists down all the necessary common utility\n- * functions being used in the plugin.\n- *\u002F\n-if ( ! class_exists( 'MoUtility' ) ) {\n-\t\u002F**\n-\t * MoUtility class\n-\t *\u002F\n-\tclass MoUtility {\n-\n-\n-\t\t\u002F**\n-\t\t * Checking Script tags\n-\t\t *\n-\t\t * @param string $template checking script tag.\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tpublic static function check_for_script_tags( $template ) {\n-\t\t\treturn preg_match( '\u002F\u003Cscript>\u002F', $template, $match );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Sanitizing array\n-\t\t *\n-\t\t * @param array $data data array to be sanitized.\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tpublic static function mo_sanitize_array( $data ) {\n-\t\t\t$sanitized_data = array();\n-\t\t\tforeach ( $data as $key => $value ) {\n-\t\t\t\tif ( is_array( $value ) ) {\n-\t\t\t\t\t$sanitized_data[ $key ] = self::mo_sanitize_array( $value );\n-\t\t\t\t} else {\n-\t\t\t\t\t$sanitized_data[ $key ] = sanitize_text_field( wp_unslash( $value ) );\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn $sanitized_data;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * MoInternal Function\n-\t\t *\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tpublic static function mo_allow_html_array() {\n-\t\t\t$allowed_tags = array(\n-\t\t\t\t'a'          => array(\n-\t\t\t\t\t'style'   => array(),\n-\t\t\t\t\t'class'   => array(),\n-\t\t\t\t\t'href'    => array(),\n-\t\t\t\t\t'rel'     => array(),\n-\t\t\t\t\t'title'   => array(),\n-\t\t\t\t\t'hidden'  => array(),\n-\t\t\t\t\t'target'  => array(),\n-\t\t\t\t\t'onclick' => array(),\n-\t\t\t\t),\n-\t\t\t\t'b'          => array(\n-\t\t\t\t\t'style' => array(),\n-\t\t\t\t\t'class' => array(),\n-\t\t\t\t\t'id'    => array(),\n-\t\t\t\t),\n-\t\t\t\t'blockquote' => array(\n-\t\t\t\t\t'cite' => array(),\n-\t\t\t\t),\n-\t\t\t\t'code'       => array(),\n-\t\t\t\t'del'        => array(\n-\t\t\t\t\t'datetime' => array(),\n-\t\t\t\t\t'title'    => array(),\n-\t\t\t\t),\n-\t\t\t\t'div'        => array(\n-\t\t\t\t\t'name'   => array(),\n-\t\t\t\t\t'dir'    => array(),\n-\t\t\t\t\t'id'     => array(),\n-\t\t\t\t\t'class'  => array(),\n-\t\t\t\t\t'title'  => array(),\n-\t\t\t\t\t'style'  => array(),\n-\t\t\t\t\t'hidden' => array(),\n-\t\t\t\t),\n-\t\t\t\t'dl'         => array(),\n-\t\t\t\t'dt'         => array(),\n-\t\t\t\t'em'         => array(),\n-\t\t\t\t'h1'         => array(),\n-\t\t\t\t'h2'         => array(),\n-\t\t\t\t'h3'         => array(),\n-\t\t\t\t'h4'         => array(),\n-\t\t\t\t'h5'         => array(),\n-\t\t\t\t'h6'         => array(),\n-\t\t\t\t'hr'         => array(),\n-\t\t\t\t'i'          => array(),\n-\t\t\t\t'textarea'   => array(\n-\t\t\t\t\t'id'          => array(),\n-\t\t\t\t\t'class'       => array(),\n-\t\t\t\t\t'name'        => array(),\n-\t\t\t\t\t'row'         => array(),\n-\t\t\t\t\t'style'       => array(),\n-\t\t\t\t\t'placeholder' => array(),\n-\t\t\t\t\t'readonly'    => array(),\n-\t\t\t\t),\n-\t\t\t\t'img'        => array(\n-\t\t\t\t\t'alt'    => array(),\n-\t\t\t\t\t'class'  => array(),\n-\t\t\t\t\t'height' => array(),\n-\t\t\t\t\t'style'  => array(),\n-\t\t\t\t\t'src'    => array(),\n-\t\t\t\t\t'width'  => array(),\n-\t\t\t\t\t'href'   => array(),\n-\t\t\t\t\t'hidden' => array(),\n-\t\t\t\t),\n-\t\t\t\t'link'       => array(\n-\t\t\t\t\t'rel'    => array(),\n-\t\t\t\t\t'type'   => array(),\n-\t\t\t\t\t'href'   => array(),\n-\t\t\t\t\t'hidden' => array(),\n-\t\t\t\t),\n-\t\t\t\t'li'         => array(\n-\t\t\t\t\t'class'  => array(),\n-\t\t\t\t\t'hidden' => array(),\n-\t\t\t\t),\n-\t\t\t\t'ol'         => array(\n-\t\t\t\t\t'class' => array(),\n-\t\t\t\t),\n-\t\t\t\t'p'          => array(\n-\t\t\t\t\t'class'  => array(),\n-\t\t\t\t\t'hidden' => array(),\n-\t\t\t\t\t'style'  => array(),\n-\t\t\t\t),\n-\t\t\t\t'q'          => array(\n-\t\t\t\t\t'cite'  => array(),\n-\t\t\t\t\t'title' => array(),\n-\t\t\t\t),\n-\t\t\t\t'span'       => array(\n-\t\t\t\t\t'id'     => array(),\n-\t\t\t\t\t'value'  => array(),\n-\t\t\t\t\t'class'  => array(),\n-\t\t\t\t\t'title'  => array(),\n-\t\t\t\t\t'style'  => array(),\n-\t\t\t\t\t'hidden' => array(),\n-\t\t\t\t),\n-\t\t\t\t'strike'     => array(),\n-\t\t\t\t'strong'     => array(),\n-\t\t\t\t'u'          => array(),\n-\t\t\t\t'ul'         => array(\n-\t\t\t\t\t'class' => array(),\n-\t\t\t\t\t'style' => array(),\n-\t\t\t\t),\n-\t\t\t\t'form'       => array(\n-\t\t\t\t\t'name'   => array(),\n-\t\t\t\t\t'method' => array(),\n-\t\t\t\t\t'id'     => array(),\n-\t\t\t\t\t'style'  => array(),\n-\t\t\t\t\t'hidden' => array(),\n-\t\t\t\t),\n-\t\t\t\t'table'      => array(\n-\t\t\t\t\t'class'       => array(),\n-\t\t\t\t\t'style'       => array(),\n-\t\t\t\t\t'cellpadding' => array(),\n-\t\t\t\t\t'cellspacing' => array(),\n-\t\t\t\t\t'border'      => array(),\n-\t\t\t\t\t'width'       => array(),\n-\t\t\t\t),\n-\t\t\t\t'tbody'      => array(),\n-\t\t\t\t'button'     => array(),\n-\t\t\t\t'tr'         => array(),\n-\t\t\t\t'td'         => array(\n-\t\t\t\t\t'class' => array(),\n-\t\t\t\t\t'style' => array(),\n-\t\t\t\t),\n-\t\t\t\t'input'      => array(\n-\t\t\t\t\t'type'          => array(),\n-\t\t\t\t\t'id'            => array(),\n-\t\t\t\t\t'name'          => array(),\n-\t\t\t\t\t'value'         => array(),\n-\t\t\t\t\t'class'         => array(),\n-\t\t\t\t\t'size '         => array(),\n-\t\t\t\t\t'tabindex'      => array(),\n-\t\t\t\t\t'hidden'        => array(),\n-\t\t\t\t\t'style'         => array(),\n-\t\t\t\t\t'placeholder'   => array(),\n-\t\t\t\t\t'disabled'      => array(),\n-\t\t\t\t\t'data-next'     => array(),\n-\t\t\t\t\t'data-previous' => array(),\n-\t\t\t\t\t'maxlength'     => array(),\n-\t\t\t\t),\n-\t\t\t\t'br'         => array(),\n-\t\t\t\t'title'      => array(\n-\t\t\t\t\t'title' => true,\n-\t\t\t\t),\n-\t\t\t);\n-\t\t\treturn $allowed_tags;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Allowing tags for popup templates\n-\t\t *\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tpublic static function mo_allow_popup_tags() {\n-\t\t\t$allowed_tags = array(\n-\t\t\t\t'head'           => array(),\n-\t\t\t\t'title'          => array(),\n-\t\t\t\t'meta'           => array(\n-\t\t\t\t\t'http-equiv' => array(),\n-\t\t\t\t\t'content'    => array(),\n-\t\t\t\t\t'name'       => array(),\n-\t\t\t\t),\n-\t\t\t\t'html'           => array(),\n-\t\t\t\t'body'           => array(),\n-\t\t\t\t'style'          => array(\n-\t\t\t\t\t'type' => array(),\n-\t\t\t\t),\n-\t\t\t\t'div'            => array(\n-\t\t\t\t\t'name'     => array(),\n-\t\t\t\t\t'dir'      => array(),\n-\t\t\t\t\t'id'       => array(),\n-\t\t\t\t\t'class'    => array(),\n-\t\t\t\t\t'title'    => array(),\n-\t\t\t\t\t'style'    => array(),\n-\t\t\t\t\t'tabindex' => array(),\n-\t\t\t\t\t'role'     => array(),\n-\t\t\t\t\t'hidden'   => array(),\n-\t\t\t\t),\n-\t\t\t\t'link'           => array(\n-\t\t\t\t\t'href'   => array(),\n-\t\t\t\t\t'target' => array(),\n-\t\t\t\t\t'rel'    => array(),\n-\t\t\t\t\t'type'   => array(),\n-\t\t\t\t\t'title'  => array(),\n-\t\t\t\t\t'hidden' => array(),\n-\t\t\t\t),\n-\t\t\t\t'input'          => array(\n-\t\t\t\t\t'type'     => array(),\n-\t\t\t\t\t'id'       => array(),\n-\t\t\t\t\t'name'     => array(),\n-\t\t\t\t\t'value'    => array(),\n-\t\t\t\t\t'class'    => array(),\n-\t\t\t\t\t'size '    => array(),\n-\t\t\t\t\t'tabindex' => array(),\n-\t\t\t\t\t'hidden'   => array(),\n-\t\t\t\t),\n-\t\t\t\t'button'         => array(\n-\t\t\t\t\t'class' => array(),\n-\t\t\t\t\t'id'    => array(),\n-\t\t\t\t\t'type'  => array(),\n-\t\t\t\t\t'name'  => array(),\n-\t\t\t\t\t'value' => array(),\n-\t\t\t\t),\n-\t\t\t\t'form'           => array(\n-\t\t\t\t\t'name'   => array(),\n-\t\t\t\t\t'method' => array(),\n-\t\t\t\t\t'action' => array(),\n-\t\t\t\t\t'id'     => array(),\n-\t\t\t\t\t'class'  => array(),\n-\t\t\t\t\t'hidden' => array(),\n-\t\t\t\t),\n-\t\t\t\t'br'             => array(),\n-\t\t\t\t'p'              => array(\n-\t\t\t\t\t'class' => true,\n-\t\t\t\t\t'style' => true,\n-\t\t\t\t\t'id'    => true,\n-\t\t\t\t),\n-\t\t\t\t'i'              => array(),\n-\t\t\t\t'u'              => array(),\n-\t\t\t\t'span'           => array(\n-\t\t\t\t\t'id'     => array(),\n-\t\t\t\t\t'value'  => array(),\n-\t\t\t\t\t'class'  => array(),\n-\t\t\t\t\t'title'  => array(),\n-\t\t\t\t\t'style'  => array(),\n-\t\t\t\t\t'hidden' => array(),\n-\t\t\t\t),\n-\t\t\t\t'a'              => array(\n-\t\t\t\t\t'href'    => true,\n-\t\t\t\t\t'target'  => true,\n-\t\t\t\t\t'rel'     => true,\n-\t\t\t\t\t'title'   => true,\n-\t\t\t\t\t'hidden'  => true,\n-\t\t\t\t\t'class'   => true,\n-\t\t\t\t\t'onclick' => true,\n-\t\t\t\t),\n-\t\t\t\t'svg'            => array(\n-\t\t\t\t\t'class'   => array(),\n-\t\t\t\t\t'id'      => array(),\n-\t\t\t\t\t'width'   => array(),\n-\t\t\t\t\t'height'  => array(),\n-\t\t\t\t\t'viewBox' => array(),\n-\t\t\t\t\t'viewbox' => array(),\n-\t\t\t\t\t'fill'    => array(),\n-\t\t\t\t),\n-\t\t\t\t'circle'         => array(\n-\t\t\t\t\t'id' => array(),\n-\t\t\t\t\t'cx' => array(),\n-\t\t\t\t\t'cy' => array(),\n-\t\t\t\t\t'cz' => array(),\n-\t\t\t\t\t'r'  => array(),\n-\t\t\t\t),\n-\t\t\t\t'g'              => array(\n-\t\t\t\t\t'fill' => array(),\n-\t\t\t\t\t'id'   => array(),\n-\t\t\t\t),\n-\t\t\t\t'path'           => array(\n-\t\t\t\t\t'd'               => array(),\n-\t\t\t\t\t'fill'            => array(),\n-\t\t\t\t\t'stroke'          => array(),\n-\t\t\t\t\t'stroke-width'    => array(),\n-\t\t\t\t\t'stroke-linecap'  => array(),\n-\t\t\t\t\t'stroke-linejoin' => array(),\n-\t\t\t\t),\n-\t\t\t\t'rect'           => array(\n-\t\t\t\t\t'x'               => array(),\n-\t\t\t\t\t'y'               => array(),\n-\t\t\t\t\t'width'           => array(),\n-\t\t\t\t\t'height'          => array(),\n-\t\t\t\t\t'rx'              => array(),\n-\t\t\t\t\t'fill'            => array(),\n-\t\t\t\t\t'stroke'          => array(),\n-\t\t\t\t\t'stroke-width'    => array(),\n-\t\t\t\t\t'stroke-linejoin' => array(),\n-\t\t\t\t),\n-\t\t\t\t'defs'           => array(),\n-\t\t\t\t'lineargradient' => array(\n-\t\t\t\t\t'id'            => array(),\n-\t\t\t\t\t'x1'            => array(),\n-\t\t\t\t\t'x2'            => array(),\n-\t\t\t\t\t'y1'            => array(),\n-\t\t\t\t\t'y2'            => array(),\n-\t\t\t\t\t'gradientunits' => array(),\n-\t\t\t\t),\n-\t\t\t);\n-\t\t\treturn $allowed_tags;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * KSES allowlist for rendered OTP popup HTML: popup chrome (forms, svg) plus post-like tags\n-\t\t * in {{MESSAGE}} so links and paragraphs (e.g. admin password hint) survive both inner and outer wp_kses passes.\n-\t\t *\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tpublic static function mo_popup_html_kses_allowed() {\n-\t\t\t$popup = self::mo_allow_popup_tags();\n-\t\t\tif ( ! function_exists( 'wp_kses_allowed_html' ) ) {\n-\t\t\t\treturn $popup;\n-\t\t\t}\n-\t\t\t$post = wp_kses_allowed_html( 'post' );\n-\t\t\tforeach ( $post as $tag => $post_attrs ) {\n-\t\t\t\tif ( ! isset( $popup[ $tag ] ) ) {\n-\t\t\t\t\t$popup[ $tag ] = $post_attrs;\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\t$popup[ $tag ] = array_merge( (array) $post_attrs, (array) $popup[ $tag ] );\n-\t\t\t}\n-\t\t\treturn $popup;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * MoInternal Function\n-\t\t *\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tpublic static function mo_allow_svg_array() {\n-\t\t\t$allowed_tags = array(\n-\t\t\t\t'svg'            => array(\n-\t\t\t\t\t'class'   => true,\n-\t\t\t\t\t'width'   => true,\n-\t\t\t\t\t'height'  => true,\n-\t\t\t\t\t'viewbox' => true,\n-\t\t\t\t\t'fill'    => true,\n-\t\t\t\t),\n-\t\t\t\t'circle'         => array(\n-\t\t\t\t\t'id'           => true,\n-\t\t\t\t\t'cx'           => true,\n-\t\t\t\t\t'cy'           => true,\n-\t\t\t\t\t'cz'           => true,\n-\t\t\t\t\t'r'            => true,\n-\t\t\t\t\t'stroke'       => true,\n-\t\t\t\t\t'stroke-width' => true,\n-\t\t\t\t),\n-\t\t\t\t'g'              => array(\n-\t\t\t\t\t'fill' => true,\n-\t\t\t\t\t'id'   => true,\n-\t\t\t\t),\n-\t\t\t\t'path'           => array(\n-\t\t\t\t\t'd'              => true,\n-\t\t\t\t\t'fill'           => true,\n-\t\t\t\t\t'id'             => true,\n-\t\t\t\t\t'fill-rule'      => true,\n-\t\t\t\t\t'clip-rule'      => true,\n-\t\t\t\t\t'stroke'         => true,\n-\t\t\t\t\t'stroke-width'   => true,\n-\t\t\t\t\t'stroke-linecap' => true,\n-\t\t\t\t),\n-\t\t\t\t'rect'           => array(\n-\t\t\t\t\t'width'  => true,\n-\t\t\t\t\t'height' => true,\n-\t\t\t\t\t'rx'     => true,\n-\t\t\t\t\t'fill'   => true,\n-\t\t\t\t),\n-\t\t\t\t'defs'           => array(),\n-\t\t\t\t'lineargradient' => array(\n-\t\t\t\t\t'id'            => true,\n-\t\t\t\t\t'x1'            => true,\n-\t\t\t\t\t'x2'            => true,\n-\t\t\t\t\t'y1'            => true,\n-\t\t\t\t\t'y2'            => true,\n-\t\t\t\t\t'gradientunits' => true,\n-\t\t\t\t),\n-\t\t\t\t'stop'           => array(\n-\t\t\t\t\t'stop-color' => true,\n-\t\t\t\t\t'offset'     => true,\n-\t\t\t\t),\n-\t\t\t);\n-\t\t\treturn $allowed_tags;\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Masking the Phone Number of User\n-\t\t *\n-\t\t * @param string $phone   Phone Number of the user.\n-\t\t *\u002F\n-\t\tpublic static function mo_mask_phone_number( $phone ) {\n-\t\t\t$length      = strlen( $phone );\n-\t\t\t$masked_part = str_repeat( '*', max( 0, $length - 3 ) );\n-\t\t\t$last_three  = substr( $phone, -3 );\n-\t\t\treturn $masked_part . $last_three;\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Masking the Email of User\n-\t\t *\n-\t\t * @param string $email    email of the user.\n-\t\t *\u002F\n-\t\tpublic static function mo_mask_email( $email ) {\n-\t\t\t$parts = explode( '@', $email );\n-\t\t\tif ( count( $parts ) !== 2 ) {\n-\t\t\t\treturn $email;\n-\t\t\t}\n-\t\t\t$username     = $parts[0];\n-\t\t\t$domain       = $parts[1];\n-\t\t\t$visible_part = substr( $username, 0, 2 );\n-\t\t\t$masked_part  = str_repeat( '*', max( 0, strlen( $username ) - 2 ) );\n-\t\t\treturn $visible_part . $masked_part . '@' . $domain;\n-\t\t}\n-\n-\t\t\u002F** Process the phone number and get_hidden_phone.\n-\t\t *\n-\t\t * @param string $phone - the phone number to processed.\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tpublic static function get_hidden_phone( $phone ) {\n-\t\t\treturn 'xxxxxxx' . substr( $phone, strlen( $phone ) - 3 );\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Process the value being passed and checks if it is empty or null\n-\t\t *\n-\t\t * @param string $value - the value to be checked.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function is_blank( $value ) {\n-\t\t\treturn ! isset( $value ) || empty( $value );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Process the plugin name is being passed and checks if it plugin is active or not\n-\t\t *\n-\t\t * @param string $plugin - the plugin name to be checked.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function is_plugin_installed( $plugin ) {\n-\t\t\tif ( ! function_exists( 'is_plugin_active' ) ) {\n-\t\t\t\tinclude_once ABSPATH . 'wp-admin\u002Fincludes\u002Fplugin.php';\n-\t\t\t}\n-\t\t\treturn is_plugin_active( $plugin );\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Creates and returns the JSON response.\n-\t\t *\n-\t\t * @param string $message - the message.\n-\t\t * @param string $type - the type of result ( success or error ).\n-\t\t * @return array\n-\t\t *\u002F\n-\t\tpublic static function create_json( $message, $type ) {\n-\t\t\treturn array(\n-\t\t\t\t'message' => $message,\n-\t\t\t\t'result'  => $type,\n-\t\t\t);\n-\t\t}\n-\t\t\u002F**\n-\t\t * Check for Country Restriction Addon\n-\t\t *\n-\t\t * @param mixed $phone .\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function check_for_selected_country_addon( $phone ) {\n-\t\t\t$countriesavail = CountryList::get_countrycode_list();\n-\t\t\t$countriesavail = apply_filters( 'selected_countries', $countriesavail );\n-\n-\t\t\tforeach ( $countriesavail as $key => $value ) {\n-\t\t\t\tif ( 'All Countries' !== $value['name'] ) {\n-\t\t\t\t\tif ( strpos( $phone, $value['countryCode'] ) !== false ) {\n-\t\t\t\t\t\treturn false;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn true;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * This function checks if cURL is installed on the server.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function mo_is_curl_installed() {\n-\t\t\treturn in_array( 'curl', get_loaded_extensions(), true );\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * The function returns the current page URL.\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tpublic static function current_page_url() {\n-\t\t\t$page_url = 'http';\n-\n-\t\t\tif ( ( isset( $_SERVER['HTTPS'] ) ) && ( sanitize_text_field( wp_unslash( $_SERVER['HTTPS'] ) ) === 'on' ) ) {\n-\t\t\t\t$page_url .= 's';\n-\t\t\t}\n-\n-\t\t\t$page_url .= ':\u002F\u002F';\n-\n-\t\t\t$server_port = isset( $_SERVER['SERVER_PORT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_PORT'] ) ) : '';\n-\t\t\t$server_uri  = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';\n-\t\t\t$server_name = isset( $_SERVER['SERVER_NAME'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ) : '';\n-\n-\t\t\tif ( '80' !== $server_port ) {\n-\t\t\t\t$page_url .= $server_name . ':' . $server_port . $server_uri;\n-\n-\t\t\t} else {\n-\t\t\t\t$page_url .= $server_name . $server_uri;\n-\t\t\t}\n-\n-\t\t\tif ( function_exists( 'apply_filters' ) ) {\n-\t\t\t\t$page_url = apply_filters( 'mo_curl_page_url', $page_url );\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Validate and escape the URL before returning.\n-\t\t\t$validated_url = filter_var( $page_url, FILTER_VALIDATE_URL );\n-\t\t\treturn $validated_url ? esc_url_raw( $validated_url ) : esc_url_raw( $page_url );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Validates a file path against a base directory to prevent LFI\u002FRFI attacks.\n-\t\t * This function does not include\u002Frequire the file. It only returns whether\n-\t\t * the given file path is valid and readable within the allowed base directory.\n-\t\t *\n-\t\t * @param string $file_path The file path to validate.\n-\t\t * @param string $base_dir  The base directory to restrict file access to.\n-\t\t * @return bool  True if the file exists, is readable, and is within base dir; otherwise false.\n-\t\t *\u002F\n-\t\tpublic static function mo_require_file( $file_path, $base_dir ) {\n-\t\t\tif ( empty( $file_path ) || empty( $base_dir ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$real_base_dir  = realpath( $base_dir );\n-\t\t\t$real_file_path = realpath( $file_path );\n-\n-\t\t\tif ( false === $real_file_path || false === $real_base_dir ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\tif ( ! is_dir( $real_base_dir ) || ! is_file( $real_file_path ) || ! is_readable( $real_file_path ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Normalize separators and ensure base dir boundary using trailing separator match.\n-\t\t\t$base_norm = rtrim( str_replace( '\\\\', '\u002F', $real_base_dir ), '\u002F' ) . '\u002F';\n-\t\t\t$file_norm = str_replace( '\\\\', '\u002F', $real_file_path );\n-\t\t\tif ( strncmp( $file_norm, $base_norm, strlen( $base_norm ) ) !== 0 ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\treturn true;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Checks if the current user has the required capabilities for admin access.\n-\t\t * This is a centralized function to ensure consistent capability checks across the plugin.\n-\t\t *\n-\t\t * @param array $capabilities Array of capabilities to check. User needs at least one. Default: ['manage_options'].\n-\t\t * @param bool  $require_admin Whether to also require is_admin() context. Default: false.\n-\t\t * @return bool True if user has required capabilities (and admin context if required), false otherwise.\n-\t\t *\u002F\n-\t\tpublic static function mo_check_admin_capability( $capabilities = array( 'manage_options' ), $require_admin = false ) {\n-\t\t\t\u002F\u002F Validate input.\n-\t\t\tif ( ! is_array( $capabilities ) || empty( $capabilities ) ) {\n-\t\t\t\t$capabilities = array( 'manage_options' );\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Check if required functions exist.\n-\t\t\tif ( ! function_exists( 'current_user_can' ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F If admin context is required, check it first.\n-\t\t\tif ( $require_admin ) {\n-\t\t\t\tif ( ! function_exists( 'is_admin' ) || ! is_admin() ) {\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Check if user has at least one of the required capabilities.\n-\t\t\tforeach ( $capabilities as $capability ) {\n-\t\t\t\tif ( is_string( $capability ) && current_user_can( $capability ) ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * The function retrieves the domain part of the email\n-\t\t *\n-\t\t * @param string $email - the email whose domain has to be validated.\n-\t\t *\n-\t\t * @return bool|string\n-\t\t *\u002F\n-\t\tpublic static function get_domain( $email ) {\n-\t\t\t$domain_name = substr( strrchr( $email, '@' ), 1 );\n-\t\t\treturn $domain_name;\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * This function validates the phone number format. Makes sure that country code\n-\t\t * is appended to the phone number. Return True or false.\n-\t\t *\n-\t\t * @param string $phone - the phone number to be validated.\n-\t\t *\n-\t\t * @return false|int\n-\t\t *\u002F\n-\t\tpublic static function validate_phone_number( $phone ) {\n-\t\t\t$phone = self::process_phone_number( $phone );\n-\n-\t\t\t\u002F\u002F Basic format validation using regex patterns.\n-\t\t\tif ( ! preg_match( MoConstants::PATTERN_PHONE, $phone ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Get country code from phone number.\n-\t\t\t$country_code = self::get_country_code( $phone );\n-\t\t\tif ( ! $country_code ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Extract national significant number (without country code).\n-\t\t\t$nsn        = substr( $phone, strlen( $country_code ) );\n-\t\t\t$nsn_length = strlen( $nsn );\n-\t\t\tif ( 0 === $nsn_length ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\t\t\t$first_digit = substr( $nsn, 0, 1 );\n-\n-\t\t\t\u002F\u002F Find the best matching country for this country code using prefixes; keep first match as fallback.\n-\t\t\t$country_list = CountryList::get_countrycode_list();\n-\t\t\t$country_data = null;\n-\t\t\t$fallback     = null;\n-\t\t\tforeach ( $country_list as $cand ) {\n-\t\t\t\tif ( ! isset( $cand['countryCode'] ) || $cand['countryCode'] !== $country_code ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\tif ( null === $fallback ) {\n-\t\t\t\t\t$fallback = $cand;\n-\t\t\t\t}\n-\t\t\t\tif ( isset( $cand['prefixes'] ) && is_array( $cand['prefixes'] ) && in_array( $first_digit, $cand['prefixes'], true ) ) {\n-\t\t\t\t\t$country_data = $cand;\n-\t\t\t\t\tbreak;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\tif ( ! $country_data && $fallback ) {\n-\t\t\t\t$country_data = $fallback;\n-\t\t\t}\n-\t\t\tif ( ! $country_data ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Validate length using min\u002Fmax from metadata if present; else default 7-15 digits.\n-\t\t\t$min_len = ( isset( $country_data['minLength'] ) && is_numeric( $country_data['minLength'] ) ) ? (int) $country_data['minLength'] : 7;\n-\t\t\t$max_len = ( isset( $country_data['maxLength'] ) && is_numeric( $country_data['maxLength'] ) ) ? (int) $country_data['maxLength'] : 15;\n-\t\t\tif ( $nsn_length \u003C $min_len || $nsn_length > $max_len ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t\u002F\u002F Validate allowed first-digit prefixes if provided.\n-\t\t\tif ( isset( $country_data['prefixes'] ) && is_array( $country_data['prefixes'] ) && ! empty( $country_data['prefixes'] ) ) {\n-\t\t\t\tif ( ! in_array( $first_digit, $country_data['prefixes'], true ) ) {\n-\t\t\t\t\treturn false;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn true;\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * This function validates the phone number format and checks if it has country code appended.\n-\t\t * Return True or false.\n-\t\t *\n-\t\t * @param string $phone - the phone number to be checked.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function is_country_code_appended( $phone ) {\n-\t\t\treturn preg_match( MoConstants::PATTERN_COUNTRY_CODE, $phone, $matches ) ? true : false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Process the phone number, return the country code appended to the phone number. If\n-\t\t * country code is not appended then return the default country code if set any by the\n-\t\t * admin.\n-\t\t *\n-\t\t * @param string $phone - the phone number to be processed.\n-\t\t *\n-\t\t * @return mixed\n-\t\t *\u002F\n-\t\tpublic static function get_country_code( $phone ) {\n-\t\t\tif ( ! $phone ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\t$phone                = preg_replace( MoConstants::PATTERN_SPACES_HYPEN, '', ltrim( trim( $phone ), '0' ) );\n-\t\t\t$default_country_code = CountryList::get_default_countrycode();\n-\t\t\t$country_list         = CountryList::get_countrycode_list();\n-\t\t\tif ( ! self::is_country_code_appended( $phone ) ) {\n-\t\t\t\treturn $default_country_code;\n-\t\t\t}\n-\t\t\tusort(\n-\t\t\t\t$country_list,\n-\t\t\t\tfunction ( $country_a, $country_b ) {\n-\t\t\t\t\treturn strlen( $country_b['countryCode'] ) - strlen( $country_a['countryCode'] );\n-\t\t\t\t}\n-\t\t\t);\n-\t\t\tforeach ( $country_list as $country_data ) {\n-\t\t\t\tif ( strpos( $phone, $country_data['countryCode'] ) === 0 ) {\n-\t\t\t\t\treturn $country_data['countryCode'];\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Process the phone number. Check if country code is appended to the phone number. If\n-\t\t * country code is not appended then add the default country code if set any by the\n-\t\t * admin.\n-\t\t *\n-\t\t * @param string $phone - the phone number to be processed.\n-\t\t *\n-\t\t * @return mixed\n-\t\t *\u002F\n-\t\tpublic static function process_phone_number( $phone ) {\n-\t\t\tif ( ! $phone ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\t$phone                = preg_replace( MoConstants::PATTERN_SPACES_HYPEN, '', ltrim( trim( $phone ), '0' ) );\n-\t\t\t$default_country_code = CountryList::get_default_countrycode();\n-\t\t\t$phone                = ! isset( $default_country_code ) || self::is_country_code_appended( $phone ) ? $phone : $default_country_code . $phone;\n-\t\t\treturn apply_filters( 'mo_process_phone', $phone );\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Checks if user has completed his registration in miniOrange.\n-\t\t *\n-\t\t * @return int\n-\t\t *\u002F\n-\t\tpublic static function micr() {\n-\t\t\t$email        = get_mo_option( 'admin_email' );\n-\t\t\t$customer_key = get_mo_option( 'admin_customer_key' );\n-\t\t\tif ( ! $email || ! $customer_key || ! is_numeric( trim( $customer_key ) ) ) {\n-\t\t\t\treturn 0;\n-\t\t\t} else {\n-\t\t\t\treturn 1;\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Checks the class for license library and returns bool by checking if the license is expired.\n-\t\t *\n-\t\t * @return bool|array\n-\t\t *\u002F\n-\t\tpublic static function mllc() {\n-\t\t\t$is_free_plugin = strcmp( MOV_TYPE, 'MiniOrangeGateway' ) === 0;\n-\t\t\treturn ( class_exists( MoConstants::LICENCE_LIBRARY, false ) || ( ! $is_free_plugin ) ) ? Mo_License_Service::is_license_expired() : array( 'STATUS' => false );\n-\t\t}\n-\t\t\u002F**\n-\t\t * Function generates a random alphanumeric value and returns it.\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tpublic static function rand() {\n-\t\t\t$length        = wp_rand( 0, 15 );\n-\t\t\t$characters    = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';\n-\t\t\t$random_string = '';\n-\t\t\tfor ( $i = 0; $i \u003C $length; $i++ ) {\n-\t\t\t\t$random_string .= $characters[ wp_rand( 0, strlen( $characters ) - 1 ) ];\n-\t\t\t}\n-\t\t\treturn $random_string;\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Checks if user has upgraded to one of the plans.\n-\t\t *\n-\t\t * @return int\n-\t\t *\u002F\n-\t\tpublic static function micv() {\n-\t\t\t$email        = get_mo_option( 'admin_email' );\n-\t\t\t$customer_key = get_mo_option( 'admin_customer_key' );\n-\t\t\t$check_ln     = get_mo_option( 'check_ln' );\n-\t\t\tif ( ! $email || ! $customer_key || ! is_numeric( trim( $customer_key ) ) ) {\n-\t\t\t\treturn 0;\n-\t\t\t} else {\n-\t\t\t\treturn $check_ln ? $check_ln : 0;\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * This function checks the license of the customer. Updates the license plan,\n-\t\t * sms and email remaining values in the database if user has upgraded.\n-\t\t *\n-\t\t * @param string $show_message - show message or not.\n-\t\t * @param string $customer_key - customerKey of the admin.\n-\t\t * @param string $api_key - apiKey of the admin.\n-\t\t * @return void\n-\t\t *\u002F\n-\t\tpublic static function handle_mo_check_ln( $show_message, $customer_key, $api_key ) {\n-\t\t\t$msg  = MoMessages::FREE_PLAN_MSG;\n-\t\t\t$plan = array();\n-\n-\t\t\t$gateway = GatewayFunctions::instance();\n-\t\t\t$content = json_decode( MocURLCall::check_customer_ln( $customer_key, $api_key, $gateway->get_application_name(), 'PREMIUM' ), true );\n-\t\t\tif ( isset( $content['status'] ) && strcasecmp( $content['status'], 'SUCCESS' ) === 0 ) {\n-\n-\t\t\t\t$email_remaining = isset( $content['emailRemaining'] ) ? $content['emailRemaining'] : 0;\n-\t\t\t\t$sms_remaining   = isset( $content['smsRemaining'] ) ? $content['smsRemaining'] : 0;\n-\t\t\t\t$license_plan    = isset( $content['licensePlan'] ) ? $content['licensePlan'] : '';\n-\n-\t\t\t\tif ( self::sanitize_check( 'licensePlan', $content ) ) {\n-\t\t\t\t\tif ( 0 === strcmp( MOV_TYPE, 'MiniOrangeGateway' ) || 0 === strcmp( MOV_TYPE, 'EnterpriseGatewayWithAddons' ) ) {\n-\t\t\t\t\t\t$msg  = MoMessages::REMAINING_TRANSACTION_MSG;\n-\t\t\t\t\t\t$plan = array(\n-\t\t\t\t\t\t\t'plan'  => $license_plan,\n-\t\t\t\t\t\t\t'sms'   => $sms_remaining,\n-\t\t\t\t\t\t\t'email' => $email_remaining,\n-\t\t\t\t\t\t);\n-\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\t$msg  = MoMessages::UPGRADE_MSG;\n-\t\t\t\t\t\t$plan = array( 'plan' => $license_plan );\n-\t\t\t\t\t}\n-\t\t\t\t\tupdate_mo_option( 'check_ln', $license_plan );\n-\t\t\t\t}\n-\t\t\t\tupdate_mo_option( 'customer_license_plan', $license_plan );\n-\t\t\t\tupdate_mo_option( 'email_transactions_remaining', $email_remaining );\n-\t\t\t\tupdate_mo_option( 'phone_transactions_remaining', $sms_remaining );\n-\t\t\t} else {\n-\t\t\t\t$content         = json_decode( MocURLCall::check_customer_ln( $customer_key, $api_key, 'wp_email_verification_intranet', 'PREMIUM' ), true );\n-\t\t\t\t$email_remaining = isset( $content['emailRemaining'] ) ? $content['emailRemaining'] : 0;\n-\t\t\t\t$sms_remaining   = isset( $content['smsRemaining'] ) ? $content['smsRemaining'] : 0;\n-\t\t\t\t$license_plan    = isset( $content['licensePlan'] ) ? $content['licensePlan'] : '';\n-\t\t\t\tupdate_mo_option( 'customer_license_plan', $license_plan );\n-\t\t\t\tupdate_mo_option( 'email_transactions_remaining', $email_remaining );\n-\t\t\t\tupdate_mo_option( 'phone_transactions_remaining', $sms_remaining );\n-\n-\t\t\t\tif ( self::sanitize_check( 'licensePlan', $content ) ) {\n-\t\t\t\t\t$msg = MoMessages::INSTALL_PREMIUM_PLUGIN;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( isset( $content['status'] ) && strcasecmp( $content['status'], 'FAILED' ) === 0 ) {\n-\t\t\t\t$content         = json_decode( MocURLCall::check_customer_ln( $customer_key, $api_key, '' ), true );\n-\t\t\t\t$email_remaining = isset( $content['emailRemaining'] ) ? $content['emailRemaining'] : 0;\n-\t\t\t\t$sms_remaining   = isset( $content['smsRemaining'] ) ? $content['smsRemaining'] : 0;\n-\t\t\t\t$license_plan    = isset( $content['licenseType'] ) ? $content['licenseType'] : '';\n-\t\t\t\tupdate_mo_option( 'customer_license_plan', $license_plan );\n-\t\t\t\tupdate_mo_option( 'email_transactions_remaining', $email_remaining );\n-\t\t\t\tupdate_mo_option( 'phone_transactions_remaining', $sms_remaining );\n-\t\t\t}\n-\t\t\tif ( isset( $content['licenseExpiry'] ) && strcasecmp( $content['status'], 'SUCCESS' ) === 0 ) {\n-\t\t\t\tif ( class_exists( MoConstants::LICENCE_LIBRARY, false ) ) {\n-\t\t\t\t\tMo_License_Service::update_license_expiry( $content['licenseExpiry'] );\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( $show_message ) {\n-\t\t\t\tdo_action( 'mo_registration_show_message', MoMessages::showMessage( $msg, $plan ), 'SUCCESS' );\n-\t\t\t}\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Initialize the form session indicating that the OTP Verification for the\n-\t\t * form has started.\n-\t\t *\n-\t\t * @param string $form - form for which session is being initialized \u002F session constant name.\n-\t\t *\u002F\n-\t\tpublic static function initialize_transaction( $form ) {\n-\t\t\tif ( empty( $form ) ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\n-\t\t\tMoPHPSessions::bootstrap();\n-\t\t\tMoPHPSessions::check_session();\n-\n-\t\t\t$reflect = new ReflectionClass( FormSessionVars::class );\n-\t\t\tforeach ( $reflect->getConstants() as $key => $value ) {\n-\t\t\t\t\u002F\u002F Don't unset the current form session variable as we're about to initialize it.\n-\t\t\t\tif ( $value !== $form ) {\n-\t\t\t\t\tMoPHPSessions::unset_session( $value );\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tSessionUtils::initialize_form( $form );\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Returns the invalid OTP message. This function checks if admin has set an\n-\t\t * invalid otp message in the settings. If so then that is returned instead of the default.\n-\t\t *\n-\t\t * @return string\n-\t\t *\u002F\n-\t\tpublic static function get_invalid_otp_method() {\n-\t\t\treturn get_mo_option( 'invalid_message', 'mo_otp_' ) ? get_mo_option( 'invalid_message', 'mo_otp_' )\n-\t\t\t: MoMessages::showMessage( MoMessages::INVALID_OTP );\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Returns TRUE or FALSE depending on if the POLYLANG plugin is active.\n-\t\t * This is used to check if the translation should use the polylang\n-\t\t * function or the default local translation.\n-\t\t *\n-\t\t * @return boolean\n-\t\t *\u002F\n-\t\tpublic static function is_polylang_installed() {\n-\t\t\treturn function_exists( 'pll__' ) && function_exists( 'pll_register_string' );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Take an array of string having the keyword to replace\n-\t\t * and the keyword to be replaced. This is used to modify\n-\t\t * the SMS templates that the user might have saved in the\n-\t\t * settings or the default ones by the plugin.\n-\t\t *\n-\t\t * @param array  $replace      The array containing search and replace keywords.\n-\t\t * @param string $input_string Entire string to be modified.\n-\t\t *\n-\t\t * @return mixed\n-\t\t *\u002F\n-\t\tpublic static function replace_string( array $replace, $input_string ) {\n-\t\t\tforeach ( $replace as $key => $value ) {\n-\t\t\t\t$input_string = str_replace( '{' . $key . '}', $value, $input_string );\n-\t\t\t}\n-\n-\t\t\treturn $input_string;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Returns a stdClass Object with status Success as a\n-\t\t * temporary result when TEST_MODE is on\n-\t\t *\n-\t\t * @return stdClass\n-\t\t *\u002F\n-\t\tprivate static function test_result() {\n-\t\t\t$temp         = new stdClass();\n-\t\t\t$temp->status = MO_FAIL_MODE ? 'ERROR' : 'SUCCESS';\n-\t\t\treturn $temp;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Checks if the whatsapp notifications and presonal business account is enabled\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function mo_is_whatsapp_notif_enabled() {\n-\t\t\treturn get_mo_option( 'mo_whatsapp_enable' )\n-\t\t\t&& get_mo_option( 'mo_whatsapp_notification_enable' )\n-\t\t\t&& get_mo_option( 'mo_whatsapp_type' ) === 'bussiness_whatsapp';\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Send the notification to the number provided and\n-\t\t * process the response to check if the message was sent\n-\t\t * successfully or not. Return TRUE or FALSE based on the\n-\t\t * API call response.\n-\t\t *\n-\t\t * @param string $number the number to be sent.\n-\t\t * @param string $msg the message to be sent.\n-\t\t * @param string $notification_type the specific type of notification (e.g., 'NEW_ACCOUNT', 'ORDER_STATUS').\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function send_phone_notif( $number, $msg, $notification_type = 'NOTIFICATION' ) {\n-\n-\t\t\t$api_call_result = function ( $number, $msg ) {\n-\t\t\t\treturn json_decode( MocURLCall::send_notif( new NotificationSettings( $number, $msg ) ) );\n-\t\t\t};\n-\n-\t\t\t$mle = self::mllc();\n-\t\t\tif ( $mle['STATUS'] ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\t\t\t$number       = self::process_phone_number( $number );\n-\t\t\t$msg          = self::replace_string( array( 'phone' => str_replace( '+', '', '%2B' . $number ) ), $msg );\n-\t\t\t$content      = MO_TEST_MODE ? self::test_result() : $api_call_result( $number, $msg );\n-\t\t\t$notif_status = strcasecmp( $content->status, 'SUCCESS' ) === 0 ? 'SMS_NOTIF_SENT' : 'SMS_NOTIF_FAILED';\n-\t\t\tapply_filters( 'mo_start_reporting', null, $number, $number, $notification_type . '_PHONE_NOTIF', $msg, $notif_status );\n-\t\t\treturn strcasecmp( $content->status, 'SUCCESS' ) === 0 ? true : false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Send the notification to the number provided and\n-\t\t * process the response to check if the message was sent\n-\t\t * successfully or not. Return TRUE or FALSE based on the\n-\t\t * API call response.\n-\t\t *\n-\t\t * @param string $number the number to be sent.\n-\t\t * @param string $template_name the template name.\n-\t\t * @param string $sms_tags the tags used in sms template.\n-\t\t * @param string $notification_type the specific type of notification (e.g., 'NEW_ACCOUNT', 'ORDER_STATUS').\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function mo_send_whatsapp_notif( $number, $template_name, $sms_tags, $notification_type = 'WHATSAPP_NOTIFICATION' ) {\n-\t\t\t$api_call_result = function ( $number, $data ) {\n-\t\t\t\treturn apply_filters( 'mo_wa_send_otp_token', 'WHATSAPP_NOTIFICATION', null, null, $number, $data );\n-\t\t\t};\n-\n-\t\t\t$data         = array(\n-\t\t\t\t'template_name' => $template_name,\n-\t\t\t\t'sms_tags'      => $sms_tags,\n-\t\t\t);\n-\t\t\t$number       = self::process_phone_number( $number );\n-\t\t\t$content      = MO_TEST_MODE ? self::test_result() : $api_call_result( $number, $data );\n-\t\t\t$notif_status = strcasecmp( $content->status, 'SUCCESS' ) === 0 ? 'SMS_NOTIF_SENT' : 'SMS_NOTIF_FAILED';\n-\t\t\tapply_filters( 'mo_start_reporting', null, $number, $number, $notification_type . '_WHATSAPP_NOTIF', $template_name, $notif_status );\n-\t\t\treturn strcasecmp( $content->status, 'SUCCESS' ) === 0 ? true : false;\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * Send the notification to the email provided and\n-\t\t * process the response to check if the message was sent\n-\t\t * successfully or not. Return TRUE or FALSE based on the\n-\t\t * API call response.\n-\t\t *\n-\t\t * @param string $from_email The From Email.\n-\t\t * @param string $from_name  The From Name.\n-\t\t * @param string $to_email   The email to send message to.\n-\t\t * @param string $subject   The subject of the email.\n-\t\t * @param string $message   The message to be sent.\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function send_email_notif( $from_email, $from_name, $to_email, $subject, $message ) {\n-\t\t\t$api_call_result = function ( $from_email, $from_name, $to_email, $subject, $message ) {\n-\t\t\t\t$notification_settings = new NotificationSettings();\n-\t\t\t\t$notification_settings->create_email_notification_settings( $from_email, $from_name, $to_email, $subject, $message );\n-\t\t\t\treturn json_decode( MocURLCall::send_notif( $notification_settings ) );\n-\t\t\t};\n-\t\t\t$content         = MO_TEST_MODE ? self::test_result() : $api_call_result( $from_email, $from_name, $to_email, $subject, $message );\n-\t\t\treturn strcasecmp( $content->status, 'SUCCESS' ) === 0 ? true : false;\n-\t\t}\n-\t\t\u002F**\n-\t\t * Check if there is an existing value in the array\u002Fbuffer and return the value\n-\t\t * that exists against that key otherwise return false.\n-\t\t * \u003Cp>\u003C\u002Fp>\n-\t\t * The function also makes sure to sanitize the values being fetched.\n-\t\t * \u003Cp>\u003C\u002Fp>\n-\t\t * If the buffer to fetch the value from is not an array then return buffer as it is.\n-\t\t *\n-\t\t * @param string       $key    the key to check against.\n-\t\t * @param   string|array $buffer the post\u002Fget or array.\n-\t\t * @return string|bool|array\n-\t\t *\u002F\n-\t\tpublic static function sanitize_check( $key, $buffer ) {\n-\t\t\tif ( ! isset( $buffer[ $key ] ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\t\t\tif ( is_array( $buffer[ $key ] ) ) {\n-\t\t\t\treturn self::mo_sanitize_array( $buffer[ $key ] );\n-\t\t\t} else {\n-\t\t\t\treturn sanitize_text_field( wp_unslash( $buffer[ $key ] ) );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Checks if user has upgraded to the on-prem plugin\n-\t\t *\u002F\n-\t\tpublic static function mclv() {\n-\t\t\t$gateway = GatewayFunctions::instance();\n-\t\t\treturn $gateway->mclv();\n-\t\t}\n-\n-\n-\t\t\u002F**Checks if the current plugin is Custom Gateway Plugin\n-\t\t *\u002F\n-\t\tpublic static function is_gateway_config() {\n-\t\t\t$gateway = GatewayFunctions::instance();\n-\t\t\treturn $gateway->is_gateway_config();\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Checks if the current plugin is MiniOrangeGateway Plugin\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function is_mg() {\n-\t\t\t$gateway = GatewayFunctions::instance();\n-\t\t\treturn $gateway->is_mg();\n-\t\t}\n-\n-\n-\t\t\u002F**\n-\t\t * This function checks if all conditions to save the form settings\n-\t\t * are true. This checks if the user saving the form settings is an admin,\n-\t\t * has registered with miniorange and the the form post has an option value\n-\t\t * mo_customer_validation_settings\n-\t\t *\n-\t\t * @param string $key_val the key to check against (typically 'mo_customer_validation_settings').\n-\t\t * @param string $form_option_key Optional POST key that must be present to proceed (form-specific).\n-\t\t *\n-\t\t * @return bool\n-\t\t *\u002F\n-\t\tpublic static function are_form_options_being_saved( $key_val, $form_option_key = '' ) {\n-\t\t\tif ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'mo_admin_actions' ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\t\t\tif ( ! self::mclv() ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\n-\t\t\t$option_value = isset( $_POST['option'] ) ? sanitize_text_field( wp_unslash( $_POST['option'] ) ) : '';\n-\t\t\tif ( empty( $option_value ) || $option_value !== $key_val ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\t\t\tif ( ! empty( $form_option_key ) ) {\n-\t\t\t\t$prefixed_key    = 'mo_customer_validation_' . $form_option_key;\n-\t\t\t\t$form_key_exists = isset( $_POST[ $prefixed_key ] ) || isset( $_POST[ $form_option_key ] );\n-\t\t\t\tif ( $form_key_exists ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t\t\u002F\u002F Form checkbox doesn't exist - form might be disabled, but we're in form settings save context.\n-\t\t\t\t$form_base     = str_replace( '_enable', '', $form_option_key );\n-\t\t\t\t$parts         = explode( '_', $form_base );\n-\t\t\t\t$form_id       = $parts[0];\n-\t\t\t\t$form_data_key = str_replace( '_enable', '_form', $form_option_key );\n-\t\t\t\tif ( isset( $_POST[ $form_data_key ] ) ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t\tforeach ( $_POST as $key => $value ) {\n-\t\t\t\t\t$key_to_check = $key;\n-\t\t\t\t\tif ( strpos( $key, 'mo_customer_validation_' ) === 0 ) {\n-\t\t\t\t\t\t$key_to_check = str_replace( 'mo_customer_validation_', '', $key );\n-\t\t\t\t\t} elseif ( strpos( $key, 'mo_' ) === 0 ) {\n-\t\t\t\t\t\t$key_to_check = str_replace( 'mo_', '', $key );\n-\t\t\t\t\t}\n-\t\t\t\t\tif ( strpos( $key_to_check, $form_id . '_' ) === 0 || strpos( $key_to_check, $form_base ) === 0 ) {\n-\t\t\t\t\t\treturn true;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t\t\u002F\u002F Return false to prevent unnecessary processing.\n-\t\t\t\treturn false;\n-\t\t\t}\n-\t\t\treturn true;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Update SMS Email transaction in DataBase\n-\t\t *\n-\t\t * @param string $response Response form Gateway.\n-\t\t * @param string $type     OTP Type email or phone.\n-\t\t *\u002F\n-\t\tpublic static function mo_update_sms_email_transations( $response, $type ) {\n-\t\t\t$content = json_decode( $response );\n-\t\t\tif ( strcasecmp( $content->status, 'SUCCESS' ) === 0 ) {\n-\t\t\t\t$option_type   = ( VerificationType::PHONE === $type ) ? 'phone_transactions_remaining' : 'email_transactions_remaining';\n-\t\t\t\t$remaining_txn = get_mo_option( $option_type );\n-\t\t\t\tif ( $remaining_txn > 0 ) {\n-\t\t\t\t\tupdate_mo_option( $option_type, $remaining_txn - 1 );\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Update WhatsApp transaction in DataBase\n-\t\t *\n-\t\t * @param string $response      Resposne form Gateway.\n-\t\t *\u002F\n-\t\tpublic static function mo_update_whatsapp_transations( $response ) {\n-\t\t\t$content = json_decode( $response );\n-\t\t\tif ( isset( $content->status ) && strcasecmp( $content->status, 'SUCCESS' ) === 0 ) {\n-\t\t\t\t$remaining_txn = get_mo_option( 'whatsapp_transactions_remaining', 'mowp_customer_validation_' );\n-\t\t\t\tif ( $remaining_txn > 0 ) {\n-\t\t\t\t\tupdate_mo_option( 'whatsapp_transactions_remaining', $remaining_txn - 1, 'mowp_customer_validation_' );\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Checks if the customer is registered or not and shows a message on the page\n-\t\t * to the user so that they can register or login themselves to use the plugin.\n-\t\t *\u002F\n-\t\tpublic static function is_addon_activated() {\n-\t\t\tif ( self::micr() && self::mclv() ) {\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\t$tab_details      = TabDetails::instance();\n-\t\t\t$server_uri       = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';\n-\t\t\t$registration_url = add_query_arg(\n-\t\t\t\tarray( 'page' => $tab_details->tab_details[ Tabs::ACCOUNT ]->menu_slug ),\n-\t\t\t\tremove_query_arg( 'addon', $server_uri )\n-\t\t\t);\n-\t\t\techo '\u003Cdiv style=\"display:block;margin-top:10px;color:red;background-color:rgba(251, 232, 0, 0.15);\n-\t\t\t\t\t\t\t\tpadding:5px;border:solid 1px rgba(255, 0, 9, 0.36);\">\n-\t\t\t \t\t\u003Ca href=\"' . esc_url( $registration_url ) . '\">' . esc_html( __( 'Validate your purchase', 'miniorange-otp-verification' ) ) . '\u003C\u002Fa>\n-\t\t\t \t\t\t\t' . esc_html( __( ' to enable the Add On', 'miniorange-otp-verification' ) ) . '\u003C\u002Fdiv>';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check if the phone number is empty and return error.\n-\t\t *\n-\t\t * @param string $phone_number phone number of the user.\n-\t\t *\u002F\n-\t\tpublic static function check_if_phone_exist( $phone_number ) {\n-\t\t\tif ( empty( $phone_number ) ) {\n-\t\t\t\twp_send_json( self::create_json( MoMessages::showMessage( MoMessages::PHONE_NOT_FOUND ), MoConstants::ERROR_JSON_TYPE ) );\n-\t\t\t}\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Checks the version of the plugin active with the mentioned name.\n-\t\t *\n-\t\t * @param string  $plugin_name     -   Plugin Name.\n-\t\t * @param integer $sequence       -   index of the version digit to get.\n-\t\t * @return integer  Version number.\n-\t\t *\u002F\n-\t\tpublic static function get_active_plugin_version( $plugin_name, $sequence = 0 ) {\n-\t\t\tif ( ! function_exists( 'get_plugins' ) ) {\n-\t\t\t\trequire_once ABSPATH . 'wp-admin\u002Fincludes\u002Fplugin.php';\n-\t\t\t}\n-\t\t\t$all_plugins   = get_plugins();\n-\t\t\t$active_plugin = get_mo_option( 'active_plugins', '' );\n-\t\t\tif ( ! is_array( $active_plugin ) ) {\n-\t\t\t\t$active_plugin = array();\n-\t\t\t}\n-\t\t\tforeach ( $all_plugins as $key => $value ) {\n-\t\t\t\tif ( isset( $value['Name'] ) && strcasecmp( $value['Name'], $plugin_name ) === 0 ) {\n-\t\t\t\t\tif ( in_array( $key, $active_plugin, true ) ) {\n-\t\t\t\t\t\t\u002F\u002F Make sure the version is set and has the requested sequence.\n-\t\t\t\t\t\tif ( isset( $value['Version'] ) && isset( $value['Version'][ $sequence ] ) ) {\n-\t\t\t\t\t\t\treturn (int) $value['Version'][ $sequence ];\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn null;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Encrypts a plaintext password using AES-256-CBC encryption.\n-\t\t *\n-\t\t * @param string $plaintext_password The plain text password to encrypt.\n-\t\t * @return string Hex-encoded encrypted string.\n-\t\t *\u002F\n-\t\tpublic static function encrypt_password( $plaintext_password ) {\n-\t\t\tif ( empty( $plaintext_password ) ) {\n-\t\t\t\treturn '';\n-\t\t\t}\n-\t\t\t$encryption_key = hash( 'sha256', wp_salt( 'auth' ), true );\n-\t\t\t$iv             = substr( hash( 'sha256', 'otp-plugin-password-iv' ), 0, 16 );\n-\t\t\t$encrypted      = openssl_encrypt(\n-\t\t\t\t$plaintext_password,\n-\t\t\t\t'AES-256-CBC',\n-\t\t\t\t$encryption_key,\n-\t\t\t\t0,\n-\t\t\t\t$iv\n-\t\t\t);\n-\t\t\tif ( false === $encrypted ) {\n-\t\t\t\treturn '';\n-\t\t\t}\n-\t\t\treturn bin2hex( $encrypted );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Decrypts an AES-256-CBC encrypted password back to plain text.\n-\t\t *\n-\t\t * @param string $encrypted_password The hex-encoded encrypted password.\n-\t\t * @return string|false Decrypted plain text password, or false on failure.\n-\t\t *\u002F\n-\t\tpublic static function decrypt_password( $encrypted_password ) {\n-\t\t\tif ( empty( $encrypted_password ) ) {\n-\t\t\t\treturn '';\n-\t\t\t}\n-\t\t\t$encryption_key = hash( 'sha256', wp_salt( 'auth' ), true );\n-\t\t\t$iv             = substr( hash( 'sha256', 'otp-plugin-password-iv' ), 0, 16 );\n-\t\t\t$encrypted_data = hex2bin( $encrypted_password );\n-\t\t\tif ( false === $encrypted_data ) {\n-\t\t\t\treturn '';\n-\t\t\t}\n-\t\t\t$decrypted = openssl_decrypt(\n-\t\t\t\t$encrypted_data,\n-\t\t\t\t'AES-256-CBC',\n-\t\t\t\t$encryption_key,\n-\t\t\t\t0,\n-\t\t\t\t$iv\n-\t\t\t);\n-\t\t\treturn false === $decrypted ? '' : $decrypted;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get the current user's IP address.\n-\t\t *\n-\t\t * @return string - IP address\n-\t\t *\u002F\n-\t\tpublic static function get_current_ip_address() {\n-\t\t\t$ip_sources = array(\n-\t\t\t\t'REMOTE_ADDR'           => array(\n-\t\t\t\t\t'trust' => true,\n-\t\t\t\t\t'risk'  => 'low',\n-\t\t\t\t),\n-\t\t\t\t'HTTP_X_FORWARDED_FOR'  => array(\n-\t\t\t\t\t'trust' => false,\n-\t\t\t\t\t'risk'  => 'medium',\n-\t\t\t\t),\n-\t\t\t\t'HTTP_X_REAL_IP'        => array(\n-\t\t\t\t\t'trust' => false,\n-\t\t\t\t\t'risk'  => 'low',\n-\t\t\t\t),\n-\t\t\t\t'HTTP_CF_CONNECTING_IP' => array(\n-\t\t\t\t\t'trust' => false,\n-\t\t\t\t\t'risk'  => 'low',\n-\t\t\t\t),\n-\t\t\t\t'HTTP_CLIENT_IP'        => array(\n-\t\t\t\t\t'trust' => false,\n-\t\t\t\t\t'risk'  => 'high',\n-\t\t\t\t),\n-\t\t\t);\n-\n-\t\t\t$found_ips      = array();\n-\t\t\t$suspicious_ips = array();\n-\n-\t\t\tforeach ( $ip_sources as $source => $metadata ) {\n-\t\t\t\tif ( empty( $_SERVER[ $source ] ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\t$raw_value = sanitize_text_field( wp_unslash( $_SERVER[ $source ] ) );\n-\t\t\t\t$ip        = self::extract_first_valid_ip( $raw_value );\n-\n-\t\t\t\tif ( $ip && self::is_valid_ip( $ip ) ) {\n-\t\t\t\t\t$found_ips[] = array(\n-\t\t\t\t\t\t'ip'    => $ip,\n-\t\t\t\t\t\t'trust' => $metadata['trust'],\n-\t\t\t\t\t\t'risk'  => $metadata['risk'],\n-\t\t\t\t\t);\n-\t\t\t\t} else {\n-\t\t\t\t\t$suspicious_ips[] = array(\n-\t\t\t\t\t\t'ip'        => $ip ? $ip : 'invalid_format',\n-\t\t\t\t\t\t'source'    => $source,\n-\t\t\t\t\t\t'raw_value' => $raw_value,\n-\t\t\t\t\t\t'reason'    => $ip ? 'contains_attack_patterns' : 'invalid_ip_format',\n-\t\t\t\t\t);\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif ( ! empty( $found_ips ) ) {\n-\t\t\t\tusort(\n-\t\t\t\t\t$found_ips,\n-\t\t\t\t\tfunction ( $a, $b ) {\n-\t\t\t\t\t\tif ( $a['trust'] === $b['trust'] ) {\n-\t\t\t\t\t\t\t$risk_order = array(\n-\t\t\t\t\t\t\t\t'low'    => 0,\n-\t\t\t\t\t\t\t\t'medium' => 1,\n-\t\t\t\t\t\t\t\t'high'   => 2,\n-\t\t\t\t\t\t\t);\n-\t\t\t\t\t\t\treturn $risk_order[ $a['risk'] ] - $risk_order[ $b['risk'] ];\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\treturn $b['trust'] - $a['trust'];\n-\t\t\t\t\t}\n-\t\t\t\t);\n-\n-\t\t\t\t$best_ip      = $found_ips[0];\n-\t\t\t\t$risk_markers = array(\n-\t\t\t\t\t'high'   => ' (high_risk_proxy)',\n-\t\t\t\t\t'medium' => ' (medium_risk_proxy)',\n-\t\t\t\t);\n-\n-\t\t\t\treturn $best_ip['ip'] . ( $risk_markers[ $best_ip['risk'] ] ?? '' );\n-\t\t\t}\n-\n-\t\t\tif ( isset( $_SERVER['REMOTE_ADDR'] ) ) {\n-\t\t\t\t$fallback_ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );\n-\t\t\t\treturn self::is_valid_ip( $fallback_ip ) ? $fallback_ip : $fallback_ip . ' (suspicious_format)';\n-\t\t\t}\n-\n-\t\t\tif ( ! empty( $suspicious_ips ) ) {\n-\t\t\t\tself::log_suspicious_ip_activity( $suspicious_ips );\n-\t\t\t\t$first_suspicious = $suspicious_ips[0];\n-\t\t\t\treturn 'invalid_format' !== $first_suspicious['ip'] ? $first_suspicious['ip'] : 'Unknown';\n-\t\t\t}\n-\n-\t\t\treturn 'Unknown';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get the first matching page ID by its title using a non-deprecated approach.\n-\t\t *\n-\t\t * @param string $page_title  Page title to search for.\n-\t\t * @param string $post_status Page status to include (default 'all').\n-\t\t * @return int|string Page ID if found; empty string if not found or title blank.\n-\t\t *\u002F\n-\t\tpublic static function mo_get_page_id_by_title( $page_title, $post_status = 'all' ) {\n-\t\t\tif ( self::is_blank( $page_title ) ) {\n-\t\t\t\treturn '';\n-\t\t\t}\n-\n-\t\t\t$pages = get_posts(\n-\t\t\t\tarray(\n-\t\t\t\t\t'post_type'        => 'page',\n-\t\t\t\t\t'title'            => $page_title,\n-\t\t\t\t\t'post_status'      => $post_status,\n-\t\t\t\t\t'numberposts'      => 1,\n-\t\t\t\t\t'suppress_filters' => false,\n-\t\t\t\t)\n-\t\t\t);\n-\n-\t\t\treturn ( ! empty( $pages ) && isset( $pages[0]->ID ) ) ? (int) $pages[0]->ID : '';\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get the permalink for the first matching page title, or a default if not found.\n-\t\t *\n-\t\t * @param string $page_title Page title to search for.\n-\t\t * @param string $default_url    Default URL to return if not found (default home_url('\u002F')).\n-\t\t * @param string $post_status Page status to include (default 'all').\n-\t\t * @return string Resolved permalink or default.\n-\t\t *\u002F\n-\t\tpublic static function mo_get_permalink_by_page_title( $page_title, $default_url = '', $post_status = 'all' ) {\n-\t\t\t$default      = $default_url ? $default_url : home_url( '\u002F' );\n-\t\t\t$page_id      = self::mo_get_page_id_by_title( $page_title, $post_status );\n-\t\t\t$redirect_url = $page_id ? get_permalink( $page_id ) : $default;\n-\t\t\treturn esc_url_raw( $redirect_url );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check for suspicious IP patterns (disabled for now).\n-\t\t *\n-\t\t * @param string $ip - IP address to validate.\n-\t\t * @return bool - True if valid and safe\n-\t\t *\u002F\n-\t\tprivate static function is_valid_ip( $ip ) {\n-\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\t\t\tif ( self::contains_attack_patterns( $ip ) ) {\n-\t\t\t\treturn false;\n-\t\t\t}\n-\t\t\treturn true;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Check for attack patterns in IP string.\n-\t\t *\n-\t\t * @param string $ip - IP address to check.\n-\t\t * @return bool - True if contains attack patterns\n-\t\t *\u002F\n-\t\tprivate static function contains_attack_patterns( $ip ) {\n-\t\t\t$suspicious_chars = array( '\u003C', '>', '\"', \"'\", '\\\\', '\u002F', '&', ';', '(', ')' );\n-\t\t\tforeach ( $suspicious_chars as $char ) {\n-\t\t\t\tif ( strpos( $ip, $char ) !== false ) {\n-\t\t\t\t\treturn true;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get relevant server variables for security logging.\n-\t\t *\n-\t\t * @return array - Sanitized server variables\n-\t\t *\u002F\n-\t\tprivate static function get_relevant_server_vars() {\n-\t\t\t$relevant_vars = array(\n-\t\t\t\t'REMOTE_ADDR',\n-\t\t\t\t'HTTP_X_FORWARDED_FOR',\n-\t\t\t\t'HTTP_X_REAL_IP',\n-\t\t\t\t'HTTP_CF_CONNECTING_IP',\n-\t\t\t\t'HTTP_CLIENT_IP',\n-\t\t\t\t'HTTP_USER_AGENT',\n-\t\t\t\t'REQUEST_URI',\n-\t\t\t\t'REQUEST_METHOD',\n-\t\t\t\t'HTTP_REFERER',\n-\t\t\t);\n-\n-\t\t\t$server_data = array();\n-\t\t\tforeach ( $relevant_vars as $var ) {\n-\t\t\t\tif ( isset( $_SERVER[ $var ] ) ) {\n-\t\t\t\t\t$server_data[ $var ] = sanitize_text_field( wp_unslash( $_SERVER[ $var ] ) );\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\treturn $server_data;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Validate IP address with basic security checks.\n-\t\t *\n-\t\t * @param string $ip_string - Comma-separated IP addresses.\n-\t\t * @return string|false - First valid IP or false\n-\t\t *\u002F\n-\t\tprivate static function extract_first_valid_ip( $ip_string ) {\n-\t\t\t$ips = explode( ',', $ip_string );\n-\t\t\tforeach ( $ips as $ip ) {\n-\t\t\t\t$ip = trim( $ip );\n-\n-\t\t\t\tif ( empty( $ip ) || ! is_string( $ip ) ) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\tif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 ) ) {\n-\t\t\t\t\treturn $ip;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\treturn false;\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Log suspicious IP activity for security analysis.\n-\t\t *\n-\t\t * @param array $suspicious_ips - Array of suspicious IP data.\n-\t\t *\u002F\n-\t\tprivate static function log_suspicious_ip_activity( $suspicious_ips ) {\n-\t\t\t$log_data = array(\n-\t\t\t\t'timestamp'       => current_time( 'mysql' ),\n-\t\t\t\t'user_agent'      => sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown' ) ),\n-\t\t\t\t'request_uri'     => sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? 'Unknown' ) ),\n-\t\t\t\t'suspicious_ips'  => $suspicious_ips,\n-\t\t\t\t'all_server_vars' => self::get_relevant_server_vars(),\n-\t\t\t);\n-\n-\t\t\t$security_logs   = get_mo_option( 'mo_otp_security_logs', array() );\n-\t\t\t$security_logs[] = $log_data;\n-\n-\t\t\tif ( count( $security_logs ) > 100 ) {\n-\t\t\t\t$security_logs = array_slice( $security_logs, -100 );\n-\t\t\t}\n-\n-\t\t\tupdate_mo_option( 'mo_otp_security_logs', $security_logs );\n-\t\t}\n-\n-\t\t\u002F**\n-\t\t * Get current page parameter value from URL query string.\n-\t\t * This function safely retrieves GET parameters without triggering PHPCS nonce verification warnings.\n-\t\t * It parses the REQUEST_URI to extract query parameters, which is safe for routing\u002Fdisplay purposes.\n-\t\t *\n-\t\t * @param string $parameter_name The name of the parameter to retrieve.\n-\t\t * @param string $default_value  Default value to return if parameter is not found.\n-\t\t * @return string The parameter value or default value.\n-\t\t *\u002F\n-\t\tpublic static function get_current_page_parameter_value( $parameter_name, $default_value = '' ) {\n-\t\t\t$path = ! empty( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';\n-\n-\t\t\t$parameter_value = '';\n-\n-\t\t\t\u002F\u002F Parse the URL to get the query string.\n-\t\t\t$query_str = wp_parse_url( $path, PHP_URL_QUERY );\n-\n-\t\t\t\u002F\u002F Parse the query string into an array.\n-\t\t\tif ( $query_str ) {\n-\t\t\t\tparse_str( $query_str, $query_params );\n-\n-\t\t\t\t\u002F\u002F Get the parameter value if it exists.\n-\t\t\t\tif ( ! empty( $query_params[ $parameter_name ] ) ) {\n-\t\t\t\t\t$parameter_value = sanitize_text_field( $query_params[ $parameter_name ] );\n-\t\t\t\t}\n-\t\t\t\tunset( $query_params );\n-\t\t\t}\n-\n-\t\t\treturn ! empty( $parameter_value ) ? $parameter_value : $default_value;\n-\t\t}\n-\t}\n-}\n+\u003C?php\r\n+\u002F**\r\n+ * Load administrator changes for MoUtility\r\n+ *\r\n+ * @package miniorange-otp-verification\u002Fhelper\r\n+ *\u002F\r\n+\r\n+namespace OTP\\Helper;\r\n+\r\n+if ( ! defined( 'ABSPATH' ) ) {\r\n+\texit;\r\n+}\r\n+\r\n+use OTP\\Objects\\NotificationSettings;\r\n+use OTP\\Objects\\TabDetails;\r\n+use OTP\\Objects\\Tabs;\r\n+use OTP\\Objects\\VerificationType;\r\n+use ReflectionClass;\r\n+use ReflectionException;\r\n+use stdClass;\r\n+use OTP\\LicenseLibrary\\Mo_License_Service;\r\n+use OTP\\Helper\\MoConstants;\r\n+use OTP\\Helper\\CountryList;\r\n+\r\n+\u002F**\r\n+ * This is the main Utility class of the plugin.\r\n+ * Lists down all the necessary common utility\r\n+ * functions being used in the plugin.\r\n+ *\u002F\r\n+if ( ! class_exists( 'MoUtility' ) ) {\r\n+\t\u002F**\r\n+\t * MoUtility class\r\n+\t *\u002F\r\n+\tclass MoUtility {\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Checking Script tags\r\n+\t\t *\r\n+\t\t * @param string $template checking script tag.\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tpublic static function check_for_script_tags( $template ) {\r\n+\t\t\treturn preg_match( '\u002F\u003Cscript>\u002F', $template, $match );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Sanitizing array\r\n+\t\t *\r\n+\t\t * @param array $data data array to be sanitized.\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_sanitize_array( $data ) {\r\n+\t\t\t$sanitized_data = array();\r\n+\t\t\tforeach ( $data as $key => $value ) {\r\n+\t\t\t\t$key = sanitize_key( $key );\r\n+\t\t\t\tif ( empty( $key ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( is_array( $value ) ) {\r\n+\t\t\t\t\t$sanitized_data[ $key ] = self::mo_sanitize_array( $value );\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\t$sanitized_data[ $key ] = sanitize_text_field( wp_unslash( $value ) );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn $sanitized_data;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * MoInternal Function\r\n+\t\t *\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_allow_html_array() {\r\n+\t\t\t$allowed_tags = array(\r\n+\t\t\t\t'a'          => array(\r\n+\t\t\t\t\t'style'   => array(),\r\n+\t\t\t\t\t'class'   => array(),\r\n+\t\t\t\t\t'href'    => array(),\r\n+\t\t\t\t\t'rel'     => array(),\r\n+\t\t\t\t\t'title'   => array(),\r\n+\t\t\t\t\t'hidden'  => array(),\r\n+\t\t\t\t\t'target'  => array(),\r\n+\t\t\t\t\t'onclick' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'b'          => array(\r\n+\t\t\t\t\t'style' => array(),\r\n+\t\t\t\t\t'class' => array(),\r\n+\t\t\t\t\t'id'    => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'blockquote' => array(\r\n+\t\t\t\t\t'cite' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'code'       => array(),\r\n+\t\t\t\t'del'        => array(\r\n+\t\t\t\t\t'datetime' => array(),\r\n+\t\t\t\t\t'title'    => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'div'        => array(\r\n+\t\t\t\t\t'name'   => array(),\r\n+\t\t\t\t\t'dir'    => array(),\r\n+\t\t\t\t\t'id'     => array(),\r\n+\t\t\t\t\t'class'  => array(),\r\n+\t\t\t\t\t'title'  => array(),\r\n+\t\t\t\t\t'style'  => array(),\r\n+\t\t\t\t\t'hidden' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'dl'         => array(),\r\n+\t\t\t\t'dt'         => array(),\r\n+\t\t\t\t'em'         => array(),\r\n+\t\t\t\t'h1'         => array(),\r\n+\t\t\t\t'h2'         => array(),\r\n+\t\t\t\t'h3'         => array(),\r\n+\t\t\t\t'h4'         => array(),\r\n+\t\t\t\t'h5'         => array(),\r\n+\t\t\t\t'h6'         => array(),\r\n+\t\t\t\t'hr'         => array(),\r\n+\t\t\t\t'i'          => array(),\r\n+\t\t\t\t'textarea'   => array(\r\n+\t\t\t\t\t'id'          => array(),\r\n+\t\t\t\t\t'class'       => array(),\r\n+\t\t\t\t\t'name'        => array(),\r\n+\t\t\t\t\t'row'         => array(),\r\n+\t\t\t\t\t'style'       => array(),\r\n+\t\t\t\t\t'placeholder' => array(),\r\n+\t\t\t\t\t'readonly'    => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'img'        => array(\r\n+\t\t\t\t\t'alt'    => array(),\r\n+\t\t\t\t\t'class'  => array(),\r\n+\t\t\t\t\t'height' => array(),\r\n+\t\t\t\t\t'style'  => array(),\r\n+\t\t\t\t\t'src'    => array(),\r\n+\t\t\t\t\t'width'  => array(),\r\n+\t\t\t\t\t'href'   => array(),\r\n+\t\t\t\t\t'hidden' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'link'       => array(\r\n+\t\t\t\t\t'rel'    => array(),\r\n+\t\t\t\t\t'type'   => array(),\r\n+\t\t\t\t\t'href'   => array(),\r\n+\t\t\t\t\t'hidden' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'li'         => array(\r\n+\t\t\t\t\t'class'  => array(),\r\n+\t\t\t\t\t'hidden' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'ol'         => array(\r\n+\t\t\t\t\t'class' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'p'          => array(\r\n+\t\t\t\t\t'class'  => array(),\r\n+\t\t\t\t\t'hidden' => array(),\r\n+\t\t\t\t\t'style'  => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'q'          => array(\r\n+\t\t\t\t\t'cite'  => array(),\r\n+\t\t\t\t\t'title' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'span'       => array(\r\n+\t\t\t\t\t'id'     => array(),\r\n+\t\t\t\t\t'value'  => array(),\r\n+\t\t\t\t\t'class'  => array(),\r\n+\t\t\t\t\t'title'  => array(),\r\n+\t\t\t\t\t'style'  => array(),\r\n+\t\t\t\t\t'hidden' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'strike'     => array(),\r\n+\t\t\t\t'strong'     => array(),\r\n+\t\t\t\t'u'          => array(),\r\n+\t\t\t\t'ul'         => array(\r\n+\t\t\t\t\t'class' => array(),\r\n+\t\t\t\t\t'style' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'form'       => array(\r\n+\t\t\t\t\t'name'   => array(),\r\n+\t\t\t\t\t'method' => array(),\r\n+\t\t\t\t\t'id'     => array(),\r\n+\t\t\t\t\t'style'  => array(),\r\n+\t\t\t\t\t'hidden' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'table'      => array(\r\n+\t\t\t\t\t'class'       => array(),\r\n+\t\t\t\t\t'style'       => array(),\r\n+\t\t\t\t\t'cellpadding' => array(),\r\n+\t\t\t\t\t'cellspacing' => array(),\r\n+\t\t\t\t\t'border'      => array(),\r\n+\t\t\t\t\t'width'       => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'tbody'      => array(),\r\n+\t\t\t\t'button'     => array(),\r\n+\t\t\t\t'tr'         => array(),\r\n+\t\t\t\t'td'         => array(\r\n+\t\t\t\t\t'class' => array(),\r\n+\t\t\t\t\t'style' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'input'      => array(\r\n+\t\t\t\t\t'type'          => array(),\r\n+\t\t\t\t\t'id'            => array(),\r\n+\t\t\t\t\t'name'          => array(),\r\n+\t\t\t\t\t'value'         => array(),\r\n+\t\t\t\t\t'class'         => array(),\r\n+\t\t\t\t\t'size '         => array(),\r\n+\t\t\t\t\t'tabindex'      => array(),\r\n+\t\t\t\t\t'hidden'        => array(),\r\n+\t\t\t\t\t'style'         => array(),\r\n+\t\t\t\t\t'placeholder'   => array(),\r\n+\t\t\t\t\t'disabled'      => array(),\r\n+\t\t\t\t\t'data-next'     => array(),\r\n+\t\t\t\t\t'data-previous' => array(),\r\n+\t\t\t\t\t'maxlength'     => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'br'         => array(),\r\n+\t\t\t\t'title'      => array(\r\n+\t\t\t\t\t'title' => true,\r\n+\t\t\t\t),\r\n+\t\t\t);\r\n+\t\t\treturn $allowed_tags;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Allowing tags for popup templates\r\n+\t\t *\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_allow_popup_tags() {\r\n+\t\t\t$allowed_tags = array(\r\n+\t\t\t\t'head'           => array(),\r\n+\t\t\t\t'title'          => array(),\r\n+\t\t\t\t'meta'           => array(\r\n+\t\t\t\t\t'http-equiv' => array(),\r\n+\t\t\t\t\t'content'    => array(),\r\n+\t\t\t\t\t'name'       => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'html'           => array(),\r\n+\t\t\t\t'body'           => array(),\r\n+\t\t\t\t'style'          => array(\r\n+\t\t\t\t\t'type' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'div'            => array(\r\n+\t\t\t\t\t'name'     => array(),\r\n+\t\t\t\t\t'dir'      => array(),\r\n+\t\t\t\t\t'id'       => array(),\r\n+\t\t\t\t\t'class'    => array(),\r\n+\t\t\t\t\t'title'    => array(),\r\n+\t\t\t\t\t'style'    => array(),\r\n+\t\t\t\t\t'tabindex' => array(),\r\n+\t\t\t\t\t'role'     => array(),\r\n+\t\t\t\t\t'hidden'   => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'link'           => array(\r\n+\t\t\t\t\t'href'   => array(),\r\n+\t\t\t\t\t'target' => array(),\r\n+\t\t\t\t\t'rel'    => array(),\r\n+\t\t\t\t\t'type'   => array(),\r\n+\t\t\t\t\t'title'  => array(),\r\n+\t\t\t\t\t'hidden' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'input'          => array(\r\n+\t\t\t\t\t'type'     => array(),\r\n+\t\t\t\t\t'id'       => array(),\r\n+\t\t\t\t\t'name'     => array(),\r\n+\t\t\t\t\t'value'    => array(),\r\n+\t\t\t\t\t'class'    => array(),\r\n+\t\t\t\t\t'size '    => array(),\r\n+\t\t\t\t\t'tabindex' => array(),\r\n+\t\t\t\t\t'hidden'   => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'button'         => array(\r\n+\t\t\t\t\t'class' => array(),\r\n+\t\t\t\t\t'id'    => array(),\r\n+\t\t\t\t\t'type'  => array(),\r\n+\t\t\t\t\t'name'  => array(),\r\n+\t\t\t\t\t'value' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'form'           => array(\r\n+\t\t\t\t\t'name'   => array(),\r\n+\t\t\t\t\t'method' => array(),\r\n+\t\t\t\t\t'action' => array(),\r\n+\t\t\t\t\t'id'     => array(),\r\n+\t\t\t\t\t'class'  => array(),\r\n+\t\t\t\t\t'hidden' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'br'             => array(),\r\n+\t\t\t\t'p'              => array(\r\n+\t\t\t\t\t'class' => true,\r\n+\t\t\t\t\t'style' => true,\r\n+\t\t\t\t\t'id'    => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'i'              => array(),\r\n+\t\t\t\t'u'              => array(),\r\n+\t\t\t\t'span'           => array(\r\n+\t\t\t\t\t'id'     => array(),\r\n+\t\t\t\t\t'value'  => array(),\r\n+\t\t\t\t\t'class'  => array(),\r\n+\t\t\t\t\t'title'  => array(),\r\n+\t\t\t\t\t'style'  => array(),\r\n+\t\t\t\t\t'hidden' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'a'              => array(\r\n+\t\t\t\t\t'href'    => true,\r\n+\t\t\t\t\t'target'  => true,\r\n+\t\t\t\t\t'rel'     => true,\r\n+\t\t\t\t\t'title'   => true,\r\n+\t\t\t\t\t'hidden'  => true,\r\n+\t\t\t\t\t'class'   => true,\r\n+\t\t\t\t\t'onclick' => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'svg'            => array(\r\n+\t\t\t\t\t'class'   => array(),\r\n+\t\t\t\t\t'id'      => array(),\r\n+\t\t\t\t\t'width'   => array(),\r\n+\t\t\t\t\t'height'  => array(),\r\n+\t\t\t\t\t'viewBox' => array(),\r\n+\t\t\t\t\t'viewbox' => array(),\r\n+\t\t\t\t\t'fill'    => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'circle'         => array(\r\n+\t\t\t\t\t'id' => array(),\r\n+\t\t\t\t\t'cx' => array(),\r\n+\t\t\t\t\t'cy' => array(),\r\n+\t\t\t\t\t'cz' => array(),\r\n+\t\t\t\t\t'r'  => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'g'              => array(\r\n+\t\t\t\t\t'fill' => array(),\r\n+\t\t\t\t\t'id'   => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'path'           => array(\r\n+\t\t\t\t\t'd'               => array(),\r\n+\t\t\t\t\t'fill'            => array(),\r\n+\t\t\t\t\t'stroke'          => array(),\r\n+\t\t\t\t\t'stroke-width'    => array(),\r\n+\t\t\t\t\t'stroke-linecap'  => array(),\r\n+\t\t\t\t\t'stroke-linejoin' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'rect'           => array(\r\n+\t\t\t\t\t'x'               => array(),\r\n+\t\t\t\t\t'y'               => array(),\r\n+\t\t\t\t\t'width'           => array(),\r\n+\t\t\t\t\t'height'          => array(),\r\n+\t\t\t\t\t'rx'              => array(),\r\n+\t\t\t\t\t'fill'            => array(),\r\n+\t\t\t\t\t'stroke'          => array(),\r\n+\t\t\t\t\t'stroke-width'    => array(),\r\n+\t\t\t\t\t'stroke-linejoin' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t\t'defs'           => array(),\r\n+\t\t\t\t'lineargradient' => array(\r\n+\t\t\t\t\t'id'            => array(),\r\n+\t\t\t\t\t'x1'            => array(),\r\n+\t\t\t\t\t'x2'            => array(),\r\n+\t\t\t\t\t'y1'            => array(),\r\n+\t\t\t\t\t'y2'            => array(),\r\n+\t\t\t\t\t'gradientunits' => array(),\r\n+\t\t\t\t),\r\n+\t\t\t);\r\n+\t\t\treturn $allowed_tags;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * KSES allowlist for rendered OTP popup HTML: popup chrome (forms, svg) plus post-like tags\r\n+\t\t * in {{MESSAGE}} so links and paragraphs (e.g. admin password hint) survive both inner and outer wp_kses passes.\r\n+\t\t *\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_popup_html_kses_allowed() {\r\n+\t\t\t$popup = self::mo_allow_popup_tags();\r\n+\t\t\tif ( ! function_exists( 'wp_kses_allowed_html' ) ) {\r\n+\t\t\t\treturn $popup;\r\n+\t\t\t}\r\n+\t\t\t$post = wp_kses_allowed_html( 'post' );\r\n+\t\t\tforeach ( $post as $tag => $post_attrs ) {\r\n+\t\t\t\tif ( ! isset( $popup[ $tag ] ) ) {\r\n+\t\t\t\t\t$popup[ $tag ] = $post_attrs;\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t\t$popup[ $tag ] = array_merge( (array) $post_attrs, (array) $popup[ $tag ] );\r\n+\t\t\t}\r\n+\t\t\treturn $popup;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * MoInternal Function\r\n+\t\t *\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_allow_svg_array() {\r\n+\t\t\t$allowed_tags = array(\r\n+\t\t\t\t'svg'            => array(\r\n+\t\t\t\t\t'class'   => true,\r\n+\t\t\t\t\t'width'   => true,\r\n+\t\t\t\t\t'height'  => true,\r\n+\t\t\t\t\t'viewbox' => true,\r\n+\t\t\t\t\t'fill'    => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'circle'         => array(\r\n+\t\t\t\t\t'id'           => true,\r\n+\t\t\t\t\t'cx'           => true,\r\n+\t\t\t\t\t'cy'           => true,\r\n+\t\t\t\t\t'cz'           => true,\r\n+\t\t\t\t\t'r'            => true,\r\n+\t\t\t\t\t'stroke'       => true,\r\n+\t\t\t\t\t'stroke-width' => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'g'              => array(\r\n+\t\t\t\t\t'fill' => true,\r\n+\t\t\t\t\t'id'   => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'path'           => array(\r\n+\t\t\t\t\t'd'              => true,\r\n+\t\t\t\t\t'fill'           => true,\r\n+\t\t\t\t\t'id'             => true,\r\n+\t\t\t\t\t'fill-rule'      => true,\r\n+\t\t\t\t\t'clip-rule'      => true,\r\n+\t\t\t\t\t'stroke'         => true,\r\n+\t\t\t\t\t'stroke-width'   => true,\r\n+\t\t\t\t\t'stroke-linecap' => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'rect'           => array(\r\n+\t\t\t\t\t'width'  => true,\r\n+\t\t\t\t\t'height' => true,\r\n+\t\t\t\t\t'rx'     => true,\r\n+\t\t\t\t\t'fill'   => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'defs'           => array(),\r\n+\t\t\t\t'lineargradient' => array(\r\n+\t\t\t\t\t'id'            => true,\r\n+\t\t\t\t\t'x1'            => true,\r\n+\t\t\t\t\t'x2'            => true,\r\n+\t\t\t\t\t'y1'            => true,\r\n+\t\t\t\t\t'y2'            => true,\r\n+\t\t\t\t\t'gradientunits' => true,\r\n+\t\t\t\t),\r\n+\t\t\t\t'stop'           => array(\r\n+\t\t\t\t\t'stop-color' => true,\r\n+\t\t\t\t\t'offset'     => true,\r\n+\t\t\t\t),\r\n+\t\t\t);\r\n+\t\t\treturn $allowed_tags;\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Masking the Phone Number of User\r\n+\t\t *\r\n+\t\t * @param string $phone   Phone Number of the user.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_mask_phone_number( $phone ) {\r\n+\t\t\t$length      = strlen( $phone );\r\n+\t\t\t$masked_part = str_repeat( '*', max( 0, $length - 3 ) );\r\n+\t\t\t$last_three  = substr( $phone, -3 );\r\n+\t\t\treturn $masked_part . $last_three;\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Masking the Email of User\r\n+\t\t *\r\n+\t\t * @param string $email    email of the user.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_mask_email( $email ) {\r\n+\t\t\t$parts = explode( '@', $email );\r\n+\t\t\tif ( count( $parts ) !== 2 ) {\r\n+\t\t\t\treturn $email;\r\n+\t\t\t}\r\n+\t\t\t$username     = $parts[0];\r\n+\t\t\t$domain       = $parts[1];\r\n+\t\t\t$visible_part = substr( $username, 0, 2 );\r\n+\t\t\t$masked_part  = str_repeat( '*', max( 0, strlen( $username ) - 2 ) );\r\n+\t\t\treturn $visible_part . $masked_part . '@' . $domain;\r\n+\t\t}\r\n+\r\n+\t\t\u002F** Process the phone number and get_hidden_phone.\r\n+\t\t *\r\n+\t\t * @param string $phone - the phone number to processed.\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tpublic static function get_hidden_phone( $phone ) {\r\n+\t\t\treturn 'xxxxxxx' . substr( $phone, strlen( $phone ) - 3 );\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Process the value being passed and checks if it is empty or null\r\n+\t\t *\r\n+\t\t * @param string $value - the value to be checked.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function is_blank( $value ) {\r\n+\t\t\treturn ! isset( $value ) || empty( $value );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Process the plugin name is being passed and checks if it plugin is active or not\r\n+\t\t *\r\n+\t\t * @param string $plugin - the plugin name to be checked.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function is_plugin_installed( $plugin ) {\r\n+\t\t\tif ( ! function_exists( 'is_plugin_active' ) ) {\r\n+\t\t\t\tinclude_once ABSPATH . 'wp-admin\u002Fincludes\u002Fplugin.php';\r\n+\t\t\t}\r\n+\t\t\treturn is_plugin_active( $plugin );\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Creates and returns the JSON response.\r\n+\t\t *\r\n+\t\t * @param string $message - the message.\r\n+\t\t * @param string $type - the type of result ( success or error ).\r\n+\t\t * @return array\r\n+\t\t *\u002F\r\n+\t\tpublic static function create_json( $message, $type ) {\r\n+\t\t\treturn array(\r\n+\t\t\t\t'message' => $message,\r\n+\t\t\t\t'result'  => $type,\r\n+\t\t\t);\r\n+\t\t}\r\n+\t\t\u002F**\r\n+\t\t * Check for Country Restriction Addon\r\n+\t\t *\r\n+\t\t * @param mixed $phone .\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function check_for_selected_country_addon( $phone ) {\r\n+\t\t\t$countriesavail = CountryList::get_countrycode_list();\r\n+\t\t\t$countriesavail = apply_filters( 'selected_countries', $countriesavail );\r\n+\r\n+\t\t\tforeach ( $countriesavail as $key => $value ) {\r\n+\t\t\t\tif ( 'All Countries' !== $value['name'] ) {\r\n+\t\t\t\t\t$country_code = isset( $value['countryCode'] ) ? $value['countryCode'] : ( isset( $value['countrycode'] ) ? $value['countrycode'] : null );\r\n+\t\t\t\t\tif ( $country_code && strpos( $phone, $country_code ) !== false ) {\r\n+\t\t\t\t\t\treturn false;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn true;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function checks if cURL is installed on the server.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_is_curl_installed() {\r\n+\t\t\treturn in_array( 'curl', get_loaded_extensions(), true );\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * The function returns the current page URL.\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tpublic static function current_page_url() {\r\n+\t\t\t$page_url = 'http';\r\n+\r\n+\t\t\tif ( ( isset( $_SERVER['HTTPS'] ) ) && ( sanitize_text_field( wp_unslash( $_SERVER['HTTPS'] ) ) === 'on' ) ) {\r\n+\t\t\t\t$page_url .= 's';\r\n+\t\t\t}\r\n+\r\n+\t\t\t$page_url .= ':\u002F\u002F';\r\n+\r\n+\t\t\t$server_port = isset( $_SERVER['SERVER_PORT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_PORT'] ) ) : '';\r\n+\t\t\t$server_uri  = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';\r\n+\t\t\t$server_name = isset( $_SERVER['SERVER_NAME'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ) : '';\r\n+\r\n+\t\t\tif ( '80' !== $server_port ) {\r\n+\t\t\t\t$page_url .= $server_name . ':' . $server_port . $server_uri;\r\n+\r\n+\t\t\t} else {\r\n+\t\t\t\t$page_url .= $server_name . $server_uri;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( function_exists( 'apply_filters' ) ) {\r\n+\t\t\t\t$page_url = apply_filters( 'mo_curl_page_url', $page_url );\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Validate and escape the URL before returning.\r\n+\t\t\t$validated_url = filter_var( $page_url, FILTER_VALIDATE_URL );\r\n+\t\t\treturn $validated_url ? esc_url_raw( $validated_url ) : esc_url_raw( $page_url );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Validates a file path against a base directory to prevent LFI\u002FRFI attacks.\r\n+\t\t * This function does not include\u002Frequire the file. It only returns whether\r\n+\t\t * the given file path is valid and readable within the allowed base directory.\r\n+\t\t *\r\n+\t\t * @param string $file_path The file path to validate.\r\n+\t\t * @param string $base_dir  The base directory to restrict file access to.\r\n+\t\t * @return bool  True if the file exists, is readable, and is within base dir; otherwise false.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_require_file( $file_path, $base_dir ) {\r\n+\t\t\tif ( empty( $file_path ) || empty( $base_dir ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$real_base_dir  = realpath( $base_dir );\r\n+\t\t\t$real_file_path = realpath( $file_path );\r\n+\r\n+\t\t\tif ( false === $real_file_path || false === $real_base_dir ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! is_dir( $real_base_dir ) || ! is_file( $real_file_path ) || ! is_readable( $real_file_path ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Normalize separators and ensure base dir boundary using trailing separator match.\r\n+\t\t\t$base_norm = rtrim( str_replace( '\\\\', '\u002F', $real_base_dir ), '\u002F' ) . '\u002F';\r\n+\t\t\t$file_norm = str_replace( '\\\\', '\u002F', $real_file_path );\r\n+\t\t\tif ( strncmp( $file_norm, $base_norm, strlen( $base_norm ) ) !== 0 ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn true;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Checks if the current user has the required capabilities for admin access.\r\n+\t\t * This is a centralized function to ensure consistent capability checks across the plugin.\r\n+\t\t *\r\n+\t\t * @param array $capabilities Array of capabilities to check. User needs at least one. Default: ['manage_options'].\r\n+\t\t * @param bool  $require_admin Whether to also require is_admin() context. Default: false.\r\n+\t\t * @return bool True if user has required capabilities (and admin context if required), false otherwise.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_check_admin_capability( $capabilities = array( 'manage_options' ), $require_admin = false ) {\r\n+\t\t\t\u002F\u002F Validate input.\r\n+\t\t\tif ( ! is_array( $capabilities ) || empty( $capabilities ) ) {\r\n+\t\t\t\t$capabilities = array( 'manage_options' );\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Check if required functions exist.\r\n+\t\t\tif ( ! function_exists( 'current_user_can' ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F If admin context is required, check it first.\r\n+\t\t\tif ( $require_admin ) {\r\n+\t\t\t\tif ( ! function_exists( 'is_admin' ) || ! is_admin() ) {\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Check if user has at least one of the required capabilities.\r\n+\t\t\tforeach ( $capabilities as $capability ) {\r\n+\t\t\t\tif ( is_string( $capability ) && current_user_can( $capability ) ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * The function retrieves the domain part of the email\r\n+\t\t *\r\n+\t\t * @param string $email - the email whose domain has to be validated.\r\n+\t\t *\r\n+\t\t * @return bool|string\r\n+\t\t *\u002F\r\n+\t\tpublic static function get_domain( $email ) {\r\n+\t\t\t$domain_name = substr( strrchr( $email, '@' ), 1 );\r\n+\t\t\treturn $domain_name;\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function validates the phone number format. Makes sure that country code\r\n+\t\t * is appended to the phone number. Return True or false.\r\n+\t\t *\r\n+\t\t * @param string $phone - the phone number to be validated.\r\n+\t\t *\r\n+\t\t * @return false|int\r\n+\t\t *\u002F\r\n+\t\tpublic static function validate_phone_number( $phone ) {\r\n+\t\t\t$phone = self::process_phone_number( $phone );\r\n+\r\n+\t\t\t\u002F\u002F Basic format validation using regex patterns.\r\n+\t\t\tif ( ! preg_match( MoConstants::PATTERN_PHONE, $phone ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Get country code from phone number.\r\n+\t\t\t$country_code = self::get_country_code( $phone );\r\n+\t\t\tif ( ! $country_code ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Extract national significant number (without country code).\r\n+\t\t\t$nsn        = substr( $phone, strlen( $country_code ) );\r\n+\t\t\t$nsn_length = strlen( $nsn );\r\n+\t\t\tif ( 0 === $nsn_length ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t\t$first_digit = substr( $nsn, 0, 1 );\r\n+\r\n+\t\t\t\u002F\u002F Find the best matching country for this country code using prefixes; keep first match as fallback.\r\n+\t\t\t$country_list = CountryList::get_countrycode_list();\r\n+\t\t\t$country_data = null;\r\n+\t\t\t$fallback     = null;\r\n+\t\t\tforeach ( $country_list as $cand ) {\r\n+\t\t\t\tif ( ! isset( $cand['countryCode'] ) || $cand['countryCode'] !== $country_code ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( null === $fallback ) {\r\n+\t\t\t\t\t$fallback = $cand;\r\n+\t\t\t\t}\r\n+\t\t\t\tif ( isset( $cand['prefixes'] ) && is_array( $cand['prefixes'] ) && in_array( $first_digit, $cand['prefixes'], true ) ) {\r\n+\t\t\t\t\t$country_data = $cand;\r\n+\t\t\t\t\tbreak;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\tif ( ! $country_data && $fallback ) {\r\n+\t\t\t\t$country_data = $fallback;\r\n+\t\t\t}\r\n+\t\t\tif ( ! $country_data ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Validate length using min\u002Fmax from metadata if present; else default 7-15 digits.\r\n+\t\t\t$min_len = ( isset( $country_data['minLength'] ) && is_numeric( $country_data['minLength'] ) ) ? (int) $country_data['minLength'] : 7;\r\n+\t\t\t$max_len = ( isset( $country_data['maxLength'] ) && is_numeric( $country_data['maxLength'] ) ) ? (int) $country_data['maxLength'] : 15;\r\n+\t\t\tif ( $nsn_length \u003C $min_len || $nsn_length > $max_len ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t\u002F\u002F Validate allowed first-digit prefixes if provided.\r\n+\t\t\tif ( isset( $country_data['prefixes'] ) && is_array( $country_data['prefixes'] ) && ! empty( $country_data['prefixes'] ) ) {\r\n+\t\t\t\tif ( ! in_array( $first_digit, $country_data['prefixes'], true ) ) {\r\n+\t\t\t\t\treturn false;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn true;\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function validates the phone number format and checks if it has country code appended.\r\n+\t\t * Return True or false.\r\n+\t\t *\r\n+\t\t * @param string $phone - the phone number to be checked.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function is_country_code_appended( $phone ) {\r\n+\t\t\treturn preg_match( MoConstants::PATTERN_COUNTRY_CODE, $phone, $matches ) ? true : false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Process the phone number, return the country code appended to the phone number. If\r\n+\t\t * country code is not appended then return the default country code if set any by the\r\n+\t\t * admin.\r\n+\t\t *\r\n+\t\t * @param string $phone - the phone number to be processed.\r\n+\t\t *\r\n+\t\t * @return mixed\r\n+\t\t *\u002F\r\n+\t\tpublic static function get_country_code( $phone ) {\r\n+\t\t\tif ( ! $phone ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t\t$phone                = preg_replace( MoConstants::PATTERN_SPACES_HYPEN, '', ltrim( trim( $phone ), '0' ) );\r\n+\t\t\t$default_country_code = CountryList::get_default_countrycode();\r\n+\t\t\t$country_list         = CountryList::get_countrycode_list();\r\n+\t\t\tif ( ! self::is_country_code_appended( $phone ) ) {\r\n+\t\t\t\treturn $default_country_code;\r\n+\t\t\t}\r\n+\t\t\tusort(\r\n+\t\t\t\t$country_list,\r\n+\t\t\t\tfunction ( $country_a, $country_b ) {\r\n+\t\t\t\t\treturn strlen( $country_b['countryCode'] ) - strlen( $country_a['countryCode'] );\r\n+\t\t\t\t}\r\n+\t\t\t);\r\n+\t\t\tforeach ( $country_list as $country_data ) {\r\n+\t\t\t\tif ( strpos( $phone, $country_data['countryCode'] ) === 0 ) {\r\n+\t\t\t\t\treturn $country_data['countryCode'];\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Process the phone number. Check if country code is appended to the phone number. If\r\n+\t\t * country code is not appended then add the default country code if set any by the\r\n+\t\t * admin.\r\n+\t\t *\r\n+\t\t * @param string $phone - the phone number to be processed.\r\n+\t\t *\r\n+\t\t * @return mixed\r\n+\t\t *\u002F\r\n+\t\tpublic static function process_phone_number( $phone ) {\r\n+\t\t\tif ( ! $phone ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t\t$phone                = preg_replace( MoConstants::PATTERN_SPACES_HYPEN, '', ltrim( trim( $phone ), '0' ) );\r\n+\t\t\t$default_country_code = CountryList::get_default_countrycode();\r\n+\t\t\t$phone                = ! isset( $default_country_code ) || self::is_country_code_appended( $phone ) ? $phone : $default_country_code . $phone;\r\n+\t\t\treturn apply_filters( 'mo_process_phone', $phone );\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Checks if user has completed his registration in miniOrange.\r\n+\t\t *\r\n+\t\t * @return int\r\n+\t\t *\u002F\r\n+\t\tpublic static function micr() {\r\n+\t\t\t$email        = get_mo_option( 'admin_email' );\r\n+\t\t\t$customer_key = get_mo_option( 'admin_customer_key' );\r\n+\t\t\tif ( ! $email || ! $customer_key || ! is_numeric( trim( $customer_key ) ) ) {\r\n+\t\t\t\treturn 0;\r\n+\t\t\t} else {\r\n+\t\t\t\treturn 1;\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Checks the class for license library and returns bool by checking if the license is expired.\r\n+\t\t *\r\n+\t\t * @return bool|array\r\n+\t\t *\u002F\r\n+\t\tpublic static function mllc() {\r\n+\t\t\t$is_free_plugin = strcmp( MOV_TYPE, 'MiniOrangeGateway' ) === 0;\r\n+\t\t\treturn ( class_exists( MoConstants::LICENCE_LIBRARY, false ) || ( ! $is_free_plugin ) ) ? Mo_License_Service::is_license_expired() : array( 'STATUS' => false );\r\n+\t\t}\r\n+\t\t\u002F**\r\n+\t\t * Function generates a random alphanumeric value and returns it.\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tpublic static function rand() {\r\n+\t\t\t$length        = wp_rand( 0, 15 );\r\n+\t\t\t$characters    = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';\r\n+\t\t\t$random_string = '';\r\n+\t\t\tfor ( $i = 0; $i \u003C $length; $i++ ) {\r\n+\t\t\t\t$random_string .= $characters[ wp_rand( 0, strlen( $characters ) - 1 ) ];\r\n+\t\t\t}\r\n+\t\t\treturn $random_string;\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Checks if user has upgraded to one of the plans.\r\n+\t\t *\r\n+\t\t * @return int\r\n+\t\t *\u002F\r\n+\t\tpublic static function micv() {\r\n+\t\t\t$email        = get_mo_option( 'admin_email' );\r\n+\t\t\t$customer_key = get_mo_option( 'admin_customer_key' );\r\n+\t\t\t$check_ln     = get_mo_option( 'check_ln' );\r\n+\t\t\tif ( ! $email || ! $customer_key || ! is_numeric( trim( $customer_key ) ) ) {\r\n+\t\t\t\treturn 0;\r\n+\t\t\t} else {\r\n+\t\t\t\treturn $check_ln ? $check_ln : 0;\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function checks the license of the customer. Updates the license plan,\r\n+\t\t * sms and email remaining values in the database if user has upgraded.\r\n+\t\t *\r\n+\t\t * @param string $show_message - show message or not.\r\n+\t\t * @param string $customer_key - customerKey of the admin.\r\n+\t\t * @param string $api_key - apiKey of the admin.\r\n+\t\t * @return void\r\n+\t\t *\u002F\r\n+\t\tpublic static function handle_mo_check_ln( $show_message, $customer_key, $api_key ) {\r\n+\t\t\t$msg  = MoMessages::FREE_PLAN_MSG;\r\n+\t\t\t$plan = array();\r\n+\r\n+\t\t\t$gateway = GatewayFunctions::instance();\r\n+\t\t\t$content = json_decode( MocURLCall::check_customer_ln( $customer_key, $api_key, $gateway->get_application_name(), 'PREMIUM' ), true );\r\n+\t\t\tif ( isset( $content['status'] ) && strcasecmp( $content['status'], 'SUCCESS' ) === 0 ) {\r\n+\r\n+\t\t\t\t$email_remaining = isset( $content['emailRemaining'] ) ? $content['emailRemaining'] : 0;\r\n+\t\t\t\t$sms_remaining   = isset( $content['smsRemaining'] ) ? $content['smsRemaining'] : 0;\r\n+\t\t\t\t$license_plan    = isset( $content['licensePlan'] ) ? $content['licensePlan'] : '';\r\n+\r\n+\t\t\t\tif ( self::sanitize_check( 'licensePlan', $content ) ) {\r\n+\t\t\t\t\tif ( 0 === strcmp( MOV_TYPE, 'MiniOrangeGateway' ) || 0 === strcmp( MOV_TYPE, 'EnterpriseGatewayWithAddons' ) ) {\r\n+\t\t\t\t\t\t$msg  = MoMessages::REMAINING_TRANSACTION_MSG;\r\n+\t\t\t\t\t\t$plan = array(\r\n+\t\t\t\t\t\t\t'plan'  => $license_plan,\r\n+\t\t\t\t\t\t\t'sms'   => $sms_remaining,\r\n+\t\t\t\t\t\t\t'email' => $email_remaining,\r\n+\t\t\t\t\t\t);\r\n+\r\n+\t\t\t\t\t} else {\r\n+\t\t\t\t\t\t$msg  = MoMessages::UPGRADE_MSG;\r\n+\t\t\t\t\t\t$plan = array( 'plan' => $license_plan );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tupdate_mo_option( 'check_ln', $license_plan );\r\n+\t\t\t\t}\r\n+\t\t\t\tupdate_mo_option( 'customer_license_plan', $license_plan );\r\n+\t\t\t\tupdate_mo_option( 'email_transactions_remaining', $email_remaining );\r\n+\t\t\t\tupdate_mo_option( 'phone_transactions_remaining', $sms_remaining );\r\n+\t\t\t} else {\r\n+\t\t\t\t$content         = json_decode( MocURLCall::check_customer_ln( $customer_key, $api_key, 'wp_email_verification_intranet', 'PREMIUM' ), true );\r\n+\t\t\t\t$email_remaining = isset( $content['emailRemaining'] ) ? $content['emailRemaining'] : 0;\r\n+\t\t\t\t$sms_remaining   = isset( $content['smsRemaining'] ) ? $content['smsRemaining'] : 0;\r\n+\t\t\t\t$license_plan    = isset( $content['licensePlan'] ) ? $content['licensePlan'] : '';\r\n+\t\t\t\tupdate_mo_option( 'customer_license_plan', $license_plan );\r\n+\t\t\t\tupdate_mo_option( 'email_transactions_remaining', $email_remaining );\r\n+\t\t\t\tupdate_mo_option( 'phone_transactions_remaining', $sms_remaining );\r\n+\r\n+\t\t\t\tif ( self::sanitize_check( 'licensePlan', $content ) ) {\r\n+\t\t\t\t\t$msg = MoMessages::INSTALL_PREMIUM_PLUGIN;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( isset( $content['status'] ) && strcasecmp( $content['status'], 'FAILED' ) === 0 ) {\r\n+\t\t\t\t$content         = json_decode( MocURLCall::check_customer_ln( $customer_key, $api_key, '' ), true );\r\n+\t\t\t\t$email_remaining = isset( $content['emailRemaining'] ) ? $content['emailRemaining'] : 0;\r\n+\t\t\t\t$sms_remaining   = isset( $content['smsRemaining'] ) ? $content['smsRemaining'] : 0;\r\n+\t\t\t\t$license_plan    = isset( $content['licenseType'] ) ? $content['licenseType'] : '';\r\n+\t\t\t\tupdate_mo_option( 'customer_license_plan', $license_plan );\r\n+\t\t\t\tupdate_mo_option( 'email_transactions_remaining', $email_remaining );\r\n+\t\t\t\tupdate_mo_option( 'phone_transactions_remaining', $sms_remaining );\r\n+\t\t\t}\r\n+\t\t\tif ( isset( $content['licenseExpiry'] ) && strcasecmp( $content['status'], 'SUCCESS' ) === 0 ) {\r\n+\t\t\t\tif ( class_exists( MoConstants::LICENCE_LIBRARY, false ) ) {\r\n+\t\t\t\t\tMo_License_Service::update_license_expiry( $content['licenseExpiry'] );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( $show_message ) {\r\n+\t\t\t\tdo_action( 'mo_registration_show_message', MoMessages::showMessage( $msg, $plan ), 'SUCCESS' );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Initialize the form session indicating that the OTP Verification for the\r\n+\t\t * form has started.\r\n+\t\t *\r\n+\t\t * @param string $form - form for which session is being initialized \u002F session constant name.\r\n+\t\t *\u002F\r\n+\t\tpublic static function initialize_transaction( $form ) {\r\n+\t\t\tif ( empty( $form ) ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\r\n+\t\t\tMoPHPSessions::bootstrap();\r\n+\t\t\tMoPHPSessions::check_session();\r\n+\r\n+\t\t\t$reflect = new ReflectionClass( FormSessionVars::class );\r\n+\t\t\tforeach ( $reflect->getConstants() as $key => $value ) {\r\n+\t\t\t\t\u002F\u002F Don't unset the current form session variable as we're about to initialize it.\r\n+\t\t\t\tif ( $value !== $form ) {\r\n+\t\t\t\t\tMoPHPSessions::unset_session( $value );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tSessionUtils::initialize_form( $form );\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Returns the invalid OTP message. This function checks if admin has set an\r\n+\t\t * invalid otp message in the settings. If so then that is returned instead of the default.\r\n+\t\t *\r\n+\t\t * @return string\r\n+\t\t *\u002F\r\n+\t\tpublic static function get_invalid_otp_method() {\r\n+\t\t\treturn get_mo_option( 'invalid_message', 'mo_otp_' ) ? get_mo_option( 'invalid_message', 'mo_otp_' )\r\n+\t\t\t: MoMessages::showMessage( MoMessages::INVALID_OTP );\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Returns TRUE or FALSE depending on if the POLYLANG plugin is active.\r\n+\t\t * This is used to check if the translation should use the polylang\r\n+\t\t * function or the default local translation.\r\n+\t\t *\r\n+\t\t * @return boolean\r\n+\t\t *\u002F\r\n+\t\tpublic static function is_polylang_installed() {\r\n+\t\t\treturn function_exists( 'pll__' ) && function_exists( 'pll_register_string' );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Take an array of string having the keyword to replace\r\n+\t\t * and the keyword to be replaced. This is used to modify\r\n+\t\t * the SMS templates that the user might have saved in the\r\n+\t\t * settings or the default ones by the plugin.\r\n+\t\t *\r\n+\t\t * @param array  $replace      The array containing search and replace keywords.\r\n+\t\t * @param string $input_string Entire string to be modified.\r\n+\t\t *\r\n+\t\t * @return mixed\r\n+\t\t *\u002F\r\n+\t\tpublic static function replace_string( array $replace, $input_string ) {\r\n+\t\t\tforeach ( $replace as $key => $value ) {\r\n+\t\t\t\t$input_string = str_replace( '{' . $key . '}', $value, $input_string );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $input_string;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Returns a stdClass Object with status Success as a\r\n+\t\t * temporary result when TEST_MODE is on\r\n+\t\t *\r\n+\t\t * @return stdClass\r\n+\t\t *\u002F\r\n+\t\tprivate static function test_result() {\r\n+\t\t\t$temp         = new stdClass();\r\n+\t\t\t$temp->status = MO_FAIL_MODE ? 'ERROR' : 'SUCCESS';\r\n+\t\t\treturn $temp;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Checks if the whatsapp notifications and presonal business account is enabled\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_is_whatsapp_notif_enabled() {\r\n+\t\t\treturn get_mo_option( 'mo_whatsapp_enable' )\r\n+\t\t\t&& get_mo_option( 'mo_whatsapp_notification_enable' )\r\n+\t\t\t&& get_mo_option( 'mo_whatsapp_type' ) === 'bussiness_whatsapp';\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Send the notification to the number provided and\r\n+\t\t * process the response to check if the message was sent\r\n+\t\t * successfully or not. Return TRUE or FALSE based on the\r\n+\t\t * API call response.\r\n+\t\t *\r\n+\t\t * @param string $number the number to be sent.\r\n+\t\t * @param string $msg the message to be sent.\r\n+\t\t * @param string $notification_type the specific type of notification (e.g., 'NEW_ACCOUNT', 'ORDER_STATUS').\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function send_phone_notif( $number, $msg, $notification_type = 'NOTIFICATION' ) {\r\n+\r\n+\t\t\t$api_call_result = function ( $number, $msg ) {\r\n+\t\t\t\treturn json_decode( MocURLCall::send_notif( new NotificationSettings( $number, $msg ) ) );\r\n+\t\t\t};\r\n+\r\n+\t\t\t$mle = self::mllc();\r\n+\t\t\tif ( $mle['STATUS'] ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t\t$number       = self::process_phone_number( $number );\r\n+\t\t\t$msg          = self::replace_string( array( 'phone' => str_replace( '+', '', '%2B' . $number ) ), $msg );\r\n+\t\t\t$content      = MO_TEST_MODE ? self::test_result() : $api_call_result( $number, $msg );\r\n+\t\t\t$notif_status = strcasecmp( $content->status, 'SUCCESS' ) === 0 ? 'SMS_NOTIF_SENT' : 'SMS_NOTIF_FAILED';\r\n+\t\t\tapply_filters( 'mo_start_reporting', null, $number, $number, $notification_type . '_PHONE_NOTIF', $msg, $notif_status );\r\n+\t\t\treturn strcasecmp( $content->status, 'SUCCESS' ) === 0 ? true : false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Send the notification to the number provided and\r\n+\t\t * process the response to check if the message was sent\r\n+\t\t * successfully or not. Return TRUE or FALSE based on the\r\n+\t\t * API call response.\r\n+\t\t *\r\n+\t\t * @param string $number the number to be sent.\r\n+\t\t * @param string $template_name the template name.\r\n+\t\t * @param string $sms_tags the tags used in sms template.\r\n+\t\t * @param string $notification_type the specific type of notification (e.g., 'NEW_ACCOUNT', 'ORDER_STATUS').\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_send_whatsapp_notif( $number, $template_name, $sms_tags, $notification_type = 'WHATSAPP_NOTIFICATION' ) {\r\n+\t\t\t$api_call_result = function ( $number, $data ) {\r\n+\t\t\t\treturn apply_filters( 'mo_wa_send_otp_token', 'WHATSAPP_NOTIFICATION', null, null, $number, $data );\r\n+\t\t\t};\r\n+\r\n+\t\t\t$data         = array(\r\n+\t\t\t\t'template_name' => $template_name,\r\n+\t\t\t\t'sms_tags'      => $sms_tags,\r\n+\t\t\t);\r\n+\t\t\t$number       = self::process_phone_number( $number );\r\n+\t\t\t$content      = MO_TEST_MODE ? self::test_result() : $api_call_result( $number, $data );\r\n+\t\t\t$notif_status = strcasecmp( $content->status, 'SUCCESS' ) === 0 ? 'SMS_NOTIF_SENT' : 'SMS_NOTIF_FAILED';\r\n+\t\t\tapply_filters( 'mo_start_reporting', null, $number, $number, $notification_type . '_WHATSAPP_NOTIF', $template_name, $notif_status );\r\n+\t\t\treturn strcasecmp( $content->status, 'SUCCESS' ) === 0 ? true : false;\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Send the notification to the email provided and\r\n+\t\t * process the response to check if the message was sent\r\n+\t\t * successfully or not. Return TRUE or FALSE based on the\r\n+\t\t * API call response.\r\n+\t\t *\r\n+\t\t * @param string $from_email The From Email.\r\n+\t\t * @param string $from_name  The From Name.\r\n+\t\t * @param string $to_email   The email to send message to.\r\n+\t\t * @param string $subject   The subject of the email.\r\n+\t\t * @param string $message   The message to be sent.\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function send_email_notif( $from_email, $from_name, $to_email, $subject, $message ) {\r\n+\t\t\t$api_call_result = function ( $from_email, $from_name, $to_email, $subject, $message ) {\r\n+\t\t\t\t$notification_settings = new NotificationSettings();\r\n+\t\t\t\t$notification_settings->create_email_notification_settings( $from_email, $from_name, $to_email, $subject, $message );\r\n+\t\t\t\treturn json_decode( MocURLCall::send_notif( $notification_settings ) );\r\n+\t\t\t};\r\n+\t\t\t$content         = MO_TEST_MODE ? self::test_result() : $api_call_result( $from_email, $from_name, $to_email, $subject, $message );\r\n+\t\t\treturn strcasecmp( $content->status, 'SUCCESS' ) === 0 ? true : false;\r\n+\t\t}\r\n+\t\t\u002F**\r\n+\t\t * Check if there is an existing value in the array\u002Fbuffer and return the value\r\n+\t\t * that exists against that key otherwise return false.\r\n+\t\t * \u003Cp>\u003C\u002Fp>\r\n+\t\t * The function also makes sure to sanitize the values being fetched.\r\n+\t\t * \u003Cp>\u003C\u002Fp>\r\n+\t\t * If the buffer to fetch the value from is not an array then return buffer as it is.\r\n+\t\t *\r\n+\t\t * @param string       $key    the key to check against.\r\n+\t\t * @param   string|array $buffer the post\u002Fget or array.\r\n+\t\t * @return string|bool|array\r\n+\t\t *\u002F\r\n+\t\tpublic static function sanitize_check( $key, $buffer ) {\r\n+\t\t\tif ( ! isset( $buffer[ $key ] ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t\tif ( is_array( $buffer[ $key ] ) ) {\r\n+\t\t\t\treturn self::mo_sanitize_array( $buffer[ $key ] );\r\n+\t\t\t} else {\r\n+\t\t\t\treturn sanitize_text_field( wp_unslash( $buffer[ $key ] ) );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Checks if user has upgraded to the on-prem plugin\r\n+\t\t *\u002F\r\n+\t\tpublic static function mclv() {\r\n+\t\t\t$gateway = GatewayFunctions::instance();\r\n+\t\t\treturn $gateway->mclv();\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**Checks if the current plugin is Custom Gateway Plugin\r\n+\t\t *\u002F\r\n+\t\tpublic static function is_gateway_config() {\r\n+\t\t\t$gateway = GatewayFunctions::instance();\r\n+\t\t\treturn $gateway->is_gateway_config();\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Checks if the current plugin is MiniOrangeGateway Plugin\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function is_mg() {\r\n+\t\t\t$gateway = GatewayFunctions::instance();\r\n+\t\t\treturn $gateway->is_mg();\r\n+\t\t}\r\n+\r\n+\r\n+\t\t\u002F**\r\n+\t\t * This function checks if all conditions to save the form settings\r\n+\t\t * are true. This checks if the user saving the form settings is an admin,\r\n+\t\t * has registered with miniorange and the the form post has an option value\r\n+\t\t * mo_customer_validation_settings\r\n+\t\t *\r\n+\t\t * @param string $key_val the key to check against (typically 'mo_customer_validation_settings').\r\n+\t\t * @param string $form_option_key Optional POST key that must be present to proceed (form-specific).\r\n+\t\t *\r\n+\t\t * @return bool\r\n+\t\t *\u002F\r\n+\t\tpublic static function are_form_options_being_saved( $key_val, $form_option_key = '' ) {\r\n+\t\t\tif ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'mo_admin_actions' ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t\tif ( ! self::mclv() ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\r\n+\t\t\t$option_value = isset( $_POST['option'] ) ? sanitize_text_field( wp_unslash( $_POST['option'] ) ) : '';\r\n+\t\t\tif ( empty( $option_value ) || $option_value !== $key_val ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t\tif ( ! empty( $form_option_key ) ) {\r\n+\t\t\t\t$prefixed_key    = 'mo_customer_validation_' . $form_option_key;\r\n+\t\t\t\t$form_key_exists = isset( $_POST[ $prefixed_key ] ) || isset( $_POST[ $form_option_key ] );\r\n+\t\t\t\tif ( $form_key_exists ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t\t\u002F\u002F Form checkbox doesn't exist - form might be disabled, but we're in form settings save context.\r\n+\t\t\t\t$form_base     = str_replace( '_enable', '', $form_option_key );\r\n+\t\t\t\t$parts         = explode( '_', $form_base );\r\n+\t\t\t\t$form_id       = $parts[0];\r\n+\t\t\t\t$form_data_key = str_replace( '_enable', '_form', $form_option_key );\r\n+\t\t\t\tif ( isset( $_POST[ $form_data_key ] ) ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t\tforeach ( $_POST as $key => $value ) {\r\n+\t\t\t\t\t$key_to_check = $key;\r\n+\t\t\t\t\tif ( strpos( $key, 'mo_customer_validation_' ) === 0 ) {\r\n+\t\t\t\t\t\t$key_to_check = str_replace( 'mo_customer_validation_', '', $key );\r\n+\t\t\t\t\t} elseif ( strpos( $key, 'mo_' ) === 0 ) {\r\n+\t\t\t\t\t\t$key_to_check = str_replace( 'mo_', '', $key );\r\n+\t\t\t\t\t}\r\n+\t\t\t\t\tif ( strpos( $key_to_check, $form_id . '_' ) === 0 || strpos( $key_to_check, $form_base ) === 0 ) {\r\n+\t\t\t\t\t\treturn true;\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t\t\u002F\u002F Return false to prevent unnecessary processing.\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t\treturn true;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Update SMS Email transaction in DataBase\r\n+\t\t *\r\n+\t\t * @param string $response Response form Gateway.\r\n+\t\t * @param string $type     OTP Type email or phone.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_update_sms_email_transations( $response, $type ) {\r\n+\t\t\t$content = json_decode( $response );\r\n+\t\t\tif ( strcasecmp( $content->status, 'SUCCESS' ) === 0 ) {\r\n+\t\t\t\t$option_type   = ( VerificationType::PHONE === $type ) ? 'phone_transactions_remaining' : 'email_transactions_remaining';\r\n+\t\t\t\t$remaining_txn = get_mo_option( $option_type );\r\n+\t\t\t\tif ( $remaining_txn > 0 ) {\r\n+\t\t\t\t\tupdate_mo_option( $option_type, $remaining_txn - 1 );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Update WhatsApp transaction in DataBase\r\n+\t\t *\r\n+\t\t * @param string $response      Resposne form Gateway.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_update_whatsapp_transations( $response ) {\r\n+\t\t\t$content = json_decode( $response );\r\n+\t\t\tif ( isset( $content->status ) && strcasecmp( $content->status, 'SUCCESS' ) === 0 ) {\r\n+\t\t\t\t$remaining_txn = get_mo_option( 'whatsapp_transactions_remaining', 'mowp_customer_validation_' );\r\n+\t\t\t\tif ( $remaining_txn > 0 ) {\r\n+\t\t\t\t\tupdate_mo_option( 'whatsapp_transactions_remaining', $remaining_txn - 1, 'mowp_customer_validation_' );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Checks if the customer is registered or not and shows a message on the page\r\n+\t\t * to the user so that they can register or login themselves to use the plugin.\r\n+\t\t *\u002F\r\n+\t\tpublic static function is_addon_activated() {\r\n+\t\t\tif ( self::micr() && self::mclv() ) {\r\n+\t\t\t\treturn;\r\n+\t\t\t}\r\n+\t\t\t$tab_details      = TabDetails::instance();\r\n+\t\t\t$server_uri       = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';\r\n+\t\t\t$registration_url = add_query_arg(\r\n+\t\t\t\tarray( 'page' => $tab_details->tab_details[ Tabs::ACCOUNT ]->menu_slug ),\r\n+\t\t\t\tremove_query_arg( 'addon', $server_uri )\r\n+\t\t\t);\r\n+\t\t\techo '\u003Cdiv style=\"display:block;margin-top:10px;color:red;background-color:rgba(251, 232, 0, 0.15);\r\n+\t\t\t\t\t\t\t\tpadding:5px;border:solid 1px rgba(255, 0, 9, 0.36);\">\r\n+\t\t\t \t\t\u003Ca href=\"' . esc_url( $registration_url ) . '\">' . esc_html( __( 'Validate your purchase', 'miniorange-otp-verification' ) ) . '\u003C\u002Fa>\r\n+\t\t\t \t\t\t\t' . esc_html( __( ' to enable the Add On', 'miniorange-otp-verification' ) ) . '\u003C\u002Fdiv>';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check if the phone number is empty and return error.\r\n+\t\t *\r\n+\t\t * @param string $phone_number phone number of the user.\r\n+\t\t *\u002F\r\n+\t\tpublic static function check_if_phone_exist( $phone_number ) {\r\n+\t\t\tif ( empty( $phone_number ) ) {\r\n+\t\t\t\twp_send_json( self::create_json( MoMessages::showMessage( MoMessages::PHONE_NOT_FOUND ), MoConstants::ERROR_JSON_TYPE ) );\r\n+\t\t\t}\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Checks the version of the plugin active with the mentioned name.\r\n+\t\t *\r\n+\t\t * @param string  $plugin_name     -   Plugin Name.\r\n+\t\t * @param integer $sequence       -   index of the version digit to get.\r\n+\t\t * @return integer  Version number.\r\n+\t\t *\u002F\r\n+\t\tpublic static function get_active_plugin_version( $plugin_name, $sequence = 0 ) {\r\n+\t\t\tif ( ! function_exists( 'get_plugins' ) ) {\r\n+\t\t\t\trequire_once ABSPATH . 'wp-admin\u002Fincludes\u002Fplugin.php';\r\n+\t\t\t}\r\n+\t\t\t$all_plugins   = get_plugins();\r\n+\t\t\t$active_plugin = get_mo_option( 'active_plugins', '' );\r\n+\t\t\tif ( ! is_array( $active_plugin ) ) {\r\n+\t\t\t\t$active_plugin = array();\r\n+\t\t\t}\r\n+\t\t\tforeach ( $all_plugins as $key => $value ) {\r\n+\t\t\t\tif ( isset( $value['Name'] ) && strcasecmp( $value['Name'], $plugin_name ) === 0 ) {\r\n+\t\t\t\t\tif ( in_array( $key, $active_plugin, true ) ) {\r\n+\t\t\t\t\t\t\u002F\u002F Make sure the version is set and has the requested sequence.\r\n+\t\t\t\t\t\tif ( isset( $value['Version'] ) && isset( $value['Version'][ $sequence ] ) ) {\r\n+\t\t\t\t\t\t\treturn (int) $value['Version'][ $sequence ];\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t}\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn null;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Encrypts a plaintext password using AES-256-CBC encryption.\r\n+\t\t *\r\n+\t\t * @param string $plaintext_password The plain text password to encrypt.\r\n+\t\t * @return string Hex-encoded encrypted string.\r\n+\t\t *\u002F\r\n+\t\tpublic static function encrypt_password( $plaintext_password ) {\r\n+\t\t\tif ( empty( $plaintext_password ) ) {\r\n+\t\t\t\treturn '';\r\n+\t\t\t}\r\n+\t\t\t$encryption_key = hash( 'sha256', wp_salt( 'auth' ), true );\r\n+\t\t\t$iv             = substr( hash( 'sha256', 'otp-plugin-password-iv' ), 0, 16 );\r\n+\t\t\t$encrypted      = openssl_encrypt(\r\n+\t\t\t\t$plaintext_password,\r\n+\t\t\t\t'AES-256-CBC',\r\n+\t\t\t\t$encryption_key,\r\n+\t\t\t\t0,\r\n+\t\t\t\t$iv\r\n+\t\t\t);\r\n+\t\t\tif ( false === $encrypted ) {\r\n+\t\t\t\treturn '';\r\n+\t\t\t}\r\n+\t\t\treturn bin2hex( $encrypted );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Decrypts an AES-256-CBC encrypted password back to plain text.\r\n+\t\t *\r\n+\t\t * @param string $encrypted_password The hex-encoded encrypted password.\r\n+\t\t * @return string|false Decrypted plain text password, or false on failure.\r\n+\t\t *\u002F\r\n+\t\tpublic static function decrypt_password( $encrypted_password ) {\r\n+\t\t\tif ( empty( $encrypted_password ) ) {\r\n+\t\t\t\treturn '';\r\n+\t\t\t}\r\n+\t\t\t$encryption_key = hash( 'sha256', wp_salt( 'auth' ), true );\r\n+\t\t\t$iv             = substr( hash( 'sha256', 'otp-plugin-password-iv' ), 0, 16 );\r\n+\t\t\t$encrypted_data = hex2bin( $encrypted_password );\r\n+\t\t\tif ( false === $encrypted_data ) {\r\n+\t\t\t\treturn '';\r\n+\t\t\t}\r\n+\t\t\t$decrypted = openssl_decrypt(\r\n+\t\t\t\t$encrypted_data,\r\n+\t\t\t\t'AES-256-CBC',\r\n+\t\t\t\t$encryption_key,\r\n+\t\t\t\t0,\r\n+\t\t\t\t$iv\r\n+\t\t\t);\r\n+\t\t\treturn false === $decrypted ? '' : $decrypted;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get the current user's IP address.\r\n+\t\t *\r\n+\t\t * @return string - IP address\r\n+\t\t *\u002F\r\n+\t\tpublic static function get_current_ip_address() {\r\n+\t\t\t$ip_sources = array(\r\n+\t\t\t\t'REMOTE_ADDR'           => array(\r\n+\t\t\t\t\t'trust' => true,\r\n+\t\t\t\t\t'risk'  => 'low',\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_X_FORWARDED_FOR'  => array(\r\n+\t\t\t\t\t'trust' => false,\r\n+\t\t\t\t\t'risk'  => 'medium',\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_X_REAL_IP'        => array(\r\n+\t\t\t\t\t'trust' => false,\r\n+\t\t\t\t\t'risk'  => 'low',\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_CF_CONNECTING_IP' => array(\r\n+\t\t\t\t\t'trust' => false,\r\n+\t\t\t\t\t'risk'  => 'low',\r\n+\t\t\t\t),\r\n+\t\t\t\t'HTTP_CLIENT_IP'        => array(\r\n+\t\t\t\t\t'trust' => false,\r\n+\t\t\t\t\t'risk'  => 'high',\r\n+\t\t\t\t),\r\n+\t\t\t);\r\n+\r\n+\t\t\t$found_ips      = array();\r\n+\t\t\t$suspicious_ips = array();\r\n+\r\n+\t\t\tforeach ( $ip_sources as $source => $metadata ) {\r\n+\t\t\t\tif ( empty( $_SERVER[ $source ] ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\t$raw_value = sanitize_text_field( wp_unslash( $_SERVER[ $source ] ) );\r\n+\t\t\t\t$ip        = self::extract_first_valid_ip( $raw_value );\r\n+\r\n+\t\t\t\tif ( $ip && self::is_valid_ip( $ip ) ) {\r\n+\t\t\t\t\t$found_ips[] = array(\r\n+\t\t\t\t\t\t'ip'    => $ip,\r\n+\t\t\t\t\t\t'trust' => $metadata['trust'],\r\n+\t\t\t\t\t\t'risk'  => $metadata['risk'],\r\n+\t\t\t\t\t);\r\n+\t\t\t\t} else {\r\n+\t\t\t\t\t$suspicious_ips[] = array(\r\n+\t\t\t\t\t\t'ip'        => $ip ? $ip : 'invalid_format',\r\n+\t\t\t\t\t\t'source'    => $source,\r\n+\t\t\t\t\t\t'raw_value' => $raw_value,\r\n+\t\t\t\t\t\t'reason'    => $ip ? 'contains_attack_patterns' : 'invalid_ip_format',\r\n+\t\t\t\t\t);\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! empty( $found_ips ) ) {\r\n+\t\t\t\tusort(\r\n+\t\t\t\t\t$found_ips,\r\n+\t\t\t\t\tfunction ( $a, $b ) {\r\n+\t\t\t\t\t\tif ( $a['trust'] === $b['trust'] ) {\r\n+\t\t\t\t\t\t\t$risk_order = array(\r\n+\t\t\t\t\t\t\t\t'low'    => 0,\r\n+\t\t\t\t\t\t\t\t'medium' => 1,\r\n+\t\t\t\t\t\t\t\t'high'   => 2,\r\n+\t\t\t\t\t\t\t);\r\n+\t\t\t\t\t\t\treturn $risk_order[ $a['risk'] ] - $risk_order[ $b['risk'] ];\r\n+\t\t\t\t\t\t}\r\n+\t\t\t\t\t\treturn $b['trust'] - $a['trust'];\r\n+\t\t\t\t\t}\r\n+\t\t\t\t);\r\n+\r\n+\t\t\t\t$best_ip      = $found_ips[0];\r\n+\t\t\t\t$risk_markers = array(\r\n+\t\t\t\t\t'high'   => ' (high_risk_proxy)',\r\n+\t\t\t\t\t'medium' => ' (medium_risk_proxy)',\r\n+\t\t\t\t);\r\n+\r\n+\t\t\t\treturn $best_ip['ip'] . ( $risk_markers[ $best_ip['risk'] ] ?? '' );\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( isset( $_SERVER['REMOTE_ADDR'] ) ) {\r\n+\t\t\t\t$fallback_ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );\r\n+\t\t\t\treturn self::is_valid_ip( $fallback_ip ) ? $fallback_ip : $fallback_ip . ' (suspicious_format)';\r\n+\t\t\t}\r\n+\r\n+\t\t\tif ( ! empty( $suspicious_ips ) ) {\r\n+\t\t\t\tself::log_suspicious_ip_activity( $suspicious_ips );\r\n+\t\t\t\t$first_suspicious = $suspicious_ips[0];\r\n+\t\t\t\treturn 'invalid_format' !== $first_suspicious['ip'] ? $first_suspicious['ip'] : 'Unknown';\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn 'Unknown';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get the first matching page ID by its title using a non-deprecated approach.\r\n+\t\t *\r\n+\t\t * @param string $page_title  Page title to search for.\r\n+\t\t * @param string $post_status Page status to include (default 'all').\r\n+\t\t * @return int|string Page ID if found; empty string if not found or title blank.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_get_page_id_by_title( $page_title, $post_status = 'all' ) {\r\n+\t\t\tif ( self::is_blank( $page_title ) ) {\r\n+\t\t\t\treturn '';\r\n+\t\t\t}\r\n+\r\n+\t\t\t$pages = get_posts(\r\n+\t\t\t\tarray(\r\n+\t\t\t\t\t'post_type'        => 'page',\r\n+\t\t\t\t\t'title'            => $page_title,\r\n+\t\t\t\t\t'post_status'      => $post_status,\r\n+\t\t\t\t\t'numberposts'      => 1,\r\n+\t\t\t\t\t'suppress_filters' => false,\r\n+\t\t\t\t)\r\n+\t\t\t);\r\n+\r\n+\t\t\treturn ( ! empty( $pages ) && isset( $pages[0]->ID ) ) ? (int) $pages[0]->ID : '';\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get the permalink for the first matching page title, or a default if not found.\r\n+\t\t *\r\n+\t\t * @param string $page_title Page title to search for.\r\n+\t\t * @param string $default_url    Default URL to return if not found (default home_url('\u002F')).\r\n+\t\t * @param string $post_status Page status to include (default 'all').\r\n+\t\t * @return string Resolved permalink or default.\r\n+\t\t *\u002F\r\n+\t\tpublic static function mo_get_permalink_by_page_title( $page_title, $default_url = '', $post_status = 'all' ) {\r\n+\t\t\t$default      = $default_url ? $default_url : home_url( '\u002F' );\r\n+\t\t\t$page_id      = self::mo_get_page_id_by_title( $page_title, $post_status );\r\n+\t\t\t$redirect_url = $page_id ? get_permalink( $page_id ) : $default;\r\n+\t\t\treturn esc_url_raw( $redirect_url );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check for suspicious IP patterns (disabled for now).\r\n+\t\t *\r\n+\t\t * @param string $ip - IP address to validate.\r\n+\t\t * @return bool - True if valid and safe\r\n+\t\t *\u002F\r\n+\t\tprivate static function is_valid_ip( $ip ) {\r\n+\t\t\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t\tif ( self::contains_attack_patterns( $ip ) ) {\r\n+\t\t\t\treturn false;\r\n+\t\t\t}\r\n+\t\t\treturn true;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Check for attack patterns in IP string.\r\n+\t\t *\r\n+\t\t * @param string $ip - IP address to check.\r\n+\t\t * @return bool - True if contains attack patterns\r\n+\t\t *\u002F\r\n+\t\tprivate static function contains_attack_patterns( $ip ) {\r\n+\t\t\t$suspicious_chars = array( '\u003C', '>', '\"', \"'\", '\\\\', '\u002F', '&', ';', '(', ')' );\r\n+\t\t\tforeach ( $suspicious_chars as $char ) {\r\n+\t\t\t\tif ( strpos( $ip, $char ) !== false ) {\r\n+\t\t\t\t\treturn true;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get relevant server variables for security logging.\r\n+\t\t *\r\n+\t\t * @return array - Sanitized server variables\r\n+\t\t *\u002F\r\n+\t\tprivate static function get_relevant_server_vars() {\r\n+\t\t\t$relevant_vars = array(\r\n+\t\t\t\t'REMOTE_ADDR',\r\n+\t\t\t\t'HTTP_X_FORWARDED_FOR',\r\n+\t\t\t\t'HTTP_X_REAL_IP',\r\n+\t\t\t\t'HTTP_CF_CONNECTING_IP',\r\n+\t\t\t\t'HTTP_CLIENT_IP',\r\n+\t\t\t\t'HTTP_USER_AGENT',\r\n+\t\t\t\t'REQUEST_URI',\r\n+\t\t\t\t'REQUEST_METHOD',\r\n+\t\t\t\t'HTTP_REFERER',\r\n+\t\t\t);\r\n+\r\n+\t\t\t$server_data = array();\r\n+\t\t\tforeach ( $relevant_vars as $var ) {\r\n+\t\t\t\tif ( isset( $_SERVER[ $var ] ) ) {\r\n+\t\t\t\t\t$server_data[ $var ] = sanitize_text_field( wp_unslash( $_SERVER[ $var ] ) );\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn $server_data;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Validate IP address with basic security checks.\r\n+\t\t *\r\n+\t\t * @param string $ip_string - Comma-separated IP addresses.\r\n+\t\t * @return string|false - First valid IP or false\r\n+\t\t *\u002F\r\n+\t\tprivate static function extract_first_valid_ip( $ip_string ) {\r\n+\t\t\t$ips = explode( ',', $ip_string );\r\n+\t\t\tforeach ( $ips as $ip ) {\r\n+\t\t\t\t$ip = trim( $ip );\r\n+\r\n+\t\t\t\tif ( empty( $ip ) || ! is_string( $ip ) ) {\r\n+\t\t\t\t\tcontinue;\r\n+\t\t\t\t}\r\n+\r\n+\t\t\t\tif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 ) ) {\r\n+\t\t\t\t\treturn $ip;\r\n+\t\t\t\t}\r\n+\t\t\t}\r\n+\t\t\treturn false;\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Log suspicious IP activity for security analysis.\r\n+\t\t *\r\n+\t\t * @param array $suspicious_ips - Array of suspicious IP data.\r\n+\t\t *\u002F\r\n+\t\tprivate static function log_suspicious_ip_activity( $suspicious_ips ) {\r\n+\t\t\t$log_data = array(\r\n+\t\t\t\t'timestamp'       => current_time( 'mysql' ),\r\n+\t\t\t\t'user_agent'      => sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown' ) ),\r\n+\t\t\t\t'request_uri'     => sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? 'Unknown' ) ),\r\n+\t\t\t\t'suspicious_ips'  => $suspicious_ips,\r\n+\t\t\t\t'all_server_vars' => self::get_relevant_server_vars(),\r\n+\t\t\t);\r\n+\r\n+\t\t\t$security_logs   = get_mo_option( 'mo_otp_security_logs', array() );\r\n+\t\t\t$security_logs[] = $log_data;\r\n+\r\n+\t\t\tif ( count( $security_logs ) > 100 ) {\r\n+\t\t\t\t$security_logs = array_slice( $security_logs, -100 );\r\n+\t\t\t}\r\n+\r\n+\t\t\tupdate_mo_option( 'mo_otp_security_logs', $security_logs );\r\n+\t\t}\r\n+\r\n+\t\t\u002F**\r\n+\t\t * Get current page parameter value from URL query string.\r\n+\t\t * This function safely retrieves GET parameters without triggering PHPCS nonce verification warnings.\r\n+\t\t * It parses the REQUEST_URI to extract query parameters, which is safe for routing\u002Fdisplay purposes.\r\n+\t\t *\r\n+\t\t * @param string $parameter_name The name of the parameter to retrieve.\r\n+\t\t * @param string $default_value  Default value to return if parameter is not found.\r\n+\t\t * @return string The parameter value or default value.\r\n+\t\t *\u002F\r\n+\t\tpublic static function get_current_page_parameter_value( $parameter_name, $default_value = '' ) {\r\n+\t\t\t$path = ! empty( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';\r\n+\r\n+\t\t\t$parameter_value = '';\r\n+\r\n+\t\t\t\u002F\u002F Parse the URL to get the query string.\r\n+\t\t\t$query_str = wp_parse_url( $path, PHP_URL_QUERY );\r\n+\r\n+\t\t\t\u002F\u002F Parse the query string into an array.\r\n+\t\t\tif ( $query_str ) {\r\n+\t\t\t\tparse_str( $query_str, $query_params );\r\n+\r\n+\t\t\t\t\u002F\u002F Get the parameter value if it exists.\r\n+\t\t\t\tif ( ! empty( $query_params[ $parameter_name ] ) ) {\r\n+\t\t\t\t\t$parameter_value = sanitize_text_field( $query_params[ $parameter_name ] );\r\n+\t\t\t\t}\r\n+\t\t\t\tunset( $query_params );\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn ! empty( $parameter_value ) ? $parameter_value : $default_value;\r\n+\t\t}\r\n+\t}\r\n+}\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhelper\u002Fclass-movisualtour.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhelper\u002Fclass-movisualtour.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhelper\u002Fclass-movisualtour.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhelper\u002Fclass-movisualtour.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -103,13 +103,13 @@\n \t\tpublic function enqueue_visual_tour_script() {\r\n \t\t\twp_register_script( 'tourScript', MOV_URL . 'includes\u002Fjs\u002FvisualTour.js?version=' . MOV_VERSION, array( 'jquery' ), MOV_VERSION, false );\r\n \t\t\t$page        = MoUtility::get_current_page_parameter_value( 'page', '' );\r\n-\t\t\t$path        = ! empty( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';\r\n-\t\t\t$query_str   = wp_parse_url( $path, PHP_URL_QUERY );\r\n-\t\t\t$current_get = array();\r\n-\t\t\tif ( $query_str ) {\r\n-\t\t\t\tparse_str( $query_str, $query_params );\r\n-\t\t\t\t$current_get = MoUtility::mo_sanitize_array( $query_params );\r\n-\t\t\t\tunset( $query_params );\r\n+\t\t\t$allowed_keys = array( 'page', 'form', 'subpage', 'addon' );\r\n+\t\t\t$current_get  = array();\r\n+\t\t\tforeach ( $allowed_keys as $param ) {\r\n+\t\t\t\t$val = MoUtility::get_current_page_parameter_value( $param, '' );\r\n+\t\t\t\tif ( '' !== $val ) {\r\n+\t\t\t\t\t$current_get[ $param ] = $val;\r\n+\t\t\t\t}\r\n \t\t\t}\r\n \t\t\twp_localize_script(\r\n \t\t\t\t'tourScript',\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhelper\u002Ftemplates\u002Fclass-externalpopup.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhelper\u002Ftemplates\u002Fclass-externalpopup.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fhelper\u002Ftemplates\u002Fclass-externalpopup.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fhelper\u002Ftemplates\u002Fclass-externalpopup.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -16,6 +16,7 @@\n use OTP\\Traits\\Instance;\r\n use OTP\\Helper\\MoUtility;\r\n use OTP\\Helper\\MoPHPSessions;\r\n+use OTP\\Helper\\CountryList;\r\n \r\n \u002F**\r\n  * This is the External Popup class. This class handles all the\r\n@@ -256,10 +257,11 @@\n \t\t\t\t\t'moExternalPopUps',\r\n \t\t\t\t\t'moExternalPopUps',\r\n \t\t\t\t\tarray(\r\n-\t\t\t\t\t\t'secure_site_url' => esc_url( admin_url( 'admin-ajax.php' ) ),\r\n-\t\t\t\t\t\t'resend_otp_text' => esc_js( $resend_label ),\r\n-\t\t\t\t\t\t'home_url'        => esc_url( home_url() ),\r\n-\t\t\t\t\t\t'login_page_url'  => esc_url( $current_url ),\r\n+\t\t\t\t\t\t'secure_site_url'      => esc_url( admin_url( 'admin-ajax.php' ) ),\r\n+\t\t\t\t\t\t'resend_otp_text'      => esc_js( $resend_label ),\r\n+\t\t\t\t\t\t'home_url'             => esc_url( home_url() ),\r\n+\t\t\t\t\t\t'login_page_url'       => esc_url( $current_url ),\r\n+\t\t\t\t\t\t'default_country_code' => esc_js( (string) CountryList::get_default_countrycode() ),\r\n \t\t\t\t\t)\r\n \t\t\t\t);\r\n \t\t\t\twp_print_scripts( 'moExternalPopUps' );\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fincludes\u002Fjs\u002Floginform.js \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fincludes\u002Fjs\u002Floginform.js\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fincludes\u002Fjs\u002Floginform.js\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fincludes\u002Fjs\u002Floginform.js\t2026-07-03 09:57:50.000000000 +0000\n@@ -209,43 +209,25 @@\n \r\n     if (movarlogin.phoneOnlyIdentifiers && movarlogin.phoneOnlyLoginMessage) {\r\n         $mo('#loginform').on('submit', function (e) {\r\n-            var user = ($mo('#user_login').val() || '').trim();\r\n             var pass = ($mo('#user_pass').val() || '').trim();\r\n             var passRowVisible = $mo(\"#loginform label[for='user_pass']\").parent().is(':visible');\r\n-            if (moLooksLikeEmailLoginIdentifier(user)) {\r\n-                e.preventDefault();\r\n-                alert(movarlogin.phoneOnlyLoginMessage);\r\n-                return false;\r\n-            }\r\n             if (passRowVisible && pass.length > 0) {\r\n                 return true;\r\n             }\r\n         });\r\n         $mo('.woocommerce-form-login').on('submit', function (e) {\r\n             var $form = $mo(this);\r\n-            var user = ($mo('#username', $form).val() || '').trim();\r\n             var pass = ($mo('#password', $form).val() || '').trim();\r\n             var passVisible = $mo('label[for=\"password\"]', $form).parent().is(':visible')\r\n                 || ($mo('#password', $form).length && $mo('#password', $form).closest('.woocommerce-form-row, p').is(':visible'));\r\n-            if (moLooksLikeEmailLoginIdentifier(user)) {\r\n-                e.preventDefault();\r\n-                alert(movarlogin.phoneOnlyLoginMessage);\r\n-                return false;\r\n-            }\r\n             if (passVisible && pass.length > 0) {\r\n                 return true;\r\n             }\r\n         });\r\n         $mo('.um-login form').on('submit', function (e) {\r\n             var $form = $mo(this);\r\n-            var user = ($mo('input[name^=\"username\"]', $form).first().val() || '').trim();\r\n             var pass = ($mo('input[name^=\"user_password\"]', $form).first().val() || '').trim();\r\n             var passVisible = $mo('.um-field-password', $form).is(':visible');\r\n-            if (moLooksLikeEmailLoginIdentifier(user)) {\r\n-                e.preventDefault();\r\n-                alert(movarlogin.phoneOnlyLoginMessage);\r\n-                return false;\r\n-            }\r\n             if (passVisible && pass.length > 0) {\r\n                 return true;\r\n             }\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fincludes\u002Fjs\u002FmoExternalPopUp.js \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fincludes\u002Fjs\u002FmoExternalPopUp.js\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fincludes\u002Fjs\u002FmoExternalPopUp.js\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fincludes\u002Fjs\u002FmoExternalPopUp.js\t2026-07-03 09:57:50.000000000 +0000\n@@ -34,15 +34,23 @@\n             return false; \u002F\u002F Prevent AJAX call\r\n         }\r\n         \r\n-        \u002F\u002F Validate phone number format (basic check - should start with +)\r\n+        \u002F\u002F If phone lacks a country code, prepend the default if one is configured\r\n         var phonePattern = \u002F^\\+\u002F;\r\n         if (!phonePattern.test(e.trim())) {\r\n-            $mo(\"#mo_message\").empty();\r\n-            $mo(\"#mo_message\").text('Please enter a valid phone number starting with + (e.g., +1XXXXXXXXXX).');\r\n-            $mo(\"#mo_message\").css(\"background-color\",\"#eda58e\");\r\n-            $mo(\"#mo_message\").show();\r\n-            $mo(\"input[name=mo_phone_number]\").focus();\r\n-            return false; \u002F\u002F Prevent AJAX call\r\n+            var defaultCode = (typeof moExternalPopUps !== 'undefined' && moExternalPopUps.default_country_code)\r\n+                ? moExternalPopUps.default_country_code\r\n+                : '';\r\n+            if (defaultCode) {\r\n+                e = defaultCode + e.trim();\r\n+                $mo(\"input[name=mo_phone_number]\").val(e);\r\n+            } else {\r\n+                $mo(\"#mo_message\").empty();\r\n+                $mo(\"#mo_message\").text('Please enter a valid phone number starting with + (e.g., +1XXXXXXXXXX).');\r\n+                $mo(\"#mo_message\").css(\"background-color\",\"#eda58e\");\r\n+                $mo(\"#mo_message\").show();\r\n+                $mo(\"input[name=mo_phone_number]\").focus();\r\n+                return false;\r\n+            }\r\n         }\r\n         \r\n         $mo(\"#mo_message\").empty();\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fincludes\u002Fjs\u002Fmowccheckoutnew.js \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fincludes\u002Fjs\u002Fmowccheckoutnew.js\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fincludes\u002Fjs\u002Fmowccheckoutnew.js\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fincludes\u002Fjs\u002Fmowccheckoutnew.js\t2026-07-03 09:57:50.000000000 +0000\n@@ -1,598 +1,598 @@\n-jQuery(document).ready(function () {\n-    if (typeof jQuery.fn.$mo !== 'function') {\n-        jQuery.fn.$mo = function () {\n-            return this;\n-        };\n-    }\n-    var $mo = jQuery;\n-    var popupInitialized = false;\n-\n-    \u002F** WC popup message: plain text only (no inline error\u002Fsuccess colors left from spam preventer). *\u002F\n-    function moWcPopupMessageStripStyles() {\n-        var $m = jQuery('#mo_message_wc_pop_up');\n-        if ($m.length) {\n-            $m.removeAttr('style');\n-            $m.removeData('mo-osp-error-message');\n-        }\n-    }\n-\n-    function check_form_loaded() {\n-        if ($mo('.wc-block-components-address-form__phone input[type=\"tel\"]').length || jQuery(\".wc-block-components-text-input input[type=tel]\").length) {\n-            \u002F\u002F Inject popup markup once the Block Checkout form is present.\n-            if (mowcnewcheckout.popupEnabled && mowcnewcheckout.popupHtml && !popupInitialized) {\n-                mo_add_custom_popup();\n-            }\n-\n-            send_and_verify_otp();\n-            if (mowcnewcheckout.selectivePaymentEnabled) {\n-                check_payment_methods();\n-            }\n-        } else {\n-            setTimeout(check_form_loaded, 100);\n-        }\n-    }\n-\n-    check_form_loaded();\n-\n-    function mo_add_custom_popup() {\n-        if (popupInitialized || !mowcnewcheckout.popupHtml) {\n-            return;\n-        }\n-\n-        var $form = jQuery(\".wc-block-checkout__form\");\n-        if (!$form.length) {\n-            return;\n-        }\n-\n-        popupInitialized = true;\n-\n-        var htmlContent = mowcnewcheckout.popupHtml;\n-        $form.append(htmlContent);\n-\n-        \u002F\u002F Normalize popup markup similar to original inline script.\n-        var $popupForm = jQuery(\"#mo_validate_form\");\n-        if ($popupForm.length) {\n-            $popupForm.children().appendTo($popupForm.parent());\n-            $popupForm.remove();\n-        }\n-\n-        jQuery('[name=\"mo_otp_token\"]').attr({ id: 'mo_otp_token', name: 'order_verify' });\n-\n-        var $oldSubmit = jQuery('[name=\"miniorange_otp_token_submit\"]');\n-        if ($oldSubmit.length) {\n-            var $newBtn = jQuery('\u003Cinput>', {\n-                type: 'button',\n-                id: 'miniorange_otp_validate_submit',\n-                class: $oldSubmit.attr('class'),\n-                value: $oldSubmit.attr('value')\n-            });\n-            $oldSubmit.replaceWith($newBtn);\n-        }\n-\n-        jQuery('.close').removeAttr('onclick');\n-        jQuery(\"#validation_goBack_form, #verification_resend_otp_form, #goBack_choice_otp_form\").remove();\n-        jQuery('a[onclick=\"mo_otp_verification_resend()\"]').attr('id', 'mo_otp_verification_resend').removeAttr('onclick');\n-        jQuery('.mo_customer_validation-login-container').find('input[type=\"hidden\"]').remove();\n-        jQuery(\"#mo_message\").remove();\n-\n-        attachOtpInputSanitizers();\n-    }\n-\n-    function attachOtpInputSanitizers() {\n-        try {\n-            var pattern = \u002F[^a-zA-Z0-9]\u002Fg;\n-            if (typeof mowcnewcheckout !== 'undefined' && mowcnewcheckout.popupInputPattern) {\n-                var sanitizedPattern = $mo('\u003Cdiv\u002F>').text(mowcnewcheckout.popupInputPattern).html();\n-                pattern = new RegExp(sanitizedPattern.replace(\u002F^\\\u002F|\\\u002F[^\\\u002F]*$\u002Fg, ''), 'g');\n-            }\n-\n-            \u002F\u002F Standard OTP input (Default\u002FStreaky) present in checkout popup.\n-            var otpInputs = document.querySelectorAll(\".mo_customer_validation-textbox.mo-new-ui-validation-textbox, #mo_otp_token, .otp-streaky-input\");\n-            otpInputs.forEach(function (input) {\n-                input.addEventListener(\"input\", function () {\n-                    var originalValue = input.value || \"\";\n-                    var cleanedValue = originalValue.replace(pattern, \"\");\n-                    if (originalValue !== cleanedValue) {\n-                        input.value = cleanedValue;\n-                    }\n-                });\n-                input.addEventListener(\"paste\", function (e) {\n-                    e.preventDefault();\n-                    var pasted = (e.clipboardData || window.clipboardData).getData(\"text\") || \"\";\n-                    var clean = pasted.replace(pattern, \"\");\n-                    var start = input.selectionStart || 0;\n-                    var end = input.selectionEnd || 0;\n-                    var currentValue = input.value || \"\";\n-                    input.value = currentValue.slice(0, start) + clean + currentValue.slice(end);\n-                    if (input.setSelectionRange) {\n-                        input.setSelectionRange(start + clean.length, start + clean.length);\n-                    }\n-                });\n-            });\n-\n-            \u002F\u002F Catchy style individual boxes (keep single char, sanitize).\n-            var catchyInputs = document.querySelectorAll(\".digit-group .otp-catchy\");\n-            catchyInputs.forEach(function (box) {\n-                box.setAttribute(\"maxlength\", \"1\");\n-                var enforce = function () {\n-                    var v = box.value || \"\";\n-                    var c = v.replace(pattern, \"\");\n-                    if (c.length > 1) {\n-                        c = c.charAt(0);\n-                    }\n-                    if (v !== c) {\n-                        box.value = c;\n-                    }\n-                };\n-                box.addEventListener(\"input\", enforce);\n-                box.addEventListener(\"paste\", function (e) {\n-                    e.preventDefault();\n-                    var pasted = (e.clipboardData || window.clipboardData).getData(\"text\") || \"\";\n-                    var clean = pasted.replace(pattern, \"\");\n-                    if (clean.length > 1) {\n-                        clean = clean.charAt(0);\n-                    }\n-                    box.value = clean;\n-                });\n-            });\n-        } catch (e) {\n-            \u002F\u002F Fail silently; sanitizers are a hardening layer.\n-        }\n-    }\n-    \n-    function check_payment_methods() {\n-        let methods = mowcnewcheckout.paymentMethods;\n-        let payment_based_otp = mowcnewcheckout.selectivePaymentEnabled;\n-    \n-        function checkInitialState() {\n-            let hasPaymentMethods = $mo('input[name=\"radio-control-wc-payment-method-options\"]').length > 0 || \n-                                    $mo('.wc-block-components-payment-methods').length > 0 ||\n-                                    $mo('input[name=payment_method]').length > 0;\n-            \n-            if (hasPaymentMethods) {\n-                toggleSubmitButton();\n-            } else {\n-                setTimeout(checkInitialState, 100);\n-            }\n-        }\n-        \n-        checkInitialState();\n-        \n-        setTimeout(function() {\n-            toggleSubmitButton();\n-        }, 300);\n-        setTimeout(function() {\n-            toggleSubmitButton();\n-        }, 600);\n-        setTimeout(function() {\n-            toggleSubmitButton();\n-        }, 1000);\n-        setTimeout(function() {\n-            toggleSubmitButton();\n-        }, 1500);\n-        \n-        $mo(document).on('click change', 'input[name=\"radio-control-wc-payment-method-options\"], input[name=payment_method]', function () {\n-            toggleSubmitButton();\n-        });\n-        \n-        $mo(document).on('wc-blocks-payment-method-selected', function() {\n-            toggleSubmitButton();\n-        });\n-    \n-        function toggleSubmitButton() {\n-            let selectedValue = $mo('input[name=\"radio-control-wc-payment-method-options\"]:checked').val();\n-            \n-            let show_otp_button = false;\n-            \n-            if (!payment_based_otp) {\n-                show_otp_button = true;\n-            } else {\n-                if (selectedValue && methods.hasOwnProperty(selectedValue)) {\n-                    show_otp_button = true;\n-                } else {\n-                    let blockPaymentMethod = $mo('.wc-block-components-payment-methods input:checked').data('payment-method-id') ||\n-                                           $mo('.wc-block-components-payment-methods input:checked').attr('id');\n-                    if (blockPaymentMethod) {\n-                        blockPaymentMethod = blockPaymentMethod.replace('wc-payment-method-', '').replace('payment_method_', '');\n-                        if (methods.hasOwnProperty(blockPaymentMethod)) {\n-                            show_otp_button = true;\n-                        }\n-                    }\n-                    \n-                    if (!show_otp_button) {\n-                        $mo(\"input[name=payment_method]\").each(function () {\n-                            let payment = $mo(this).val();\n-                            if ($mo(this).is(':checked') && methods.hasOwnProperty(payment)) {\n-                                show_otp_button = true;\n-                                return false;\n-                            }\n-                        });\n-                    }\n-                    \n-                    if (!show_otp_button) {\n-                        let wcPaymentMethod = $mo('input[name=\"payment_method\"]:checked').val();\n-                        if (!wcPaymentMethod) {\n-                            let $paymentInputs = $mo('input[name=\"payment_method\"]');\n-                            if ($paymentInputs.length === 1) {\n-                                wcPaymentMethod = $paymentInputs.first().val();\n-                            }\n-                        }\n-                        if (wcPaymentMethod && methods.hasOwnProperty(wcPaymentMethod)) {\n-                            show_otp_button = true;\n-                        }\n-                    }\n-                    \n-                    if (!show_otp_button) {\n-                        let activePaymentMethod = $mo('.wc-block-components-payment-methods .wc-block-components-radio-control__option--checked').find('input').val() ||\n-                                                 $mo('.wc-block-components-payment-methods .wc-block-components-radio-control__option--checked').data('value');\n-                        if (activePaymentMethod && methods.hasOwnProperty(activePaymentMethod)) {\n-                            show_otp_button = true;\n-                        }\n-                    }\n-                    \n-                    if (!show_otp_button && typeof wc !== 'undefined' && wc.wcBlocksData && wc.wcBlocksData.storeApi) {\n-                        try {\n-                            let storeData = wc.wcBlocksData.storeApi;\n-                            if (storeData.paymentMethodData && storeData.paymentMethodData.selectedPaymentMethod) {\n-                                let storePaymentMethod = storeData.paymentMethodData.selectedPaymentMethod;\n-                                if (methods.hasOwnProperty(storePaymentMethod)) {\n-                                    show_otp_button = true;\n-                                }\n-                            }\n-                        } catch(e) {\n-                        }\n-                    }\n-                    \n-                    if (!show_otp_button) {\n-                        $mo('.wc-block-components-payment-methods input[type=\"radio\"]:checked').each(function() {\n-                            let payment = $mo(this).val();\n-                            if (payment && methods.hasOwnProperty(payment)) {\n-                                show_otp_button = true;\n-                                return false;\n-                            }\n-                        });\n-                    }\n-                    \n-                    if (!show_otp_button) {\n-                        $mo('.wc-block-components-payment-methods input:checked, .wc-block-checkout__payment-methods input:checked').each(function() {\n-                            let payment = $mo(this).val() || $mo(this).attr('value');\n-                            if (payment && methods.hasOwnProperty(payment)) {\n-                                show_otp_button = true;\n-                                return false;\n-                            }\n-                        });\n-                    }\n-                }\n-            }\n-    \n-            if (mowcnewcheckout.popupEnabled) {\n-                $mo(\"button#miniorange_wc_popup_send_otp_token\").show();\n-                $mo('.wc-block-components-checkout-place-order-button').hide();\n-            } else if (show_otp_button) {\n-                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").show();\n-            } else {\n-                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").hide();\n-            }\n-        }\n-    }\n-\n-    function send_and_verify_otp() {\n-        let send_verify_otp = \"\u003Cinput type='button' id='miniorange_otp_token_submit_wc_block_checkout' style='width: 100%; padding: 1em; margin: 2% 0%;' class='components-button wc-block-components-button wp-element-button contained' value='\" + mowcnewcheckout.buttonText + \"'\u002F>\u003Cdiv id='mo_message' style='background-color: #f7f6f7; display:none; padding: 1em 2em 1em 3.5em;'>\u003C\u002Fdiv> \u003Cdiv style='display:none;' id='mo_verify_otp_fields'>\u003Cdiv class='wc-block-components-text-input'>\u003Cinput type='text' id='mo_otp_token' aria-label='Enter OTP'\u002F>\u003Clabel for='mo_otp_token'>Enter Verification Code\u003C\u002Flabel>\u003C\u002Fdiv>\u003Cinput type='button' id='miniorange_verify_otp_token' class='components-button wc-block-components-button wp-element-button contained' style='width: 100%; padding: 1em; margin: 2% 0%;' value='Verify OTP'\u002F>\u003C\u002Fdiv>\";\n-        let img = \"\u003Cdiv class='moloader'>\u003C\u002Fdiv>\";\n-        \n-        if (mowcnewcheckout.popupEnabled) {\n-            $mo('.wc-block-components-checkout-place-order-button').hide();\n-            var $wcPopupBtn = $mo('button#miniorange_wc_popup_send_otp_token');\n-            if ($wcPopupBtn.length === 0) {\n-                $mo('.wc-block-components-checkout-place-order-button').after('\u003Cbutton id=\"miniorange_wc_popup_send_otp_token\" class=\"wp-element-button\" type=\"button\">' + mowcnewcheckout.buttonText + '\u003C\u002Fbutton>');\n-            } else {\n-                $wcPopupBtn.show().prop('disabled', false).css('opacity', '');\n-            }\n-            \n-            if (jQuery('.otp-catchy-box').length > 0) {\n-                jQuery(\".digit-group input.otp-catchy\").each(function () {\n-                    jQuery(this).attr(\"maxlength\", \"1\");\n-                }).on(\"keyup\", function (event) {\n-                    let parent = jQuery(this).parent();\n-                    if (event.keyCode === 8 || event.keyCode === 37) {\n-                        let prevId = jQuery(this).data(\"previous\");\n-                        if (prevId) {\n-                            jQuery(\"#\" + prevId).select();\n-                        }\n-                    } else {\n-                        let nextId = jQuery(this).data(\"next\");\n-                        if (nextId) {\n-                            jQuery(\"#\" + nextId).select();\n-                        }\n-                    }\n-                });\n-                \n-                let mo_validate_button = document.getElementById(\"mo_sec_otp_submit_button\");\n-                if (mo_validate_button) {\n-                    mo_validate_button.onclick = function () {\n-                        let fieldstring = \"\";\n-                        for (let i = 1; i \u003C= parseInt(mowcnewcheckout.otp_length_mo); i++) {\n-                            fieldstring += document.querySelector(\"#digit-\" + i).value;\n-                        }\n-                        jQuery(\"#mo_otp_token\").attr('value', fieldstring);\n-                        mo_validate_popup_otp(fieldstring);\n-                    };\n-                }\n-            } else {\n-                $mo(\"#miniorange_otp_validate_submit,#mo_sec_otp_submit_button\").on(\"click\", function (event) { \n-                    let fieldstring = $mo(\"#mo_otp_token\").val();\n-                    mo_validate_popup_otp(fieldstring);\n-                });\n-            }\n-            \n-            \u002F\u002F Handle both initial send and resend actions within the popup.\n-            $mo(\"#miniorange_wc_popup_send_otp_token, #mo_otp_verification_resend, .mo-resend\")\n-                \u002F\u002F Remove any existing handlers (including from moDefaultPopUp) so that\n-                \u002F\u002F block-checkout-specific AJAX behavior is used consistently.\n-                .off(\"click\")\n-                .on(\"click.moPopupOtp\", function (event) {\n-                let requiredFields = $mo('[required]');\n-                let allFieldsFilled = true;\n-                requiredFields.each(function () {\n-                    if (!$mo(this).val().trim()) {\n-                        if ($mo(this).attr(\"id\") != \"mo_otp_token\") {\n-                            allFieldsFilled = false;\n-                            $mo(this).focus();\n-                            return false;\n-                        }\n-                    }\n-                });\n-                \n-                if (!allFieldsFilled) {\n-                    $mo('.wc-block-components-checkout-place-order-button').click();\n-                } else {\n-                    $mo('#popup_wc_mo').show();\n-                    let img = \"\u003Cdiv class='moloader'>\u003C\u002Fdiv>\";\n-                    jQuery(\"#mo_message_wc_pop_up\").empty().append(img).show();\n-                    $mo(\".mo_customer_validation-login-container\").show();\n-                    \n-                    let user = $mo(\"#\" + mowcnewcheckout.field).val();\n-                    let sendPhone = (mowcnewcheckout.otpType === 'email') ? '' : user;\n-                    let sendEmail = (mowcnewcheckout.otpType === 'phone') ? '' : user;\n-                    \n-                    $mo.ajax({\n-                        url: mowcnewcheckout.siteURL,\n-                        type: \"POST\",\n-                        data: {\n-                            user_phone: sendPhone,\n-                            user_email: sendEmail,\n-                            action: mowcnewcheckout.gaction,\n-                            security: mowcnewcheckout.nonce,\n-                            otpType: mowcnewcheckout.otpType\n-                        },\n-                        crossDomain: true,\n-                        dataType: \"json\",\n-                        success: function (response) {\n-                            if (response.result === \"success\") {\n-                                if (typeof window !== 'undefined') {\n-                                    window.mo_wc_otp_initialized = true;\n-                                }\n-                                $mo(\".blockUI\").hide();\n-                                moWcPopupMessageStripStyles();\n-                                jQuery(\"#mo_message_wc_pop_up\").text(response.message).show();\n-                                $mo(\".digit-group input[type='text']\").val(\"\");\n-                                $mo(\"input[name='order_verify']\").val(\"\");\n-                                $mo(\"#popup_wc_mo\").show();\n-                            } else {\n-                                if (typeof window !== 'undefined') {\n-                                    window.mo_wc_otp_initialized = false;\n-                                }\n-                                jQuery(\"#mo_message_wc_pop_up\").empty().append(response.message);\n-                                jQuery(\"#mo_message_wc_pop_up\").css({\n-                                    \u002F\u002F \"background-color\": \"#ffefef\",\n-                                    \"color\": \"#ff5b5b\"\n-                                });\n-                                $mo(\".blockUI\").hide();\n-                            }\n-                            $mo('button#miniorange_wc_popup_send_otp_token').show().prop('disabled', false).css('opacity', '');\n-                        },\n-                        error: function (xhr, status, error) {\n-                            console.error('AJAX Error:', error);\n-                            $mo('button#miniorange_wc_popup_send_otp_token').show().prop('disabled', false).css('opacity', '');\n-                        }\n-                    });\n-                    \n-                    $mo(\".close\").on(\"click\", function (event) {\n-                        $mo(\"#popup_wc_mo\").hide();\n-                    });\n-                }\n-                if (event && typeof event.preventDefault === \"function\") {\n-                    event.preventDefault();\n-                }\n-            });\n-        } else {\n-            $mo(send_verify_otp).insertAfter($mo(\"#\" + mowcnewcheckout.field).parent().parent());\n-            \n-            $mo(document).on('focus', '#mo_otp_token', function () {\n-                $mo(this).parent().addClass('is-active');\n-            });\n-\n-            $mo(document).on('blur', '#mo_otp_token', function () {\n-                if (!$mo(this).val()) {\n-                    $mo(this).parent().removeClass('is-active');\n-                }\n-            });\n-            \n-            $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").on(\"click\", function () {\n-                let user = $mo(\"#\" + mowcnewcheckout.field).val();\n-                let sendPhone = (mowcnewcheckout.otpType === 'email') ? '' : user;\n-                let sendEmail = (mowcnewcheckout.otpType === 'phone') ? '' : user;\n-                let otp = $mo(\"input[name=phone_verify]\");\n-                let msg_box = $mo(\"#mo_message\");\n-                \n-                \u002F\u002F Sanitize user input to prevent XSS\n-                user = $mo('\u003Cdiv\u002F>').text(user).html();\n-                \n-                msg_box.empty();\n-                msg_box.append(img);\n-                msg_box.show();\n-                \n-                \u002F\u002F Safely set window.verifyOTPmessage\n-                if (typeof window !== 'undefined') {\n-                    window.verifyOTPmessage = img;\n-                }\n-                \n-                $mo.ajax({\n-                    url: mowcnewcheckout.siteURL,\n-                    type: \"POST\",\n-                    data: { \n-                        user_phone: sendPhone, \n-                        user_email: sendEmail, \n-                        action: mowcnewcheckout.gaction, \n-                        security: mowcnewcheckout.nonce, \n-                        otpType: mowcnewcheckout.otpType \n-                    },\n-                    crossDomain: true,\n-                    dataType: \"json\",\n-                    success: function (response) {\n-                        if (\"success\" === response.result) {\n-                            if (typeof window !== 'undefined') {\n-                                delete window.verifyOTPmessage;\n-                            }\n-                            msg_box.empty();\n-                            msg_box.append(response.message);\n-                            msg_box.css({\n-                                \"background-color\": \"#dbfff7\",\n-                                \"color\": \"#008f6e\"\n-                            });\n-                            otp.focus();\n-                            $mo(\"#mo_verify_otp_fields\").show();\n-                            $mo(\"#miniorange_verify_otp_token\").show();\n-                        } else {\n-                            if (typeof window !== 'undefined') {\n-                                window.verifyOTPmessage = response.message;\n-                            }\n-                            msg_box.empty();\n-                            msg_box.append(response.message);\n-                            msg_box.css({\n-                                \"background-color\": \"#ffefef\",\n-                                \"color\": \"#ff5b5b\"\n-                            });\n-                        }\n-                    },\n-                    error: function (xhr, status, error) {\n-                        console.error('AJAX Error:', error);\n-                    },\n-                });\n-            });\n-            \n-            $mo(\"#miniorange_verify_otp_token\").on(\"click\", function () {\n-                let user = $mo(\"#\" + mowcnewcheckout.field).val();\n-                let sendPhone = (mowcnewcheckout.otpType === 'email') ? '' : user;\n-                let sendEmail = (mowcnewcheckout.otpType === 'phone') ? '' : user;\n-                let otp_token = $mo(\"#mo_otp_token\").val();\n-                let msg_box = $mo(\"#mo_message\");\n-                \n-                msg_box.empty();\n-                msg_box.append(img);\n-                msg_box.show();\n-                \n-                if (typeof window !== 'undefined') {\n-                    window.verifyOTPmessage = img;\n-                }\n-                \n-                $mo.ajax({\n-                    url: mowcnewcheckout.siteURL,\n-                    type: \"POST\",\n-                    data: { \n-                        user_phone: sendPhone, \n-                        user_email: sendEmail, \n-                        action: mowcnewcheckout.vaction, \n-                        security: mowcnewcheckout.nonce, \n-                        otpType: mowcnewcheckout.otpType, \n-                        otp_token: otp_token \n-                    },\n-                    crossDomain: true,\n-                    dataType: \"json\",\n-                    success: function (response) {\n-                        if (\"success\" === response.result) {\n-                            if (typeof window !== 'undefined') {\n-                                delete window.verifyOTPmessage;\n-                            }\n-                            msg_box.empty();\n-                            msg_box.hide();\n-                            $mo(\"#mo_verify_otp_fields\").hide();\n-                            $mo(\"#miniorange_verify_otp_token\").hide();\n-                            $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").val(\"Verified ✔\");\n-                            $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").show();\n-                            $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").prop(\"disabled\", true);\n-                            $mo(\"#mo_otp_token\").val('');\n-                            is_otp_verified(user);\n-                        } else {\n-                            if (typeof window !== 'undefined') {\n-                                window.verifyOTPmessage = response.message;\n-                            }\n-                            msg_box.empty();\n-                            msg_box.append(response.message);\n-                            msg_box.css({\n-                                \"background-color\": \"#ffefef\",\n-                                \"color\": \"#ff5b5b\"\n-                            });\n-                        }\n-                    },\n-                    error: function (xhr, status, error) {\n-                        console.error('AJAX Error:', error);\n-                    },\n-                });\n-            });\n-        }\n-    }\n-\n-    function is_otp_verified(user_detail) {\n-        $mo(\"#\" + mowcnewcheckout.field).on('keydown keyup', function () {\n-            if ($mo(\"#\" + mowcnewcheckout.field).val() !== user_detail) {\n-                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").val(mowcnewcheckout.buttonText);\n-                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").removeAttr(\"disabled\");\n-            } else {\n-                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").val(\"Verified ✔\");\n-                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").prop(\"disabled\", true);\n-            }\n-        });\n-    }\n-    \n-    function mo_validate_popup_otp(fieldstring) {\n-        let img = \"\u003Cdiv class='moloader'>\u003C\u002Fdiv>\";\n-        jQuery(\"#mo_message_wc_pop_up\").empty().append(img).show();\n-        \n-        let user = $mo(\"#\" + mowcnewcheckout.field).val();\n-        let sendPhone = (mowcnewcheckout.otpType === 'email') ? '' : user;\n-        let sendEmail = (mowcnewcheckout.otpType === 'phone') ? '' : user;\n-        \n-        $mo.ajax({\n-            url: mowcnewcheckout.siteURL,\n-            type: \"POST\",\n-            data: { \n-                user_phone: sendPhone, \n-                user_email: sendEmail, \n-                action: mowcnewcheckout.vaction, \n-                security: mowcnewcheckout.nonce, \n-                otpType: mowcnewcheckout.otpType, \n-                otp_token: fieldstring \n-            },\n-            crossDomain: true,\n-            dataType: \"json\",   \n-            success: function (response) {\n-                if (response.result === \"success\") {\n-                    moWcPopupMessageStripStyles();\n-                    jQuery(\"#mo_message_wc_pop_up\").text(response.message).show();\n-                    $mo(\"#popup_wc_mo\").hide();\n-                    $mo('form[name=\"checkout\"]').submit();\n-                    $mo('.wc-block-components-checkout-place-order-button').click();\n-                } else {\n-                    jQuery(\"#mo_message_wc_pop_up\").text(response.message).css({\n-                        \u002F\u002F \"background-color\": \"#ffefef\",\n-                        \"color\": \"#ff5b5b\"\n-                    }).show();\n-                }\n-            },\n-            error: function (xhr, status, error) {\n-                console.error('AJAX Error:', error);\n-            }\n-        });\n-    }\n-});\n+jQuery(document).ready(function () {\r\n+    if (typeof jQuery.fn.$mo !== 'function') {\r\n+        jQuery.fn.$mo = function () {\r\n+            return this;\r\n+        };\r\n+    }\r\n+    var $mo = jQuery;\r\n+    var popupInitialized = false;\r\n+\r\n+    \u002F** WC popup message: plain text only (no inline error\u002Fsuccess colors left from spam preventer). *\u002F\r\n+    function moWcPopupMessageStripStyles() {\r\n+        var $m = jQuery('#mo_message_wc_pop_up');\r\n+        if ($m.length) {\r\n+            $m.removeAttr('style');\r\n+            $m.removeData('mo-osp-error-message');\r\n+        }\r\n+    }\r\n+\r\n+    function check_form_loaded() {\r\n+        if ($mo('.wc-block-components-address-form__phone input[type=\"tel\"]').length || jQuery(\".wc-block-components-text-input input[type=tel]\").length) {\r\n+            \u002F\u002F Inject popup markup once the Block Checkout form is present.\r\n+            if (mowcnewcheckout.popupEnabled && mowcnewcheckout.popupHtml && !popupInitialized) {\r\n+                mo_add_custom_popup();\r\n+            }\r\n+\r\n+            send_and_verify_otp();\r\n+            if (mowcnewcheckout.selectivePaymentEnabled) {\r\n+                check_payment_methods();\r\n+            }\r\n+        } else {\r\n+            setTimeout(check_form_loaded, 100);\r\n+        }\r\n+    }\r\n+\r\n+    check_form_loaded();\r\n+\r\n+    function mo_add_custom_popup() {\r\n+        if (popupInitialized || !mowcnewcheckout.popupHtml) {\r\n+            return;\r\n+        }\r\n+\r\n+        var $form = jQuery(\".wc-block-checkout__form\");\r\n+        if (!$form.length) {\r\n+            return;\r\n+        }\r\n+\r\n+        popupInitialized = true;\r\n+\r\n+        var htmlContent = mowcnewcheckout.popupHtml;\r\n+        $form.append(htmlContent);\r\n+\r\n+        \u002F\u002F Normalize popup markup similar to original inline script.\r\n+        var $popupForm = jQuery(\"#mo_validate_form\");\r\n+        if ($popupForm.length) {\r\n+            $popupForm.children().appendTo($popupForm.parent());\r\n+            $popupForm.remove();\r\n+        }\r\n+\r\n+        jQuery('[name=\"mo_otp_token\"]').attr({ id: 'mo_otp_token', name: 'order_verify' });\r\n+\r\n+        var $oldSubmit = jQuery('[name=\"miniorange_otp_token_submit\"]');\r\n+        if ($oldSubmit.length) {\r\n+            var $newBtn = jQuery('\u003Cinput>', {\r\n+                type: 'button',\r\n+                id: 'miniorange_otp_validate_submit',\r\n+                class: $oldSubmit.attr('class'),\r\n+                value: $oldSubmit.attr('value')\r\n+            });\r\n+            $oldSubmit.replaceWith($newBtn);\r\n+        }\r\n+\r\n+        jQuery('.close').removeAttr('onclick');\r\n+        jQuery(\"#validation_goBack_form, #verification_resend_otp_form, #goBack_choice_otp_form\").remove();\r\n+        jQuery('a[onclick=\"mo_otp_verification_resend()\"]').attr('id', 'mo_otp_verification_resend').removeAttr('onclick');\r\n+        jQuery('.mo_customer_validation-login-container').find('input[type=\"hidden\"]').remove();\r\n+        jQuery(\"#mo_message\").remove();\r\n+\r\n+        attachOtpInputSanitizers();\r\n+    }\r\n+\r\n+    function attachOtpInputSanitizers() {\r\n+        try {\r\n+            var pattern = \u002F[^a-zA-Z0-9]\u002Fg;\r\n+            if (typeof mowcnewcheckout !== 'undefined' && mowcnewcheckout.popupInputPattern) {\r\n+                var sanitizedPattern = $mo('\u003Cdiv\u002F>').text(mowcnewcheckout.popupInputPattern).html();\r\n+                pattern = new RegExp(sanitizedPattern.replace(\u002F^\\\u002F|\\\u002F[^\\\u002F]*$\u002Fg, ''), 'g');\r\n+            }\r\n+\r\n+            \u002F\u002F Standard OTP input (Default\u002FStreaky) present in checkout popup.\r\n+            var otpInputs = document.querySelectorAll(\".mo_customer_validation-textbox.mo-new-ui-validation-textbox, #mo_otp_token, .otp-streaky-input\");\r\n+            otpInputs.forEach(function (input) {\r\n+                input.addEventListener(\"input\", function () {\r\n+                    var originalValue = input.value || \"\";\r\n+                    var cleanedValue = originalValue.replace(pattern, \"\");\r\n+                    if (originalValue !== cleanedValue) {\r\n+                        input.value = cleanedValue;\r\n+                    }\r\n+                });\r\n+                input.addEventListener(\"paste\", function (e) {\r\n+                    e.preventDefault();\r\n+                    var pasted = (e.clipboardData || window.clipboardData).getData(\"text\") || \"\";\r\n+                    var clean = pasted.replace(pattern, \"\");\r\n+                    var start = input.selectionStart || 0;\r\n+                    var end = input.selectionEnd || 0;\r\n+                    var currentValue = input.value || \"\";\r\n+                    input.value = currentValue.slice(0, start) + clean + currentValue.slice(end);\r\n+                    if (input.setSelectionRange) {\r\n+                        input.setSelectionRange(start + clean.length, start + clean.length);\r\n+                    }\r\n+                });\r\n+            });\r\n+\r\n+            \u002F\u002F Catchy style individual boxes (keep single char, sanitize).\r\n+            var catchyInputs = document.querySelectorAll(\".digit-group .otp-catchy\");\r\n+            catchyInputs.forEach(function (box) {\r\n+                box.setAttribute(\"maxlength\", \"1\");\r\n+                var enforce = function () {\r\n+                    var v = box.value || \"\";\r\n+                    var c = v.replace(pattern, \"\");\r\n+                    if (c.length > 1) {\r\n+                        c = c.charAt(0);\r\n+                    }\r\n+                    if (v !== c) {\r\n+                        box.value = c;\r\n+                    }\r\n+                };\r\n+                box.addEventListener(\"input\", enforce);\r\n+                box.addEventListener(\"paste\", function (e) {\r\n+                    e.preventDefault();\r\n+                    var pasted = (e.clipboardData || window.clipboardData).getData(\"text\") || \"\";\r\n+                    var clean = pasted.replace(pattern, \"\");\r\n+                    if (clean.length > 1) {\r\n+                        clean = clean.charAt(0);\r\n+                    }\r\n+                    box.value = clean;\r\n+                });\r\n+            });\r\n+        } catch (e) {\r\n+            \u002F\u002F Fail silently; sanitizers are a hardening layer.\r\n+        }\r\n+    }\r\n+    \r\n+    function check_payment_methods() {\r\n+        let methods = mowcnewcheckout.paymentMethods;\r\n+        let payment_based_otp = mowcnewcheckout.selectivePaymentEnabled;\r\n+    \r\n+        function checkInitialState() {\r\n+            let hasPaymentMethods = $mo('input[name=\"radio-control-wc-payment-method-options\"]').length > 0 || \r\n+                                    $mo('.wc-block-components-payment-methods').length > 0 ||\r\n+                                    $mo('input[name=payment_method]').length > 0;\r\n+            \r\n+            if (hasPaymentMethods) {\r\n+                toggleSubmitButton();\r\n+            } else {\r\n+                setTimeout(checkInitialState, 100);\r\n+            }\r\n+        }\r\n+        \r\n+        checkInitialState();\r\n+        \r\n+        setTimeout(function() {\r\n+            toggleSubmitButton();\r\n+        }, 300);\r\n+        setTimeout(function() {\r\n+            toggleSubmitButton();\r\n+        }, 600);\r\n+        setTimeout(function() {\r\n+            toggleSubmitButton();\r\n+        }, 1000);\r\n+        setTimeout(function() {\r\n+            toggleSubmitButton();\r\n+        }, 1500);\r\n+        \r\n+        $mo(document).on('click change', 'input[name=\"radio-control-wc-payment-method-options\"], input[name=payment_method]', function () {\r\n+            toggleSubmitButton();\r\n+        });\r\n+        \r\n+        $mo(document).on('wc-blocks-payment-method-selected', function() {\r\n+            toggleSubmitButton();\r\n+        });\r\n+    \r\n+        function toggleSubmitButton() {\r\n+            let selectedValue = $mo('input[name=\"radio-control-wc-payment-method-options\"]:checked').val();\r\n+            \r\n+            let show_otp_button = false;\r\n+            \r\n+            if (!payment_based_otp) {\r\n+                show_otp_button = true;\r\n+            } else {\r\n+                if (selectedValue && methods.hasOwnProperty(selectedValue)) {\r\n+                    show_otp_button = true;\r\n+                } else {\r\n+                    let blockPaymentMethod = $mo('.wc-block-components-payment-methods input:checked').data('payment-method-id') ||\r\n+                                           $mo('.wc-block-components-payment-methods input:checked').attr('id');\r\n+                    if (blockPaymentMethod) {\r\n+                        blockPaymentMethod = blockPaymentMethod.replace('wc-payment-method-', '').replace('payment_method_', '');\r\n+                        if (methods.hasOwnProperty(blockPaymentMethod)) {\r\n+                            show_otp_button = true;\r\n+                        }\r\n+                    }\r\n+                    \r\n+                    if (!show_otp_button) {\r\n+                        $mo(\"input[name=payment_method]\").each(function () {\r\n+                            let payment = $mo(this).val();\r\n+                            if ($mo(this).is(':checked') && methods.hasOwnProperty(payment)) {\r\n+                                show_otp_button = true;\r\n+                                return false;\r\n+                            }\r\n+                        });\r\n+                    }\r\n+                    \r\n+                    if (!show_otp_button) {\r\n+                        let wcPaymentMethod = $mo('input[name=\"payment_method\"]:checked').val();\r\n+                        if (!wcPaymentMethod) {\r\n+                            let $paymentInputs = $mo('input[name=\"payment_method\"]');\r\n+                            if ($paymentInputs.length === 1) {\r\n+                                wcPaymentMethod = $paymentInputs.first().val();\r\n+                            }\r\n+                        }\r\n+                        if (wcPaymentMethod && methods.hasOwnProperty(wcPaymentMethod)) {\r\n+                            show_otp_button = true;\r\n+                        }\r\n+                    }\r\n+                    \r\n+                    if (!show_otp_button) {\r\n+                        let activePaymentMethod = $mo('.wc-block-components-payment-methods .wc-block-components-radio-control__option--checked').find('input').val() ||\r\n+                                                 $mo('.wc-block-components-payment-methods .wc-block-components-radio-control__option--checked').data('value');\r\n+                        if (activePaymentMethod && methods.hasOwnProperty(activePaymentMethod)) {\r\n+                            show_otp_button = true;\r\n+                        }\r\n+                    }\r\n+                    \r\n+                    if (!show_otp_button && typeof wc !== 'undefined' && wc.wcBlocksData && wc.wcBlocksData.storeApi) {\r\n+                        try {\r\n+                            let storeData = wc.wcBlocksData.storeApi;\r\n+                            if (storeData.paymentMethodData && storeData.paymentMethodData.selectedPaymentMethod) {\r\n+                                let storePaymentMethod = storeData.paymentMethodData.selectedPaymentMethod;\r\n+                                if (methods.hasOwnProperty(storePaymentMethod)) {\r\n+                                    show_otp_button = true;\r\n+                                }\r\n+                            }\r\n+                        } catch(e) {\r\n+                        }\r\n+                    }\r\n+                    \r\n+                    if (!show_otp_button) {\r\n+                        $mo('.wc-block-components-payment-methods input[type=\"radio\"]:checked').each(function() {\r\n+                            let payment = $mo(this).val();\r\n+                            if (payment && methods.hasOwnProperty(payment)) {\r\n+                                show_otp_button = true;\r\n+                                return false;\r\n+                            }\r\n+                        });\r\n+                    }\r\n+                    \r\n+                    if (!show_otp_button) {\r\n+                        $mo('.wc-block-components-payment-methods input:checked, .wc-block-checkout__payment-methods input:checked').each(function() {\r\n+                            let payment = $mo(this).val() || $mo(this).attr('value');\r\n+                            if (payment && methods.hasOwnProperty(payment)) {\r\n+                                show_otp_button = true;\r\n+                                return false;\r\n+                            }\r\n+                        });\r\n+                    }\r\n+                }\r\n+            }\r\n+    \r\n+            if (mowcnewcheckout.popupEnabled) {\r\n+                $mo(\"button#miniorange_wc_popup_send_otp_token\").show();\r\n+                $mo('.wc-block-components-checkout-place-order-button').hide();\r\n+            } else if (show_otp_button) {\r\n+                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").show();\r\n+            } else {\r\n+                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").hide();\r\n+            }\r\n+        }\r\n+    }\r\n+\r\n+    function send_and_verify_otp() {\r\n+        let send_verify_otp = \"\u003Cinput type='button' id='miniorange_otp_token_submit_wc_block_checkout' style='width: 100%; padding: 1em; margin: 2% 0%;' class='components-button wc-block-components-button wp-element-button contained' value='\" + mowcnewcheckout.buttonText + \"'\u002F>\u003Cdiv id='mo_message' style='background-color: #f7f6f7; display:none; padding: 1em 2em 1em 3.5em;'>\u003C\u002Fdiv> \u003Cdiv style='display:none;' id='mo_verify_otp_fields'>\u003Cdiv class='wc-block-components-text-input'>\u003Cinput type='text' id='mo_otp_token' aria-label='Enter OTP'\u002F>\u003Clabel for='mo_otp_token'>Enter Verification Code\u003C\u002Flabel>\u003C\u002Fdiv>\u003Cinput type='button' id='miniorange_verify_otp_token' class='components-button wc-block-components-button wp-element-button contained' style='width: 100%; padding: 1em; margin: 2% 0%;' value='Verify OTP'\u002F>\u003C\u002Fdiv>\";\r\n+        let img = \"\u003Cdiv class='moloader'>\u003C\u002Fdiv>\";\r\n+        \r\n+        if (mowcnewcheckout.popupEnabled) {\r\n+            $mo('.wc-block-components-checkout-place-order-button').hide();\r\n+            var $wcPopupBtn = $mo('button#miniorange_wc_popup_send_otp_token');\r\n+            if ($wcPopupBtn.length === 0) {\r\n+                $mo('.wc-block-components-checkout-place-order-button').after('\u003Cbutton id=\"miniorange_wc_popup_send_otp_token\" class=\"wp-element-button\" type=\"button\">' + mowcnewcheckout.buttonText + '\u003C\u002Fbutton>');\r\n+            } else {\r\n+                $wcPopupBtn.show().prop('disabled', false).css('opacity', '');\r\n+            }\r\n+            \r\n+            if (jQuery('.otp-catchy-box').length > 0) {\r\n+                jQuery(\".digit-group input.otp-catchy\").each(function () {\r\n+                    jQuery(this).attr(\"maxlength\", \"1\");\r\n+                }).on(\"keyup\", function (event) {\r\n+                    let parent = jQuery(this).parent();\r\n+                    if (event.keyCode === 8 || event.keyCode === 37) {\r\n+                        let prevId = jQuery(this).data(\"previous\");\r\n+                        if (prevId) {\r\n+                            jQuery(\"#\" + prevId).select();\r\n+                        }\r\n+                    } else {\r\n+                        let nextId = jQuery(this).data(\"next\");\r\n+                        if (nextId) {\r\n+                            jQuery(\"#\" + nextId).select();\r\n+                        }\r\n+                    }\r\n+                });\r\n+                \r\n+                let mo_validate_button = document.getElementById(\"mo_sec_otp_submit_button\");\r\n+                if (mo_validate_button) {\r\n+                    mo_validate_button.onclick = function () {\r\n+                        let fieldstring = \"\";\r\n+                        for (let i = 1; i \u003C= parseInt(mowcnewcheckout.otp_length_mo); i++) {\r\n+                            fieldstring += document.querySelector(\"#digit-\" + i).value;\r\n+                        }\r\n+                        jQuery(\"#mo_otp_token\").attr('value', fieldstring);\r\n+                        mo_validate_popup_otp(fieldstring);\r\n+                    };\r\n+                }\r\n+            } else {\r\n+                $mo(\"#miniorange_otp_validate_submit,#mo_sec_otp_submit_button\").on(\"click\", function (event) { \r\n+                    let fieldstring = $mo(\"#mo_otp_token\").val();\r\n+                    mo_validate_popup_otp(fieldstring);\r\n+                });\r\n+            }\r\n+            \r\n+            \u002F\u002F Handle both initial send and resend actions within the popup.\r\n+            $mo(\"#miniorange_wc_popup_send_otp_token, #mo_otp_verification_resend, .mo-resend\")\r\n+                \u002F\u002F Remove any existing handlers (including from moDefaultPopUp) so that\r\n+                \u002F\u002F block-checkout-specific AJAX behavior is used consistently.\r\n+                .off(\"click\")\r\n+                .on(\"click.moPopupOtp\", function (event) {\r\n+                let requiredFields = $mo('[required]');\r\n+                let allFieldsFilled = true;\r\n+                requiredFields.each(function () {\r\n+                    if (!$mo(this).val().trim()) {\r\n+                        if ($mo(this).attr(\"id\") != \"mo_otp_token\") {\r\n+                            allFieldsFilled = false;\r\n+                            $mo(this).focus();\r\n+                            return false;\r\n+                        }\r\n+                    }\r\n+                });\r\n+                \r\n+                if (!allFieldsFilled) {\r\n+                    $mo('.wc-block-components-checkout-place-order-button').click();\r\n+                } else {\r\n+                    $mo('#popup_wc_mo').show();\r\n+                    let img = \"\u003Cdiv class='moloader'>\u003C\u002Fdiv>\";\r\n+                    jQuery(\"#mo_message_wc_pop_up\").empty().append(img).show();\r\n+                    $mo(\".mo_customer_validation-login-container\").show();\r\n+                    \r\n+                    let user = $mo(\"#\" + mowcnewcheckout.field).val();\r\n+                    let sendPhone = (mowcnewcheckout.otpType === 'email') ? '' : user;\r\n+                    let sendEmail = (mowcnewcheckout.otpType === 'phone') ? '' : user;\r\n+                    \r\n+                    $mo.ajax({\r\n+                        url: mowcnewcheckout.siteURL,\r\n+                        type: \"POST\",\r\n+                        data: {\r\n+                            user_phone: sendPhone,\r\n+                            user_email: sendEmail,\r\n+                            action: mowcnewcheckout.gaction,\r\n+                            security: mowcnewcheckout.nonce,\r\n+                            otpType: mowcnewcheckout.otpType\r\n+                        },\r\n+                        crossDomain: true,\r\n+                        dataType: \"json\",\r\n+                        success: function (response) {\r\n+                            if (response.result === \"success\") {\r\n+                                if (typeof window !== 'undefined') {\r\n+                                    window.mo_wc_otp_initialized = true;\r\n+                                }\r\n+                                $mo(\".blockUI\").hide();\r\n+                                moWcPopupMessageStripStyles();\r\n+                                jQuery(\"#mo_message_wc_pop_up\").text(response.message).show();\r\n+                                $mo(\".digit-group input[type='text']\").val(\"\");\r\n+                                $mo(\"input[name='order_verify']\").val(\"\");\r\n+                                $mo(\"#popup_wc_mo\").show();\r\n+                            } else {\r\n+                                if (typeof window !== 'undefined') {\r\n+                                    window.mo_wc_otp_initialized = false;\r\n+                                }\r\n+                                jQuery(\"#mo_message_wc_pop_up\").empty().append(response.message);\r\n+                                jQuery(\"#mo_message_wc_pop_up\").css({\r\n+                                    \u002F\u002F \"background-color\": \"#ffefef\",\r\n+                                    \"color\": \"#ff5b5b\"\r\n+                                });\r\n+                                $mo(\".blockUI\").hide();\r\n+                            }\r\n+                            $mo('button#miniorange_wc_popup_send_otp_token').show().prop('disabled', false).css('opacity', '');\r\n+                        },\r\n+                        error: function (xhr, status, error) {\r\n+                            console.error('AJAX Error:', error);\r\n+                            $mo('button#miniorange_wc_popup_send_otp_token').show().prop('disabled', false).css('opacity', '');\r\n+                        }\r\n+                    });\r\n+                    \r\n+                    $mo(\".close\").on(\"click\", function (event) {\r\n+                        $mo(\"#popup_wc_mo\").hide();\r\n+                    });\r\n+                }\r\n+                if (event && typeof event.preventDefault === \"function\") {\r\n+                    event.preventDefault();\r\n+                }\r\n+            });\r\n+        } else {\r\n+            $mo(send_verify_otp).insertAfter($mo(\"#\" + mowcnewcheckout.field).parent().parent());\r\n+            \r\n+            $mo(document).on('focus', '#mo_otp_token', function () {\r\n+                $mo(this).parent().addClass('is-active');\r\n+            });\r\n+\r\n+            $mo(document).on('blur', '#mo_otp_token', function () {\r\n+                if (!$mo(this).val()) {\r\n+                    $mo(this).parent().removeClass('is-active');\r\n+                }\r\n+            });\r\n+            \r\n+            $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").on(\"click\", function () {\r\n+                let user = $mo(\"#\" + mowcnewcheckout.field).val();\r\n+                let sendPhone = (mowcnewcheckout.otpType === 'email') ? '' : user;\r\n+                let sendEmail = (mowcnewcheckout.otpType === 'phone') ? '' : user;\r\n+                let otp = $mo(\"input[name=phone_verify]\");\r\n+                let msg_box = $mo(\"#mo_message\");\r\n+                \r\n+                \u002F\u002F Sanitize user input to prevent XSS\r\n+                user = $mo('\u003Cdiv\u002F>').text(user).html();\r\n+                \r\n+                msg_box.empty();\r\n+                msg_box.append(img);\r\n+                msg_box.show();\r\n+                \r\n+                \u002F\u002F Safely set window.verifyOTPmessage\r\n+                if (typeof window !== 'undefined') {\r\n+                    window.verifyOTPmessage = img;\r\n+                }\r\n+                \r\n+                $mo.ajax({\r\n+                    url: mowcnewcheckout.siteURL,\r\n+                    type: \"POST\",\r\n+                    data: { \r\n+                        user_phone: sendPhone, \r\n+                        user_email: sendEmail, \r\n+                        action: mowcnewcheckout.gaction, \r\n+                        security: mowcnewcheckout.nonce, \r\n+                        otpType: mowcnewcheckout.otpType \r\n+                    },\r\n+                    crossDomain: true,\r\n+                    dataType: \"json\",\r\n+                    success: function (response) {\r\n+                        if (\"success\" === response.result) {\r\n+                            if (typeof window !== 'undefined') {\r\n+                                delete window.verifyOTPmessage;\r\n+                            }\r\n+                            msg_box.empty();\r\n+                            msg_box.append(response.message);\r\n+                            msg_box.css({\r\n+                                \"background-color\": \"#dbfff7\",\r\n+                                \"color\": \"#008f6e\"\r\n+                            });\r\n+                            otp.focus();\r\n+                            $mo(\"#mo_verify_otp_fields\").show();\r\n+                            $mo(\"#miniorange_verify_otp_token\").show();\r\n+                        } else {\r\n+                            if (typeof window !== 'undefined') {\r\n+                                window.verifyOTPmessage = response.message;\r\n+                            }\r\n+                            msg_box.empty();\r\n+                            msg_box.append(response.message);\r\n+                            msg_box.css({\r\n+                                \"background-color\": \"#ffefef\",\r\n+                                \"color\": \"#ff5b5b\"\r\n+                            });\r\n+                        }\r\n+                    },\r\n+                    error: function (xhr, status, error) {\r\n+                        console.error('AJAX Error:', error);\r\n+                    },\r\n+                });\r\n+            });\r\n+            \r\n+            $mo(\"#miniorange_verify_otp_token\").on(\"click\", function () {\r\n+                let user = $mo(\"#\" + mowcnewcheckout.field).val();\r\n+                let sendPhone = (mowcnewcheckout.otpType === 'email') ? '' : user;\r\n+                let sendEmail = (mowcnewcheckout.otpType === 'phone') ? '' : user;\r\n+                let otp_token = $mo(\"#mo_otp_token\").val();\r\n+                let msg_box = $mo(\"#mo_message\");\r\n+                \r\n+                msg_box.empty();\r\n+                msg_box.append(img);\r\n+                msg_box.show();\r\n+                \r\n+                if (typeof window !== 'undefined') {\r\n+                    window.verifyOTPmessage = img;\r\n+                }\r\n+                \r\n+                $mo.ajax({\r\n+                    url: mowcnewcheckout.siteURL,\r\n+                    type: \"POST\",\r\n+                    data: { \r\n+                        user_phone: sendPhone, \r\n+                        user_email: sendEmail, \r\n+                        action: mowcnewcheckout.vaction, \r\n+                        security: mowcnewcheckout.nonce, \r\n+                        otpType: mowcnewcheckout.otpType, \r\n+                        otp_token: otp_token \r\n+                    },\r\n+                    crossDomain: true,\r\n+                    dataType: \"json\",\r\n+                    success: function (response) {\r\n+                        if (\"success\" === response.result) {\r\n+                            if (typeof window !== 'undefined') {\r\n+                                delete window.verifyOTPmessage;\r\n+                            }\r\n+                            msg_box.empty();\r\n+                            msg_box.hide();\r\n+                            $mo(\"#mo_verify_otp_fields\").hide();\r\n+                            $mo(\"#miniorange_verify_otp_token\").hide();\r\n+                            $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").val(\"Verified ✔\");\r\n+                            $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").show();\r\n+                            $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").prop(\"disabled\", true);\r\n+                            $mo(\"#mo_otp_token\").val('');\r\n+                            is_otp_verified(user);\r\n+                        } else {\r\n+                            if (typeof window !== 'undefined') {\r\n+                                window.verifyOTPmessage = response.message;\r\n+                            }\r\n+                            msg_box.empty();\r\n+                            msg_box.append(response.message);\r\n+                            msg_box.css({\r\n+                                \"background-color\": \"#ffefef\",\r\n+                                \"color\": \"#ff5b5b\"\r\n+                            });\r\n+                        }\r\n+                    },\r\n+                    error: function (xhr, status, error) {\r\n+                        console.error('AJAX Error:', error);\r\n+                    },\r\n+                });\r\n+            });\r\n+        }\r\n+    }\r\n+\r\n+    function is_otp_verified(user_detail) {\r\n+        $mo(\"#\" + mowcnewcheckout.field).on('keydown keyup', function () {\r\n+            if ($mo(\"#\" + mowcnewcheckout.field).val() !== user_detail) {\r\n+                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").val(mowcnewcheckout.buttonText);\r\n+                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").removeAttr(\"disabled\");\r\n+            } else {\r\n+                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").val(\"Verified ✔\");\r\n+                $mo(\"#miniorange_otp_token_submit_wc_block_checkout\").prop(\"disabled\", true);\r\n+            }\r\n+        });\r\n+    }\r\n+    \r\n+    function mo_validate_popup_otp(fieldstring) {\r\n+        let img = \"\u003Cdiv class='moloader'>\u003C\u002Fdiv>\";\r\n+        jQuery(\"#mo_message_wc_pop_up\").empty().append(img).show();\r\n+        \r\n+        let user = $mo(\"#\" + mowcnewcheckout.field).val();\r\n+        let sendPhone = (mowcnewcheckout.otpType === 'email') ? '' : user;\r\n+        let sendEmail = (mowcnewcheckout.otpType === 'phone') ? '' : user;\r\n+        \r\n+        $mo.ajax({\r\n+            url: mowcnewcheckout.siteURL,\r\n+            type: \"POST\",\r\n+            data: { \r\n+                user_phone: sendPhone, \r\n+                user_email: sendEmail, \r\n+                action: mowcnewcheckout.vaction, \r\n+                security: mowcnewcheckout.nonce, \r\n+                otpType: mowcnewcheckout.otpType, \r\n+                otp_token: fieldstring \r\n+            },\r\n+            crossDomain: true,\r\n+            dataType: \"json\",   \r\n+            success: function (response) {\r\n+                if (response.result === \"success\") {\r\n+                    moWcPopupMessageStripStyles();\r\n+                    jQuery(\"#mo_message_wc_pop_up\").text(response.message).show();\r\n+                    $mo(\"#popup_wc_mo\").hide();\r\n+                    $mo('form[name=\"checkout\"]').submit();\r\n+                    $mo('.wc-block-components-checkout-place-order-button').click();\r\n+                } else {\r\n+                    jQuery(\"#mo_message_wc_pop_up\").text(response.message).css({\r\n+                        \u002F\u002F \"background-color\": \"#ffefef\",\r\n+                        \"color\": \"#ff5b5b\"\r\n+                    }).show();\r\n+                }\r\n+            },\r\n+            error: function (xhr, status, error) {\r\n+                console.error('AJAX Error:', error);\r\n+            }\r\n+        });\r\n+    }\r\n+});\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fincludes\u002Fjs\u002Fsettings.js \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fincludes\u002Fjs\u002Fsettings.js\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fincludes\u002Fjs\u002Fsettings.js\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fincludes\u002Fjs\u002Fsettings.js\t2026-07-03 09:57:50.000000000 +0000\n@@ -1195,27 +1195,73 @@\n             });\r\n     });\r\n \r\n+    function moMakeWhatsappLink( id, extraClass, textContent ) {\r\n+        var a = document.createElement( 'a' );\r\n+        a.id        = id;\r\n+        a.className = 'mo-whatsapp-links' + ( extraClass ? ' ' + extraClass : '' );\r\n+        a.href      = moadminsettings.whatsapp_tab;\r\n+        a.target    = '_blank';\r\n+        a.rel       = 'noopener noreferrer';\r\n+        a.textContent = textContent;\r\n+        return a;\r\n+    }\r\n+\r\n     let whatsapp_settings;\r\n     if (!moadminsettings.whatsapp_file) {\r\n-        whatsapp_settings = '\u003Cdiv id=\"mo_free_whatsapp_html\" class=\"mo-whatsapp-links mo_whatsapp_marketing\">[ \u003Ca id=\"mo_whatsapp_not_enabled\" href=\"' + moadminsettings.whatsapp_tab + '\" target=\"_blank\">' + moadminsettings.whatsapp_disabled_text + ' \u003C\u002Fa>\\\r\n-                                    \u003Cspan class=\"tooltip mo_whatsapp_tooltip\">\\\r\n-                                         \u003Csvg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\">\\\r\n-\t\t\t\t\t\t\t\t\t\t\t\u003Cg id=\"d4a43e0162b45f718f49244b403ea8f4\">\\\r\n-\t\t\t\t\t\t\t\t\t\t\t\t\u003Cg id=\"4ea4c3dca364b4cff4fba75ac98abb38\">\\\r\n-\t\t\t\t\t\t\t\t\t\t\t\t\t\u003Cg id=\"2413972edc07f152c2356073861cb269\">\\\r\n-\t\t\t\t\t\t\t\t\t\t\t\t\t\t\u003Cpath id=\"2deabe5f8681ff270d3f37797985a977\" d=\"M20.8007 20.5644H3.19925C2.94954 20.5644 2.73449 20.3887 2.68487 20.144L0.194867 7.94109C0.153118 7.73681 0.236091 7.52728 0.406503 7.40702C0.576651 7.28649 0.801941 7.27862 0.980492 7.38627L7.69847 11.4354L11.5297 3.72677C11.6177 3.54979 11.7978 3.43688 11.9955 3.43531C12.1817 3.43452 12.3749 3.54323 12.466 3.71889L16.4244 11.3598L23.0197 7.38654C23.1985 7.27888 23.4233 7.28702 23.5937 7.40728C23.7641 7.52754 23.8471 7.73707 23.8056 7.94136L21.3156 20.1443C21.2652 20.3887 21.0501 20.5644 20.8007 20.5644Z\" fill=\"orange\">\u003C\u002Fpath>\\\r\n-\t\t\t\t\t\t\t\t\t\t\t\t\t\u003C\u002Fg>\\\r\n+        var waDiv  = document.createElement( 'div' );\r\n+        waDiv.id        = 'mo_free_whatsapp_html';\r\n+        waDiv.className = 'mo-whatsapp-links mo_whatsapp_marketing';\r\n+        var waLink = moMakeWhatsappLink( 'mo_whatsapp_not_enabled', '', moadminsettings.whatsapp_disabled_text + ' ' );\r\n+        waDiv.appendChild( document.createTextNode( '[ ' ) );\r\n+        waDiv.appendChild( waLink );\r\n+\r\n+        var tooltipSpan = document.createElement( 'span' );\r\n+        tooltipSpan.className = 'tooltip mo_whatsapp_tooltip';\r\n+        tooltipSpan.innerHTML = '\\\r\n+                                     \u003Csvg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\">\\\r\n+\t\t\t\t\t\t\t\t\t\t\u003Cg id=\"d4a43e0162b45f718f49244b403ea8f4\">\\\r\n+\t\t\t\t\t\t\t\t\t\t\t\u003Cg id=\"4ea4c3dca364b4cff4fba75ac98abb38\">\\\r\n+\t\t\t\t\t\t\t\t\t\t\t\t\u003Cg id=\"2413972edc07f152c2356073861cb269\">\\\r\n+\t\t\t\t\t\t\t\t\t\t\t\t\t\u003Cpath id=\"2deabe5f8681ff270d3f37797985a977\" d=\"M20.8007 20.5644H3.19925C2.94954 20.5644 2.73449 20.3887 2.68487 20.144L0.194867 7.94109C0.153118 7.73681 0.236091 7.52728 0.406503 7.40702C0.576651 7.28649 0.801941 7.27862 0.980492 7.38627L7.69847 11.4354L11.5297 3.72677C11.6177 3.54979 11.7978 3.43688 11.9955 3.43531C12.1817 3.43452 12.3749 3.54323 12.466 3.71889L16.4244 11.3598L23.0197 7.38654C23.1985 7.27888 23.4233 7.28702 23.5937 7.40728C23.7641 7.52754 23.8471 7.73707 23.8056 7.94136L21.3156 20.1443C21.2652 20.3887 21.0501 20.5644 20.8007 20.5644Z\" fill=\"orange\">\u003C\u002Fpath>\\\r\n \t\t\t\t\t\t\t\t\t\t\t\t\u003C\u002Fg>\\\r\n \t\t\t\t\t\t\t\t\t\t\t\u003C\u002Fg>\\\r\n-\t\t\t\t\t\t\t\t\t\t\u003C\u002Fsvg>\\\r\n-                                        \u003Cspan class=\"tooltiptext prem_form_tooltip\" style=\"transform:translateY(-7%);\">\\\r\n-                                            \u003Cspan class=\"header prem_form_header\">\u003Cb>WhatsApp + Twilio Gateway Plan Feature\u003C\u002Fb>\u003C\u002Fspan>\\\r\n-                                            \u003Cspan class=\"body\">To use OTPs over WhatsApp, upgrade to the WhatsApp + Twilio Gateway Plan.\u003Cbr>Check \u003Ca class=\"font-semibold text-yellow-500\" href=\" ' + moadminsettings.pricing_plan_url + '\" target=\"_blank\">Licensing Tab\u003C\u002Fa> to learn more.\u003C\u002Fspan>\\\r\n-                                        \u003C\u002Fspan>\\&nbsp]\u003C\u002Fdiv>';\r\n+\t\t\t\t\t\t\t\t\t\t\u003C\u002Fg>\\\r\n+\t\t\t\t\t\t\t\t\t\u003C\u002Fsvg>';\r\n+\r\n+        var tooltipText = document.createElement( 'span' );\r\n+        tooltipText.className = 'tooltiptext prem_form_tooltip';\r\n+        tooltipText.setAttribute( 'style', 'transform:translateY(-7%);' );\r\n+\r\n+        var tooltipHeader = document.createElement( 'span' );\r\n+        tooltipHeader.className = 'header prem_form_header';\r\n+        tooltipHeader.innerHTML = '\u003Cb>WhatsApp + Twilio Gateway Plan Feature\u003C\u002Fb>';\r\n+\r\n+        var tooltipBody = document.createElement( 'span' );\r\n+        tooltipBody.className = 'body';\r\n+\r\n+        var pricingLink = document.createElement( 'a' );\r\n+        pricingLink.className = 'font-semibold text-yellow-500';\r\n+        pricingLink.href      = moadminsettings.pricing_plan_url || '';\r\n+        pricingLink.target    = '_blank';\r\n+        pricingLink.textContent = 'Licensing Tab';\r\n+\r\n+        tooltipBody.appendChild( document.createTextNode( 'To use OTPs over WhatsApp, upgrade to the WhatsApp + Twilio Gateway Plan.' ) );\r\n+        tooltipBody.appendChild( document.createElement( 'br' ) );\r\n+        tooltipBody.appendChild( document.createTextNode( 'Check ' ) );\r\n+        tooltipBody.appendChild( pricingLink );\r\n+        tooltipBody.appendChild( document.createTextNode( ' to learn more.' ) );\r\n+\r\n+        tooltipText.appendChild( tooltipHeader );\r\n+        tooltipText.appendChild( tooltipBody );\r\n+        tooltipSpan.appendChild( tooltipText );\r\n+        waDiv.appendChild( tooltipSpan );\r\n+        waDiv.appendChild( document.createTextNode( ' ]' ) );\r\n+\r\n+        whatsapp_settings = waDiv;\r\n     } else  if( moadminsettings.iswhatsappenable ){\r\n         whatsapp_settings = '\u003Cspan  id=\"mo_whatsapp_enabled\" class=\"addon-table-list-status mo-whatsapp-links\">[ '+ moadminsettings.whatsapp_enabled_text +' ]\u003C\u002Fspan>';\r\n     } else {\r\n-        whatsapp_settings = '\u003Ca id=\"mo_whatsapp_not_enabled\" class=\"mo-whatsapp-links\" href=\"' + moadminsettings.whatsapp_tab + '\" target=\"_blank\">[ ' + moadminsettings.whatsapp_disabled_text + ' ]\u003C\u002Fa>';\r\n+        whatsapp_settings = moMakeWhatsappLink( 'mo_whatsapp_not_enabled', '', '[ ' + moadminsettings.whatsapp_disabled_text + ' ]' );\r\n     }\r\n \r\n     \u002F\u002FExceptional forms:\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fincludes\u002Fjs\u002FvisualTour.js \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fincludes\u002Fjs\u002FvisualTour.js\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fincludes\u002Fjs\u002FvisualTour.js\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fincludes\u002Fjs\u002FvisualTour.js\t2026-07-03 09:57:50.000000000 +0000\n@@ -17,7 +17,7 @@\n  * @param pointerNumber int value\r\n  *\u002F\r\n function startTour(pointerNumber) {\r\n-    if (!moTour.tourData) return;\r\n+    if (!moTour.tourData || !moTour.tourData.length) return;\r\n \r\n     if (Object.keys(moTour.currentPage).length > 1) return;\r\n \r\n@@ -31,6 +31,12 @@\n function createCard(pointerNumber) {\r\n     let tourElement = moTour.tourData[pointerNumber];\r\n \r\n+    if (!tourElement) {\r\n+        resetTour();\r\n+        tourComplete();\r\n+        return;\r\n+    }\r\n+\r\n     if (\r\n         !$mo(\"#\" + tourElement.targetE).is(\":visible\") &&\r\n         (pointerNumber !== 0 || tourElement.targetE !== \"\")\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fminiorange_validation_settings.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fminiorange_validation_settings.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Fminiorange_validation_settings.php\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Fminiorange_validation_settings.php\t2026-07-03 09:57:50.000000000 +0000\n@@ -3,7 +3,7 @@\n  * Plugin Name: miniOrange OTP Login, Verification and SMS Notifications\r\n  * Plugin URI: http:\u002F\u002Fminiorange.com\r\n  * Description: Email & SMS OTP verification on 60+ forms, SMS notifications for WooCommerce, passwordless login, Login with phone, support for external OTP gateways.\r\n- * Version: 5.5.1\r\n+ * Version: 5.5.2\r\n  * Author: miniOrange\r\n  * Author URI: https:\u002F\u002Fminiorange.com\r\n  * Text Domain: miniorange-otp-verification\r\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Freadme.txt \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Freadme.txt\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.1\u002Freadme.txt\t2026-06-04 00:07:06.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fminiorange-otp-verification\u002F5.5.2\u002Freadme.txt\t2026-07-03 09:57:50.000000000 +0000\n@@ -5,7 +5,7 @@\n Requires at least: 3.5\r\n Tested up to: 7.0\r\n Requires PHP: 5.3.0\r\n-Stable tag: 5.5.1\r\n+Stable tag: 5.5.2\r\n License: Expat\r\n License URI: https:\u002F\u002Fplugins.miniorange.com\u002Fmit-license\r\n OTP Verification via Email\u002FSMS\u002FWhatsApp,SMS Notifications for WooCommerce,OTP Login with Phone,PasswordLess Login.Custom Gateway for OTP Verification\r\n@@ -211,6 +211,9 @@\n 8. OTP Verification Plugin Settings\r\n \r\n == Changelog ==\r\n+= 5.5.2 =\r\n+* Introdcing abilites.\r\n+* Login form fixes and security fixes.\r\n = 5.5.1 =\r\n * Session-transient related bug fixes.\r\n * Spam preventor addon bug fixes.\r\n@@ -306,6 +309,9 @@\n \r\n \r\n == Upgrade Notice ==\r\n+= 5.5.2 =\r\n+* Introdcing abilites.\r\n+* Login form fixes and security fixes.\r\n = 5.5.1 =\r\n * Session-transient related bug fixes.\r\n * Spam preventor addon bug fixes.\r\n","1. Identify a page on the target site using the Ultimate Member password reset form (e.g., \u002Fpassword-reset\u002F).\n2. View the page source or use a browser console to extract the 'form_nonce' value from the globally localized 'moumprvar' JavaScript object.\n3. Prepare a POST request to the same password reset page.\n4. Include the following parameters in the request body: 'username_b' (set to the target administrator's username), 'form_nonce' (the value extracted in step 2), and 'um_for_reset_password=1'.\n5. Submit the request and monitor the HTTP response without following redirects.\n6. Capture the 'Location' header from the 302 Found response, which will contain a complete password reset URL for the targeted administrator (containing the 'rp_key' and 'rp_login' parameters).\n7. Navigate to the captured URL to set a new password for the administrator account.","gemini-3-flash-preview","2026-07-15 22:36:21","2026-07-15 22:37:07",{"type":41,"vulnerable_version":42,"fixed_version":11,"vulnerable_browse":43,"vulnerable_zip":44,"fixed_browse":45,"fixed_zip":46,"all_tags":47},"plugin","5.5.1","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fminiorange-otp-verification\u002Ftags\u002F5.5.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fminiorange-otp-verification.5.5.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fminiorange-otp-verification\u002Ftags\u002F5.5.2","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fminiorange-otp-verification.5.5.2.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fminiorange-otp-verification\u002Ftags"]