'captcha-bot-protection', 'title' => __('reCAPTCHA settings', 'ai-eshop-optimizer'), )); echo $aieo_cap_open; echo '

' . esc_html__('Social Login settings storage is unavailable, so bot protection cannot be configured.', 'ai-eshop-optimizer') . '

'; echo $aieo_cap_close; return; } $captcha = AIEO_SSO_Buttons_Data::captcha_settings(); $nonce = wp_create_nonce(self::NONCE); $surfaces_meta = array( 'register' => __('Registration form', 'ai-eshop-optimizer'), 'login' => __('Login form', 'ai-eshop-optimizer'), 'lost_password' => __('Lost password form', 'ai-eshop-optimizer'), 'magic_link' => __('Magic link request', 'ai-eshop-optimizer'), 'product_request' => __('Product request / quote form', 'ai-eshop-optimizer'), 'contact_form' => __('Contact form (Rooster One)', 'ai-eshop-optimizer'), ); ob_start(); $this->print_settings_js($nonce); $aieo_cap_js = (string) ob_get_clean(); $aieo_cap_opts = array('nonce' => $nonce, 'nonce_name' => 'nonce'); list($aieo_cap_open, $aieo_cap_close) = AIEO_DMM_UI::card(array( 'anchor' => 'captcha-bot-protection', 'title' => __('reCAPTCHA settings', 'ai-eshop-optimizer'), 'description' => __('Blocks automated account registration and credential-stuffing logins. reCAPTCHA v3 is invisible: it scores each submission in the background instead of showing a puzzle, so genuine customers never see anything.', 'ai-eshop-optimizer'), 'footer' => (class_exists('\Rooster\Admin\States') ? \Rooster\Admin\States::save_button('aieo-captcha-settings-form') : '') . $aieo_cap_js, )); echo $aieo_cap_open; $aieo_cap_get = __('Get keys → Google reCAPTCHA admin console', 'ai-eshop-optimizer') . ' — ' . __('choose "Score based (v3)" and add this site\'s domain.', 'ai-eshop-optimizer'); $aieo_cap_get_html = '' . esc_html__('Get keys → Google reCAPTCHA admin console', 'ai-eshop-optimizer') . ' — ' . esc_html__('choose "Score based (v3)" and add this site\'s domain.', 'ai-eshop-optimizer'); echo class_exists('\Rooster\Admin\Guidance') ? \Rooster\Admin\Guidance::text($aieo_cap_get, 'guidance', array('html' => $aieo_cap_get_html)) : '

' . $aieo_cap_get_html . '

