'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 '' . 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(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 '' . 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']), ); } }