'; ob_start(); echo ''; $row = static function (array $field) { return \Rooster\Admin\Ui::field_row($field); }; $has_secret = ((string) $captcha['secret_key'] !== ''); if (class_exists('\Rooster\Admin\Fields')) { $aieo_cap_secret = \Rooster\Admin\Fields::secret('captcha[secret_key]', $has_secret, array( 'id' => 'aieo-captcha-secret-key', 'label' => __('Secret key', 'ai-eshop-optimizer'), 'placeholder' => __('Paste the secret key', 'ai-eshop-optimizer'), )); } else { $aieo_cap_secret = \Rooster\Admin\Ui::input('captcha[secret_key]', '', array( 'id' => 'aieo-captcha-secret-key', 'type' => 'password', 'autocomplete' => 'new-password', 'placeholder' => $has_secret ? __('•••••••• stored — leave blank to keep', 'ai-eshop-optimizer') : __('Paste the secret key', 'ai-eshop-optimizer'), )); } $surfaces = ''; foreach ($surfaces_meta as $code => $label) { $surfaces .= AIEO_Kit::check('captcha[surfaces][' . $code . ']', $label, !empty($captcha['surfaces'][$code]), array('id' => 'aieo-captcha-surface-' . sanitize_key($code))); } echo '
' . $row(array( 'id' => 'aieo-captcha-enabled', 'label' => __('Enable', 'ai-eshop-optimizer'), 'control' => AIEO_Kit::check('captcha[enabled]', __('Protect the surfaces ticked below.', 'ai-eshop-optimizer'), !empty($captcha['enabled']), array('id' => 'aieo-captcha-enabled')), 'help' => __('Stays inactive until BOTH keys below are filled in, so a half-finished setup can never lock customers out.', 'ai-eshop-optimizer'), )) . $row(array( 'id' => 'aieo-captcha-site-key', 'label' => __('Site key', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('captcha[site_key]', (string) $captcha['site_key'], array('id' => 'aieo-captcha-site-key', 'medium' => true, 'code' => true, 'autocomplete' => 'off', 'placeholder' => '6Lc...')), )) . $row(array( 'id' => 'aieo-captcha-secret-key', 'label' => __('Secret key', 'ai-eshop-optimizer'), 'control' => $aieo_cap_secret, 'help' => __('Never shown again after saving. Leave blank to keep the stored key; type a new one to replace it.', 'ai-eshop-optimizer'), )) . $this->key_status_row($captcha) . $row(array( 'id' => 'aieo-captcha-surfaces', 'group' => true, 'label' => __('Protect which forms', 'ai-eshop-optimizer'), 'control' => '
' . $surfaces . '
', 'help' => __('Registration is the usual target of automated sign-ups. Only tick Login as well if you are also seeing credential-stuffing attempts, since it adds a verification round-trip for every returning customer.', 'ai-eshop-optimizer'), )) . $row(array( 'id' => 'aieo-captcha-threshold', 'label' => __('Score threshold', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('captcha[threshold]', (string) $captcha['threshold'], array('id' => 'aieo-captcha-threshold', 'type' => 'number', 'xs' => true, 'min' => 0, 'max' => 1, 'step' => 'any')), 'help' => __('reCAPTCHA scores 1.0 (almost certainly human) down to 0.0 (almost certainly a bot). Submissions below this are rejected. 0.5 is Google\'s recommended starting point — raise it if bots get through, lower it if real customers are blocked.', 'ai-eshop-optimizer'), )) . $row(array( 'id' => 'aieo-captcha-fail-open', 'label' => __('If Google is unreachable', 'ai-eshop-optimizer'), 'control' => AIEO_Kit::check('captcha[fail_open]', __('Let the submission through (recommended).', 'ai-eshop-optimizer'), !empty($captcha['fail_open']), array('id' => 'aieo-captcha-fail-open')), 'help' => __('A network failure between your server and Google affects every customer, not just bots — blocking then would close your shop. Untick only if you would rather refuse logins than risk letting a bot through.', 'ai-eshop-optimizer'), )) . $row(array( 'id' => 'aieo-captcha-hide-badge', 'label' => __('Hide the reCAPTCHA badge', 'ai-eshop-optimizer'), 'control' => AIEO_Kit::check('captcha[hide_badge]', __('Hide the floating badge in the corner.', 'ai-eshop-optimizer'), !empty($captcha['hide_badge']), array('id' => 'aieo-captcha-hide-badge')), 'help' => __('Google\'s terms allow hiding the badge only if the reCAPTCHA attribution text is shown instead — we add that text under the protected form automatically when this is ticked.', 'ai-eshop-optimizer'), )) . '
'; echo AIEO_DMM_UI::card_form('aieo_sso_captcha_save_settings', $this->get_id(), 'aieo-captcha-settings-form', (string) ob_get_clean(), $aieo_cap_opts); echo $aieo_cap_close; $this->render_per_form_card($captcha, $aieo_cap_opts); $this->render_status_card(); } private function key_status_row($captcha) { if (!$this->load_keys()) { return ''; } $status = AIEO_Captcha_Keys::status(); $host = AIEO_Captcha_Keys::expected_host(); $html = ''; if (!is_array($status)) { $html .= '

' . esc_html__('Not tested yet.', 'ai-eshop-optimizer') . ' ' . esc_html(sprintf( __('reCAPTCHA keys only work on the domains registered against them, so a key copied from another site fails here. Test before switching protection on.', 'ai-eshop-optimizer'), $host )) . '

'; } elseif (!empty($status['ok']) && 'full' === $status['stage']) { $when = isset($status['checked_at']) ? (int) $status['checked_at'] : 0; $html .= '

' . esc_html__('Verified on this domain.', 'ai-eshop-optimizer') . ' ' . esc_html((string) $status['message']) . '

'; if ($when) { $html .= '

' . esc_html(sprintf( __('Last checked %s ago.', 'ai-eshop-optimizer'), human_time_diff($when, time()) )) . '

'; } } else { $html .= '

' . esc_html__('Failed.', 'ai-eshop-optimizer') . ' ' . esc_html((string) $status['message']) . '

'; } $html .= '' . ' '; return \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-captcha-key-check', 'group' => true, 'label' => __('Key check', 'ai-eshop-optimizer'), 'control' => $html, 'help' => __('Saves the form, then asks your browser for a real token on this domain and verifies it with the stored secret. This is the only check that proves both keys work here.', 'ai-eshop-optimizer'), )); } private function render_per_form_card($captcha, array $form_opts = array()) { if (!$this->load_registry()) { return; } $forms = AIEO_Form_Registry::forms(); $creates = AIEO_Form_Registry::creates_users(); $stored = isset($captcha['surfaces']) && is_array($captcha['surfaces']) ? $captcha['surfaces'] : array(); $builtin = array('login', 'register', 'lost_password', 'magic_link', 'product_request', 'contact_form'); $orphans = array(); foreach ($stored as $key => $on) { $key = (string) $key; if (in_array($key, $builtin, true) || empty($on) || isset($forms[$key])) { continue; } if (!AIEO_Form_Registry::is_canonical_surface($key)) { continue; } $orphans[$key] = true; } $has_rows = $forms || $orphans; $form_id = 'aieo-card-captcha-per-form-form'; list($aieo_cap_open, $aieo_cap_close) = AIEO_DMM_UI::card(array( 'anchor' => 'captcha-per-form', 'icon' => 'list-checks', 'title' => __('Protect individual forms', 'ai-eshop-optimizer'), 'description' => __('Choose which of the site\'s forms run the captcha check before they are submitted.', 'ai-eshop-optimizer'), 'footer' => $has_rows && class_exists('\Rooster\Admin\States') ? \Rooster\Admin\States::save_button($form_id) : '', )); echo $aieo_cap_open; if (!AIEO_Form_Registry::enforcement_available()) { $builtin_live = class_exists('AIEO_SSO_Buttons_Data') && AIEO_SSO_Buttons_Data::captcha_is_active(); $aieo_cap_note = \Rooster\Admin\Ui::notice( esc_html__('Ticking a form here stores your choice, and the list below already tells you which forms create user accounts. Enforcement for third-party form plugins arrives in a following release, so a ticked form is NOT protected yet.', 'ai-eshop-optimizer') . ' ' . esc_html($builtin_live ? __('The four surfaces above are the ones currently protected.', 'ai-eshop-optimizer') : __('Nothing is being protected at the moment: bot protection is switched off or its keys are not set.', 'ai-eshop-optimizer')), 'warning', array( 'title' => __('Recorded, not yet enforced.', 'ai-eshop-optimizer'), 'inline' => true, ) ); echo $aieo_cap_note; } if (!$has_rows) { $aieo_cap_none = class_exists('\Rooster\Admin\States') ? \Rooster\Admin\States::state('first-use', array('title' => __('No forms found from a supported form plugin.', 'ai-eshop-optimizer'))) : '

' . esc_html__('No forms found from a supported form plugin.', 'ai-eshop-optimizer') . '

'; echo $aieo_cap_none; $this->render_registry_caveats(); echo $aieo_cap_close; return; } $unprotected = array(); foreach ($creates as $surface => $yes) { $ticked = !empty($stored[$surface]); $enforced = AIEO_Form_Registry::is_enforced($surface); if (!$ticked || !$enforced) { $unprotected[$surface] = $ticked ? 'awaiting-enforcement' : 'unticked'; } } if ($unprotected) { $aieo_cap_flag = sprintf( _n('%d form creates user accounts and is not protected.', '%d forms create user accounts and are not protected.', count($unprotected), 'ai-eshop-optimizer'), count($unprotected) ); $aieo_cap_items = ''; foreach (array_keys($unprotected) as $surface) { $parsed = AIEO_Form_Registry::parse_surface($surface); $prov = $parsed ? AIEO_Form_Registry::provider_label($parsed['provider']) : ''; if (isset($forms[$surface])) { $label = $forms[$surface]['title']; $note = ''; } else { $label = $parsed ? sprintf( __('Form #%s', 'ai-eshop-optimizer'), $parsed['form_id'] ) : $surface; $note = ' ' . esc_html__('— inactive or trashed, but its account-creation feed is still switched on', 'ai-eshop-optimizer') . ''; } if ('awaiting-enforcement' === $unprotected[$surface]) { $note .= ' ' . esc_html__('— ticked, but enforcement for this form plugin is not live yet', 'ai-eshop-optimizer') . ''; } $aieo_cap_items .= '
  • ' . esc_html($label) . ($prov !== '' ? ' (' . esc_html($prov) . ')' : '') . $note . '
  • '; } $aieo_cap_flag = \Rooster\Admin\Ui::notice( '' . esc_html($aieo_cap_flag) . '', 'destructive', array( 'inline' => true, 'detail' => '', ) ); echo $aieo_cap_flag; } $max_rows = (int) apply_filters('aieo_captcha_max_form_rows', 300); $rendered = array(); $shown = 0; $omitted = 0; ob_start(); $cap_rows = array(); foreach ($forms as $surface => $row) { if ($shown >= $max_rows) { $omitted++; continue; } $shown++; $rendered[] = $surface; $on = !empty($stored[$surface]); $makes = !empty($creates[$surface]); $assessed = ('gravity' === $row['provider']); $safe = $on && AIEO_Form_Registry::is_enforced($surface); if (!$assessed) { $creates_label = __('Not determined', 'ai-eshop-optimizer'); $creates_tone = 'secondary'; } elseif ($makes) { $creates_label = __('Yes', 'ai-eshop-optimizer'); $creates_tone = $safe ? 'success' : 'danger'; } else { $creates_label = __('No', 'ai-eshop-optimizer'); $creates_tone = 'secondary'; } $cap_rows[] = array( 'surface' => (string) $surface, 'title' => (string) $row['title'], 'form_id' => '#' . $row['form_id'], 'provider' => AIEO_Form_Registry::provider_label($row['provider']), 'creates' => $creates_label, 'creates_tone' => $creates_tone, 'checked' => $on, 'orphan' => false, ); } foreach (array_keys($orphans) as $surface) { if ($shown >= $max_rows) { $omitted++; continue; } $shown++; $rendered[] = $surface; $parsed = AIEO_Form_Registry::parse_surface($surface); $cap_rows[] = array( 'surface' => (string) $surface, 'title' => (string) $surface, 'form_id' => '', 'provider' => $parsed ? AIEO_Form_Registry::provider_label($parsed['provider']) : '', 'creates' => __('Form not found — plugin inactive?', 'ai-eshop-optimizer'), 'creates_tone' => 'warning', 'checked' => true, 'orphan' => true, ); } AIEO_Table::render(array( 'id' => 'aieo-captcha-form-table', 'columns' => array( array( 'key' => 'checked', 'label' => __('Protect', 'ai-eshop-optimizer'), 'type' => 'control', 'width' => '96px', 'header_html' => '', 'format' => array( 'render' => static function ($row) { return ''; }, ), ), array( 'key' => 'title', 'label' => __('Form', 'ai-eshop-optimizer'), 'type' => 'text', 'suffix' => 'form_id', ), array( 'key' => 'provider', 'label' => __('Form plugin', 'ai-eshop-optimizer'), 'type' => 'text', 'width' => '160px', ), array( 'key' => 'creates', 'label' => __('Creates accounts', 'ai-eshop-optimizer'), 'type' => 'badge', 'width' => '170px', 'format' => array( 'tone_key' => 'creates_tone' ), ), ), 'rows' => $cap_rows, 'row_key' => 'surface', 'row_class' => static function ($row) { return $row['orphan'] ? 'aieo-t-row--muted' : ''; }, 'page_id' => 'captcha', )); echo ''; if ($omitted > 0) { echo '

    ' . esc_html(sprintf( _n( 'Showing the first %1$d form; %2$d more is not listed. Forms beyond the limit keep whatever setting they already had.', 'Showing the first %1$d forms; %2$d more are not listed. Forms beyond the limit keep whatever setting they already had.', $omitted, 'ai-eshop-optimizer' ), $shown, $omitted )) . '

    '; } echo AIEO_DMM_UI::card_form('aieo_sso_captcha_save_settings', $this->get_id(), $form_id, (string) ob_get_clean(), $form_opts); $this->render_registry_caveats(); echo $aieo_cap_close; } private function render_registry_caveats() { $unassessed = AIEO_Form_Registry::unassessed_providers(); if ($unassessed) { $labels = array_map(array('AIEO_Form_Registry', 'provider_label'), $unassessed); echo '

    ' . esc_html(sprintf( __('Account creation could not be determined for: %s. Those forms may still create users through an add-on we cannot read, so treat "Not determined" as "check it yourself".', 'ai-eshop-optimizer'), implode(', ', $labels) )) . '

    '; } if (AIEO_Form_Registry::elementor_present_but_unlisted()) { echo '

    ' . esc_html__('Elementor Pro is active but its forms are not listed: they live inside page content rather than as records, and scanning every page to find them would slow this screen down. They are not covered here.', 'ai-eshop-optimizer') . '

    '; } } private function render_status_card() { list($aieo_cap_open, $aieo_cap_close) = AIEO_DMM_UI::card(array( 'anchor' => 'captcha-current-state', 'icon' => 'shield', 'title' => __('Submissions blocked by captcha', 'ai-eshop-optimizer'), 'description' => __('How many submissions the captcha refused in the last seven days, on every form it protects.', 'ai-eshop-optimizer'), )); echo $aieo_cap_open; $cls = $this->captcha_class(); if ($cls !== '') { $blocked = (int) call_user_func(array($cls, 'blocked_count'), 7); $aieo_cap_stat = \Rooster\Admin\Ui::stat(rtrim(__('Blocked in the last 7 days:', 'ai-eshop-optimizer'), ': '), number_format_i18n($blocked)); echo '
    ' . $aieo_cap_stat . '
    '; } $last_error = get_transient('aieo_captcha_last_error'); if (!empty($last_error)) { $aieo_cap_err = \Rooster\Admin\Ui::notice( esc_html__('Last verification problem:', 'ai-eshop-optimizer') . ' ' . esc_html((string) $last_error) . '', 'destructive', array('inline' => true) ); echo $aieo_cap_err; } $aieo_cap_out = __('Locked out? Add this line to wp-config.php to switch the captcha off without the admin:', 'ai-eshop-optimizer'); $aieo_cap_out_html = esc_html($aieo_cap_out) . ' define( \'AIEO_CAPTCHA_DISABLE\', true );'; echo class_exists('\Rooster\Admin\Guidance') ? \Rooster\Admin\Guidance::text($aieo_cap_out . ' define( \'AIEO_CAPTCHA_DISABLE\', true );', 'guidance', array('html' => $aieo_cap_out_html, 'question' => __('Locked out?', 'ai-eshop-optimizer'))) : '

    ' . $aieo_cap_out_html . '

    '; echo $aieo_cap_close; } private function print_settings_js($nonce) { $ajax = admin_url('admin-ajax.php'); $test_action = $this->load_keys() ? AIEO_Captcha_Keys::TEST_ACTION : 'aieo_keytest'; ?> verify_admin_ajax(); if (!$this->load_keys()) { wp_send_json_error(array('ok' => false, 'message' => __('Key checker unavailable.', 'ai-eshop-optimizer')), 500); } $token = isset($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : ''; $result = AIEO_Captcha_Keys::verify_token($token); wp_send_json_success(array( 'ok' => !empty($result['ok']), 'stage' => isset($result['stage']) ? $result['stage'] : '', 'message' => isset($result['message']) ? $result['message'] : '', 'reload' => true, )); } private function verify_admin_ajax() { if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array('message' => 'permission_denied'), 403); } check_ajax_referer(self::NONCE, 'nonce'); } public function ajax_save_settings() { $this->verify_admin_ajax(); if (!class_exists('AIEO_SSO_Buttons_Data')) { wp_send_json_error(array('message' => 'storage_unavailable'), 500); } $all = AIEO_SSO_Buttons_Data::get_settings(); $fp_before = $this->load_keys() ? AIEO_Captcha_Keys::fingerprint() : ''; $all['captcha'] = $this->sanitize_captcha_post(); AIEO_SSO_Buttons_Data::save_settings($all); $this->purge_page_caches(); $extra = array(); if ($this->load_keys() && AIEO_Captcha_Keys::fingerprint() !== $fp_before) { AIEO_Captcha_Keys::forget(); $extra['retest_required'] = __('Keys changed. Press "Save and test keys on this domain" to confirm they work here before switching protection on.', 'ai-eshop-optimizer'); } wp_send_json_success($extra); } private function purge_page_caches() { do_action('aieo_purge_page_cache'); do_action('aieo_captcha_settings_saved'); } private function sanitize_captcha_post() { $current = AIEO_SSO_Buttons_Data::captcha_settings(); if (empty($_POST['captcha_present'])) { return $current; } $in = isset($_POST['captcha']) && is_array($_POST['captcha']) ? wp_unslash($_POST['captcha']) : array(); $secret = isset($in['secret_key']) ? sanitize_text_field((string) $in['secret_key']) : ''; if ('' === trim(str_replace("\u{2022}", '', $secret))) { $secret = (string) $current['secret_key']; } $threshold = isset($in['threshold']) ? (float) $in['threshold'] : (float) $current['threshold']; $threshold = max(0.0, min(1.0, $threshold)); $surfaces_in = isset($in['surfaces']) && is_array($in['surfaces']) ? $in['surfaces'] : array(); $builtin = array('login', 'register', 'lost_password', 'magic_link', 'product_request', 'contact_form'); $surfaces = array(); foreach ($builtin as $surface) { $surfaces[$surface] = !empty($surfaces_in[$surface]); } $raw_rendered = isset($in['surfaces_rendered']) ? $in['surfaces_rendered'] : array(); if (is_array($raw_rendered)) { $rendered = array_values(array_map('strval', array_filter($raw_rendered, 'is_scalar'))); } else { $rendered = array_values(array_filter(array_map('trim', explode("\n", (string) $raw_rendered)), 'strlen')); } $registry_ok = $this->load_registry(); if (!$registry_ok) { $rendered = array(); } $stored = isset($current['surfaces']) && is_array($current['surfaces']) ? $current['surfaces'] : array(); foreach ($stored as $key => $on) { $key = (string) $key; if (in_array($key, $builtin, true) || empty($on)) { continue; } if (in_array($key, $rendered, true)) { continue; } if ($registry_ok && !AIEO_Form_Registry::is_canonical_surface($key)) { continue; } $surfaces[$key] = true; } if ($registry_ok) { foreach ($rendered as $key) { if (in_array($key, $builtin, true)) { continue; } if (!AIEO_Form_Registry::is_canonical_surface($key)) { continue; } if (!empty($surfaces_in[$key])) { $surfaces[$key] = true; } } } return array( 'enabled' => !empty($in['enabled']), 'site_key' => isset($in['site_key']) ? sanitize_text_field((string) $in['site_key']) : '', 'secret_key' => $secret, 'threshold' => $threshold, 'surfaces' => $surfaces, 'fail_open' => !empty($in['fail_open']), 'hide_badge' => !empty($in['hide_badge']), ); } }