From c086676aa4cd89f94f135f1769419b0c92228987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Thu, 16 Apr 2026 15:56:31 +0200 Subject: [PATCH 01/20] Add Friendly Captcha v2 as alternative spam prevention provider - New PHP classes: FriendlyCaptcha, FriendlyCaptchaInterface, SettingsFriendlyCaptcha - New enqueue class for Friendly Captcha SDK CDN script - Config constants, env var helpers, fallback flags, filter registration - Backend captcha dispatch in AbstractIntegrationFormSubmit (FC or reCAPTCHA) - DI wiring for all 13 concrete FormSubmit routes - Frontend: state management, widget initialization, form submit integration - Mutual exclusivity: FC disabled when reCAPTCHA is active - Settings UI with setup instructions and warning when reCAPTCHA is enabled --- src/Blocks/components/form/assets/form.js | 56 +++++- .../form/assets/friendly-captcha.js | 97 ++++++++++ src/Blocks/components/form/assets/index.js | 7 + .../components/form/assets/state-init.js | 12 ++ src/Blocks/components/form/assets/state.js | 11 ++ src/Blocks/manifest.json | 1 + src/Config/Config.php | 1 + src/Enqueue/Blocks/EnqueueBlocks.php | 35 +++- .../EnqueueFriendlyCaptcha.php | 118 +++++++++++ src/FriendlyCaptcha/FriendlyCaptcha.php | 139 +++++++++++++ .../FriendlyCaptchaInterface.php | 27 +++ .../SettingsFriendlyCaptcha.php | 183 ++++++++++++++++++ src/Hooks/Filters.php | 6 + src/Hooks/FiltersSettingsBuilder.php | 11 ++ src/Hooks/Variables.php | 20 ++ src/Labels/Labels.php | 3 + .../Routes/AbstractIntegrationFormSubmit.php | 48 ++++- .../FormSubmitActiveCampaignRoute.php | 4 + .../Airtable/FormSubmitAirtableRoute.php | 4 + .../Goodbits/FormSubmitGoodbitsRoute.php | 4 + .../Greenhouse/FormSubmitGreenhouseRoute.php | 4 + .../Hubspot/FormSubmitHubspotRoute.php | 4 + .../Integrations/Jira/FormSubmitJiraRoute.php | 4 + .../Mailchimp/FormSubmitMailchimpRoute.php | 4 + .../Mailerlite/FormSubmitMailerliteRoute.php | 4 + .../Moments/FormSubmitMomentsRoute.php | 4 + .../FormSubmitNationbuilderRoute.php | 4 + .../Pipedrive/FormSubmitPipedriveRoute.php | 4 + .../Talentlyft/FormSubmitTalentlyftRoute.php | 4 + .../Workable/FormSubmitWorkableRoute.php | 4 + src/Troubleshooting/SettingsFallback.php | 6 + 31 files changed, 817 insertions(+), 16 deletions(-) create mode 100644 src/Blocks/components/form/assets/friendly-captcha.js create mode 100644 src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php create mode 100644 src/FriendlyCaptcha/FriendlyCaptcha.php create mode 100644 src/FriendlyCaptcha/FriendlyCaptchaInterface.php create mode 100644 src/FriendlyCaptcha/SettingsFriendlyCaptcha.php diff --git a/src/Blocks/components/form/assets/form.js b/src/Blocks/components/form/assets/form.js index 3df726e85..c3b41050c 100644 --- a/src/Blocks/components/form/assets/form.js +++ b/src/Blocks/components/form/assets/form.js @@ -588,6 +588,28 @@ export class Form { } } + /** + * Run form with Friendly Captcha validation. + * + * @param {string} formId Form Id. + * @param {object} filter Additional filter to pass. + * + * @returns {void} + */ + runFormFriendlyCaptcha(formId, filter = {}) { + if (!this.state.getStateFriendlyCaptchaIsUsed()) { + return; + } + + const token = window[prefix]?.friendlyCaptcha?.getResponse() ?? ''; + + this.setFormDataFriendlyCaptcha({ + token, + }); + + this.formSubmit(formId, filter); + } + /** * Execute enterprise captcha. * @@ -1159,6 +1181,22 @@ export class Form { ], this.FORM_DATA); } + /** + * Set form data object for all forms - friendly captcha. + * + * @param {object} data Friendly Captcha data. + * + * @returns {void} + */ + setFormDataFriendlyCaptcha(data) { + this.utils.buildFormDataItems([ + { + name: this.state.getStateParam('friendlyCaptcha'), + value: data, + }, + ], this.FORM_DATA); + } + //////////////////////////////////////////////////////////////// // Fields //////////////////////////////////////////////////////////////// @@ -1820,6 +1858,8 @@ export class Form { if (this.state.getStateCaptchaIsUsed()) { this.runFormCaptcha(formId, filterFinal); + } else if (this.state.getStateFriendlyCaptchaIsUsed()) { + this.runFormFriendlyCaptcha(formId, filterFinal); } else { this.formSubmit(formId, filterFinal); } @@ -1835,6 +1875,8 @@ export class Form { if (this.state.getStateCaptchaIsUsed()) { this.runFormCaptcha(formId, filterNormal); + } else if (this.state.getStateFriendlyCaptchaIsUsed()) { + this.runFormFriendlyCaptcha(formId, filterNormal); } else { this.formSubmit(formId, filterNormal); } @@ -1954,6 +1996,8 @@ export class Form { if (!this.state.getStateConfigIsAdmin() && this.state.getStateFormConfigUseSingleSubmit(formId)) { if (this.state.getStateCaptchaIsUsed()) { this.runFormCaptcha(formId); + } else if (this.state.getStateFriendlyCaptchaIsUsed()) { + this.runFormFriendlyCaptcha(formId); } else { this.formSubmit(formId); } @@ -1978,7 +2022,7 @@ export class Form { custom?.showDropdown(); } - if (!this.state.getStateSettingsDisableScrollToFieldOnFocus()) { + if (!this.state.getStateSettingsDisableScrollToFieldOnFocus()) { this.utils.scrollAction(field); } @@ -2037,6 +2081,8 @@ export class Form { ) { if (this.state.getStateCaptchaIsUsed()) { this.runFormCaptcha(formId); + } else if (this.state.getStateFriendlyCaptchaIsUsed()) { + this.runFormFriendlyCaptcha(formId); } else { this.formSubmit(formId); } @@ -2083,6 +2129,8 @@ export class Form { if (!this.state.getStateConfigIsAdmin() && this.state.getStateFormConfigUseSingleSubmit(formId)) { if (this.state.getStateCaptchaIsUsed()) { this.runFormCaptcha(formId); + } else if (this.state.getStateFriendlyCaptchaIsUsed()) { + this.runFormFriendlyCaptcha(formId); } else { this.formSubmit(formId); } @@ -2193,6 +2241,12 @@ export class Form { setFormDataCaptcha: (data) => { this.setFormDataCaptcha(data); }, + setFormDataFriendlyCaptcha: (data) => { + this.setFormDataFriendlyCaptcha(data); + }, + runFormFriendlyCaptcha: (formId, filter = {}) => { + this.runFormFriendlyCaptcha(formId, filter); + }, setupInputField: (formId, name) => { this.setupInputField(formId, name); }, diff --git a/src/Blocks/components/form/assets/friendly-captcha.js b/src/Blocks/components/form/assets/friendly-captcha.js new file mode 100644 index 000000000..417f84ff3 --- /dev/null +++ b/src/Blocks/components/form/assets/friendly-captcha.js @@ -0,0 +1,97 @@ +/* global frcaptcha */ + +import { prefix, setStateWindow } from './state-init'; + +/** + * FriendlyCaptcha class. + */ +export class FriendlyCaptcha { + constructor(utils) { + /** @type {import('./utils').Utils} */ + this.utils = utils; + /** @type {import('./state').State} */ + this.state = this.utils.getState(); + + // Set all public methods. + this.publicMethods(); + } + + //////////////////////////////////////////////////////////////// + // Public methods + //////////////////////////////////////////////////////////////// + + /** + * Init all actions. + * + * @returns {void} + */ + init() { + if (!this.state.getStateFriendlyCaptchaIsUsed()) { + return; + } + + this.initWidget(); + } + + /** + * Initialize Friendly Captcha widget. + * + * @returns {void} + */ + initWidget() { + const siteKey = this.state.getStateFriendlyCaptchaSiteKey(); + + if (typeof frcaptcha === 'undefined') { + return; + } + + // Create a hidden container for the widget. + const container = document.createElement('div'); + container.style.display = 'none'; + document.body.appendChild(container); + + this.widget = frcaptcha.createWidget({ + element: container, + sitekey: siteKey, + startMode: 'auto', + }); + } + + /** + * Get the current response token from the widget. + * + * @returns {string} The solution token. + */ + getResponse() { + return this.widget?.getResponse() ?? ''; + } + + //////////////////////////////////////////////////////////////// + // Private methods - not shared to the public window object. + //////////////////////////////////////////////////////////////// + + /** + * Set all public methods. + * + * @returns {void} + */ + publicMethods() { + setStateWindow(); + + if (window[prefix].friendlyCaptcha) { + return; + } + + window[prefix].friendlyCaptcha = { + init: () => { + this.init(); + }, + initWidget: () => { + this.initWidget(); + }, + getResponse: () => { + return this.getResponse(); + }, + }; + } +} diff --git a/src/Blocks/components/form/assets/index.js b/src/Blocks/components/form/assets/index.js index 5c638c844..754c32898 100644 --- a/src/Blocks/components/form/assets/index.js +++ b/src/Blocks/components/form/assets/index.js @@ -24,6 +24,13 @@ domReady(() => { }); } + // Load Friendly Captcha if used. + if (state.getStateFriendlyCaptchaIsUsed()) { + import('./friendly-captcha').then(({ FriendlyCaptcha }) => { + new FriendlyCaptcha(utils).init(); + }); + } + if (!state.getStateSettingsFormDisableAutoInit()) { if (document.querySelectorAll(state.getStateSelector('form', true))?.length) { import('./form').then(({ Form }) => { diff --git a/src/Blocks/components/form/assets/state-init.js b/src/Blocks/components/form/assets/state-init.js index d9f2b000e..c3531359b 100644 --- a/src/Blocks/components/form/assets/state-init.js +++ b/src/Blocks/components/form/assets/state-init.js @@ -105,6 +105,9 @@ export const StateEnum = { CAPTCHA_LOAD_ON_INIT: 'loadOnInit', CAPTCHA_HIDE_BADGE: 'hideBadge', + FRIENDLY_CAPTCHA: 'friendlyCaptcha', + FRIENDLY_CAPTCHA_SITE_KEY: 'siteKey', + ENRICHMENT: 'enrichment', ENRICHMENT_FORM_PREFILL: 'formPrefill', ENRICHMENT_EXPIRATION: 'expiration', @@ -178,6 +181,7 @@ export function setStateInitial() { window[prefix].state = { [StateEnum.FORMS]: [], [StateEnum.CAPTCHA]: {}, + [StateEnum.FRIENDLY_CAPTCHA]: {}, [StateEnum.ENRICHMENT]: {}, [StateEnum.GEOLOCATION]: {}, [StateEnum.SETTINGS]: {}, @@ -268,6 +272,14 @@ export function setStateInitial() { setState([StateEnum.CAPTCHA_HIDE_BADGE], Boolean(captcha.hideBadge), StateEnum.CAPTCHA); } + // Friendly Captcha. + const friendlyCaptcha = esFormsLocalization.friendlyCaptcha ?? {}; + setState([StateEnum.IS_USED], Boolean(friendlyCaptcha.isUsed), StateEnum.FRIENDLY_CAPTCHA); + + if (friendlyCaptcha.isUsed) { + setState([StateEnum.FRIENDLY_CAPTCHA_SITE_KEY], friendlyCaptcha.siteKey, StateEnum.FRIENDLY_CAPTCHA); + } + // Geolocation. const geolocation = esFormsLocalization.geolocation ?? {}; setState([StateEnum.IS_USED], Boolean(geolocation.isUsed), StateEnum.GEOLOCATION); diff --git a/src/Blocks/components/form/assets/state.js b/src/Blocks/components/form/assets/state.js index 8d1e3dbd8..6cd566d5b 100644 --- a/src/Blocks/components/form/assets/state.js +++ b/src/Blocks/components/form/assets/state.js @@ -431,6 +431,17 @@ export class State { return getState([StateEnum.CAPTCHA_HIDE_BADGE], StateEnum.CAPTCHA); }; + //////////////////////////////////////////////////////////////// + // Friendly Captcha getters. + //////////////////////////////////////////////////////////////// + + getStateFriendlyCaptchaIsUsed = () => { + return getState([StateEnum.IS_USED], StateEnum.FRIENDLY_CAPTCHA); + }; + getStateFriendlyCaptchaSiteKey = () => { + return getState([StateEnum.FRIENDLY_CAPTCHA_SITE_KEY], StateEnum.FRIENDLY_CAPTCHA); + }; + //////////////////////////////////////////////////////////////// // Geolocation getters. //////////////////////////////////////////////////////////////// diff --git a/src/Blocks/manifest.json b/src/Blocks/manifest.json index 4ef4febae..6bb14dd5f 100644 --- a/src/Blocks/manifest.json +++ b/src/Blocks/manifest.json @@ -594,6 +594,7 @@ "hubspotPageUrl": "es-form-hubspot-page-url", "mailchimpTags": "es-form-mailchimp-tags", "captcha": "es-form-captcha", + "friendlyCaptcha": "es-form-friendly-captcha", "direct": "es-form-direct", "itemId": "es-form-item-id", "innerId": "es-form-inner-id", diff --git a/src/Config/Config.php b/src/Config/Config.php index eb28ecfda..a09ce571a 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -465,6 +465,7 @@ class Config public const FD_ACTION_EXTERNAL = 'actionExternal'; public const FD_API_STEPS = 'apiSteps'; public const FD_CAPTCHA = 'captcha'; + public const FD_FRIENDLY_CAPTCHA = 'friendlyCaptcha'; public const FD_STORAGE = 'storage'; public const FD_IS_VALID = 'isValid'; public const FD_IS_API_VALID = 'isApiValid'; diff --git a/src/Enqueue/Blocks/EnqueueBlocks.php b/src/Enqueue/Blocks/EnqueueBlocks.php index 82a55a8cb..15768cfda 100644 --- a/src/Enqueue/Blocks/EnqueueBlocks.php +++ b/src/Enqueue/Blocks/EnqueueBlocks.php @@ -20,6 +20,8 @@ use EightshiftForms\CustomPostType\Forms; use EightshiftForms\Enqueue\SharedEnqueue; use EightshiftForms\Enqueue\Captcha\EnqueueCaptcha; +use EightshiftForms\Enqueue\FriendlyCaptcha\EnqueueFriendlyCaptcha; +use EightshiftForms\FriendlyCaptcha\SettingsFriendlyCaptcha; use EightshiftForms\Geolocation\GeolocationInterface; use EightshiftForms\Geolocation\SettingsGeolocation; use EightshiftForms\Hooks\FiltersOutputMock; @@ -346,6 +348,21 @@ public function enqueueBlockFrontendScript(): void ]; } + // Check if Friendly Captcha data is set and valid. + if (\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + $output['friendlyCaptcha'] = [ + 'isUsed' => true, + 'siteKey' => SettingsHelpers::getOptionWithConstant( + Variables::getFriendlyCaptchaSiteKey(), + SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY + ), + ]; + } else { + $output['friendlyCaptcha'] = [ + 'isUsed' => false, + ]; + } + $output['isAdmin'] = false; if (\is_user_logged_in()) { @@ -364,20 +381,20 @@ public function enqueueBlockFrontendScript(): void */ protected function getFrontendScriptDependencies(): array { - if (!\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - return []; - } + $output = []; $scriptsDependency = HooksHelpers::getFilterName(['scripts', 'dependency', 'blocksFrontend']); - $scriptsDependencyOutput = []; if (\has_filter($scriptsDependency)) { - $scriptsDependencyOutput = \apply_filters($scriptsDependency, []); + $output = \apply_filters($scriptsDependency, []); } - return [ - "{$this->getAssetsPrefix()}-" . EnqueueCaptcha::CAPTCHA_ENQUEUE_HANDLE, - ...$scriptsDependencyOutput, - ]; + if (\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + $output[] = "{$this->getAssetsPrefix()}-" . EnqueueFriendlyCaptcha::FRIENDLY_CAPTCHA_ENQUEUE_HANDLE; + } elseif (\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + $output[] = "{$this->getAssetsPrefix()}-" . EnqueueCaptcha::CAPTCHA_ENQUEUE_HANDLE; + } + + return $output; } } diff --git a/src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php b/src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php new file mode 100644 index 000000000..43ed7645d --- /dev/null +++ b/src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php @@ -0,0 +1,118 @@ + List of all the script dependencies. + */ + protected function getFrontendScriptDependencies(): array + { + if (!\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + return []; + } + + $scriptsDependency = HooksHelpers::getFilterName(['scripts', 'dependency', 'friendlyCaptcha']); + $scriptsDependencyOutput = []; + + if (\has_filter($scriptsDependency)) { + $scriptsDependencyOutput = \apply_filters($scriptsDependency, []); + } + + return $scriptsDependencyOutput; + } + + /** + * Method that returns frontend script for Friendly Captcha if settings are correct. + * + * @return void + */ + public function enqueueScriptsFriendlyCaptcha(): void + { + // Check if Friendly Captcha data is set and valid. + $isSettingsGlobalValid = \apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false); + + // Bailout if settings are not ok. + if (!$isSettingsGlobalValid) { + return; + } + + $handle = "{$this->getAssetsPrefix()}-" . self::FRIENDLY_CAPTCHA_ENQUEUE_HANDLE; + + \wp_register_script( + $handle, + 'https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk@0.1.12/site.min.js', + $this->getFrontendScriptDependencies(), + $this->getAssetsVersion(), + \is_wp_version_compatible('6.3') ? $this->scriptArgs() : $this->scriptInFooter() + ); + \wp_enqueue_script($handle); + } + + /** + * Load script 'defer' or 'async'. + * + * @return string Whether to enqueue the script normally, with defer or async. + */ + protected function scriptStrategy(): string + { + return 'defer'; + } + + /** + * Method that returns assets name used to prefix asset handlers. + * + * @return string + */ + public function getAssetsPrefix(): string + { + return Config::MAIN_PLUGIN_ENQUEUE_ASSETS_PREFIX; + } + + /** + * Method that returns assets version for versioning asset handlers. + * + * @return string + */ + public function getAssetsVersion(): string + { + return Helpers::getPluginVersion(); + } +} diff --git a/src/FriendlyCaptcha/FriendlyCaptcha.php b/src/FriendlyCaptcha/FriendlyCaptcha.php new file mode 100644 index 000000000..18e662f61 --- /dev/null +++ b/src/FriendlyCaptcha/FriendlyCaptcha.php @@ -0,0 +1,139 @@ +labels = $labels; + } + + /** + * Check Friendly Captcha request. + * + * @param string $response Response token from frontend. + * @param array $formDetails Form details. + * + * @throws BadRequestException If captcha is not valid. + * + * @return array + */ + public function check(string $response, array $formDetails = []): array + { + if (!\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + return [ + AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), + AbstractBaseRoute::R_DEBUG => [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_FEATURE_DISABLED, + ], + ]; + } + + $debug = [ + 'response' => $response, + 'formDetails' => $formDetails, + ]; + + if (!$response) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('friendlyCaptchaBadRequest'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_MISSING_TOKEN, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + $siteKey = SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaSiteKey(), SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY); + $apiKey = SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaApiKey(), SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_API_KEY); + + $apiResponse = \wp_remote_post( + 'https://global.frcapi.com/api/v2/captcha/siteverify', + [ + 'headers' => [ + 'Content-Type' => 'application/json; charset=utf-8', + 'X-API-Key' => $apiKey, + ], + 'data_format' => 'body', + 'body' => \wp_json_encode([ + 'response' => $response, + 'sitekey' => $siteKey, + ]), + ] + ); + + // Generic error msg from WP. + if (\is_wp_error($apiResponse)) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('submitWpError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_WP_ERROR, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + // Get body from the response. + $responseBody = \json_decode(\wp_remote_retrieve_body($apiResponse), true) ?? []; + + $debug = \array_merge($debug, [ + 'responseBody' => $responseBody, + ]); + + // Check the success field. + if (empty($responseBody['success'])) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('friendlyCaptchaError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + return [ + AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), + AbstractBaseRoute::R_DEBUG => [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_SUCCESS, + AbstractBaseRoute::R_DEBUG => $debug, + ], + ]; + } +} diff --git a/src/FriendlyCaptcha/FriendlyCaptchaInterface.php b/src/FriendlyCaptcha/FriendlyCaptchaInterface.php new file mode 100644 index 000000000..9adae41fc --- /dev/null +++ b/src/FriendlyCaptcha/FriendlyCaptchaInterface.php @@ -0,0 +1,27 @@ + $formDetails Form details. + * + * @return array + */ + public function check(string $response, array $formDetails = []): array; +} diff --git a/src/FriendlyCaptcha/SettingsFriendlyCaptcha.php b/src/FriendlyCaptcha/SettingsFriendlyCaptcha.php new file mode 100644 index 000000000..5b46a8c75 --- /dev/null +++ b/src/FriendlyCaptcha/SettingsFriendlyCaptcha.php @@ -0,0 +1,183 @@ +labels = $labels; + } + + /** + * Register all the hooks. + * + * @return void + */ + public function register(): void + { + \add_filter(self::FILTER_SETTINGS_GLOBAL_NAME, [$this, 'getSettingsGlobalData']); + \add_filter(self::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, [$this, 'isSettingsGlobalValid']); + } + + /** + * Determine if settings global are valid. + * + * @return boolean + */ + public function isSettingsGlobalValid(): bool + { + $isUsed = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY); + $siteKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaSiteKey(), self::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY); + $apiKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaApiKey(), self::SETTINGS_FRIENDLY_CAPTCHA_API_KEY); + + if (!$isUsed || !$siteKey || !$apiKey) { + return false; + } + + // Mutual exclusivity: if Google reCAPTCHA is also valid, Friendly Captcha is not valid. + if (\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + return false; + } + + return true; + } + + /** + * Get global settings array for building settings page. + * + * @return array> + */ + public function getSettingsGlobalData(): array + { + // Bailout if feature is not active. + if (!SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY)) { + return SettingsOutputHelpers::getNoActiveFeature(); + } + + // Show warning if Google reCAPTCHA is also active. + if (SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY)) { + return [ + SettingsOutputHelpers::getIntro(self::SETTINGS_TYPE_KEY), + [ + 'component' => 'highlighted-content', + 'highlightedContentTitle' => \__('Google reCAPTCHA is active', 'eightshift-forms'), + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'highlightedContentSubtitle' => \__('Please disable Google reCAPTCHA before configuring Friendly Captcha.
Only one spam prevention provider can be active at a time.', 'eightshift-forms'), + 'highlightedContentIcon' => 'warning', + ], + ]; + } + + return [ + SettingsOutputHelpers::getIntro(self::SETTINGS_TYPE_KEY), + [ + 'component' => 'intro', + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'introSubtitle' => \__('Protect your forms from spam and abuse using Friendly Captcha.
A privacy-focused, GDPR-compliant alternative to Google reCAPTCHA.', 'eightshift-forms'), + ], + [ + 'component' => 'tabs', + 'tabsContent' => [ + [ + 'component' => 'tab', + 'tabLabel' => \__('General', 'eightshift-forms'), + 'tabContent' => [ + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getFriendlyCaptchaSiteKey(), + self::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY, + 'ES_FRIENDLY_CAPTCHA_SITE_KEY', + \__('Site key', 'eightshift-forms'), + ), + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getFriendlyCaptchaApiKey(), + self::SETTINGS_FRIENDLY_CAPTCHA_API_KEY, + 'ES_FRIENDLY_CAPTCHA_API_KEY', + \__('API key', 'eightshift-forms'), + ), + ], + ], + [ + 'component' => 'tab', + 'tabLabel' => \__('Help', 'eightshift-forms'), + 'tabContent' => [ + [ + 'component' => 'steps', + 'stepsTitle' => \__('How to get the Friendly Captcha API keys?', 'eightshift-forms'), + 'stepsContent' => [ + // translators: %s will be replaced with the external link. + \sprintf(\__('Visit the Friendly Captcha dashboard.', 'eightshift-forms'), 'https://app.friendlycaptcha.eu/dashboard'), + \__('Create a new application and copy the Site key.', 'eightshift-forms'), + \__('Go to API Keys and create a new API key.', 'eightshift-forms'), + \__('Copy both keys into the fields under the General tab or use the global constants.', 'eightshift-forms'), + \__('In the Friendly Captcha dashboard, set the widget mode to Non-interactive for an invisible captcha experience.', 'eightshift-forms'), + ], + ], + ], + ], + ], + ], + ]; + } +} diff --git a/src/Hooks/Filters.php b/src/Hooks/Filters.php index 0f876f673..b7f50c68e 100644 --- a/src/Hooks/Filters.php +++ b/src/Hooks/Filters.php @@ -31,6 +31,7 @@ use EightshiftForms\Troubleshooting\SettingsDebug; use EightshiftForms\Troubleshooting\SettingsFallback; use EightshiftForms\Captcha\SettingsCaptcha; +use EightshiftForms\FriendlyCaptcha\SettingsFriendlyCaptcha; use EightshiftForms\General\SettingsGeneral; use EightshiftForms\Integrations\Calculator\SettingsCalculator; use EightshiftForms\Integrations\Corvus\SettingsCorvus; @@ -166,6 +167,7 @@ private static function getPublicFilters(): array 'dependency' => [ 'admin', 'captcha', + 'friendlyCaptcha', 'blocksEditor', 'blocksFrontend', ], @@ -428,6 +430,10 @@ private static function getSettingsNonTranslatableNames(): array SettingsCaptcha::SETTINGS_CAPTCHA_PROJECT_ID_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_API_KEY, + SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY, + SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY, + SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_API_KEY, + SettingsGeolocation::SETTINGS_GEOLOCATION_USE_KEY, SettingsEnrichment::SETTINGS_ENRICHMENT_USE_KEY, diff --git a/src/Hooks/FiltersSettingsBuilder.php b/src/Hooks/FiltersSettingsBuilder.php index 34854fb48..a4cc680b4 100644 --- a/src/Hooks/FiltersSettingsBuilder.php +++ b/src/Hooks/FiltersSettingsBuilder.php @@ -56,6 +56,7 @@ use EightshiftForms\Troubleshooting\SettingsDebug; use EightshiftForms\Troubleshooting\SettingsFallback; use EightshiftForms\Captcha\SettingsCaptcha; +use EightshiftForms\FriendlyCaptcha\SettingsFriendlyCaptcha; use EightshiftForms\Entries\SettingsEntries; use EightshiftForms\Integrations\Calculator\SettingsCalculator; use EightshiftForms\Integrations\Corvus\SettingsCorvus; @@ -160,6 +161,16 @@ public function getSettingsFiltersData(): array 'externalLink' => 'https://www.google.com/recaptcha/about/', ], ], + SettingsFriendlyCaptcha::SETTINGS_TYPE_KEY => [ + 'settingsGlobal' => SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_NAME, + 'type' => Config::SETTINGS_INTERNAL_TYPE_ADVANCED, + 'use' => SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY, + 'labels' => [ + 'title' => \__('Friendly Captcha', 'eightshift-forms'), + 'desc' => \__('Privacy-focused spam prevention using Friendly Captcha.', 'eightshift-forms'), + 'externalLink' => 'https://friendlycaptcha.com/', + ], + ], SettingsGeolocation::SETTINGS_TYPE_KEY => [ 'settingsGlobal' => SettingsGeolocation::FILTER_SETTINGS_GLOBAL_NAME, 'type' => Config::SETTINGS_INTERNAL_TYPE_ADVANCED, diff --git a/src/Hooks/Variables.php b/src/Hooks/Variables.php index 7936127d7..a63ecf871 100644 --- a/src/Hooks/Variables.php +++ b/src/Hooks/Variables.php @@ -115,6 +115,26 @@ public static function getGoogleReCaptchaProjectIdKey() return \defined('ES_GOOGLE_RECAPTCHA_PROJECT_ID_KEY') ? \ES_GOOGLE_RECAPTCHA_PROJECT_ID_KEY : ''; } + /** + * Get Friendly Captcha site key. + * + * @return string + */ + public static function getFriendlyCaptchaSiteKey() + { + return \defined('ES_FRIENDLY_CAPTCHA_SITE_KEY') ? \ES_FRIENDLY_CAPTCHA_SITE_KEY : ''; + } + + /** + * Get Friendly Captcha API key. + * + * @return string + */ + public static function getFriendlyCaptchaApiKey() + { + return \defined('ES_FRIENDLY_CAPTCHA_API_KEY') ? \ES_FRIENDLY_CAPTCHA_API_KEY : ''; + } + /** * Get forms geolocation ip. Default: empty. * diff --git a/src/Labels/Labels.php b/src/Labels/Labels.php index 716082ea6..1d4aa8579 100644 --- a/src/Labels/Labels.php +++ b/src/Labels/Labels.php @@ -425,6 +425,9 @@ private function getCaptchaLabels(): array 'captchaScoreSpam' => \__('The request was marked as a potential spam request. Please try again.', 'eightshift-forms'), 'captchaError' => \__('Spam prevention system encountered an error. Please try again.', 'eightshift-forms'), 'captchaSuccess' => \__('Success', 'eightshift-forms'), + 'friendlyCaptchaBadRequest' => \__('Spam prevention system encountered an error. Friendly Captcha request is invalid or malformed.', 'eightshift-forms'), + 'friendlyCaptchaError' => \__('Spam prevention system encountered an error. Please try again.', 'eightshift-forms'), + 'friendlyCaptchaSuccess' => \__('Success', 'eightshift-forms'), ]; } diff --git a/src/Rest/Routes/AbstractIntegrationFormSubmit.php b/src/Rest/Routes/AbstractIntegrationFormSubmit.php index fa5893686..023a6ffb9 100644 --- a/src/Rest/Routes/AbstractIntegrationFormSubmit.php +++ b/src/Rest/Routes/AbstractIntegrationFormSubmit.php @@ -12,6 +12,8 @@ use EightshiftForms\Captcha\CaptchaInterface; // phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse use EightshiftForms\Enrichment\EnrichmentInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; // phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse +use EightshiftForms\FriendlyCaptcha\SettingsFriendlyCaptcha; use EightshiftForms\Entries\EntriesHelper; use EightshiftForms\Entries\SettingsEntries; use EightshiftForms\Exception\BadRequestException; @@ -75,6 +77,13 @@ abstract class AbstractIntegrationFormSubmit extends AbstractBaseRoute */ protected $captcha; + /** + * Instance variable of FriendlyCaptchaInterface data. + * + * @var FriendlyCaptchaInterface + */ + protected $friendlyCaptcha; + /** * Instance variable of enrichment data. * @@ -89,6 +98,7 @@ abstract class AbstractIntegrationFormSubmit extends AbstractBaseRoute * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailer methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. */ @@ -97,6 +107,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment ) { @@ -104,6 +115,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; } @@ -225,12 +237,19 @@ public function routeCallback(WP_REST_Request $request) // Validate captcha. if ($this->shouldCheckCaptcha()) { - $this->getCaptcha()->check( - $formDetails[Config::FD_CAPTCHA]['token'] ?? '', - $formDetails[Config::FD_CAPTCHA]['action'] ?? '', - $formDetails[Config::FD_CAPTCHA]['isEnterprise'] ?? false, - $formDetails - ); + if (\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + $this->getFriendlyCaptcha()->check( + $formDetails[Config::FD_FRIENDLY_CAPTCHA]['token'] ?? '', + $formDetails + ); + } else { + $this->getCaptcha()->check( + $formDetails[Config::FD_CAPTCHA]['token'] ?? '', + $formDetails[Config::FD_CAPTCHA]['action'] ?? '', + $formDetails[Config::FD_CAPTCHA]['isEnterprise'] ?? false, + $formDetails + ); + } } // Map enrichment data. @@ -757,6 +776,16 @@ protected function getCaptcha() return $this->captcha; } + /** + * Returns Friendly Captcha class. + * + * @return FriendlyCaptchaInterface + */ + protected function getFriendlyCaptcha() + { + return $this->friendlyCaptcha; + } + /** * Returns enrichment class. * @@ -1200,6 +1229,9 @@ protected function getFormDetailsApi($request): array // Get form captcha from params. $output[Config::FD_CAPTCHA] = $params[Config::FD_CAPTCHA] ?? []; + // Get form Friendly Captcha from params. + $output[Config::FD_FRIENDLY_CAPTCHA] = $params[Config::FD_FRIENDLY_CAPTCHA] ?? []; + // Get form post Id from params. $output[Config::FD_POST_ID] = $params[Config::FD_POST_ID] ?? ''; @@ -1278,6 +1310,10 @@ protected function prepareApiParams(WP_REST_Request $request, string $type = sel $output[Config::FD_CAPTCHA] = $value['value']; $output[Config::FD_PARAMS][$key] = $value; break; + case UtilsHelper::getStateParam('friendlyCaptcha'): + $output[Config::FD_FRIENDLY_CAPTCHA] = $value['value']; + $output[Config::FD_PARAMS][$key] = $value; + break; case UtilsHelper::getStateParam('actionExternal'): $output[Config::FD_ACTION_EXTERNAL] = $value['value']; $output[Config::FD_PARAMS][$key] = $value; diff --git a/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php b/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php index b25925394..b32214b88 100644 --- a/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php +++ b/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\ActiveCampaign; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ActiveCampaign\ActiveCampaignClientInterface; use EightshiftForms\Integrations\ActiveCampaign\SettingsActiveCampaign; @@ -51,6 +52,7 @@ class FormSubmitActiveCampaignRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ActiveCampaignClientInterface $activeCampaignClient Inject ActiveCampaign methods. @@ -60,6 +62,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ActiveCampaignClientInterface $activeCampaignClient @@ -68,6 +71,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->activeCampaignClient = $activeCampaignClient; diff --git a/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php b/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php index 89ff706de..ce92b209a 100644 --- a/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php +++ b/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Airtable; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Airtable\AirtableClientInterface; use EightshiftForms\Integrations\Airtable\SettingsAirtable; @@ -50,6 +51,7 @@ class FormSubmitAirtableRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param AirtableClientInterface $airtableClient Inject airtableClient methods. @@ -59,6 +61,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, AirtableClientInterface $airtableClient @@ -67,6 +70,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->airtableClient = $airtableClient; diff --git a/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php b/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php index 47f831243..257edec68 100644 --- a/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php +++ b/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Goodbits; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Goodbits\SettingsGoodbits; @@ -50,6 +51,7 @@ class FormSubmitGoodbitsRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $goodbitsClient Inject goodbitsClient methods. @@ -59,6 +61,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $goodbitsClient @@ -67,6 +70,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->goodbitsClient = $goodbitsClient; diff --git a/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php b/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php index b95f94caa..26221bb9f 100644 --- a/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php +++ b/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Greenhouse; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Greenhouse\SettingsGreenhouse; @@ -50,6 +51,7 @@ class FormSubmitGreenhouseRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $greenhouseClient Inject greenhouseClient methods. @@ -59,6 +61,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $greenhouseClient @@ -67,6 +70,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->greenhouseClient = $greenhouseClient; diff --git a/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php b/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php index 61cb79877..0ab5e17ab 100644 --- a/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php +++ b/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Hubspot; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Clearbit\ClearbitClientInterface; use EightshiftForms\Integrations\Hubspot\HubspotClientInterface; @@ -58,6 +59,7 @@ class FormSubmitHubspotRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param HubspotClientInterface $hubspotClient Inject hubspotClient methods. @@ -68,6 +70,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, HubspotClientInterface $hubspotClient, @@ -77,6 +80,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->hubspotClient = $hubspotClient; diff --git a/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php b/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php index 34d8b7a78..90c35fb81 100644 --- a/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php +++ b/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Jira; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Jira\JiraClientInterface; use EightshiftForms\Integrations\Jira\SettingsJira; @@ -50,6 +51,7 @@ class FormSubmitJiraRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param JiraClientInterface $jiraClient Inject jiraClient methods. @@ -59,6 +61,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, JiraClientInterface $jiraClient @@ -67,6 +70,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->jiraClient = $jiraClient; diff --git a/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php b/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php index 1ad54fa0f..2201590fc 100644 --- a/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php +++ b/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Mailchimp; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Mailchimp\MailchimpClientInterface; use EightshiftForms\Integrations\Mailchimp\SettingsMailchimp; @@ -50,6 +51,7 @@ class FormSubmitMailchimpRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param MailchimpClientInterface $mailchimpClient Inject mailchimpClient methods. @@ -59,6 +61,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, MailchimpClientInterface $mailchimpClient @@ -67,6 +70,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->mailchimpClient = $mailchimpClient; diff --git a/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php b/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php index 23758b574..628f9fefe 100644 --- a/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php +++ b/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Mailerlite; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Mailerlite\SettingsMailerlite; @@ -50,6 +51,7 @@ class FormSubmitMailerliteRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $mailerliteClient Inject mailerliteClient methods. @@ -59,6 +61,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $mailerliteClient @@ -67,6 +70,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->mailerliteClient = $mailerliteClient; diff --git a/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php b/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php index c3fe739aa..c81caaf1c 100644 --- a/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php +++ b/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Moments; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Moments\MomentsEventsInterface; @@ -59,6 +60,7 @@ class FormSubmitMomentsRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $momentsClient Inject momentsClient methods. @@ -69,6 +71,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $momentsClient, @@ -78,6 +81,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->momentsClient = $momentsClient; diff --git a/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php b/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php index cf3fcde93..6e91371ea 100644 --- a/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php +++ b/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Nationbuilder; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Nationbuilder\NationbuilderClientInterface; use EightshiftForms\Integrations\Nationbuilder\SettingsNationbuilder; @@ -50,6 +51,7 @@ class FormSubmitNationbuilderRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param NationbuilderClientInterface $nationbuilderClient Inject nationbuilderClient methods. @@ -59,6 +61,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, NationbuilderClientInterface $nationbuilderClient @@ -67,6 +70,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->nationbuilderClient = $nationbuilderClient; diff --git a/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php b/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php index 7b09f586b..49c0aee86 100644 --- a/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php +++ b/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Pipedrive; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Pipedrive\PipedriveClientInterface; use EightshiftForms\Integrations\Pipedrive\SettingsPipedrive; @@ -50,6 +51,7 @@ class FormSubmitPipedriveRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param PipedriveClientInterface $pipedriveClient Inject pipedriveClient methods. @@ -59,6 +61,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, PipedriveClientInterface $pipedriveClient @@ -67,6 +70,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->pipedriveClient = $pipedriveClient; diff --git a/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php b/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php index 349865d05..622b81948 100644 --- a/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php +++ b/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Talentlyft; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Talentlyft\SettingsTalentlyft; @@ -50,6 +51,7 @@ class FormSubmitTalentlyftRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $talentlyftClient Inject talentlyftClient methods. @@ -59,6 +61,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $talentlyftClient @@ -67,6 +70,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->talentlyftClient = $talentlyftClient; diff --git a/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php b/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php index c3abe351c..eeaafa257 100644 --- a/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php +++ b/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\Integrations\Workable; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Workable\SettingsWorkable; @@ -50,6 +51,7 @@ class FormSubmitWorkableRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. + * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $workableClient Inject workableClient methods. @@ -59,6 +61,7 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, + FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $workableClient @@ -67,6 +70,7 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->workableClient = $workableClient; diff --git a/src/Troubleshooting/SettingsFallback.php b/src/Troubleshooting/SettingsFallback.php index 04a270c47..7b3f72877 100644 --- a/src/Troubleshooting/SettingsFallback.php +++ b/src/Troubleshooting/SettingsFallback.php @@ -97,6 +97,12 @@ class SettingsFallback implements ServiceInterface, SettingsFallbackDataInterfac public const SETTINGS_FALLBACK_FLAG_CAPTCHA_SUCCESS = 'captchaSuccess'; public const SETTINGS_FALLBACK_FLAG_CAPTCHA_DEBUG_SKIP_CHECK = 'captchaDebugSkipCheck'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_FEATURE_DISABLED = 'friendlyCaptchaFeatureDisabled'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_MISSING_TOKEN = 'friendlyCaptchaRequestMissingToken'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_WP_ERROR = 'friendlyCaptchaRequestWpError'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR = 'friendlyCaptchaOutputError'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_SUCCESS = 'friendlyCaptchaSuccess'; + public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_FEATURE_DISABLED = 'geolocationFeatureDisabled'; public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_MALFORMED_DECRYPT_DATA = 'geolocationMalformedDecryptData'; public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_DETECTION_FAILED = 'geolocationDetectionFailed'; From 3cd12f956f1703803413cf33178b5fa85243935f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Thu, 16 Apr 2026 17:07:15 +0200 Subject: [PATCH 02/20] Add EU endpoint toggle and update settings labels - Replace endpoint select with toggle checkbox for EU endpoint - Rename reCAPTCHA sidebar title to Google reCAPTCHA - Add friendlyCaptcha icon to manifest - Pass endpoint config to frontend localization --- .../form/assets/friendly-captcha.js | 1 + .../components/form/assets/state-init.js | 2 + src/Blocks/components/form/assets/state.js | 3 ++ src/Blocks/manifest.json | 1 + src/Enqueue/Blocks/EnqueueBlocks.php | 1 + src/FriendlyCaptcha/FriendlyCaptcha.php | 2 +- .../SettingsFriendlyCaptcha.php | 51 +++++++++++++++++++ src/Hooks/Filters.php | 1 + src/Hooks/FiltersSettingsBuilder.php | 4 +- 9 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/Blocks/components/form/assets/friendly-captcha.js b/src/Blocks/components/form/assets/friendly-captcha.js index 417f84ff3..0d7e993e0 100644 --- a/src/Blocks/components/form/assets/friendly-captcha.js +++ b/src/Blocks/components/form/assets/friendly-captcha.js @@ -54,6 +54,7 @@ export class FriendlyCaptcha { element: container, sitekey: siteKey, startMode: 'auto', + apiEndpoint: this.state.getStateFriendlyCaptchaEndpoint(), }); } diff --git a/src/Blocks/components/form/assets/state-init.js b/src/Blocks/components/form/assets/state-init.js index c3531359b..23bdc3da3 100644 --- a/src/Blocks/components/form/assets/state-init.js +++ b/src/Blocks/components/form/assets/state-init.js @@ -107,6 +107,7 @@ export const StateEnum = { FRIENDLY_CAPTCHA: 'friendlyCaptcha', FRIENDLY_CAPTCHA_SITE_KEY: 'siteKey', + FRIENDLY_CAPTCHA_ENDPOINT: 'endpoint', ENRICHMENT: 'enrichment', ENRICHMENT_FORM_PREFILL: 'formPrefill', @@ -278,6 +279,7 @@ export function setStateInitial() { if (friendlyCaptcha.isUsed) { setState([StateEnum.FRIENDLY_CAPTCHA_SITE_KEY], friendlyCaptcha.siteKey, StateEnum.FRIENDLY_CAPTCHA); + setState([StateEnum.FRIENDLY_CAPTCHA_ENDPOINT], friendlyCaptcha.endpoint, StateEnum.FRIENDLY_CAPTCHA); } // Geolocation. diff --git a/src/Blocks/components/form/assets/state.js b/src/Blocks/components/form/assets/state.js index 6cd566d5b..097b0f583 100644 --- a/src/Blocks/components/form/assets/state.js +++ b/src/Blocks/components/form/assets/state.js @@ -441,6 +441,9 @@ export class State { getStateFriendlyCaptchaSiteKey = () => { return getState([StateEnum.FRIENDLY_CAPTCHA_SITE_KEY], StateEnum.FRIENDLY_CAPTCHA); }; + getStateFriendlyCaptchaEndpoint = () => { + return getState([StateEnum.FRIENDLY_CAPTCHA_ENDPOINT], StateEnum.FRIENDLY_CAPTCHA); + }; //////////////////////////////////////////////////////////////// // Geolocation getters. diff --git a/src/Blocks/manifest.json b/src/Blocks/manifest.json index 6bb14dd5f..83f045f79 100644 --- a/src/Blocks/manifest.json +++ b/src/Blocks/manifest.json @@ -772,6 +772,7 @@ "general": "", "validation": "", "captcha": "", + "friendlyCaptcha": "", "geolocation": "", "enrichment": "", "blocks": "", diff --git a/src/Enqueue/Blocks/EnqueueBlocks.php b/src/Enqueue/Blocks/EnqueueBlocks.php index 15768cfda..e56fa1e6a 100644 --- a/src/Enqueue/Blocks/EnqueueBlocks.php +++ b/src/Enqueue/Blocks/EnqueueBlocks.php @@ -356,6 +356,7 @@ public function enqueueBlockFrontendScript(): void Variables::getFriendlyCaptchaSiteKey(), SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY ), + 'endpoint' => SettingsFriendlyCaptcha::getEndpoint(), ]; } else { $output['friendlyCaptcha'] = [ diff --git a/src/FriendlyCaptcha/FriendlyCaptcha.php b/src/FriendlyCaptcha/FriendlyCaptcha.php index 18e662f61..7621a561e 100644 --- a/src/FriendlyCaptcha/FriendlyCaptcha.php +++ b/src/FriendlyCaptcha/FriendlyCaptcha.php @@ -81,7 +81,7 @@ public function check(string $response, array $formDetails = []): array $apiKey = SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaApiKey(), SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_API_KEY); $apiResponse = \wp_remote_post( - 'https://global.frcapi.com/api/v2/captcha/siteverify', + SettingsFriendlyCaptcha::getEndpointUrl(), [ 'headers' => [ 'Content-Type' => 'application/json; charset=utf-8', diff --git a/src/FriendlyCaptcha/SettingsFriendlyCaptcha.php b/src/FriendlyCaptcha/SettingsFriendlyCaptcha.php index 5b46a8c75..3f3190be9 100644 --- a/src/FriendlyCaptcha/SettingsFriendlyCaptcha.php +++ b/src/FriendlyCaptcha/SettingsFriendlyCaptcha.php @@ -53,6 +53,17 @@ class SettingsFriendlyCaptcha implements SettingGlobalInterface, ServiceInterfac */ public const SETTINGS_FRIENDLY_CAPTCHA_API_KEY = 'friendly-captcha-api-key'; + /** + * Friendly Captcha use EU endpoint key. + */ + public const SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY = 'friendly-captcha-use-eu-endpoint'; + + /** + * Friendly Captcha API endpoint URLs. + */ + public const FRIENDLY_CAPTCHA_ENDPOINT_GLOBAL_URL = 'https://global.frcapi.com/api/v2/captcha/siteverify'; + public const FRIENDLY_CAPTCHA_ENDPOINT_EU_URL = 'https://eu.frcapi.com/api/v2/captcha/siteverify'; + /** * Instance variable for labels data. * @@ -156,6 +167,26 @@ public function getSettingsGlobalData(): array 'ES_FRIENDLY_CAPTCHA_API_KEY', \__('API key', 'eightshift-forms'), ), + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'checkboxes', + 'checkboxesFieldLabel' => '', + 'checkboxesName' => SettingsHelpers::getSettingName(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY), + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'checkboxesFieldHelp' => \__('The EU endpoint is hosted in Germany and ensures visitor data never leaves the EU.
Requires a Friendly Captcha Advanced or Enterprise plan.', 'eightshift-forms'), + 'checkboxesContent' => [ + [ + 'component' => 'checkbox', + 'checkboxLabel' => \__('Use EU endpoint', 'eightshift-forms'), + 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY), + 'checkboxValue' => self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, + 'checkboxAsToggle' => true, + ], + ], + ], ], ], [ @@ -180,4 +211,24 @@ public function getSettingsGlobalData(): array ], ]; } + + /** + * Get the selected endpoint value. + * + * @return string + */ + public static function getEndpoint(): string + { + return SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY) ? 'eu' : 'global'; + } + + /** + * Get the siteverify URL for the selected endpoint. + * + * @return string + */ + public static function getEndpointUrl(): string + { + return self::getEndpoint() === 'eu' ? self::FRIENDLY_CAPTCHA_ENDPOINT_EU_URL : self::FRIENDLY_CAPTCHA_ENDPOINT_GLOBAL_URL; + } } diff --git a/src/Hooks/Filters.php b/src/Hooks/Filters.php index b7f50c68e..6cab44957 100644 --- a/src/Hooks/Filters.php +++ b/src/Hooks/Filters.php @@ -433,6 +433,7 @@ private static function getSettingsNonTranslatableNames(): array SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY, SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY, SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_API_KEY, + SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, SettingsGeolocation::SETTINGS_GEOLOCATION_USE_KEY, diff --git a/src/Hooks/FiltersSettingsBuilder.php b/src/Hooks/FiltersSettingsBuilder.php index a4cc680b4..9dbcd0ef8 100644 --- a/src/Hooks/FiltersSettingsBuilder.php +++ b/src/Hooks/FiltersSettingsBuilder.php @@ -156,8 +156,8 @@ public function getSettingsFiltersData(): array 'type' => Config::SETTINGS_INTERNAL_TYPE_ADVANCED, 'use' => SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, 'labels' => [ - 'title' => \__('Spam prevention', 'eightshift-forms'), - 'desc' => \__('Prevent misuse of your forms by adding Google ReCaptcha.', 'eightshift-forms'), + 'title' => \__('Google reCAPTCHA', 'eightshift-forms'), + 'desc' => \__('Prevent misuse of your forms by adding Google reCAPTCHA.', 'eightshift-forms'), 'externalLink' => 'https://www.google.com/recaptcha/about/', ], ], From 88cf26f0b0cf56024e7fbbd68c8050c1ec7028e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Fri, 17 Apr 2026 09:24:36 +0200 Subject: [PATCH 03/20] Bump Friendly Captcha version --- src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php b/src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php index 43ed7645d..192edc7c8 100644 --- a/src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php +++ b/src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php @@ -78,7 +78,7 @@ public function enqueueScriptsFriendlyCaptcha(): void \wp_register_script( $handle, - 'https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk@0.1.12/site.min.js', + 'https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk@0.2.0/site.min.js', $this->getFrontendScriptDependencies(), $this->getAssetsVersion(), \is_wp_version_compatible('6.3') ? $this->scriptArgs() : $this->scriptInFooter() From c09a38100fb0ceb19559a6769e7aeac48ba9ceac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Mon, 20 Apr 2026 11:23:17 +0200 Subject: [PATCH 04/20] Update version --- CHANGELOG.md | 7 +++++++ eightshift-forms.php | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 227c1bfe9..88405a38d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a CHANGELOG](https://keepachangelog.com/). +## [9.2.0] + +### Added + +- Added Friendly Captcha as an alternative spam prevention provider alongside Google reCAPTCHA. Includes site key/API key configuration, EU endpoint toggle, automatic SDK loading, and mutual exclusivity with reCAPTCHA. + ## [9.1.6] ### Changed @@ -1813,6 +1819,7 @@ This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a - Initial production release. +[9.2.0]: https://github.com/infinum/eightshift-forms/compare/9.1.6...9.2.0 [9.1.6]: https://github.com/infinum/eightshift-forms/compare/9.1.5...9.1.6 [9.1.5]: https://github.com/infinum/eightshift-forms/compare/9.1.4...9.1.5 [9.1.4]: https://github.com/infinum/eightshift-forms/compare/9.1.3...9.1.4 diff --git a/eightshift-forms.php b/eightshift-forms.php index 539a13ba1..ff5397b81 100644 --- a/eightshift-forms.php +++ b/eightshift-forms.php @@ -6,7 +6,7 @@ * Description: Eightshift Forms is a complete form builder plugin that utilizes modern Block editor features with multiple third-party integrations, bringing your project to a new level. * Author: WordPress team @Infinum * Author URI: https://eightshift.com/ - * Version: 9.1.6 + * Version: 9.2.0 * Text Domain: eightshift-forms * * @package EightshiftForms diff --git a/package.json b/package.json index 9979f4cf7..d3eaf568d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eightshift/eightshift-forms", - "version": "9.1.6", + "version": "9.2.0", "description": "This repository contains all the tools you need to start building a modern WordPress project.", "authors": [ { From de4f98576bf8f1395e7f0bdf1c4e14a7332f622b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Mon, 20 Apr 2026 16:08:36 +0200 Subject: [PATCH 05/20] Update logic to share the same Captcha interface --- src/Captcha/FriendlyCaptcha.php | 179 +++++++++ src/Captcha/SettingsCaptcha.php | 353 +++++++++--------- src/Captcha/SettingsCaptchaProvider.php | 143 +++++++ src/Captcha/SettingsFriendlyCaptcha.php | 230 ++++++++++++ src/Config/Config.php | 1 - src/Enqueue/Blocks/EnqueueBlocks.php | 11 +- .../EnqueueFriendlyCaptcha.php | 6 +- src/FriendlyCaptcha/FriendlyCaptcha.php | 139 ------- .../FriendlyCaptchaInterface.php | 27 -- .../SettingsFriendlyCaptcha.php | 234 ------------ src/Hooks/Filters.php | 2 +- src/Hooks/FiltersSettingsBuilder.php | 21 +- .../Routes/AbstractIntegrationFormSubmit.php | 46 +-- .../Routes/General/CaptchaValidateRoute.php | 18 + .../FormSubmitActiveCampaignRoute.php | 4 - .../Airtable/FormSubmitAirtableRoute.php | 4 - .../Goodbits/FormSubmitGoodbitsRoute.php | 4 - .../Greenhouse/FormSubmitGreenhouseRoute.php | 4 - .../Hubspot/FormSubmitHubspotRoute.php | 4 - .../Integrations/Jira/FormSubmitJiraRoute.php | 4 - .../Mailchimp/FormSubmitMailchimpRoute.php | 4 - .../Mailerlite/FormSubmitMailerliteRoute.php | 4 - .../Moments/FormSubmitMomentsRoute.php | 4 - .../FormSubmitNationbuilderRoute.php | 4 - .../Pipedrive/FormSubmitPipedriveRoute.php | 4 - .../Talentlyft/FormSubmitTalentlyftRoute.php | 4 - .../Workable/FormSubmitWorkableRoute.php | 4 - 27 files changed, 778 insertions(+), 684 deletions(-) create mode 100644 src/Captcha/FriendlyCaptcha.php create mode 100644 src/Captcha/SettingsCaptchaProvider.php create mode 100644 src/Captcha/SettingsFriendlyCaptcha.php rename src/Enqueue/{FriendlyCaptcha => Captcha}/EnqueueFriendlyCaptcha.php (94%) delete mode 100644 src/FriendlyCaptcha/FriendlyCaptcha.php delete mode 100644 src/FriendlyCaptcha/FriendlyCaptchaInterface.php delete mode 100644 src/FriendlyCaptcha/SettingsFriendlyCaptcha.php diff --git a/src/Captcha/FriendlyCaptcha.php b/src/Captcha/FriendlyCaptcha.php new file mode 100644 index 000000000..e0ebbb235 --- /dev/null +++ b/src/Captcha/FriendlyCaptcha.php @@ -0,0 +1,179 @@ +labels = $labels; + } + + /** + * Check captcha request. + * + * Friendly Captcha does not use `$action` or `$isEnterprise` — they are + * accepted only for interface compatibility with Google reCAPTCHA. + * + * @param string $token Token from frontend. + * @param string $action Action to check (unused). + * @param boolean $isEnterprise Type of captcha (unused). + * @param array $formDetails Form details. + * + * @return array + */ + public function check(string $token, string $action, bool $isEnterprise, array $formDetails = []): array + { + if (!\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + return $this->buildSuccess(SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_FEATURE_DISABLED, []); + } + + $debug = [ + 'token' => $token, + 'formDetails' => $formDetails, + ]; + + if (!$token) { + $this->throwError( + $this->labels->getLabel('friendlyCaptchaBadRequest'), + SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_MISSING_TOKEN, + $debug + ); + } + + $response = $this->remoteCall($token); + + if (\is_wp_error($response)) { + $this->throwError( + $this->labels->getLabel('submitWpError'), + SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_WP_ERROR, + $debug + ); + } + + $body = \json_decode(\wp_remote_retrieve_body($response), true) ?? []; + + $debug['responseBody'] = $body; + + if (empty($body['success'])) { + $this->throwError( + $this->labels->getLabel('friendlyCaptchaError'), + SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR, + $debug + ); + } + + return $this->buildSuccess(SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_SUCCESS, $debug); + } + + /** + * Make the remote verification call to Friendly Captcha. + * + * @param string $token Verification token from the frontend widget. + * + * @return array|WP_Error + */ + private function remoteCall(string $token): array|WP_Error + { + $siteKey = SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaSiteKey(), SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY); + $apiKey = SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaApiKey(), SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_API_KEY); + + return \wp_remote_post( + SettingsFriendlyCaptcha::getEndpointUrl(), + [ + 'headers' => [ + 'Content-Type' => 'application/json; charset=utf-8', + 'X-API-Key' => $apiKey, + ], + 'data_format' => 'body', + 'body' => \wp_json_encode([ + 'response' => $token, + 'sitekey' => $siteKey, + ]), + ] + ); + } + + /** + * Build the success response envelope. + * + * @param string $flag Fallback flag constant. + * @param array $debug Debug payload. + * @param array $data Extra R_DATA payload. + * + * @return array + */ + private function buildSuccess(string $flag, array $debug, array $data = []): array + { + $output = [ + AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), + AbstractBaseRoute::R_DEBUG => [ + AbstractBaseRoute::R_DEBUG_KEY => $flag, + AbstractBaseRoute::R_DEBUG => $debug, + ], + ]; + + if ($data) { + $output[AbstractBaseRoute::R_DATA] = $data; + } + + return $output; + } + + /** + * Throw a BadRequestException with the project's debug envelope. + * + * @param string $message Localized error message. + * @param string $flag Fallback flag constant. + * @param array $debug Debug payload. + * @param array $extraData Optional extra data. + * + * @throws BadRequestException Always. + * + * @return void + */ + private function throwError(string $message, string $flag, array $debug, array $extraData = []): void + { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $message, + [ + AbstractBaseRoute::R_DEBUG_KEY => $flag, + AbstractBaseRoute::R_DEBUG => $debug, + ], + $extraData + ); + // phpcs:enable + } +} diff --git a/src/Captcha/SettingsCaptcha.php b/src/Captcha/SettingsCaptcha.php index 29f2d57d6..d10a16a08 100644 --- a/src/Captcha/SettingsCaptcha.php +++ b/src/Captcha/SettingsCaptcha.php @@ -133,6 +133,10 @@ public function register(): void */ public function isSettingsGlobalValid(): bool { + if (SettingsCaptchaProvider::getActiveProvider() !== SettingsCaptchaProvider::PROVIDER_GOOGLE) { + return false; + } + $isUsed = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_USE_KEY, self::SETTINGS_CAPTCHA_USE_KEY); $siteKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), self::SETTINGS_CAPTCHA_SITE_KEY); $secretKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSecretKey(), self::SETTINGS_CAPTCHA_SECRET_KEY); @@ -157,202 +161,215 @@ public function isSettingsGlobalValid(): bool /** * Get global settings array for building settings page. * + * Retained for BC in case the `FILTER_SETTINGS_GLOBAL_NAME` filter is still + * consulted; the menu entry is now handled by `SettingsCaptchaProvider`. + * * @return array> */ public function getSettingsGlobalData(): array { - // Bailout if feature is not active. if (!SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_USE_KEY, self::SETTINGS_CAPTCHA_USE_KEY)) { return SettingsOutputHelpers::getNoActiveFeature(); } - $isEnterprise = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, self::SETTINGS_CAPTCHA_ENTERPRISE_KEY); - $isInit = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY); - return [ SettingsOutputHelpers::getIntro(self::SETTINGS_TYPE_KEY), [ - 'component' => 'intro', - // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings - 'introSubtitle' => \__('Protect your website from spam and abuse using Google\'s reCAPTCHA.
A captcha is a simple task that is easy for humans to do, but difficult for bots.', 'eightshift-forms'), + 'component' => 'tabs', + 'tabsContent' => self::getProviderTabs(), ], + ]; + } + + /** + * Tab definitions for the Google reCAPTCHA provider, composed into the + * merged captcha page by `SettingsCaptchaProvider`. + * + * @return array> + */ + public static function getProviderTabs(): array + { + $isEnterprise = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, self::SETTINGS_CAPTCHA_ENTERPRISE_KEY); + $isInit = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY); + + return [ [ - 'component' => 'tabs', - 'tabsContent' => [ + 'component' => 'tab', + 'tabLabel' => \__('General', 'eightshift-forms'), + 'tabContent' => [ [ - 'component' => 'tab', - 'tabLabel' => \__('General', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'checkboxes', - 'checkboxesFieldHideLabel' => true, - 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Use reCAPTCHA Enterprise', 'eightshift-forms'), - 'checkboxIsChecked' => $isEnterprise, - 'checkboxValue' => self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, - 'checkboxSingleSubmit' => true, - 'checkboxAsToggle' => true, - ], - ], - ], + 'component' => 'intro', + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'introSubtitle' => \__('Protect your website from spam and abuse using Google\'s reCAPTCHA.
A captcha is a simple task that is easy for humans to do, but difficult for bots.', 'eightshift-forms'), + ], + [ + 'component' => 'checkboxes', + 'checkboxesFieldHideLabel' => true, + 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY), + 'checkboxesContent' => [ [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, + 'component' => 'checkbox', + 'checkboxLabel' => \__('Use reCAPTCHA Enterprise', 'eightshift-forms'), + 'checkboxIsChecked' => $isEnterprise, + 'checkboxValue' => self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, + 'checkboxSingleSubmit' => true, + 'checkboxAsToggle' => true, ], - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getGoogleReCaptchaSiteKey(), - self::SETTINGS_CAPTCHA_SITE_KEY, - 'ES_GOOGLE_RECAPTCHA_SITE_KEY', - \__('Site key', 'eightshift-forms'), - ), - - ...(!$isEnterprise ? [ - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getGoogleReCaptchaSecretKey(), - self::SETTINGS_CAPTCHA_SECRET_KEY, - 'ES_GOOGLE_RECAPTCHA_SECRET_KEY', - \__('Secret key', 'eightshift-forms'), - ), - ] : [ - SettingsOutputHelpers::getInputFieldWithGlobalVariable( - Variables::getGoogleReCaptchaProjectIdKey(), - self::SETTINGS_CAPTCHA_PROJECT_ID_KEY, - 'ES_GOOGLE_RECAPTCHA_PROJECT_ID_KEY', - \__('Project ID', 'eightshift-forms'), - ), - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getGoogleReCaptchaApiKey(), - self::SETTINGS_CAPTCHA_API_KEY, - 'ES_GOOGLE_RECAPTCHA_API_KEY', - \__('API key', 'eightshift-forms'), - ), - ]), ], ], [ - 'component' => 'tab', - 'tabLabel' => \__('Advanced', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'checkboxes', - 'checkboxesFieldHideLabel' => true, - 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Hide badge', 'eightshift-forms'), - 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), - 'checkboxValue' => self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, - 'checkboxSingleSubmit' => true, - 'checkboxAsToggle' => true, - 'checkboxHelp' => \__('Not recommended, as it is against Google\'s terms of use.', 'eightshift-forms'), - ], - ], - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - [ - 'component' => 'input', - 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SCORE_KEY), - 'inputFieldLabel' => \__('"Spam unlikely" threshold', 'eightshift-forms'), - 'inputFieldHelp' => \__('The level above which a submission is not considered spam. Should be between 0.1 and 1.0.
In most cases, a user will receive as core between 0.8 and 0.9.', 'eightshift-forms'), - 'inputType' => 'number', - 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SCORE_KEY), - 'inputMin' => 0.1, - 'inputMax' => 1, - 'inputStep' => 0.1, - 'inputIsNumber' => true, - 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SCORE_DEFAULT_KEY, - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - [ - 'component' => 'input', - 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), - 'inputFieldLabel' => \__('"On submit" action name', 'eightshift-forms'), - 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA on form submission.', 'eightshift-forms'), - 'inputType' => 'text', - 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), - 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getGoogleReCaptchaSiteKey(), + self::SETTINGS_CAPTCHA_SITE_KEY, + 'ES_GOOGLE_RECAPTCHA_SITE_KEY', + \__('Site key', 'eightshift-forms'), + ), + + ...(!$isEnterprise ? [ + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getGoogleReCaptchaSecretKey(), + self::SETTINGS_CAPTCHA_SECRET_KEY, + 'ES_GOOGLE_RECAPTCHA_SECRET_KEY', + \__('Secret key', 'eightshift-forms'), + ), + ] : [ + SettingsOutputHelpers::getInputFieldWithGlobalVariable( + Variables::getGoogleReCaptchaProjectIdKey(), + self::SETTINGS_CAPTCHA_PROJECT_ID_KEY, + 'ES_GOOGLE_RECAPTCHA_PROJECT_ID_KEY', + \__('Project ID', 'eightshift-forms'), + ), + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getGoogleReCaptchaApiKey(), + self::SETTINGS_CAPTCHA_API_KEY, + 'ES_GOOGLE_RECAPTCHA_API_KEY', + \__('API key', 'eightshift-forms'), + ), + ]), + ], + ], + [ + 'component' => 'tab', + 'tabLabel' => \__('Advanced', 'eightshift-forms'), + 'tabContent' => [ + [ + 'component' => 'checkboxes', + 'checkboxesFieldHideLabel' => true, + 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), + 'checkboxesContent' => [ [ - 'component' => 'checkboxes', - 'checkboxesFieldHideLabel' => true, - 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Load Captcha on website load', 'eightshift-forms'), - 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), - 'checkboxValue' => self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, - 'checkboxHelp' => \__('By default, Captcha is only loaded on pages that contain forms. However, with this option, you can load Captcha on every page.', 'eightshift-forms'), - 'checkboxSingleSubmit' => true, - 'checkboxAsToggle' => true, - 'checkboxAsToggleSize' => 'medium', - ], - ], + 'component' => 'checkbox', + 'checkboxLabel' => \__('Hide badge', 'eightshift-forms'), + 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), + 'checkboxValue' => self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, + 'checkboxSingleSubmit' => true, + 'checkboxAsToggle' => true, + 'checkboxHelp' => \__('Not recommended, as it is against Google\'s terms of use.', 'eightshift-forms'), ], - $isInit ? [ - 'component' => 'input', - 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), - 'inputFieldLabel' => \__('Action name', 'eightshift-forms'), - 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA when Captcha is loaded on every page.', 'eightshift-forms'), - 'inputType' => 'text', - 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), - 'inputPlaceholder' => self::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, - ] : [], ], ], [ - 'component' => 'tab', - 'tabLabel' => \__('Help', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'steps', - 'stepsTitle' => \__('How to get the Free reCAPTCHA API key?', 'eightshift-forms'), - 'stepsContent' => [ - // translators: %s will be replaced with the external link. - \sprintf(\__('Visit this link.', 'eightshift-forms'), 'https://www.google.com/recaptcha/admin/create'), - \__('Configure all the options. Make sure to select reCaptcha version 3!', 'eightshift-forms'), - \__('Copy the API key into the field under the API tab or use the global constant.', 'eightshift-forms'), - ], - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'input', + 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SCORE_KEY), + 'inputFieldLabel' => \__('"Spam unlikely" threshold', 'eightshift-forms'), + 'inputFieldHelp' => \__('The level above which a submission is not considered spam. Should be between 0.1 and 1.0.
In most cases, a user will receive as core between 0.8 and 0.9.', 'eightshift-forms'), + 'inputType' => 'number', + 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SCORE_KEY), + 'inputMin' => 0.1, + 'inputMax' => 1, + 'inputStep' => 0.1, + 'inputIsNumber' => true, + 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SCORE_DEFAULT_KEY, + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'input', + 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), + 'inputFieldLabel' => \__('"On submit" action name', 'eightshift-forms'), + 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA on form submission.', 'eightshift-forms'), + 'inputType' => 'text', + 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), + 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'checkboxes', + 'checkboxesFieldHideLabel' => true, + 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), + 'checkboxesContent' => [ [ - 'component' => 'steps', - 'stepsTitle' => \__('How to get the Enterprise reCAPTCHA API key?', 'eightshift-forms'), - 'stepsContent' => [ - // translators: %s will be replaced with the external link. - \sprintf(\__('Visit Google Cloud Console.', 'eightshift-forms'), 'https://console.cloud.google.com/'), - \__('Create a new project and set that project as Project ID.', 'eightshift-forms'), - \__('Search and go to reCAPTCHA product.', 'eightshift-forms'), - \__('You will probably need to set billing service for this product.', 'eightshift-forms'), - \__('Create a new key and set that key as Site key.', 'eightshift-forms'), - // translators: %s will be replaced with the website domain. - \sprintf(\__('Limit the key to your website domain. Domain: %s (exact, no trailing slash and protocol).', 'eightshift-forms'), \preg_replace("(^https?://)", "", \site_url())), - \__('Search and go to API & Services product.', 'eightshift-forms'), - \__('Go to Credentials section and create a new API key.', 'eightshift-forms'), - // translators: %s will be replaced with the website domain. - \sprintf(\__('Create a new key for Website, add restrictions to your website domain %s (exact, no trailing slash, with protocol) and set API restrictions to reCAPTCHA Enterprise.', 'eightshift-forms'), \esc_url(\site_url())), - \__('Set that key as API key.', 'eightshift-forms'), - ], + 'component' => 'checkbox', + 'checkboxLabel' => \__('Load Captcha on website load', 'eightshift-forms'), + 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), + 'checkboxValue' => self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, + 'checkboxHelp' => \__('By default, Captcha is only loaded on pages that contain forms. However, with this option, you can load Captcha on every page.', 'eightshift-forms'), + 'checkboxSingleSubmit' => true, + 'checkboxAsToggle' => true, + 'checkboxAsToggleSize' => 'medium', ], ], ], + $isInit ? [ + 'component' => 'input', + 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), + 'inputFieldLabel' => \__('Action name', 'eightshift-forms'), + 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA when Captcha is loaded on every page.', 'eightshift-forms'), + 'inputType' => 'text', + 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), + 'inputPlaceholder' => self::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, + ] : [], + ], + ], + [ + 'component' => 'tab', + 'tabLabel' => \__('Help', 'eightshift-forms'), + 'tabContent' => [ + [ + 'component' => 'steps', + 'stepsTitle' => \__('How to get the Free reCAPTCHA API key?', 'eightshift-forms'), + 'stepsContent' => [ + // translators: %s will be replaced with the external link. + \sprintf(\__('Visit this link.', 'eightshift-forms'), 'https://www.google.com/recaptcha/admin/create'), + \__('Configure all the options. Make sure to select reCaptcha version 3!', 'eightshift-forms'), + \__('Copy the API key into the field under the API tab or use the global constant.', 'eightshift-forms'), + ], + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'steps', + 'stepsTitle' => \__('How to get the Enterprise reCAPTCHA API key?', 'eightshift-forms'), + 'stepsContent' => [ + // translators: %s will be replaced with the external link. + \sprintf(\__('Visit Google Cloud Console.', 'eightshift-forms'), 'https://console.cloud.google.com/'), + \__('Create a new project and set that project as Project ID.', 'eightshift-forms'), + \__('Search and go to reCAPTCHA product.', 'eightshift-forms'), + \__('You will probably need to set billing service for this product.', 'eightshift-forms'), + \__('Create a new key and set that key as Site key.', 'eightshift-forms'), + // translators: %s will be replaced with the website domain. + \sprintf(\__('Limit the key to your website domain. Domain: %s (exact, no trailing slash and protocol).', 'eightshift-forms'), \preg_replace("(^https?://)", "", \site_url())), + \__('Search and go to API & Services product.', 'eightshift-forms'), + \__('Go to Credentials section and create a new API key.', 'eightshift-forms'), + // translators: %s will be replaced with the website domain. + \sprintf(\__('Create a new key for Website, add restrictions to your website domain %s (exact, no trailing slash, with protocol) and set API restrictions to reCAPTCHA Enterprise.', 'eightshift-forms'), \esc_url(\site_url())), + \__('Set that key as API key.', 'eightshift-forms'), + ], + ], ], ], ]; diff --git a/src/Captcha/SettingsCaptchaProvider.php b/src/Captcha/SettingsCaptchaProvider.php new file mode 100644 index 000000000..3ccd55b32 --- /dev/null +++ b/src/Captcha/SettingsCaptchaProvider.php @@ -0,0 +1,143 @@ +> + */ + public function getSettingsGlobalData(): array + { + // Bail out if the master toggle (shared with Google reCAPTCHA history) is off. + if (!SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY)) { + return SettingsOutputHelpers::getNoActiveFeature(); + } + + $provider = self::getActiveProvider(); + + $output = [ + SettingsOutputHelpers::getIntro('captcha'), + [ + 'component' => 'select', + 'selectName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_PROVIDER_KEY), + 'selectFieldLabel' => \__('Provider', 'eightshift-forms'), + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'selectFieldHelp' => \__('Pick which captcha service validates submissions. Switching the provider reloads the fields below.', 'eightshift-forms'), + 'selectSingleSubmit' => true, + 'selectContent' => [ + [ + 'component' => 'select-option', + 'selectOptionLabel' => \__('Google reCAPTCHA', 'eightshift-forms'), + 'selectOptionValue' => self::PROVIDER_GOOGLE, + 'selectOptionIsSelected' => $provider === self::PROVIDER_GOOGLE, + ], + [ + 'component' => 'select-option', + 'selectOptionLabel' => \__('Friendly Captcha', 'eightshift-forms'), + 'selectOptionValue' => self::PROVIDER_FRIENDLY, + 'selectOptionIsSelected' => $provider === self::PROVIDER_FRIENDLY, + ], + ], + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + ]; + + $providerTabs = $provider === self::PROVIDER_FRIENDLY + ? SettingsFriendlyCaptcha::getProviderTabs() + : SettingsCaptcha::getProviderTabs(); + + if ($providerTabs) { + $output[] = [ + 'component' => 'tabs', + 'tabsContent' => $providerTabs, + ]; + } + + return $output; + } +} diff --git a/src/Captcha/SettingsFriendlyCaptcha.php b/src/Captcha/SettingsFriendlyCaptcha.php new file mode 100644 index 000000000..fb1e42998 --- /dev/null +++ b/src/Captcha/SettingsFriendlyCaptcha.php @@ -0,0 +1,230 @@ +labels = $labels; + } + + /** + * Register all the hooks. + * + * @return void + */ + public function register(): void + { + \add_filter(self::FILTER_SETTINGS_GLOBAL_NAME, [$this, 'getSettingsGlobalData']); + \add_filter(self::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, [$this, 'isSettingsGlobalValid']); + } + + /** + * Determine if settings global are valid. + * + * @return boolean + */ + public function isSettingsGlobalValid(): bool + { + if (SettingsCaptchaProvider::getActiveProvider() !== SettingsCaptchaProvider::PROVIDER_FRIENDLY) { + return false; + } + + if (!SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY)) { + return false; + } + + $siteKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaSiteKey(), self::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY); + $apiKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaApiKey(), self::SETTINGS_FRIENDLY_CAPTCHA_API_KEY); + + return $siteKey && $apiKey; + } + + /** + * Get global settings array for building settings page. + * + * Retained for BC in case the `FILTER_SETTINGS_GLOBAL_NAME` filter is still + * consulted; the menu entry is now handled by `SettingsCaptchaProvider`. + * + * @return array> + */ + public function getSettingsGlobalData(): array + { + if (!SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY)) { + return SettingsOutputHelpers::getNoActiveFeature(); + } + + return [ + SettingsOutputHelpers::getIntro(self::SETTINGS_TYPE_KEY), + [ + 'component' => 'tabs', + 'tabsContent' => self::getProviderTabs(), + ], + ]; + } + + /** + * Tab definitions for the Friendly Captcha provider, composed into the + * merged captcha page by `SettingsCaptchaProvider`. + * + * @return array> + */ + public static function getProviderTabs(): array + { + return [ + [ + 'component' => 'tab', + 'tabLabel' => \__('General', 'eightshift-forms'), + 'tabContent' => [ + [ + 'component' => 'intro', + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'introSubtitle' => \__('Protect your forms from spam and abuse using Friendly Captcha.
A privacy-focused, GDPR-compliant alternative to Google reCAPTCHA.', 'eightshift-forms'), + ], + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getFriendlyCaptchaSiteKey(), + self::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY, + 'ES_FRIENDLY_CAPTCHA_SITE_KEY', + \__('Site key', 'eightshift-forms'), + ), + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getFriendlyCaptchaApiKey(), + self::SETTINGS_FRIENDLY_CAPTCHA_API_KEY, + 'ES_FRIENDLY_CAPTCHA_API_KEY', + \__('API key', 'eightshift-forms'), + ), + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'checkboxes', + 'checkboxesFieldLabel' => '', + 'checkboxesName' => SettingsHelpers::getSettingName(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY), + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'checkboxesFieldHelp' => \__('The EU endpoint is hosted in Germany and ensures visitor data never leaves the EU.
Requires a Friendly Captcha Advanced or Enterprise plan.', 'eightshift-forms'), + 'checkboxesContent' => [ + [ + 'component' => 'checkbox', + 'checkboxLabel' => \__('Use EU endpoint', 'eightshift-forms'), + 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY), + 'checkboxValue' => self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, + 'checkboxAsToggle' => true, + ], + ], + ], + ], + ], + [ + 'component' => 'tab', + 'tabLabel' => \__('Help', 'eightshift-forms'), + 'tabContent' => [ + [ + 'component' => 'steps', + 'stepsTitle' => \__('How to get the Friendly Captcha API keys?', 'eightshift-forms'), + 'stepsContent' => [ + // translators: %s will be replaced with the external link. + \sprintf(\__('Visit the Friendly Captcha dashboard.', 'eightshift-forms'), 'https://app.friendlycaptcha.eu/dashboard'), + \__('Create a new application and copy the Site key.', 'eightshift-forms'), + \__('Go to API Keys and create a new API key.', 'eightshift-forms'), + \__('Copy both keys into the fields under the General tab or use the global constants.', 'eightshift-forms'), + \__('In the Friendly Captcha dashboard, set the widget mode to Non-interactive for an invisible captcha experience.', 'eightshift-forms'), + ], + ], + ], + ], + ]; + } + + /** + * Get the selected endpoint value. + * + * @return string + */ + public static function getEndpoint(): string + { + return SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY) ? 'eu' : 'global'; + } + + /** + * Get the siteverify URL for the selected endpoint. + * + * @return string + */ + public static function getEndpointUrl(): string + { + return self::getEndpoint() === 'eu' ? self::FRIENDLY_CAPTCHA_ENDPOINT_EU_URL : self::FRIENDLY_CAPTCHA_ENDPOINT_GLOBAL_URL; + } +} diff --git a/src/Config/Config.php b/src/Config/Config.php index a09ce571a..eb28ecfda 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -465,7 +465,6 @@ class Config public const FD_ACTION_EXTERNAL = 'actionExternal'; public const FD_API_STEPS = 'apiSteps'; public const FD_CAPTCHA = 'captcha'; - public const FD_FRIENDLY_CAPTCHA = 'friendlyCaptcha'; public const FD_STORAGE = 'storage'; public const FD_IS_VALID = 'isValid'; public const FD_IS_API_VALID = 'isApiValid'; diff --git a/src/Enqueue/Blocks/EnqueueBlocks.php b/src/Enqueue/Blocks/EnqueueBlocks.php index e56fa1e6a..d6a0f9658 100644 --- a/src/Enqueue/Blocks/EnqueueBlocks.php +++ b/src/Enqueue/Blocks/EnqueueBlocks.php @@ -16,12 +16,13 @@ use EightshiftForms\Enrichment\SettingsEnrichment; use EightshiftForms\Settings\SettingsSettings; use EightshiftForms\Captcha\SettingsCaptcha; +use EightshiftForms\Captcha\SettingsCaptchaProvider; +use EightshiftForms\Captcha\SettingsFriendlyCaptcha; use EightshiftForms\CustomPostType\Result; use EightshiftForms\CustomPostType\Forms; use EightshiftForms\Enqueue\SharedEnqueue; use EightshiftForms\Enqueue\Captcha\EnqueueCaptcha; -use EightshiftForms\Enqueue\FriendlyCaptcha\EnqueueFriendlyCaptcha; -use EightshiftForms\FriendlyCaptcha\SettingsFriendlyCaptcha; +use EightshiftForms\Enqueue\Captcha\EnqueueFriendlyCaptcha; use EightshiftForms\Geolocation\GeolocationInterface; use EightshiftForms\Geolocation\SettingsGeolocation; use EightshiftForms\Hooks\FiltersOutputMock; @@ -390,8 +391,10 @@ protected function getFrontendScriptDependencies(): array $output = \apply_filters($scriptsDependency, []); } - if (\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - $output[] = "{$this->getAssetsPrefix()}-" . EnqueueFriendlyCaptcha::FRIENDLY_CAPTCHA_ENQUEUE_HANDLE; + if (SettingsCaptchaProvider::getActiveProvider() === SettingsCaptchaProvider::PROVIDER_FRIENDLY) { + if (\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + $output[] = "{$this->getAssetsPrefix()}-" . EnqueueFriendlyCaptcha::FRIENDLY_CAPTCHA_ENQUEUE_HANDLE; + } } elseif (\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { $output[] = "{$this->getAssetsPrefix()}-" . EnqueueCaptcha::CAPTCHA_ENQUEUE_HANDLE; } diff --git a/src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php b/src/Enqueue/Captcha/EnqueueFriendlyCaptcha.php similarity index 94% rename from src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php rename to src/Enqueue/Captcha/EnqueueFriendlyCaptcha.php index 192edc7c8..d7980992f 100644 --- a/src/Enqueue/FriendlyCaptcha/EnqueueFriendlyCaptcha.php +++ b/src/Enqueue/Captcha/EnqueueFriendlyCaptcha.php @@ -3,14 +3,14 @@ /** * The Theme/Frontend Enqueue specific functionality - Friendly Captcha. * - * @package EightshiftForms\Enqueue\FriendlyCaptcha + * @package EightshiftForms\Enqueue\Captcha */ declare(strict_types=1); -namespace EightshiftForms\Enqueue\FriendlyCaptcha; +namespace EightshiftForms\Enqueue\Captcha; -use EightshiftForms\FriendlyCaptcha\SettingsFriendlyCaptcha; +use EightshiftForms\Captcha\SettingsFriendlyCaptcha; use EightshiftForms\Config\Config; use EightshiftForms\Helpers\HooksHelpers; use EightshiftFormsVendor\EightshiftLibs\Enqueue\Theme\AbstractEnqueueTheme; diff --git a/src/FriendlyCaptcha/FriendlyCaptcha.php b/src/FriendlyCaptcha/FriendlyCaptcha.php deleted file mode 100644 index 7621a561e..000000000 --- a/src/FriendlyCaptcha/FriendlyCaptcha.php +++ /dev/null @@ -1,139 +0,0 @@ -labels = $labels; - } - - /** - * Check Friendly Captcha request. - * - * @param string $response Response token from frontend. - * @param array $formDetails Form details. - * - * @throws BadRequestException If captcha is not valid. - * - * @return array - */ - public function check(string $response, array $formDetails = []): array - { - if (!\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - return [ - AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), - AbstractBaseRoute::R_DEBUG => [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_FEATURE_DISABLED, - ], - ]; - } - - $debug = [ - 'response' => $response, - 'formDetails' => $formDetails, - ]; - - if (!$response) { - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('friendlyCaptchaBadRequest'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_MISSING_TOKEN, - AbstractBaseRoute::R_DEBUG => $debug, - ] - ); - // phpcs:enable - } - - $siteKey = SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaSiteKey(), SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY); - $apiKey = SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaApiKey(), SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_API_KEY); - - $apiResponse = \wp_remote_post( - SettingsFriendlyCaptcha::getEndpointUrl(), - [ - 'headers' => [ - 'Content-Type' => 'application/json; charset=utf-8', - 'X-API-Key' => $apiKey, - ], - 'data_format' => 'body', - 'body' => \wp_json_encode([ - 'response' => $response, - 'sitekey' => $siteKey, - ]), - ] - ); - - // Generic error msg from WP. - if (\is_wp_error($apiResponse)) { - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('submitWpError'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_WP_ERROR, - AbstractBaseRoute::R_DEBUG => $debug, - ] - ); - // phpcs:enable - } - - // Get body from the response. - $responseBody = \json_decode(\wp_remote_retrieve_body($apiResponse), true) ?? []; - - $debug = \array_merge($debug, [ - 'responseBody' => $responseBody, - ]); - - // Check the success field. - if (empty($responseBody['success'])) { - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('friendlyCaptchaError'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR, - AbstractBaseRoute::R_DEBUG => $debug, - ] - ); - // phpcs:enable - } - - return [ - AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), - AbstractBaseRoute::R_DEBUG => [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_SUCCESS, - AbstractBaseRoute::R_DEBUG => $debug, - ], - ]; - } -} diff --git a/src/FriendlyCaptcha/FriendlyCaptchaInterface.php b/src/FriendlyCaptcha/FriendlyCaptchaInterface.php deleted file mode 100644 index 9adae41fc..000000000 --- a/src/FriendlyCaptcha/FriendlyCaptchaInterface.php +++ /dev/null @@ -1,27 +0,0 @@ - $formDetails Form details. - * - * @return array - */ - public function check(string $response, array $formDetails = []): array; -} diff --git a/src/FriendlyCaptcha/SettingsFriendlyCaptcha.php b/src/FriendlyCaptcha/SettingsFriendlyCaptcha.php deleted file mode 100644 index 3f3190be9..000000000 --- a/src/FriendlyCaptcha/SettingsFriendlyCaptcha.php +++ /dev/null @@ -1,234 +0,0 @@ -labels = $labels; - } - - /** - * Register all the hooks. - * - * @return void - */ - public function register(): void - { - \add_filter(self::FILTER_SETTINGS_GLOBAL_NAME, [$this, 'getSettingsGlobalData']); - \add_filter(self::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, [$this, 'isSettingsGlobalValid']); - } - - /** - * Determine if settings global are valid. - * - * @return boolean - */ - public function isSettingsGlobalValid(): bool - { - $isUsed = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY); - $siteKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaSiteKey(), self::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY); - $apiKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaApiKey(), self::SETTINGS_FRIENDLY_CAPTCHA_API_KEY); - - if (!$isUsed || !$siteKey || !$apiKey) { - return false; - } - - // Mutual exclusivity: if Google reCAPTCHA is also valid, Friendly Captcha is not valid. - if (\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - return false; - } - - return true; - } - - /** - * Get global settings array for building settings page. - * - * @return array> - */ - public function getSettingsGlobalData(): array - { - // Bailout if feature is not active. - if (!SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY)) { - return SettingsOutputHelpers::getNoActiveFeature(); - } - - // Show warning if Google reCAPTCHA is also active. - if (SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY)) { - return [ - SettingsOutputHelpers::getIntro(self::SETTINGS_TYPE_KEY), - [ - 'component' => 'highlighted-content', - 'highlightedContentTitle' => \__('Google reCAPTCHA is active', 'eightshift-forms'), - // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings - 'highlightedContentSubtitle' => \__('Please disable Google reCAPTCHA before configuring Friendly Captcha.
Only one spam prevention provider can be active at a time.', 'eightshift-forms'), - 'highlightedContentIcon' => 'warning', - ], - ]; - } - - return [ - SettingsOutputHelpers::getIntro(self::SETTINGS_TYPE_KEY), - [ - 'component' => 'intro', - // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings - 'introSubtitle' => \__('Protect your forms from spam and abuse using Friendly Captcha.
A privacy-focused, GDPR-compliant alternative to Google reCAPTCHA.', 'eightshift-forms'), - ], - [ - 'component' => 'tabs', - 'tabsContent' => [ - [ - 'component' => 'tab', - 'tabLabel' => \__('General', 'eightshift-forms'), - 'tabContent' => [ - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getFriendlyCaptchaSiteKey(), - self::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY, - 'ES_FRIENDLY_CAPTCHA_SITE_KEY', - \__('Site key', 'eightshift-forms'), - ), - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getFriendlyCaptchaApiKey(), - self::SETTINGS_FRIENDLY_CAPTCHA_API_KEY, - 'ES_FRIENDLY_CAPTCHA_API_KEY', - \__('API key', 'eightshift-forms'), - ), - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - [ - 'component' => 'checkboxes', - 'checkboxesFieldLabel' => '', - 'checkboxesName' => SettingsHelpers::getSettingName(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY), - // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings - 'checkboxesFieldHelp' => \__('The EU endpoint is hosted in Germany and ensures visitor data never leaves the EU.
Requires a Friendly Captcha Advanced or Enterprise plan.', 'eightshift-forms'), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Use EU endpoint', 'eightshift-forms'), - 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY), - 'checkboxValue' => self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, - 'checkboxAsToggle' => true, - ], - ], - ], - ], - ], - [ - 'component' => 'tab', - 'tabLabel' => \__('Help', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'steps', - 'stepsTitle' => \__('How to get the Friendly Captcha API keys?', 'eightshift-forms'), - 'stepsContent' => [ - // translators: %s will be replaced with the external link. - \sprintf(\__('Visit the Friendly Captcha dashboard.', 'eightshift-forms'), 'https://app.friendlycaptcha.eu/dashboard'), - \__('Create a new application and copy the Site key.', 'eightshift-forms'), - \__('Go to API Keys and create a new API key.', 'eightshift-forms'), - \__('Copy both keys into the fields under the General tab or use the global constants.', 'eightshift-forms'), - \__('In the Friendly Captcha dashboard, set the widget mode to Non-interactive for an invisible captcha experience.', 'eightshift-forms'), - ], - ], - ], - ], - ], - ], - ]; - } - - /** - * Get the selected endpoint value. - * - * @return string - */ - public static function getEndpoint(): string - { - return SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY) ? 'eu' : 'global'; - } - - /** - * Get the siteverify URL for the selected endpoint. - * - * @return string - */ - public static function getEndpointUrl(): string - { - return self::getEndpoint() === 'eu' ? self::FRIENDLY_CAPTCHA_ENDPOINT_EU_URL : self::FRIENDLY_CAPTCHA_ENDPOINT_GLOBAL_URL; - } -} diff --git a/src/Hooks/Filters.php b/src/Hooks/Filters.php index 6cab44957..6f67b2c86 100644 --- a/src/Hooks/Filters.php +++ b/src/Hooks/Filters.php @@ -31,7 +31,7 @@ use EightshiftForms\Troubleshooting\SettingsDebug; use EightshiftForms\Troubleshooting\SettingsFallback; use EightshiftForms\Captcha\SettingsCaptcha; -use EightshiftForms\FriendlyCaptcha\SettingsFriendlyCaptcha; +use EightshiftForms\Captcha\SettingsFriendlyCaptcha; use EightshiftForms\General\SettingsGeneral; use EightshiftForms\Integrations\Calculator\SettingsCalculator; use EightshiftForms\Integrations\Corvus\SettingsCorvus; diff --git a/src/Hooks/FiltersSettingsBuilder.php b/src/Hooks/FiltersSettingsBuilder.php index 9dbcd0ef8..980fab02f 100644 --- a/src/Hooks/FiltersSettingsBuilder.php +++ b/src/Hooks/FiltersSettingsBuilder.php @@ -56,7 +56,7 @@ use EightshiftForms\Troubleshooting\SettingsDebug; use EightshiftForms\Troubleshooting\SettingsFallback; use EightshiftForms\Captcha\SettingsCaptcha; -use EightshiftForms\FriendlyCaptcha\SettingsFriendlyCaptcha; +use EightshiftForms\Captcha\SettingsCaptchaProvider; use EightshiftForms\Entries\SettingsEntries; use EightshiftForms\Integrations\Calculator\SettingsCalculator; use EightshiftForms\Integrations\Corvus\SettingsCorvus; @@ -151,24 +151,13 @@ public function getSettingsFiltersData(): array 'title' => \__('Advanced', 'eightshift-forms'), ], ], - SettingsCaptcha::SETTINGS_TYPE_KEY => [ - 'settingsGlobal' => SettingsCaptcha::FILTER_SETTINGS_GLOBAL_NAME, + 'captcha' => [ + 'settingsGlobal' => SettingsCaptchaProvider::FILTER_SETTINGS_GLOBAL_NAME, 'type' => Config::SETTINGS_INTERNAL_TYPE_ADVANCED, 'use' => SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, 'labels' => [ - 'title' => \__('Google reCAPTCHA', 'eightshift-forms'), - 'desc' => \__('Prevent misuse of your forms by adding Google reCAPTCHA.', 'eightshift-forms'), - 'externalLink' => 'https://www.google.com/recaptcha/about/', - ], - ], - SettingsFriendlyCaptcha::SETTINGS_TYPE_KEY => [ - 'settingsGlobal' => SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_NAME, - 'type' => Config::SETTINGS_INTERNAL_TYPE_ADVANCED, - 'use' => SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY, - 'labels' => [ - 'title' => \__('Friendly Captcha', 'eightshift-forms'), - 'desc' => \__('Privacy-focused spam prevention using Friendly Captcha.', 'eightshift-forms'), - 'externalLink' => 'https://friendlycaptcha.com/', + 'title' => \__('Captcha', 'eightshift-forms'), + 'desc' => \__('Prevent misuse of your forms by adding a captcha provider.', 'eightshift-forms'), ], ], SettingsGeolocation::SETTINGS_TYPE_KEY => [ diff --git a/src/Rest/Routes/AbstractIntegrationFormSubmit.php b/src/Rest/Routes/AbstractIntegrationFormSubmit.php index 023a6ffb9..0969e3314 100644 --- a/src/Rest/Routes/AbstractIntegrationFormSubmit.php +++ b/src/Rest/Routes/AbstractIntegrationFormSubmit.php @@ -12,8 +12,6 @@ use EightshiftForms\Captcha\CaptchaInterface; // phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse use EightshiftForms\Enrichment\EnrichmentInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; // phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse -use EightshiftForms\FriendlyCaptcha\SettingsFriendlyCaptcha; use EightshiftForms\Entries\EntriesHelper; use EightshiftForms\Entries\SettingsEntries; use EightshiftForms\Exception\BadRequestException; @@ -77,13 +75,6 @@ abstract class AbstractIntegrationFormSubmit extends AbstractBaseRoute */ protected $captcha; - /** - * Instance variable of FriendlyCaptchaInterface data. - * - * @var FriendlyCaptchaInterface - */ - protected $friendlyCaptcha; - /** * Instance variable of enrichment data. * @@ -98,7 +89,6 @@ abstract class AbstractIntegrationFormSubmit extends AbstractBaseRoute * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailer methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. */ @@ -107,7 +97,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment ) { @@ -115,7 +104,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; } @@ -237,19 +225,12 @@ public function routeCallback(WP_REST_Request $request) // Validate captcha. if ($this->shouldCheckCaptcha()) { - if (\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - $this->getFriendlyCaptcha()->check( - $formDetails[Config::FD_FRIENDLY_CAPTCHA]['token'] ?? '', - $formDetails - ); - } else { - $this->getCaptcha()->check( - $formDetails[Config::FD_CAPTCHA]['token'] ?? '', - $formDetails[Config::FD_CAPTCHA]['action'] ?? '', - $formDetails[Config::FD_CAPTCHA]['isEnterprise'] ?? false, - $formDetails - ); - } + $this->getCaptcha()->check( + $formDetails[Config::FD_CAPTCHA]['token'] ?? '', + $formDetails[Config::FD_CAPTCHA]['action'] ?? '', + $formDetails[Config::FD_CAPTCHA]['isEnterprise'] ?? false, + $formDetails + ); } // Map enrichment data. @@ -776,16 +757,6 @@ protected function getCaptcha() return $this->captcha; } - /** - * Returns Friendly Captcha class. - * - * @return FriendlyCaptchaInterface - */ - protected function getFriendlyCaptcha() - { - return $this->friendlyCaptcha; - } - /** * Returns enrichment class. * @@ -1229,9 +1200,6 @@ protected function getFormDetailsApi($request): array // Get form captcha from params. $output[Config::FD_CAPTCHA] = $params[Config::FD_CAPTCHA] ?? []; - // Get form Friendly Captcha from params. - $output[Config::FD_FRIENDLY_CAPTCHA] = $params[Config::FD_FRIENDLY_CAPTCHA] ?? []; - // Get form post Id from params. $output[Config::FD_POST_ID] = $params[Config::FD_POST_ID] ?? ''; @@ -1311,7 +1279,7 @@ protected function prepareApiParams(WP_REST_Request $request, string $type = sel $output[Config::FD_PARAMS][$key] = $value; break; case UtilsHelper::getStateParam('friendlyCaptcha'): - $output[Config::FD_FRIENDLY_CAPTCHA] = $value['value']; + $output[Config::FD_CAPTCHA] = $value['value']; $output[Config::FD_PARAMS][$key] = $value; break; case UtilsHelper::getStateParam('actionExternal'): diff --git a/src/Rest/Routes/General/CaptchaValidateRoute.php b/src/Rest/Routes/General/CaptchaValidateRoute.php index 42046fd7f..cde5d9e88 100644 --- a/src/Rest/Routes/General/CaptchaValidateRoute.php +++ b/src/Rest/Routes/General/CaptchaValidateRoute.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Rest\Routes\General; use EightshiftForms\Captcha\CaptchaInterface; +use EightshiftForms\Captcha\SettingsCaptchaProvider; use EightshiftForms\Labels\LabelsInterface; use EightshiftForms\Helpers\DeveloperHelpers; use EightshiftForms\Rest\Routes\AbstractBaseRoute; @@ -56,6 +57,23 @@ public function __construct( $this->captcha = $captcha; } + /** + * Register the route only when Google reCAPTCHA is the active provider. + * + * Friendly Captcha solves entirely in the browser and has no pre-submission + * verification endpoint — exposing this route for it would misroute tokens. + * + * @return void + */ + public function register(): void + { + if (SettingsCaptchaProvider::getActiveProvider() !== SettingsCaptchaProvider::PROVIDER_GOOGLE) { + return; + } + + parent::register(); + } + /** * Get the base url of the route * diff --git a/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php b/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php index b32214b88..b25925394 100644 --- a/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php +++ b/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\ActiveCampaign; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ActiveCampaign\ActiveCampaignClientInterface; use EightshiftForms\Integrations\ActiveCampaign\SettingsActiveCampaign; @@ -52,7 +51,6 @@ class FormSubmitActiveCampaignRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ActiveCampaignClientInterface $activeCampaignClient Inject ActiveCampaign methods. @@ -62,7 +60,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ActiveCampaignClientInterface $activeCampaignClient @@ -71,7 +68,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->activeCampaignClient = $activeCampaignClient; diff --git a/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php b/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php index ce92b209a..89ff706de 100644 --- a/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php +++ b/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Airtable; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Airtable\AirtableClientInterface; use EightshiftForms\Integrations\Airtable\SettingsAirtable; @@ -51,7 +50,6 @@ class FormSubmitAirtableRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param AirtableClientInterface $airtableClient Inject airtableClient methods. @@ -61,7 +59,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, AirtableClientInterface $airtableClient @@ -70,7 +67,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->airtableClient = $airtableClient; diff --git a/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php b/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php index 257edec68..47f831243 100644 --- a/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php +++ b/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Goodbits; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Goodbits\SettingsGoodbits; @@ -51,7 +50,6 @@ class FormSubmitGoodbitsRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $goodbitsClient Inject goodbitsClient methods. @@ -61,7 +59,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $goodbitsClient @@ -70,7 +67,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->goodbitsClient = $goodbitsClient; diff --git a/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php b/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php index 26221bb9f..b95f94caa 100644 --- a/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php +++ b/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Greenhouse; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Greenhouse\SettingsGreenhouse; @@ -51,7 +50,6 @@ class FormSubmitGreenhouseRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $greenhouseClient Inject greenhouseClient methods. @@ -61,7 +59,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $greenhouseClient @@ -70,7 +67,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->greenhouseClient = $greenhouseClient; diff --git a/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php b/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php index 0ab5e17ab..61cb79877 100644 --- a/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php +++ b/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Hubspot; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Clearbit\ClearbitClientInterface; use EightshiftForms\Integrations\Hubspot\HubspotClientInterface; @@ -59,7 +58,6 @@ class FormSubmitHubspotRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param HubspotClientInterface $hubspotClient Inject hubspotClient methods. @@ -70,7 +68,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, HubspotClientInterface $hubspotClient, @@ -80,7 +77,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->hubspotClient = $hubspotClient; diff --git a/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php b/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php index 90c35fb81..34d8b7a78 100644 --- a/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php +++ b/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Jira; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Jira\JiraClientInterface; use EightshiftForms\Integrations\Jira\SettingsJira; @@ -51,7 +50,6 @@ class FormSubmitJiraRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param JiraClientInterface $jiraClient Inject jiraClient methods. @@ -61,7 +59,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, JiraClientInterface $jiraClient @@ -70,7 +67,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->jiraClient = $jiraClient; diff --git a/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php b/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php index 2201590fc..1ad54fa0f 100644 --- a/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php +++ b/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Mailchimp; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Mailchimp\MailchimpClientInterface; use EightshiftForms\Integrations\Mailchimp\SettingsMailchimp; @@ -51,7 +50,6 @@ class FormSubmitMailchimpRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param MailchimpClientInterface $mailchimpClient Inject mailchimpClient methods. @@ -61,7 +59,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, MailchimpClientInterface $mailchimpClient @@ -70,7 +67,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->mailchimpClient = $mailchimpClient; diff --git a/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php b/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php index 628f9fefe..23758b574 100644 --- a/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php +++ b/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Mailerlite; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Mailerlite\SettingsMailerlite; @@ -51,7 +50,6 @@ class FormSubmitMailerliteRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $mailerliteClient Inject mailerliteClient methods. @@ -61,7 +59,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $mailerliteClient @@ -70,7 +67,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->mailerliteClient = $mailerliteClient; diff --git a/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php b/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php index c81caaf1c..c3fe739aa 100644 --- a/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php +++ b/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Moments; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Moments\MomentsEventsInterface; @@ -60,7 +59,6 @@ class FormSubmitMomentsRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $momentsClient Inject momentsClient methods. @@ -71,7 +69,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $momentsClient, @@ -81,7 +78,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->momentsClient = $momentsClient; diff --git a/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php b/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php index 6e91371ea..cf3fcde93 100644 --- a/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php +++ b/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Nationbuilder; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Nationbuilder\NationbuilderClientInterface; use EightshiftForms\Integrations\Nationbuilder\SettingsNationbuilder; @@ -51,7 +50,6 @@ class FormSubmitNationbuilderRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param NationbuilderClientInterface $nationbuilderClient Inject nationbuilderClient methods. @@ -61,7 +59,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, NationbuilderClientInterface $nationbuilderClient @@ -70,7 +67,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->nationbuilderClient = $nationbuilderClient; diff --git a/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php b/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php index 49c0aee86..7b09f586b 100644 --- a/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php +++ b/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Pipedrive; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\Pipedrive\PipedriveClientInterface; use EightshiftForms\Integrations\Pipedrive\SettingsPipedrive; @@ -51,7 +50,6 @@ class FormSubmitPipedriveRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param PipedriveClientInterface $pipedriveClient Inject pipedriveClient methods. @@ -61,7 +59,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, PipedriveClientInterface $pipedriveClient @@ -70,7 +67,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->pipedriveClient = $pipedriveClient; diff --git a/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php b/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php index 622b81948..349865d05 100644 --- a/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php +++ b/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Talentlyft; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Talentlyft\SettingsTalentlyft; @@ -51,7 +50,6 @@ class FormSubmitTalentlyftRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $talentlyftClient Inject talentlyftClient methods. @@ -61,7 +59,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $talentlyftClient @@ -70,7 +67,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->talentlyftClient = $talentlyftClient; diff --git a/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php b/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php index eeaafa257..c3abe351c 100644 --- a/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php +++ b/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\Integrations\Workable; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\FriendlyCaptcha\FriendlyCaptchaInterface; use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Integrations\ClientInterface; use EightshiftForms\Integrations\Workable\SettingsWorkable; @@ -51,7 +50,6 @@ class FormSubmitWorkableRoute extends AbstractIntegrationFormSubmit * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. * @param CaptchaInterface $captcha Inject captcha methods. - * @param FriendlyCaptchaInterface $friendlyCaptcha Inject Friendly Captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $workableClient Inject workableClient methods. @@ -61,7 +59,6 @@ public function __construct( ValidatorInterface $validator, LabelsInterface $labels, CaptchaInterface $captcha, - FriendlyCaptchaInterface $friendlyCaptcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $workableClient @@ -70,7 +67,6 @@ public function __construct( $this->validator = $validator; $this->labels = $labels; $this->captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->workableClient = $workableClient; From fc1fdfad1c82f6be842bfd784bd0bf8ffaa81397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Tue, 21 Apr 2026 10:00:26 +0200 Subject: [PATCH 06/20] Add CaptchaDispatcher to route server-side verification to the active provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Friendly Captcha tokens were never verified server-side — the Google Captcha class was always injected and bailed early when Google was not the active provider. Introduce CaptchaDispatcher which implements CaptchaInterface and delegates check() to whichever provider SettingsCaptchaProvider::getActiveProvider() returns. All integration submit routes and CaptchaValidateRoute now declare CaptchaInterface $captchaDispatcher so autowiring resolves to the dispatcher instead of the Google class directly. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Captcha/CaptchaDispatcher.php | 62 +++++++++++++++++++ .../Routes/AbstractIntegrationFormSubmit.php | 6 +- .../Routes/General/CaptchaValidateRoute.php | 6 +- .../FormSubmitActiveCampaignRoute.php | 6 +- .../Airtable/FormSubmitAirtableRoute.php | 6 +- .../Goodbits/FormSubmitGoodbitsRoute.php | 6 +- .../Greenhouse/FormSubmitGreenhouseRoute.php | 6 +- .../Hubspot/FormSubmitHubspotRoute.php | 6 +- .../Integrations/Jira/FormSubmitJiraRoute.php | 6 +- .../Mailchimp/FormSubmitMailchimpRoute.php | 6 +- .../Mailerlite/FormSubmitMailerliteRoute.php | 6 +- .../Moments/FormSubmitMomentsRoute.php | 6 +- .../FormSubmitNationbuilderRoute.php | 6 +- .../Pipedrive/FormSubmitPipedriveRoute.php | 6 +- .../Talentlyft/FormSubmitTalentlyftRoute.php | 6 +- .../Workable/FormSubmitWorkableRoute.php | 6 +- 16 files changed, 107 insertions(+), 45 deletions(-) create mode 100644 src/Captcha/CaptchaDispatcher.php diff --git a/src/Captcha/CaptchaDispatcher.php b/src/Captcha/CaptchaDispatcher.php new file mode 100644 index 000000000..10c385a61 --- /dev/null +++ b/src/Captcha/CaptchaDispatcher.php @@ -0,0 +1,62 @@ +captcha = $captcha; + $this->friendlyCaptcha = $friendlyCaptcha; + } + + /** + * Delegate to the active captcha provider. + * + * @param string $token Token from frontend. + * @param string $action Action to check. + * @param boolean $isEnterprise Type of captcha. + * @param array $formDetails Form details. + * + * @return array + */ + public function check(string $token, string $action, bool $isEnterprise, array $formDetails = []): array + { + if (SettingsCaptchaProvider::getActiveProvider() === SettingsCaptchaProvider::PROVIDER_FRIENDLY) { + return $this->friendlyCaptcha->check($token, $action, $isEnterprise, $formDetails); + } + + return $this->captcha->check($token, $action, $isEnterprise, $formDetails); + } +} diff --git a/src/Rest/Routes/AbstractIntegrationFormSubmit.php b/src/Rest/Routes/AbstractIntegrationFormSubmit.php index 0969e3314..eeb2aab99 100644 --- a/src/Rest/Routes/AbstractIntegrationFormSubmit.php +++ b/src/Rest/Routes/AbstractIntegrationFormSubmit.php @@ -88,7 +88,7 @@ abstract class AbstractIntegrationFormSubmit extends AbstractBaseRoute * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailer methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. */ @@ -96,14 +96,14 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment ) { $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; } diff --git a/src/Rest/Routes/General/CaptchaValidateRoute.php b/src/Rest/Routes/General/CaptchaValidateRoute.php index cde5d9e88..652866989 100644 --- a/src/Rest/Routes/General/CaptchaValidateRoute.php +++ b/src/Rest/Routes/General/CaptchaValidateRoute.php @@ -43,18 +43,18 @@ class CaptchaValidateRoute extends AbstractSimpleFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. */ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha + CaptchaInterface $captchaDispatcher ) { $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; } /** diff --git a/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php b/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php index b25925394..af21e12ca 100644 --- a/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php +++ b/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php @@ -50,7 +50,7 @@ class FormSubmitActiveCampaignRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ActiveCampaignClientInterface $activeCampaignClient Inject ActiveCampaign methods. @@ -59,7 +59,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, ActiveCampaignClientInterface $activeCampaignClient @@ -67,7 +67,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->activeCampaignClient = $activeCampaignClient; diff --git a/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php b/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php index 89ff706de..7f3c97669 100644 --- a/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php +++ b/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php @@ -49,7 +49,7 @@ class FormSubmitAirtableRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param AirtableClientInterface $airtableClient Inject airtableClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, AirtableClientInterface $airtableClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->airtableClient = $airtableClient; diff --git a/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php b/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php index 47f831243..41af131ba 100644 --- a/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php +++ b/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php @@ -49,7 +49,7 @@ class FormSubmitGoodbitsRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $goodbitsClient Inject goodbitsClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $goodbitsClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->goodbitsClient = $goodbitsClient; diff --git a/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php b/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php index b95f94caa..68c67bc73 100644 --- a/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php +++ b/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php @@ -49,7 +49,7 @@ class FormSubmitGreenhouseRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $greenhouseClient Inject greenhouseClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $greenhouseClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->greenhouseClient = $greenhouseClient; diff --git a/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php b/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php index 61cb79877..2c09a904a 100644 --- a/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php +++ b/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php @@ -57,7 +57,7 @@ class FormSubmitHubspotRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param HubspotClientInterface $hubspotClient Inject hubspotClient methods. @@ -67,7 +67,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, HubspotClientInterface $hubspotClient, @@ -76,7 +76,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->hubspotClient = $hubspotClient; diff --git a/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php b/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php index 34d8b7a78..d078d89c5 100644 --- a/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php +++ b/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php @@ -49,7 +49,7 @@ class FormSubmitJiraRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param JiraClientInterface $jiraClient Inject jiraClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, JiraClientInterface $jiraClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->jiraClient = $jiraClient; diff --git a/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php b/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php index 1ad54fa0f..9f8b0969b 100644 --- a/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php +++ b/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php @@ -49,7 +49,7 @@ class FormSubmitMailchimpRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param MailchimpClientInterface $mailchimpClient Inject mailchimpClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, MailchimpClientInterface $mailchimpClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->mailchimpClient = $mailchimpClient; diff --git a/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php b/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php index 23758b574..15c918eb3 100644 --- a/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php +++ b/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php @@ -49,7 +49,7 @@ class FormSubmitMailerliteRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $mailerliteClient Inject mailerliteClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $mailerliteClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->mailerliteClient = $mailerliteClient; diff --git a/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php b/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php index c3fe739aa..ef2b3a62d 100644 --- a/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php +++ b/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php @@ -58,7 +58,7 @@ class FormSubmitMomentsRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $momentsClient Inject momentsClient methods. @@ -68,7 +68,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $momentsClient, @@ -77,7 +77,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->momentsClient = $momentsClient; diff --git a/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php b/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php index cf3fcde93..f44e5e591 100644 --- a/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php +++ b/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php @@ -49,7 +49,7 @@ class FormSubmitNationbuilderRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param NationbuilderClientInterface $nationbuilderClient Inject nationbuilderClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, NationbuilderClientInterface $nationbuilderClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->nationbuilderClient = $nationbuilderClient; diff --git a/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php b/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php index 7b09f586b..e9fb70bda 100644 --- a/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php +++ b/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php @@ -49,7 +49,7 @@ class FormSubmitPipedriveRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param PipedriveClientInterface $pipedriveClient Inject pipedriveClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, PipedriveClientInterface $pipedriveClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->pipedriveClient = $pipedriveClient; diff --git a/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php b/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php index 349865d05..068695078 100644 --- a/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php +++ b/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php @@ -49,7 +49,7 @@ class FormSubmitTalentlyftRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $talentlyftClient Inject talentlyftClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $talentlyftClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->talentlyftClient = $talentlyftClient; diff --git a/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php b/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php index c3abe351c..436883b5b 100644 --- a/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php +++ b/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php @@ -49,7 +49,7 @@ class FormSubmitWorkableRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captcha Inject captcha methods. + * @param CaptchaInterface $captchaDispatcher Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $workableClient Inject workableClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captcha, + CaptchaInterface $captchaDispatcher, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $workableClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captcha; + $this->captcha = $captchaDispatcher; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->workableClient = $workableClient; From 9d694e7876892e0d8e71395b2ec00496d2baab9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Tue, 21 Apr 2026 10:26:17 +0200 Subject: [PATCH 07/20] Revert label to spam prevention. Fix z-index on select --- src/Blocks/components/select/select-admin.scss | 1 + src/Hooks/FiltersSettingsBuilder.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Blocks/components/select/select-admin.scss b/src/Blocks/components/select/select-admin.scss index 69619affe..c39101ebe 100644 --- a/src/Blocks/components/select/select-admin.scss +++ b/src/Blocks/components/select/select-admin.scss @@ -43,5 +43,6 @@ div.es-select { --es-input-radius: var(--global-es-radius-8) var(--global-es-radius-8) 0 0; box-shadow: var(--global-esf-box-shadow-l); + z-index: 2; } } diff --git a/src/Hooks/FiltersSettingsBuilder.php b/src/Hooks/FiltersSettingsBuilder.php index 980fab02f..ffd953e9c 100644 --- a/src/Hooks/FiltersSettingsBuilder.php +++ b/src/Hooks/FiltersSettingsBuilder.php @@ -156,7 +156,7 @@ public function getSettingsFiltersData(): array 'type' => Config::SETTINGS_INTERNAL_TYPE_ADVANCED, 'use' => SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, 'labels' => [ - 'title' => \__('Captcha', 'eightshift-forms'), + 'title' => \__('Spam prevention', 'eightshift-forms'), 'desc' => \__('Prevent misuse of your forms by adding a captcha provider.', 'eightshift-forms'), ], ], From 652b8b83aa00ed643177076cb0231d2d664474c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Tue, 21 Apr 2026 10:47:47 +0200 Subject: [PATCH 08/20] Fix docblock comment --- src/Blocks/components/form/assets/form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Blocks/components/form/assets/form.js b/src/Blocks/components/form/assets/form.js index c3b41050c..a10ba0ee2 100644 --- a/src/Blocks/components/form/assets/form.js +++ b/src/Blocks/components/form/assets/form.js @@ -1182,7 +1182,7 @@ export class Form { } /** - * Set form data object for all forms - friendly captcha. + * Set form data object for all forms - Friendly Captcha. * * @param {object} data Friendly Captcha data. * From dfd33eb749150abf1e7c45a371b73753e995f924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Tue, 21 Apr 2026 11:31:02 +0200 Subject: [PATCH 09/20] Reset token on form submit --- src/Blocks/components/form/assets/form.js | 11 ++++++++--- .../components/form/assets/friendly-captcha.js | 12 ++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Blocks/components/form/assets/form.js b/src/Blocks/components/form/assets/form.js index a10ba0ee2..6d5b79cd9 100644 --- a/src/Blocks/components/form/assets/form.js +++ b/src/Blocks/components/form/assets/form.js @@ -596,18 +596,23 @@ export class Form { * * @returns {void} */ - runFormFriendlyCaptcha(formId, filter = {}) { + async runFormFriendlyCaptcha(formId, filter = {}) { if (!this.state.getStateFriendlyCaptchaIsUsed()) { return; } - const token = window[prefix]?.friendlyCaptcha?.getResponse() ?? ''; + const widget = window[prefix]?.friendlyCaptcha; + const token = widget?.getResponse() ?? ''; this.setFormDataFriendlyCaptcha({ token, }); - this.formSubmit(formId, filter); + await this.formSubmit(formId, filter); + + // Reset the widget after every server response so a fresh single-use + // token is ready for the next submission attempt. + widget?.reset(); } /** diff --git a/src/Blocks/components/form/assets/friendly-captcha.js b/src/Blocks/components/form/assets/friendly-captcha.js index 0d7e993e0..2fd84745d 100644 --- a/src/Blocks/components/form/assets/friendly-captcha.js +++ b/src/Blocks/components/form/assets/friendly-captcha.js @@ -67,6 +67,15 @@ export class FriendlyCaptcha { return this.widget?.getResponse() ?? ''; } + /** + * Reset the widget so it generates a fresh token. + * + * @returns {void} + */ + reset() { + this.widget?.reset(); + } + //////////////////////////////////////////////////////////////// // Private methods - not shared to the public window object. //////////////////////////////////////////////////////////////// @@ -93,6 +102,9 @@ export class FriendlyCaptcha { getResponse: () => { return this.getResponse(); }, + reset: () => { + this.reset(); + }, }; } } From 9292a5500b71442988d4b51cb436db449773d511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Wed, 22 Apr 2026 10:16:21 +0200 Subject: [PATCH 10/20] Invert captcha dispatcher so routes keep type-hinting CaptchaInterface $captcha MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per PR #613 review: the original Captcha class was Google-specific and sat behind the generic CaptchaInterface $captcha param, which made the whole dispatch mechanism depend on renaming the param in every consumer. Move the Google reCAPTCHA implementation into Recaptcha.php and reuse Captcha.php as the dispatcher. Autowiring now resolves CaptchaInterface $captcha to the dispatcher across every integration route and third-party extension transparently — no consumer changes. Settings follow the same inversion: - SettingsCaptcha (new): provider selector + master toggle, claims the es_forms_settings_global_captcha filter slot. - SettingsRecaptcha (was SettingsCaptcha): Google-only settings, now under the es_forms_settings_global_recaptcha filter. - SettingsCaptchaProvider deleted; its responsibilities live on SettingsCaptcha. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Captcha/Captcha.php | 365 +--------------- src/Captcha/CaptchaDispatcher.php | 62 --- src/Captcha/Recaptcha.php | 390 ++++++++++++++++++ src/Captcha/SettingsCaptcha.php | 362 +++------------- src/Captcha/SettingsCaptchaProvider.php | 143 ------- src/Captcha/SettingsFriendlyCaptcha.php | 6 +- src/Captcha/SettingsRecaptcha.php | 372 +++++++++++++++++ src/Enqueue/Blocks/EnqueueBlocks.php | 20 +- src/Enqueue/Captcha/EnqueueCaptcha.php | 10 +- src/Hooks/Filters.php | 12 +- src/Hooks/FiltersSettingsBuilder.php | 3 +- .../Routes/AbstractIntegrationFormSubmit.php | 6 +- .../Routes/General/CaptchaValidateRoute.php | 10 +- .../FormSubmitActiveCampaignRoute.php | 6 +- .../Airtable/FormSubmitAirtableRoute.php | 6 +- .../Goodbits/FormSubmitGoodbitsRoute.php | 6 +- .../Greenhouse/FormSubmitGreenhouseRoute.php | 6 +- .../Hubspot/FormSubmitHubspotRoute.php | 6 +- .../Integrations/Jira/FormSubmitJiraRoute.php | 6 +- .../Mailchimp/FormSubmitMailchimpRoute.php | 6 +- .../Mailerlite/FormSubmitMailerliteRoute.php | 6 +- .../Moments/FormSubmitMomentsRoute.php | 6 +- .../FormSubmitNationbuilderRoute.php | 6 +- .../Pipedrive/FormSubmitPipedriveRoute.php | 6 +- .../Talentlyft/FormSubmitTalentlyftRoute.php | 6 +- .../Workable/FormSubmitWorkableRoute.php | 6 +- 26 files changed, 922 insertions(+), 917 deletions(-) delete mode 100644 src/Captcha/CaptchaDispatcher.php create mode 100644 src/Captcha/Recaptcha.php delete mode 100644 src/Captcha/SettingsCaptchaProvider.php create mode 100644 src/Captcha/SettingsRecaptcha.php diff --git a/src/Captcha/Captcha.php b/src/Captcha/Captcha.php index f8f397a56..3520e7b61 100644 --- a/src/Captcha/Captcha.php +++ b/src/Captcha/Captcha.php @@ -1,7 +1,7 @@ labels = $labels; - $this->security = $security; + $this->recaptcha = $recaptcha; + $this->friendlyCaptcha = $friendlyCaptcha; } /** - * Check captcha request. + * Delegate to the active captcha provider. * * @param string $token Token from frontend. * @param string $action Action to check. * @param boolean $isEnterprise Type of captcha. * @param array $formDetails Form details. * - * @throws BadRequestException If captcha is not valid. - * * @return array */ public function check(string $token, string $action, bool $isEnterprise, array $formDetails = []): array { - if (!\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - return [ - AbstractBaseRoute::R_MSG => $this->labels->getLabel('captchaSuccess'), - AbstractBaseRoute::R_DEBUG => [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_FEATURE_DISABLED, - ], - ]; - } - - $isRetry = (bool) ($formDetails[Config::FD_CAPTCHA]['isRetry'] ?? false); - - $debug = [ - 'token' => $token, - 'action' => $action, - 'isEnterprise' => $isEnterprise, - 'isRetry' => $isRetry, - 'formDetails' => $formDetails, - ]; - - if (!$token) { - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('captchaBadRequest'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_REQUEST_MISSING_TOKEN, - AbstractBaseRoute::R_DEBUG => $debug, - ] - ); - // phpcs:enable - } - - if ($isEnterprise) { - $response = $this->onEnterprise($token, $action); - } else { - $response = $this->onFree($token); - } - - // Generic error msg from WP. - if (\is_wp_error($response)) { - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('submitWpError'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_REQUEST_WP_ERROR, - AbstractBaseRoute::R_DEBUG => $debug, - ] - ); - // phpcs:enable - } - - // Get body from the response. - $responseBody = \json_decode(\wp_remote_retrieve_body($response), true) ?? []; - - if ($isEnterprise) { - return $this->getEnterpriseOutput($responseBody, $action, $debug, $isRetry); - } - - return $this->getFreeOutput($responseBody, $action, $debug, $isRetry); - } - - /** - * Get Enterprise response from api. - * - * @param string $token Token for captcha. - * @param string $action Action name. - * - * @return array|WP_Error - */ - private function onEnterprise(string $token, string $action): array|WP_Error - { - $siteKey = SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), SettingsCaptcha::SETTINGS_CAPTCHA_SITE_KEY); - $apiKey = SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaApiKey(), SettingsCaptcha::SETTINGS_CAPTCHA_API_KEY); - $projectIdKey = SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaProjectIdKey(), SettingsCaptcha::SETTINGS_CAPTCHA_PROJECT_ID_KEY); - - $event = [ - 'siteKey' => $siteKey, - 'token' => $token, - 'expectedAction' => $action, - ]; - - // Google recommends sending userAgent and userIpAddress to improve detection. - $userAgent = $this->security->getUserAgent(); - if ($userAgent !== '') { - $event['userAgent'] = $userAgent; - } - - $userIp = $this->security->getIpAddress(); - if ($userIp !== '') { - $event['userIpAddress'] = $userIp; - } - - return \wp_remote_post( - "https://recaptchaenterprise.googleapis.com/v1/projects/{$projectIdKey}/assessments?key={$apiKey}", - [ - 'headers' => [ - 'Content-Type' => 'application/json; charset=utf-8', - 'Referer' => \site_url(), - ], - 'data_format' => 'body', - 'body' => \wp_json_encode([ - 'event' => $event, - ]), - ] - ); - } - - /** - * Get Enterprise response from api. - * - * @param string $token Token for captcha. - * - * @return array|WP_Error - */ - private function onFree(string $token): array|WP_Error - { - $secretKey = SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSecretKey(), SettingsCaptcha::SETTINGS_CAPTCHA_SECRET_KEY); - - return \wp_remote_post( - "https://www.google.com/recaptcha/api/siteverify", - [ - 'body' => [ - 'secret' => $secretKey, - 'response' => $token, - ], - ] - ); - } - - /** - * Get enterprise output. - * - * @param array $responseBody Response body from API. - * @param string $action Action name. - * @param array $debug Debug data. - * @param bool $isRetry Whether this request is itself a client retry. - * - * @throws BadRequestException If captcha is not valid. - * - * @return mixed - */ - private function getEnterpriseOutput(array $responseBody, string $action, array $debug, bool $isRetry) - { - $debug = \array_merge($debug, [ - 'responseBody' => $responseBody, - 'action' => $action, - ]); - - if (!isset($responseBody['tokenProperties']['valid']) || !$responseBody['tokenProperties']['valid']) { - $errorCode = $responseBody['tokenProperties']['invalidReason'] ?? ''; - - $debug['invalidReason'] = $errorCode; - - $retry = \in_array($errorCode, self::ENTERPRISE_RETRY_REASONS, true); - - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('captchaError'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_ENTERPRISE_OUTPUT_ERROR, - AbstractBaseRoute::R_DEBUG => $debug, - ], - [ - UtilsHelper::getStateResponseOutputKey('captchaRetry') => $retry, - UtilsHelper::getStateResponseOutputKey('captchaSkipLogging') => $retry && !$isRetry, - ] - ); - // phpcs:enable - } - - // If response is error. - if (!isset($responseBody['riskAnalysis']['score'])) { - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('captchaError'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_ENTERPRISE_OUTPUT_ERROR, - AbstractBaseRoute::R_DEBUG => $debug, - ] - ); - // phpcs:enable - } - - return $this->validate( - $responseBody, - $action, - $responseBody['tokenProperties']['action'] ?? '', - $responseBody['riskAnalysis']['score'] ?? 0.0, - $debug - ); - } - - /** - * Get free output. - * - * @param array $responseBody Response body from API. - * @param string $action Action name. - * @param array $debug Debug data. - * @param bool $isRetry Whether this request is itself a client retry. - * - * @throws BadRequestException If captcha is not valid. - * - * @return mixed - */ - private function getFreeOutput(array $responseBody, string $action, array $debug, bool $isRetry) - { - $debug = \array_merge($debug, [ - 'responseBody' => $responseBody, - 'action' => $action, - ]); - - // If response is error. - if (!isset($responseBody['score'])) { - $errorCodes = $responseBody['error-codes'] ?? []; - - $debug['errorCodes'] = $errorCodes; - - $retry = (bool) \array_intersect(self::FREE_RETRY_REASONS, $errorCodes); - - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('captchaError'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_FREE_OUTPUT_ERROR, - AbstractBaseRoute::R_DEBUG => $debug, - ], - [ - UtilsHelper::getStateResponseOutputKey('captchaRetry') => $retry, - UtilsHelper::getStateResponseOutputKey('captchaSkipLogging') => $retry && !$isRetry, - ] - ); - // phpcs:enable + if (SettingsCaptcha::getActiveProvider() === SettingsCaptcha::PROVIDER_FRIENDLY) { + return $this->friendlyCaptcha->check($token, $action, $isEnterprise, $formDetails); } - return $this->validate( - $responseBody, - $action, - $responseBody['action'] ?? '', - $responseBody['score'] ?? 0.0, - $debug - ); - } - - /** - * Validate and return if issue. - * - * @param mixed $responseBody Response body from API. - * @param string $action Action name. - * @param string $actionResponse Action response from API. - * @param float $score Score value Score value from API. - * @param array $debug Debug data. - * - * @throws BadRequestException If captcha is not valid. - * - * @return mixed - */ - private function validate( - $responseBody, - string $action, - string $actionResponse, - float $score, - array $debug - ) { - $debug = \array_merge($debug, [ - 'responseBody' => $responseBody, - 'action' => $action, - 'actionResponse' => $actionResponse, - 'score' => $score, - ]); - - // Bailout if action is not correct. - if ($actionResponse !== $action) { - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('captchaWrongAction'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_WRONG_ACTION, - AbstractBaseRoute::R_DEBUG => $debug, - ] - ); - // phpcs:enable - } - - $setScore = SettingsHelpers::getOptionValue(SettingsCaptcha::SETTINGS_CAPTCHA_SCORE_KEY) ?: SettingsCaptcha::SETTINGS_CAPTCHA_SCORE_DEFAULT_KEY; // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found - - // Bailout on spam. - if (\floatval($score) < \floatval($setScore)) { - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('captchaScoreSpam'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_SCORE_SPAM, - AbstractBaseRoute::R_DEBUG => $debug, - ], - [ - UtilsHelper::getStateResponseOutputKey('captchaIsSpam') => true, - ] - ); - // phpcs:enable - } - - return [ - AbstractBaseRoute::R_MSG => $this->labels->getLabel('captchaSuccess'), - AbstractBaseRoute::R_DEBUG => [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_SUCCESS, - AbstractBaseRoute::R_DEBUG => $debug, - ], - AbstractBaseRoute::R_DATA => [ - UtilsHelper::getStateResponseOutputKey('captchaIsSpam') => false, - ], - ]; + return $this->recaptcha->check($token, $action, $isEnterprise, $formDetails); } } + diff --git a/src/Captcha/CaptchaDispatcher.php b/src/Captcha/CaptchaDispatcher.php deleted file mode 100644 index 10c385a61..000000000 --- a/src/Captcha/CaptchaDispatcher.php +++ /dev/null @@ -1,62 +0,0 @@ -captcha = $captcha; - $this->friendlyCaptcha = $friendlyCaptcha; - } - - /** - * Delegate to the active captcha provider. - * - * @param string $token Token from frontend. - * @param string $action Action to check. - * @param boolean $isEnterprise Type of captcha. - * @param array $formDetails Form details. - * - * @return array - */ - public function check(string $token, string $action, bool $isEnterprise, array $formDetails = []): array - { - if (SettingsCaptchaProvider::getActiveProvider() === SettingsCaptchaProvider::PROVIDER_FRIENDLY) { - return $this->friendlyCaptcha->check($token, $action, $isEnterprise, $formDetails); - } - - return $this->captcha->check($token, $action, $isEnterprise, $formDetails); - } -} diff --git a/src/Captcha/Recaptcha.php b/src/Captcha/Recaptcha.php new file mode 100644 index 000000000..8005ed2b5 --- /dev/null +++ b/src/Captcha/Recaptcha.php @@ -0,0 +1,390 @@ +labels = $labels; + $this->security = $security; + } + + /** + * Check captcha request. + * + * @param string $token Token from frontend. + * @param string $action Action to check. + * @param boolean $isEnterprise Type of captcha. + * @param array $formDetails Form details. + * + * @throws BadRequestException If captcha is not valid. + * + * @return array + */ + public function check(string $token, string $action, bool $isEnterprise, array $formDetails = []): array + { + if (!\apply_filters(SettingsRecaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + return [ + AbstractBaseRoute::R_MSG => $this->labels->getLabel('captchaSuccess'), + AbstractBaseRoute::R_DEBUG => [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_FEATURE_DISABLED, + ], + ]; + } + + $isRetry = (bool) ($formDetails[Config::FD_CAPTCHA]['isRetry'] ?? false); + + $debug = [ + 'token' => $token, + 'action' => $action, + 'isEnterprise' => $isEnterprise, + 'isRetry' => $isRetry, + 'formDetails' => $formDetails, + ]; + + if (!$token) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('captchaBadRequest'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_REQUEST_MISSING_TOKEN, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + if ($isEnterprise) { + $response = $this->onEnterprise($token, $action); + } else { + $response = $this->onFree($token); + } + + // Generic error msg from WP. + if (\is_wp_error($response)) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('submitWpError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_REQUEST_WP_ERROR, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + // Get body from the response. + $responseBody = \json_decode(\wp_remote_retrieve_body($response), true) ?? []; + + if ($isEnterprise) { + return $this->getEnterpriseOutput($responseBody, $action, $debug, $isRetry); + } + + return $this->getFreeOutput($responseBody, $action, $debug, $isRetry); + } + + /** + * Get Enterprise response from api. + * + * @param string $token Token for captcha. + * @param string $action Action name. + * + * @return array|WP_Error + */ + private function onEnterprise(string $token, string $action): array|WP_Error + { + $siteKey = SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), SettingsRecaptcha::SETTINGS_CAPTCHA_SITE_KEY); + $apiKey = SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaApiKey(), SettingsRecaptcha::SETTINGS_CAPTCHA_API_KEY); + $projectIdKey = SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaProjectIdKey(), SettingsRecaptcha::SETTINGS_CAPTCHA_PROJECT_ID_KEY); + + $event = [ + 'siteKey' => $siteKey, + 'token' => $token, + 'expectedAction' => $action, + ]; + + // Google recommends sending userAgent and userIpAddress to improve detection. + $userAgent = $this->security->getUserAgent(); + if ($userAgent !== '') { + $event['userAgent'] = $userAgent; + } + + $userIp = $this->security->getIpAddress(); + if ($userIp !== '') { + $event['userIpAddress'] = $userIp; + } + + return \wp_remote_post( + "https://recaptchaenterprise.googleapis.com/v1/projects/{$projectIdKey}/assessments?key={$apiKey}", + [ + 'headers' => [ + 'Content-Type' => 'application/json; charset=utf-8', + 'Referer' => \site_url(), + ], + 'data_format' => 'body', + 'body' => \wp_json_encode([ + 'event' => $event, + ]), + ] + ); + } + + /** + * Get Enterprise response from api. + * + * @param string $token Token for captcha. + * + * @return array|WP_Error + */ + private function onFree(string $token): array|WP_Error + { + $secretKey = SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSecretKey(), SettingsRecaptcha::SETTINGS_CAPTCHA_SECRET_KEY); + + return \wp_remote_post( + "https://www.google.com/recaptcha/api/siteverify", + [ + 'body' => [ + 'secret' => $secretKey, + 'response' => $token, + ], + ] + ); + } + + /** + * Get enterprise output. + * + * @param array $responseBody Response body from API. + * @param string $action Action name. + * @param array $debug Debug data. + * @param bool $isRetry Whether this request is itself a client retry. + * + * @throws BadRequestException If captcha is not valid. + * + * @return mixed + */ + private function getEnterpriseOutput(array $responseBody, string $action, array $debug, bool $isRetry) + { + $debug = \array_merge($debug, [ + 'responseBody' => $responseBody, + 'action' => $action, + ]); + + if (!isset($responseBody['tokenProperties']['valid']) || !$responseBody['tokenProperties']['valid']) { + $errorCode = $responseBody['tokenProperties']['invalidReason'] ?? ''; + + $debug['invalidReason'] = $errorCode; + + $retry = \in_array($errorCode, self::ENTERPRISE_RETRY_REASONS, true); + + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('captchaError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_ENTERPRISE_OUTPUT_ERROR, + AbstractBaseRoute::R_DEBUG => $debug, + ], + [ + UtilsHelper::getStateResponseOutputKey('captchaRetry') => $retry, + UtilsHelper::getStateResponseOutputKey('captchaSkipLogging') => $retry && !$isRetry, + ] + ); + // phpcs:enable + } + + // If response is error. + if (!isset($responseBody['riskAnalysis']['score'])) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('captchaError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_ENTERPRISE_OUTPUT_ERROR, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + return $this->validate( + $responseBody, + $action, + $responseBody['tokenProperties']['action'] ?? '', + $responseBody['riskAnalysis']['score'] ?? 0.0, + $debug + ); + } + + /** + * Get free output. + * + * @param array $responseBody Response body from API. + * @param string $action Action name. + * @param array $debug Debug data. + * @param bool $isRetry Whether this request is itself a client retry. + * + * @throws BadRequestException If captcha is not valid. + * + * @return mixed + */ + private function getFreeOutput(array $responseBody, string $action, array $debug, bool $isRetry) + { + $debug = \array_merge($debug, [ + 'responseBody' => $responseBody, + 'action' => $action, + ]); + + // If response is error. + if (!isset($responseBody['score'])) { + $errorCodes = $responseBody['error-codes'] ?? []; + + $debug['errorCodes'] = $errorCodes; + + $retry = (bool) \array_intersect(self::FREE_RETRY_REASONS, $errorCodes); + + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('captchaError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_FREE_OUTPUT_ERROR, + AbstractBaseRoute::R_DEBUG => $debug, + ], + [ + UtilsHelper::getStateResponseOutputKey('captchaRetry') => $retry, + UtilsHelper::getStateResponseOutputKey('captchaSkipLogging') => $retry && !$isRetry, + ] + ); + // phpcs:enable + } + + return $this->validate( + $responseBody, + $action, + $responseBody['action'] ?? '', + $responseBody['score'] ?? 0.0, + $debug + ); + } + + /** + * Validate and return if issue. + * + * @param mixed $responseBody Response body from API. + * @param string $action Action name. + * @param string $actionResponse Action response from API. + * @param float $score Score value Score value from API. + * @param array $debug Debug data. + * + * @throws BadRequestException If captcha is not valid. + * + * @return mixed + */ + private function validate( + $responseBody, + string $action, + string $actionResponse, + float $score, + array $debug + ) { + $debug = \array_merge($debug, [ + 'responseBody' => $responseBody, + 'action' => $action, + 'actionResponse' => $actionResponse, + 'score' => $score, + ]); + + // Bailout if action is not correct. + if ($actionResponse !== $action) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('captchaWrongAction'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_WRONG_ACTION, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + $setScore = SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_SCORE_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_SCORE_DEFAULT_KEY; // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found + + // Bailout on spam. + if (\floatval($score) < \floatval($setScore)) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('captchaScoreSpam'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_SCORE_SPAM, + AbstractBaseRoute::R_DEBUG => $debug, + ], + [ + UtilsHelper::getStateResponseOutputKey('captchaIsSpam') => true, + ] + ); + // phpcs:enable + } + + return [ + AbstractBaseRoute::R_MSG => $this->labels->getLabel('captchaSuccess'), + AbstractBaseRoute::R_DEBUG => [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_SUCCESS, + AbstractBaseRoute::R_DEBUG => $debug, + ], + AbstractBaseRoute::R_DATA => [ + UtilsHelper::getStateResponseOutputKey('captchaIsSpam') => false, + ], + ]; + } +} diff --git a/src/Captcha/SettingsCaptcha.php b/src/Captcha/SettingsCaptcha.php index d10a16a08..a0f283637 100644 --- a/src/Captcha/SettingsCaptcha.php +++ b/src/Captcha/SettingsCaptcha.php @@ -1,7 +1,7 @@ labels = $labels; + \add_filter(self::FILTER_SETTINGS_GLOBAL_NAME, [$this, 'getSettingsGlobalData']); + \add_filter(self::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, [$this, 'isSettingsGlobalValid']); } /** - * Register all the hooks + * Resolve the provider currently selected by the admin. * - * @return void + * Falls back to Google so existing installs (which only have `captcha-use` + * set) keep working untouched after upgrade. + * + * @return string Provider identifier — either `google` or `friendly`. */ - public function register(): void + public static function getActiveProvider(): string { - \add_filter(self::FILTER_SETTINGS_GLOBAL_NAME, [$this, 'getSettingsGlobalData']); - \add_filter(self::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, [$this, 'isSettingsGlobalValid']); + $value = SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_PROVIDER_KEY); + + return $value === self::PROVIDER_FRIENDLY ? self::PROVIDER_FRIENDLY : self::PROVIDER_GOOGLE; } /** - * Determine if settings global are valid. + * The merged page is valid whenever the active provider's own validity filter says so. * - * @return boolean + * @return bool */ public function isSettingsGlobalValid(): bool { - if (SettingsCaptchaProvider::getActiveProvider() !== SettingsCaptchaProvider::PROVIDER_GOOGLE) { - return false; - } - - $isUsed = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_USE_KEY, self::SETTINGS_CAPTCHA_USE_KEY); - $siteKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), self::SETTINGS_CAPTCHA_SITE_KEY); - $secretKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSecretKey(), self::SETTINGS_CAPTCHA_SECRET_KEY); - $apiKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaApiKey(), self::SETTINGS_CAPTCHA_API_KEY); - $projectIdKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaProjectIdKey(), self::SETTINGS_CAPTCHA_PROJECT_ID_KEY); + $providerFilter = self::getActiveProvider() === self::PROVIDER_FRIENDLY + ? SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME + : SettingsRecaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME; - $isEnterprise = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, self::SETTINGS_CAPTCHA_ENTERPRISE_KEY); - - if ($isEnterprise) { - if (!$isUsed || !$siteKey || !$apiKey || !$projectIdKey) { - return false; - } - } else { - if (!$isUsed || !$siteKey || !$secretKey) { - return false; - } - } - - return true; + return (bool) \apply_filters($providerFilter, false); } /** - * Get global settings array for building settings page. - * - * Retained for BC in case the `FILTER_SETTINGS_GLOBAL_NAME` filter is still - * consulted; the menu entry is now handled by `SettingsCaptchaProvider`. + * Build the merged settings page. * * @return array> */ @@ -172,206 +99,49 @@ public function getSettingsGlobalData(): array return SettingsOutputHelpers::getNoActiveFeature(); } - return [ - SettingsOutputHelpers::getIntro(self::SETTINGS_TYPE_KEY), - [ - 'component' => 'tabs', - 'tabsContent' => self::getProviderTabs(), - ], - ]; - } + $provider = self::getActiveProvider(); - /** - * Tab definitions for the Google reCAPTCHA provider, composed into the - * merged captcha page by `SettingsCaptchaProvider`. - * - * @return array> - */ - public static function getProviderTabs(): array - { - $isEnterprise = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, self::SETTINGS_CAPTCHA_ENTERPRISE_KEY); - $isInit = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY); - - return [ - [ - 'component' => 'tab', - 'tabLabel' => \__('General', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'intro', - // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings - 'introSubtitle' => \__('Protect your website from spam and abuse using Google\'s reCAPTCHA.
A captcha is a simple task that is easy for humans to do, but difficult for bots.', 'eightshift-forms'), - ], - [ - 'component' => 'checkboxes', - 'checkboxesFieldHideLabel' => true, - 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Use reCAPTCHA Enterprise', 'eightshift-forms'), - 'checkboxIsChecked' => $isEnterprise, - 'checkboxValue' => self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, - 'checkboxSingleSubmit' => true, - 'checkboxAsToggle' => true, - ], - ], - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getGoogleReCaptchaSiteKey(), - self::SETTINGS_CAPTCHA_SITE_KEY, - 'ES_GOOGLE_RECAPTCHA_SITE_KEY', - \__('Site key', 'eightshift-forms'), - ), - - ...(!$isEnterprise ? [ - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getGoogleReCaptchaSecretKey(), - self::SETTINGS_CAPTCHA_SECRET_KEY, - 'ES_GOOGLE_RECAPTCHA_SECRET_KEY', - \__('Secret key', 'eightshift-forms'), - ), - ] : [ - SettingsOutputHelpers::getInputFieldWithGlobalVariable( - Variables::getGoogleReCaptchaProjectIdKey(), - self::SETTINGS_CAPTCHA_PROJECT_ID_KEY, - 'ES_GOOGLE_RECAPTCHA_PROJECT_ID_KEY', - \__('Project ID', 'eightshift-forms'), - ), - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getGoogleReCaptchaApiKey(), - self::SETTINGS_CAPTCHA_API_KEY, - 'ES_GOOGLE_RECAPTCHA_API_KEY', - \__('API key', 'eightshift-forms'), - ), - ]), - ], - ], + $output = [ + SettingsOutputHelpers::getIntro('captcha'), [ - 'component' => 'tab', - 'tabLabel' => \__('Advanced', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'checkboxes', - 'checkboxesFieldHideLabel' => true, - 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Hide badge', 'eightshift-forms'), - 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), - 'checkboxValue' => self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, - 'checkboxSingleSubmit' => true, - 'checkboxAsToggle' => true, - 'checkboxHelp' => \__('Not recommended, as it is against Google\'s terms of use.', 'eightshift-forms'), - ], - ], - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - [ - 'component' => 'input', - 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SCORE_KEY), - 'inputFieldLabel' => \__('"Spam unlikely" threshold', 'eightshift-forms'), - 'inputFieldHelp' => \__('The level above which a submission is not considered spam. Should be between 0.1 and 1.0.
In most cases, a user will receive as core between 0.8 and 0.9.', 'eightshift-forms'), - 'inputType' => 'number', - 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SCORE_KEY), - 'inputMin' => 0.1, - 'inputMax' => 1, - 'inputStep' => 0.1, - 'inputIsNumber' => true, - 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SCORE_DEFAULT_KEY, - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], + 'component' => 'select', + 'selectName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_PROVIDER_KEY), + 'selectFieldLabel' => \__('Provider', 'eightshift-forms'), + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'selectFieldHelp' => \__('Pick which captcha service validates submissions. Switching the provider reloads the fields below.', 'eightshift-forms'), + 'selectSingleSubmit' => true, + 'selectContent' => [ [ - 'component' => 'input', - 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), - 'inputFieldLabel' => \__('"On submit" action name', 'eightshift-forms'), - 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA on form submission.', 'eightshift-forms'), - 'inputType' => 'text', - 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), - 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, + 'component' => 'select-option', + 'selectOptionLabel' => \__('Google reCAPTCHA', 'eightshift-forms'), + 'selectOptionValue' => self::PROVIDER_GOOGLE, + 'selectOptionIsSelected' => $provider === self::PROVIDER_GOOGLE, ], [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, + 'component' => 'select-option', + 'selectOptionLabel' => \__('Friendly Captcha', 'eightshift-forms'), + 'selectOptionValue' => self::PROVIDER_FRIENDLY, + 'selectOptionIsSelected' => $provider === self::PROVIDER_FRIENDLY, ], - [ - 'component' => 'checkboxes', - 'checkboxesFieldHideLabel' => true, - 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Load Captcha on website load', 'eightshift-forms'), - 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), - 'checkboxValue' => self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, - 'checkboxHelp' => \__('By default, Captcha is only loaded on pages that contain forms. However, with this option, you can load Captcha on every page.', 'eightshift-forms'), - 'checkboxSingleSubmit' => true, - 'checkboxAsToggle' => true, - 'checkboxAsToggleSize' => 'medium', - ], - ], - ], - $isInit ? [ - 'component' => 'input', - 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), - 'inputFieldLabel' => \__('Action name', 'eightshift-forms'), - 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA when Captcha is loaded on every page.', 'eightshift-forms'), - 'inputType' => 'text', - 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), - 'inputPlaceholder' => self::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, - ] : [], ], ], [ - 'component' => 'tab', - 'tabLabel' => \__('Help', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'steps', - 'stepsTitle' => \__('How to get the Free reCAPTCHA API key?', 'eightshift-forms'), - 'stepsContent' => [ - // translators: %s will be replaced with the external link. - \sprintf(\__('Visit this link.', 'eightshift-forms'), 'https://www.google.com/recaptcha/admin/create'), - \__('Configure all the options. Make sure to select reCaptcha version 3!', 'eightshift-forms'), - \__('Copy the API key into the field under the API tab or use the global constant.', 'eightshift-forms'), - ], - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - [ - 'component' => 'steps', - 'stepsTitle' => \__('How to get the Enterprise reCAPTCHA API key?', 'eightshift-forms'), - 'stepsContent' => [ - // translators: %s will be replaced with the external link. - \sprintf(\__('Visit Google Cloud Console.', 'eightshift-forms'), 'https://console.cloud.google.com/'), - \__('Create a new project and set that project as Project ID.', 'eightshift-forms'), - \__('Search and go to reCAPTCHA product.', 'eightshift-forms'), - \__('You will probably need to set billing service for this product.', 'eightshift-forms'), - \__('Create a new key and set that key as Site key.', 'eightshift-forms'), - // translators: %s will be replaced with the website domain. - \sprintf(\__('Limit the key to your website domain. Domain: %s (exact, no trailing slash and protocol).', 'eightshift-forms'), \preg_replace("(^https?://)", "", \site_url())), - \__('Search and go to API & Services product.', 'eightshift-forms'), - \__('Go to Credentials section and create a new API key.', 'eightshift-forms'), - // translators: %s will be replaced with the website domain. - \sprintf(\__('Create a new key for Website, add restrictions to your website domain %s (exact, no trailing slash, with protocol) and set API restrictions to reCAPTCHA Enterprise.', 'eightshift-forms'), \esc_url(\site_url())), - \__('Set that key as API key.', 'eightshift-forms'), - ], - ], - ], + 'component' => 'divider', + 'dividerExtraVSpacing' => true, ], ]; + + $providerTabs = $provider === self::PROVIDER_FRIENDLY + ? SettingsFriendlyCaptcha::getProviderTabs() + : SettingsRecaptcha::getProviderTabs(); + + if ($providerTabs) { + $output[] = [ + 'component' => 'tabs', + 'tabsContent' => $providerTabs, + ]; + } + + return $output; } } diff --git a/src/Captcha/SettingsCaptchaProvider.php b/src/Captcha/SettingsCaptchaProvider.php deleted file mode 100644 index 3ccd55b32..000000000 --- a/src/Captcha/SettingsCaptchaProvider.php +++ /dev/null @@ -1,143 +0,0 @@ -> - */ - public function getSettingsGlobalData(): array - { - // Bail out if the master toggle (shared with Google reCAPTCHA history) is off. - if (!SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY)) { - return SettingsOutputHelpers::getNoActiveFeature(); - } - - $provider = self::getActiveProvider(); - - $output = [ - SettingsOutputHelpers::getIntro('captcha'), - [ - 'component' => 'select', - 'selectName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_PROVIDER_KEY), - 'selectFieldLabel' => \__('Provider', 'eightshift-forms'), - // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings - 'selectFieldHelp' => \__('Pick which captcha service validates submissions. Switching the provider reloads the fields below.', 'eightshift-forms'), - 'selectSingleSubmit' => true, - 'selectContent' => [ - [ - 'component' => 'select-option', - 'selectOptionLabel' => \__('Google reCAPTCHA', 'eightshift-forms'), - 'selectOptionValue' => self::PROVIDER_GOOGLE, - 'selectOptionIsSelected' => $provider === self::PROVIDER_GOOGLE, - ], - [ - 'component' => 'select-option', - 'selectOptionLabel' => \__('Friendly Captcha', 'eightshift-forms'), - 'selectOptionValue' => self::PROVIDER_FRIENDLY, - 'selectOptionIsSelected' => $provider === self::PROVIDER_FRIENDLY, - ], - ], - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - ]; - - $providerTabs = $provider === self::PROVIDER_FRIENDLY - ? SettingsFriendlyCaptcha::getProviderTabs() - : SettingsCaptcha::getProviderTabs(); - - if ($providerTabs) { - $output[] = [ - 'component' => 'tabs', - 'tabsContent' => $providerTabs, - ]; - } - - return $output; - } -} diff --git a/src/Captcha/SettingsFriendlyCaptcha.php b/src/Captcha/SettingsFriendlyCaptcha.php index fb1e42998..b04fd9656 100644 --- a/src/Captcha/SettingsFriendlyCaptcha.php +++ b/src/Captcha/SettingsFriendlyCaptcha.php @@ -98,7 +98,7 @@ public function register(): void */ public function isSettingsGlobalValid(): bool { - if (SettingsCaptchaProvider::getActiveProvider() !== SettingsCaptchaProvider::PROVIDER_FRIENDLY) { + if (SettingsCaptcha::getActiveProvider() !== SettingsCaptcha::PROVIDER_FRIENDLY) { return false; } @@ -116,7 +116,7 @@ public function isSettingsGlobalValid(): bool * Get global settings array for building settings page. * * Retained for BC in case the `FILTER_SETTINGS_GLOBAL_NAME` filter is still - * consulted; the menu entry is now handled by `SettingsCaptchaProvider`. + * consulted; the menu entry is now handled by `SettingsCaptcha`. * * @return array> */ @@ -137,7 +137,7 @@ public function getSettingsGlobalData(): array /** * Tab definitions for the Friendly Captcha provider, composed into the - * merged captcha page by `SettingsCaptchaProvider`. + * merged captcha page by `SettingsCaptcha`. * * @return array> */ diff --git a/src/Captcha/SettingsRecaptcha.php b/src/Captcha/SettingsRecaptcha.php new file mode 100644 index 000000000..216a8ffe4 --- /dev/null +++ b/src/Captcha/SettingsRecaptcha.php @@ -0,0 +1,372 @@ +labels = $labels; + } + + /** + * Register all the hooks + * + * @return void + */ + public function register(): void + { + \add_filter(self::FILTER_SETTINGS_GLOBAL_NAME, [$this, 'getSettingsGlobalData']); + \add_filter(self::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, [$this, 'isSettingsGlobalValid']); + } + + /** + * Determine if settings global are valid. + * + * @return boolean + */ + public function isSettingsGlobalValid(): bool + { + if (SettingsCaptcha::getActiveProvider() !== SettingsCaptcha::PROVIDER_GOOGLE) { + return false; + } + + $isUsed = SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY); + $siteKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), self::SETTINGS_CAPTCHA_SITE_KEY); + $secretKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSecretKey(), self::SETTINGS_CAPTCHA_SECRET_KEY); + $apiKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaApiKey(), self::SETTINGS_CAPTCHA_API_KEY); + $projectIdKey = (bool) SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaProjectIdKey(), self::SETTINGS_CAPTCHA_PROJECT_ID_KEY); + + $isEnterprise = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, self::SETTINGS_CAPTCHA_ENTERPRISE_KEY); + + if ($isEnterprise) { + if (!$isUsed || !$siteKey || !$apiKey || !$projectIdKey) { + return false; + } + } else { + if (!$isUsed || !$siteKey || !$secretKey) { + return false; + } + } + + return true; + } + + /** + * Get global settings array for building settings page. + * + * Retained for BC in case the `FILTER_SETTINGS_GLOBAL_NAME` filter is still + * consulted; the menu entry is now handled by `SettingsCaptcha`. + * + * @return array> + */ + public function getSettingsGlobalData(): array + { + if (!SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY)) { + return SettingsOutputHelpers::getNoActiveFeature(); + } + + return [ + SettingsOutputHelpers::getIntro(self::SETTINGS_TYPE_KEY), + [ + 'component' => 'tabs', + 'tabsContent' => self::getProviderTabs(), + ], + ]; + } + + /** + * Tab definitions for the Google reCAPTCHA provider, composed into the + * merged captcha page by `SettingsCaptcha`. + * + * @return array> + */ + public static function getProviderTabs(): array + { + $isEnterprise = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, self::SETTINGS_CAPTCHA_ENTERPRISE_KEY); + $isInit = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY); + + return [ + [ + 'component' => 'tab', + 'tabLabel' => \__('General', 'eightshift-forms'), + 'tabContent' => [ + [ + 'component' => 'intro', + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'introSubtitle' => \__('Protect your website from spam and abuse using Google\'s reCAPTCHA.
A captcha is a simple task that is easy for humans to do, but difficult for bots.', 'eightshift-forms'), + ], + [ + 'component' => 'checkboxes', + 'checkboxesFieldHideLabel' => true, + 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY), + 'checkboxesContent' => [ + [ + 'component' => 'checkbox', + 'checkboxLabel' => \__('Use reCAPTCHA Enterprise', 'eightshift-forms'), + 'checkboxIsChecked' => $isEnterprise, + 'checkboxValue' => self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, + 'checkboxSingleSubmit' => true, + 'checkboxAsToggle' => true, + ], + ], + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getGoogleReCaptchaSiteKey(), + self::SETTINGS_CAPTCHA_SITE_KEY, + 'ES_GOOGLE_RECAPTCHA_SITE_KEY', + \__('Site key', 'eightshift-forms'), + ), + + ...(!$isEnterprise ? [ + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getGoogleReCaptchaSecretKey(), + self::SETTINGS_CAPTCHA_SECRET_KEY, + 'ES_GOOGLE_RECAPTCHA_SECRET_KEY', + \__('Secret key', 'eightshift-forms'), + ), + ] : [ + SettingsOutputHelpers::getInputFieldWithGlobalVariable( + Variables::getGoogleReCaptchaProjectIdKey(), + self::SETTINGS_CAPTCHA_PROJECT_ID_KEY, + 'ES_GOOGLE_RECAPTCHA_PROJECT_ID_KEY', + \__('Project ID', 'eightshift-forms'), + ), + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getGoogleReCaptchaApiKey(), + self::SETTINGS_CAPTCHA_API_KEY, + 'ES_GOOGLE_RECAPTCHA_API_KEY', + \__('API key', 'eightshift-forms'), + ), + ]), + ], + ], + [ + 'component' => 'tab', + 'tabLabel' => \__('Advanced', 'eightshift-forms'), + 'tabContent' => [ + [ + 'component' => 'checkboxes', + 'checkboxesFieldHideLabel' => true, + 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), + 'checkboxesContent' => [ + [ + 'component' => 'checkbox', + 'checkboxLabel' => \__('Hide badge', 'eightshift-forms'), + 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), + 'checkboxValue' => self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, + 'checkboxSingleSubmit' => true, + 'checkboxAsToggle' => true, + 'checkboxHelp' => \__('Not recommended, as it is against Google\'s terms of use.', 'eightshift-forms'), + ], + ], + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'input', + 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SCORE_KEY), + 'inputFieldLabel' => \__('"Spam unlikely" threshold', 'eightshift-forms'), + 'inputFieldHelp' => \__('The level above which a submission is not considered spam. Should be between 0.1 and 1.0.
In most cases, a user will receive as core between 0.8 and 0.9.', 'eightshift-forms'), + 'inputType' => 'number', + 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SCORE_KEY), + 'inputMin' => 0.1, + 'inputMax' => 1, + 'inputStep' => 0.1, + 'inputIsNumber' => true, + 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SCORE_DEFAULT_KEY, + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'input', + 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), + 'inputFieldLabel' => \__('"On submit" action name', 'eightshift-forms'), + 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA on form submission.', 'eightshift-forms'), + 'inputType' => 'text', + 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), + 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'checkboxes', + 'checkboxesFieldHideLabel' => true, + 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), + 'checkboxesContent' => [ + [ + 'component' => 'checkbox', + 'checkboxLabel' => \__('Load Captcha on website load', 'eightshift-forms'), + 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), + 'checkboxValue' => self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, + 'checkboxHelp' => \__('By default, Captcha is only loaded on pages that contain forms. However, with this option, you can load Captcha on every page.', 'eightshift-forms'), + 'checkboxSingleSubmit' => true, + 'checkboxAsToggle' => true, + 'checkboxAsToggleSize' => 'medium', + ], + ], + ], + $isInit ? [ + 'component' => 'input', + 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), + 'inputFieldLabel' => \__('Action name', 'eightshift-forms'), + 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA when Captcha is loaded on every page.', 'eightshift-forms'), + 'inputType' => 'text', + 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), + 'inputPlaceholder' => self::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, + ] : [], + ], + ], + [ + 'component' => 'tab', + 'tabLabel' => \__('Help', 'eightshift-forms'), + 'tabContent' => [ + [ + 'component' => 'steps', + 'stepsTitle' => \__('How to get the Free reCAPTCHA API key?', 'eightshift-forms'), + 'stepsContent' => [ + // translators: %s will be replaced with the external link. + \sprintf(\__('Visit this link.', 'eightshift-forms'), 'https://www.google.com/recaptcha/admin/create'), + \__('Configure all the options. Make sure to select reCaptcha version 3!', 'eightshift-forms'), + \__('Copy the API key into the field under the API tab or use the global constant.', 'eightshift-forms'), + ], + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'steps', + 'stepsTitle' => \__('How to get the Enterprise reCAPTCHA API key?', 'eightshift-forms'), + 'stepsContent' => [ + // translators: %s will be replaced with the external link. + \sprintf(\__('Visit Google Cloud Console.', 'eightshift-forms'), 'https://console.cloud.google.com/'), + \__('Create a new project and set that project as Project ID.', 'eightshift-forms'), + \__('Search and go to reCAPTCHA product.', 'eightshift-forms'), + \__('You will probably need to set billing service for this product.', 'eightshift-forms'), + \__('Create a new key and set that key as Site key.', 'eightshift-forms'), + // translators: %s will be replaced with the website domain. + \sprintf(\__('Limit the key to your website domain. Domain: %s (exact, no trailing slash and protocol).', 'eightshift-forms'), \preg_replace("(^https?://)", "", \site_url())), + \__('Search and go to API & Services product.', 'eightshift-forms'), + \__('Go to Credentials section and create a new API key.', 'eightshift-forms'), + // translators: %s will be replaced with the website domain. + \sprintf(\__('Create a new key for Website, add restrictions to your website domain %s (exact, no trailing slash, with protocol) and set API restrictions to reCAPTCHA Enterprise.', 'eightshift-forms'), \esc_url(\site_url())), + \__('Set that key as API key.', 'eightshift-forms'), + ], + ], + ], + ], + ]; + } +} diff --git a/src/Enqueue/Blocks/EnqueueBlocks.php b/src/Enqueue/Blocks/EnqueueBlocks.php index d6a0f9658..c0403ceb5 100644 --- a/src/Enqueue/Blocks/EnqueueBlocks.php +++ b/src/Enqueue/Blocks/EnqueueBlocks.php @@ -16,8 +16,8 @@ use EightshiftForms\Enrichment\SettingsEnrichment; use EightshiftForms\Settings\SettingsSettings; use EightshiftForms\Captcha\SettingsCaptcha; -use EightshiftForms\Captcha\SettingsCaptchaProvider; use EightshiftForms\Captcha\SettingsFriendlyCaptcha; +use EightshiftForms\Captcha\SettingsRecaptcha; use EightshiftForms\CustomPostType\Result; use EightshiftForms\CustomPostType\Forms; use EightshiftForms\Enqueue\SharedEnqueue; @@ -333,15 +333,15 @@ public function enqueueBlockFrontendScript(): void ]; // Check if Captcha data is set and valid. - if (\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + if (\apply_filters(SettingsRecaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { $output['captcha'] = [ 'isUsed' => true, - 'isEnterprise' => SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY), - 'siteKey' => SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), SettingsCaptcha::SETTINGS_CAPTCHA_SITE_KEY), - 'submitAction' => SettingsHelpers::getOptionValue(SettingsCaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY) ?: SettingsCaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found - 'initAction' => SettingsHelpers::getOptionValue(SettingsCaptcha::SETTINGS_CAPTCHA_INIT_ACTION_KEY) ?: SettingsCaptcha::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found - 'loadOnInit' => SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), - 'hideBadge' => SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), + 'isEnterprise' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY), + 'siteKey' => SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), SettingsRecaptcha::SETTINGS_CAPTCHA_SITE_KEY), + 'submitAction' => SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found + 'initAction' => SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_INIT_ACTION_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found + 'loadOnInit' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), + 'hideBadge' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), ]; } else { $output['captcha'] = [ @@ -391,11 +391,11 @@ protected function getFrontendScriptDependencies(): array $output = \apply_filters($scriptsDependency, []); } - if (SettingsCaptchaProvider::getActiveProvider() === SettingsCaptchaProvider::PROVIDER_FRIENDLY) { + if (SettingsCaptcha::getActiveProvider() === SettingsCaptcha::PROVIDER_FRIENDLY) { if (\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { $output[] = "{$this->getAssetsPrefix()}-" . EnqueueFriendlyCaptcha::FRIENDLY_CAPTCHA_ENQUEUE_HANDLE; } - } elseif (\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + } elseif (\apply_filters(SettingsRecaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { $output[] = "{$this->getAssetsPrefix()}-" . EnqueueCaptcha::CAPTCHA_ENQUEUE_HANDLE; } diff --git a/src/Enqueue/Captcha/EnqueueCaptcha.php b/src/Enqueue/Captcha/EnqueueCaptcha.php index 04f7c8eb4..7d1d4cafd 100644 --- a/src/Enqueue/Captcha/EnqueueCaptcha.php +++ b/src/Enqueue/Captcha/EnqueueCaptcha.php @@ -12,7 +12,7 @@ use EightshiftForms\Helpers\SettingsHelpers; use EightshiftForms\Hooks\Variables; -use EightshiftForms\Captcha\SettingsCaptcha; +use EightshiftForms\Captcha\SettingsRecaptcha; use EightshiftForms\Config\Config; use EightshiftForms\Helpers\HooksHelpers; use EightshiftFormsVendor\EightshiftLibs\Enqueue\Theme\AbstractEnqueueTheme; @@ -47,7 +47,7 @@ public function register(): void */ protected function getFrontendScriptDependencies(): array { - if (!\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + if (!\apply_filters(SettingsRecaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { return []; } @@ -69,7 +69,7 @@ protected function getFrontendScriptDependencies(): array public function enqueueScriptsCaptcha(): void { // Check if Captcha data is set and valid. - $isSettingsGlobalValid = \apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false); + $isSettingsGlobalValid = \apply_filters(SettingsRecaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false); // Bailout if settings are not ok. if (!$isSettingsGlobalValid) { @@ -78,9 +78,9 @@ public function enqueueScriptsCaptcha(): void $handle = "{$this->getAssetsPrefix()}-" . self::CAPTCHA_ENQUEUE_HANDLE; - $siteKey = SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), SettingsCaptcha::SETTINGS_CAPTCHA_SITE_KEY); + $siteKey = SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), SettingsRecaptcha::SETTINGS_CAPTCHA_SITE_KEY); - $isEnterprise = SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY); + $isEnterprise = SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY); $url = "https://www.google.com/recaptcha/api.js?render={$siteKey}"; diff --git a/src/Hooks/Filters.php b/src/Hooks/Filters.php index 6f67b2c86..487b818aa 100644 --- a/src/Hooks/Filters.php +++ b/src/Hooks/Filters.php @@ -32,6 +32,7 @@ use EightshiftForms\Troubleshooting\SettingsFallback; use EightshiftForms\Captcha\SettingsCaptcha; use EightshiftForms\Captcha\SettingsFriendlyCaptcha; +use EightshiftForms\Captcha\SettingsRecaptcha; use EightshiftForms\General\SettingsGeneral; use EightshiftForms\Integrations\Calculator\SettingsCalculator; use EightshiftForms\Integrations\Corvus\SettingsCorvus; @@ -424,11 +425,12 @@ private static function getSettingsNonTranslatableNames(): array { return [ SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, - SettingsCaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY, - SettingsCaptcha::SETTINGS_CAPTCHA_SITE_KEY, - SettingsCaptcha::SETTINGS_CAPTCHA_SECRET_KEY, - SettingsCaptcha::SETTINGS_CAPTCHA_PROJECT_ID_KEY, - SettingsCaptcha::SETTINGS_CAPTCHA_API_KEY, + SettingsCaptcha::SETTINGS_CAPTCHA_PROVIDER_KEY, + SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY, + SettingsRecaptcha::SETTINGS_CAPTCHA_SITE_KEY, + SettingsRecaptcha::SETTINGS_CAPTCHA_SECRET_KEY, + SettingsRecaptcha::SETTINGS_CAPTCHA_PROJECT_ID_KEY, + SettingsRecaptcha::SETTINGS_CAPTCHA_API_KEY, SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY, SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY, diff --git a/src/Hooks/FiltersSettingsBuilder.php b/src/Hooks/FiltersSettingsBuilder.php index ffd953e9c..e18f18bfb 100644 --- a/src/Hooks/FiltersSettingsBuilder.php +++ b/src/Hooks/FiltersSettingsBuilder.php @@ -56,7 +56,6 @@ use EightshiftForms\Troubleshooting\SettingsDebug; use EightshiftForms\Troubleshooting\SettingsFallback; use EightshiftForms\Captcha\SettingsCaptcha; -use EightshiftForms\Captcha\SettingsCaptchaProvider; use EightshiftForms\Entries\SettingsEntries; use EightshiftForms\Integrations\Calculator\SettingsCalculator; use EightshiftForms\Integrations\Corvus\SettingsCorvus; @@ -152,7 +151,7 @@ public function getSettingsFiltersData(): array ], ], 'captcha' => [ - 'settingsGlobal' => SettingsCaptchaProvider::FILTER_SETTINGS_GLOBAL_NAME, + 'settingsGlobal' => SettingsCaptcha::FILTER_SETTINGS_GLOBAL_NAME, 'type' => Config::SETTINGS_INTERNAL_TYPE_ADVANCED, 'use' => SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, 'labels' => [ diff --git a/src/Rest/Routes/AbstractIntegrationFormSubmit.php b/src/Rest/Routes/AbstractIntegrationFormSubmit.php index eeb2aab99..0969e3314 100644 --- a/src/Rest/Routes/AbstractIntegrationFormSubmit.php +++ b/src/Rest/Routes/AbstractIntegrationFormSubmit.php @@ -88,7 +88,7 @@ abstract class AbstractIntegrationFormSubmit extends AbstractBaseRoute * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailer methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. */ @@ -96,14 +96,14 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment ) { $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; } diff --git a/src/Rest/Routes/General/CaptchaValidateRoute.php b/src/Rest/Routes/General/CaptchaValidateRoute.php index 652866989..8a9ec3ba0 100644 --- a/src/Rest/Routes/General/CaptchaValidateRoute.php +++ b/src/Rest/Routes/General/CaptchaValidateRoute.php @@ -11,7 +11,7 @@ namespace EightshiftForms\Rest\Routes\General; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\Captcha\SettingsCaptchaProvider; +use EightshiftForms\Captcha\SettingsCaptcha; use EightshiftForms\Labels\LabelsInterface; use EightshiftForms\Helpers\DeveloperHelpers; use EightshiftForms\Rest\Routes\AbstractBaseRoute; @@ -43,18 +43,18 @@ class CaptchaValidateRoute extends AbstractSimpleFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. */ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher + CaptchaInterface $captcha ) { $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; } /** @@ -67,7 +67,7 @@ public function __construct( */ public function register(): void { - if (SettingsCaptchaProvider::getActiveProvider() !== SettingsCaptchaProvider::PROVIDER_GOOGLE) { + if (SettingsCaptcha::getActiveProvider() !== SettingsCaptcha::PROVIDER_GOOGLE) { return; } diff --git a/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php b/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php index af21e12ca..b25925394 100644 --- a/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php +++ b/src/Rest/Routes/Integrations/ActiveCampaign/FormSubmitActiveCampaignRoute.php @@ -50,7 +50,7 @@ class FormSubmitActiveCampaignRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ActiveCampaignClientInterface $activeCampaignClient Inject ActiveCampaign methods. @@ -59,7 +59,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ActiveCampaignClientInterface $activeCampaignClient @@ -67,7 +67,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->activeCampaignClient = $activeCampaignClient; diff --git a/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php b/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php index 7f3c97669..89ff706de 100644 --- a/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php +++ b/src/Rest/Routes/Integrations/Airtable/FormSubmitAirtableRoute.php @@ -49,7 +49,7 @@ class FormSubmitAirtableRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param AirtableClientInterface $airtableClient Inject airtableClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, AirtableClientInterface $airtableClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->airtableClient = $airtableClient; diff --git a/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php b/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php index 41af131ba..47f831243 100644 --- a/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php +++ b/src/Rest/Routes/Integrations/Goodbits/FormSubmitGoodbitsRoute.php @@ -49,7 +49,7 @@ class FormSubmitGoodbitsRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $goodbitsClient Inject goodbitsClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $goodbitsClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->goodbitsClient = $goodbitsClient; diff --git a/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php b/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php index 68c67bc73..b95f94caa 100644 --- a/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php +++ b/src/Rest/Routes/Integrations/Greenhouse/FormSubmitGreenhouseRoute.php @@ -49,7 +49,7 @@ class FormSubmitGreenhouseRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $greenhouseClient Inject greenhouseClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $greenhouseClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->greenhouseClient = $greenhouseClient; diff --git a/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php b/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php index 2c09a904a..61cb79877 100644 --- a/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php +++ b/src/Rest/Routes/Integrations/Hubspot/FormSubmitHubspotRoute.php @@ -57,7 +57,7 @@ class FormSubmitHubspotRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param HubspotClientInterface $hubspotClient Inject hubspotClient methods. @@ -67,7 +67,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, HubspotClientInterface $hubspotClient, @@ -76,7 +76,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->hubspotClient = $hubspotClient; diff --git a/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php b/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php index d078d89c5..34d8b7a78 100644 --- a/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php +++ b/src/Rest/Routes/Integrations/Jira/FormSubmitJiraRoute.php @@ -49,7 +49,7 @@ class FormSubmitJiraRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param JiraClientInterface $jiraClient Inject jiraClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, JiraClientInterface $jiraClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->jiraClient = $jiraClient; diff --git a/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php b/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php index 9f8b0969b..1ad54fa0f 100644 --- a/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php +++ b/src/Rest/Routes/Integrations/Mailchimp/FormSubmitMailchimpRoute.php @@ -49,7 +49,7 @@ class FormSubmitMailchimpRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param MailchimpClientInterface $mailchimpClient Inject mailchimpClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, MailchimpClientInterface $mailchimpClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->mailchimpClient = $mailchimpClient; diff --git a/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php b/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php index 15c918eb3..23758b574 100644 --- a/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php +++ b/src/Rest/Routes/Integrations/Mailerlite/FormSubmitMailerliteRoute.php @@ -49,7 +49,7 @@ class FormSubmitMailerliteRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $mailerliteClient Inject mailerliteClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $mailerliteClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->mailerliteClient = $mailerliteClient; diff --git a/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php b/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php index ef2b3a62d..c3fe739aa 100644 --- a/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php +++ b/src/Rest/Routes/Integrations/Moments/FormSubmitMomentsRoute.php @@ -58,7 +58,7 @@ class FormSubmitMomentsRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $momentsClient Inject momentsClient methods. @@ -68,7 +68,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $momentsClient, @@ -77,7 +77,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->momentsClient = $momentsClient; diff --git a/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php b/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php index f44e5e591..cf3fcde93 100644 --- a/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php +++ b/src/Rest/Routes/Integrations/Nationbuilder/FormSubmitNationbuilderRoute.php @@ -49,7 +49,7 @@ class FormSubmitNationbuilderRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param NationbuilderClientInterface $nationbuilderClient Inject nationbuilderClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, NationbuilderClientInterface $nationbuilderClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->nationbuilderClient = $nationbuilderClient; diff --git a/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php b/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php index e9fb70bda..7b09f586b 100644 --- a/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php +++ b/src/Rest/Routes/Integrations/Pipedrive/FormSubmitPipedriveRoute.php @@ -49,7 +49,7 @@ class FormSubmitPipedriveRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param PipedriveClientInterface $pipedriveClient Inject pipedriveClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, PipedriveClientInterface $pipedriveClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->pipedriveClient = $pipedriveClient; diff --git a/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php b/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php index 068695078..349865d05 100644 --- a/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php +++ b/src/Rest/Routes/Integrations/Talentlyft/FormSubmitTalentlyftRoute.php @@ -49,7 +49,7 @@ class FormSubmitTalentlyftRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $talentlyftClient Inject talentlyftClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $talentlyftClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->talentlyftClient = $talentlyftClient; diff --git a/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php b/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php index 436883b5b..c3abe351c 100644 --- a/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php +++ b/src/Rest/Routes/Integrations/Workable/FormSubmitWorkableRoute.php @@ -49,7 +49,7 @@ class FormSubmitWorkableRoute extends AbstractIntegrationFormSubmit * @param SecurityInterface $security Inject security methods. * @param ValidatorInterface $validator Inject validator methods. * @param LabelsInterface $labels Inject labels methods. - * @param CaptchaInterface $captchaDispatcher Inject captcha methods. + * @param CaptchaInterface $captcha Inject captcha methods. * @param MailerInterface $mailer Inject mailerInterface methods. * @param EnrichmentInterface $enrichment Inject enrichment methods. * @param ClientInterface $workableClient Inject workableClient methods. @@ -58,7 +58,7 @@ public function __construct( SecurityInterface $security, ValidatorInterface $validator, LabelsInterface $labels, - CaptchaInterface $captchaDispatcher, + CaptchaInterface $captcha, MailerInterface $mailer, EnrichmentInterface $enrichment, ClientInterface $workableClient @@ -66,7 +66,7 @@ public function __construct( $this->security = $security; $this->validator = $validator; $this->labels = $labels; - $this->captcha = $captchaDispatcher; + $this->captcha = $captcha; $this->mailer = $mailer; $this->enrichment = $enrichment; $this->workableClient = $workableClient; From e915a1b90f09970c2246209be857871debef6425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Wed, 22 Apr 2026 10:26:55 +0200 Subject: [PATCH 11/20] Address review feedback: unify captcha surface, simplify FriendlyCaptcha, drop dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up on PR #613 review from @iruzevic: - FriendlyCaptcha: inline throws and remote call, match Recaptcha's flow one-for-one (no throwError/buildSuccess/remoteCall helpers). - Move Friendly endpoint URL constants and getEndpoint()/getEndpointUrl() from SettingsFriendlyCaptcha to FriendlyCaptcha — the class that uses them. - Remove unused SETTINGS_FRIENDLY_CAPTCHA_USE_KEY constant. - CaptchaValidateRoute: drop the register()-time settings check; the dispatcher's check() already no-ops when the provider is inactive. - AbstractIntegrationFormSubmit: drop the duplicate `friendlyCaptcha` state-param case. Single `captcha` param handles both providers. - EnqueueBlocks: single `captcha` localization payload with a `type` discriminator ('google' | 'friendly'). Drops the parallel `friendlyCaptcha` top-level key. - state-init.js / state.js: collapse the FRIENDLY_CAPTCHA_* state slot and getters into CAPTCHA_*. Adds CAPTCHA_TYPE / CAPTCHA_ENDPOINT plus type-constant enums. - form.js: single runFormCaptcha() dispatches internally based on getStateCaptchaType(). Delete runFormFriendlyCaptcha, setFormDataFriendlyCaptcha, and the duplicated branches across five call sites. - friendly-captcha.js: typeof frcaptcha check moved to the top of initWidget(); this.widget initialized in constructor; unused getResponse()/reset() class methods replaced with inline closures on the window API; init() guards on captcha type. - index.js: only eager-load the Friendly widget on pages that actually contain a form. - manifest.json: drop the Friendly-specific icon and state param name — one captcha icon and one `captcha` param cover both providers. - Re-use SettingsCaptcha::SETTINGS_TYPE_KEY in FiltersSettingsBuilder now that the dispatcher owns it. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Blocks/components/form/assets/form.js | 102 ++++------- .../form/assets/friendly-captcha.js | 40 ++--- src/Blocks/components/form/assets/index.js | 26 +-- .../components/form/assets/state-init.js | 33 ++-- src/Blocks/components/form/assets/state.js | 16 +- src/Blocks/manifest.json | 2 - src/Captcha/FriendlyCaptcha.php | 161 ++++++++---------- src/Captcha/SettingsCaptcha.php | 5 + src/Captcha/SettingsFriendlyCaptcha.php | 31 ---- src/Captcha/SettingsRecaptcha.php | 2 +- src/Enqueue/Blocks/EnqueueBlocks.php | 55 +++--- src/Hooks/Filters.php | 1 - src/Hooks/FiltersSettingsBuilder.php | 2 +- .../Routes/AbstractIntegrationFormSubmit.php | 4 - .../Routes/General/CaptchaValidateRoute.php | 18 -- 15 files changed, 185 insertions(+), 313 deletions(-) diff --git a/src/Blocks/components/form/assets/form.js b/src/Blocks/components/form/assets/form.js index 6d5b79cd9..9e966503b 100644 --- a/src/Blocks/components/form/assets/form.js +++ b/src/Blocks/components/form/assets/form.js @@ -573,11 +573,28 @@ export class Form { * * @returns {void} */ - runFormCaptcha(formId, filter = {}) { + async runFormCaptcha(formId, filter = {}) { if (!this.state.getStateCaptchaIsUsed()) { return; } + if (this.state.getStateCaptchaType() === StateEnum.CAPTCHA_TYPE_FRIENDLY) { + const widget = window[prefix]?.friendlyCaptcha; + const token = widget?.getResponse() ?? ''; + + this.setFormDataCaptcha({ + token, + }); + + await this.formSubmit(formId, filter); + + // Reset the widget after every server response so a fresh single-use + // token is ready for the next submission attempt. + widget?.reset(); + + return; + } + const actionName = this.state.getStateCaptchaSubmitAction(); const siteKey = this.state.getStateCaptchaSiteKey(); @@ -588,33 +605,6 @@ export class Form { } } - /** - * Run form with Friendly Captcha validation. - * - * @param {string} formId Form Id. - * @param {object} filter Additional filter to pass. - * - * @returns {void} - */ - async runFormFriendlyCaptcha(formId, filter = {}) { - if (!this.state.getStateFriendlyCaptchaIsUsed()) { - return; - } - - const widget = window[prefix]?.friendlyCaptcha; - const token = widget?.getResponse() ?? ''; - - this.setFormDataFriendlyCaptcha({ - token, - }); - - await this.formSubmit(formId, filter); - - // Reset the widget after every server response so a fresh single-use - // token is ready for the next submission attempt. - widget?.reset(); - } - /** * Execute enterprise captcha. * @@ -1186,22 +1176,6 @@ export class Form { ], this.FORM_DATA); } - /** - * Set form data object for all forms - Friendly Captcha. - * - * @param {object} data Friendly Captcha data. - * - * @returns {void} - */ - setFormDataFriendlyCaptcha(data) { - this.utils.buildFormDataItems([ - { - name: this.state.getStateParam('friendlyCaptcha'), - value: data, - }, - ], this.FORM_DATA); - } - //////////////////////////////////////////////////////////////// // Fields //////////////////////////////////////////////////////////////// @@ -1863,8 +1837,6 @@ export class Form { if (this.state.getStateCaptchaIsUsed()) { this.runFormCaptcha(formId, filterFinal); - } else if (this.state.getStateFriendlyCaptchaIsUsed()) { - this.runFormFriendlyCaptcha(formId, filterFinal); } else { this.formSubmit(formId, filterFinal); } @@ -1880,8 +1852,6 @@ export class Form { if (this.state.getStateCaptchaIsUsed()) { this.runFormCaptcha(formId, filterNormal); - } else if (this.state.getStateFriendlyCaptchaIsUsed()) { - this.runFormFriendlyCaptcha(formId, filterNormal); } else { this.formSubmit(formId, filterNormal); } @@ -2000,12 +1970,10 @@ export class Form { // Used only on frontend for single submit. if (!this.state.getStateConfigIsAdmin() && this.state.getStateFormConfigUseSingleSubmit(formId)) { if (this.state.getStateCaptchaIsUsed()) { - this.runFormCaptcha(formId); - } else if (this.state.getStateFriendlyCaptchaIsUsed()) { - this.runFormFriendlyCaptcha(formId); - } else { - this.formSubmit(formId); - } + this.runFormCaptcha(formId); + } else { + this.formSubmit(formId); + } } }; @@ -2085,12 +2053,10 @@ export class Form { (typeCustom === 'range' || typeCustom === 'number' || typeCustom === 'checkbox' || typeCustom === 'radio' || typeCustom === 'rating') ) { if (this.state.getStateCaptchaIsUsed()) { - this.runFormCaptcha(formId); - } else if (this.state.getStateFriendlyCaptchaIsUsed()) { - this.runFormFriendlyCaptcha(formId); - } else { - this.formSubmit(formId); - } + this.runFormCaptcha(formId); + } else { + this.formSubmit(formId); + } } }; @@ -2133,12 +2099,10 @@ export class Form { // Used only on frontend for single submit. if (!this.state.getStateConfigIsAdmin() && this.state.getStateFormConfigUseSingleSubmit(formId)) { if (this.state.getStateCaptchaIsUsed()) { - this.runFormCaptcha(formId); - } else if (this.state.getStateFriendlyCaptchaIsUsed()) { - this.runFormFriendlyCaptcha(formId); - } else { - this.formSubmit(formId); - } + this.runFormCaptcha(formId); + } else { + this.formSubmit(formId); + } } }; @@ -2246,12 +2210,6 @@ export class Form { setFormDataCaptcha: (data) => { this.setFormDataCaptcha(data); }, - setFormDataFriendlyCaptcha: (data) => { - this.setFormDataFriendlyCaptcha(data); - }, - runFormFriendlyCaptcha: (formId, filter = {}) => { - this.runFormFriendlyCaptcha(formId, filter); - }, setupInputField: (formId, name) => { this.setupInputField(formId, name); }, diff --git a/src/Blocks/components/form/assets/friendly-captcha.js b/src/Blocks/components/form/assets/friendly-captcha.js index 2fd84745d..0acdf938c 100644 --- a/src/Blocks/components/form/assets/friendly-captcha.js +++ b/src/Blocks/components/form/assets/friendly-captcha.js @@ -1,6 +1,6 @@ /* global frcaptcha */ -import { prefix, setStateWindow } from './state-init'; +import { StateEnum, prefix, setStateWindow } from './state-init'; /** * FriendlyCaptcha class. @@ -12,6 +12,8 @@ export class FriendlyCaptcha { /** @type {import('./state').State} */ this.state = this.utils.getState(); + this.widget = null; + // Set all public methods. this.publicMethods(); } @@ -26,7 +28,11 @@ export class FriendlyCaptcha { * @returns {void} */ init() { - if (!this.state.getStateFriendlyCaptchaIsUsed()) { + if (!this.state.getStateCaptchaIsUsed()) { + return; + } + + if (this.state.getStateCaptchaType() !== StateEnum.CAPTCHA_TYPE_FRIENDLY) { return; } @@ -39,12 +45,12 @@ export class FriendlyCaptcha { * @returns {void} */ initWidget() { - const siteKey = this.state.getStateFriendlyCaptchaSiteKey(); - if (typeof frcaptcha === 'undefined') { return; } + const siteKey = this.state.getStateCaptchaSiteKey(); + // Create a hidden container for the widget. const container = document.createElement('div'); container.style.display = 'none'; @@ -54,28 +60,10 @@ export class FriendlyCaptcha { element: container, sitekey: siteKey, startMode: 'auto', - apiEndpoint: this.state.getStateFriendlyCaptchaEndpoint(), + apiEndpoint: this.state.getStateCaptchaEndpoint(), }); } - /** - * Get the current response token from the widget. - * - * @returns {string} The solution token. - */ - getResponse() { - return this.widget?.getResponse() ?? ''; - } - - /** - * Reset the widget so it generates a fresh token. - * - * @returns {void} - */ - reset() { - this.widget?.reset(); - } - //////////////////////////////////////////////////////////////// // Private methods - not shared to the public window object. //////////////////////////////////////////////////////////////// @@ -99,11 +87,9 @@ export class FriendlyCaptcha { initWidget: () => { this.initWidget(); }, - getResponse: () => { - return this.getResponse(); - }, + getResponse: () => this.widget?.getResponse() ?? '', reset: () => { - this.reset(); + this.widget?.reset(); }, }; } diff --git a/src/Blocks/components/form/assets/index.js b/src/Blocks/components/form/assets/index.js index 754c32898..84f837233 100644 --- a/src/Blocks/components/form/assets/index.js +++ b/src/Blocks/components/form/assets/index.js @@ -1,7 +1,7 @@ /* global esFormsLocalization */ import domReady from '@wordpress/dom-ready'; -import { setStateInitial } from './state-init'; +import { StateEnum, setStateInitial } from './state-init'; import { Utils } from './utils'; // Global variable must be set for everything to work. @@ -17,18 +17,20 @@ const utils = new Utils(); const state = utils.getState(); domReady(() => { - // Load captcha if using initial. if (state.getStateCaptchaIsUsed()) { - import('./captcha').then(({ Captcha }) => { - new Captcha(utils).init(); - }); - } - - // Load Friendly Captcha if used. - if (state.getStateFriendlyCaptchaIsUsed()) { - import('./friendly-captcha').then(({ FriendlyCaptcha }) => { - new FriendlyCaptcha(utils).init(); - }); + if (state.getStateCaptchaType() === StateEnum.CAPTCHA_TYPE_FRIENDLY) { + // Friendly Captcha renders its widget inside the form, so only load + // it on pages that actually contain one. + if (document.querySelectorAll(state.getStateSelector('form', true))?.length) { + import('./friendly-captcha').then(({ FriendlyCaptcha }) => { + new FriendlyCaptcha(utils).init(); + }); + } + } else { + import('./captcha').then(({ Captcha }) => { + new Captcha(utils).init(); + }); + } } if (!state.getStateSettingsFormDisableAutoInit()) { diff --git a/src/Blocks/components/form/assets/state-init.js b/src/Blocks/components/form/assets/state-init.js index 23bdc3da3..f5fa048f1 100644 --- a/src/Blocks/components/form/assets/state-init.js +++ b/src/Blocks/components/form/assets/state-init.js @@ -98,16 +98,17 @@ export const StateEnum = { SETTINGS_FORM_MISCONFIGURED_MSG: 'formMisconfigured', CAPTCHA: 'captcha', + CAPTCHA_TYPE: 'type', CAPTCHA_SITE_KEY: 'site_key', CAPTCHA_IS_ENTERPRISE: 'isEnterprise', CAPTCHA_SUBMIT_ACTION: 'submitAction', CAPTCHA_INIT_ACTION: 'initAction', CAPTCHA_LOAD_ON_INIT: 'loadOnInit', CAPTCHA_HIDE_BADGE: 'hideBadge', + CAPTCHA_ENDPOINT: 'endpoint', - FRIENDLY_CAPTCHA: 'friendlyCaptcha', - FRIENDLY_CAPTCHA_SITE_KEY: 'siteKey', - FRIENDLY_CAPTCHA_ENDPOINT: 'endpoint', + CAPTCHA_TYPE_GOOGLE: 'google', + CAPTCHA_TYPE_FRIENDLY: 'friendly', ENRICHMENT: 'enrichment', ENRICHMENT_FORM_PREFILL: 'formPrefill', @@ -182,7 +183,6 @@ export function setStateInitial() { window[prefix].state = { [StateEnum.FORMS]: [], [StateEnum.CAPTCHA]: {}, - [StateEnum.FRIENDLY_CAPTCHA]: {}, [StateEnum.ENRICHMENT]: {}, [StateEnum.GEOLOCATION]: {}, [StateEnum.SETTINGS]: {}, @@ -260,26 +260,23 @@ export function setStateInitial() { setState([StateEnum.SETTINGS_FORM_CAPTCHA_ERROR_MSG], esFormsLocalization.formCaptchaErrorMsg ?? '', StateEnum.SETTINGS); setState([StateEnum.SETTINGS_FORM_MISCONFIGURED_MSG], esFormsLocalization.formMisconfigured ?? '', StateEnum.SETTINGS); - // Captcha. + // Captcha — single payload discriminated by `type`. const captcha = esFormsLocalization.captcha ?? {}; setState([StateEnum.IS_USED], Boolean(captcha.isUsed), StateEnum.CAPTCHA); if (captcha.isUsed) { + setState([StateEnum.CAPTCHA_TYPE], captcha.type, StateEnum.CAPTCHA); setState([StateEnum.CAPTCHA_SITE_KEY], captcha.siteKey, StateEnum.CAPTCHA); - setState([StateEnum.CAPTCHA_IS_ENTERPRISE], Boolean(captcha.isEnterprise), StateEnum.CAPTCHA); - setState([StateEnum.CAPTCHA_SUBMIT_ACTION], captcha.submitAction, StateEnum.CAPTCHA); - setState([StateEnum.CAPTCHA_INIT_ACTION], captcha.initAction, StateEnum.CAPTCHA); - setState([StateEnum.CAPTCHA_LOAD_ON_INIT], Boolean(captcha.loadOnInit), StateEnum.CAPTCHA); - setState([StateEnum.CAPTCHA_HIDE_BADGE], Boolean(captcha.hideBadge), StateEnum.CAPTCHA); - } - - // Friendly Captcha. - const friendlyCaptcha = esFormsLocalization.friendlyCaptcha ?? {}; - setState([StateEnum.IS_USED], Boolean(friendlyCaptcha.isUsed), StateEnum.FRIENDLY_CAPTCHA); - if (friendlyCaptcha.isUsed) { - setState([StateEnum.FRIENDLY_CAPTCHA_SITE_KEY], friendlyCaptcha.siteKey, StateEnum.FRIENDLY_CAPTCHA); - setState([StateEnum.FRIENDLY_CAPTCHA_ENDPOINT], friendlyCaptcha.endpoint, StateEnum.FRIENDLY_CAPTCHA); + if (captcha.type === StateEnum.CAPTCHA_TYPE_FRIENDLY) { + setState([StateEnum.CAPTCHA_ENDPOINT], captcha.endpoint, StateEnum.CAPTCHA); + } else { + setState([StateEnum.CAPTCHA_IS_ENTERPRISE], Boolean(captcha.isEnterprise), StateEnum.CAPTCHA); + setState([StateEnum.CAPTCHA_SUBMIT_ACTION], captcha.submitAction, StateEnum.CAPTCHA); + setState([StateEnum.CAPTCHA_INIT_ACTION], captcha.initAction, StateEnum.CAPTCHA); + setState([StateEnum.CAPTCHA_LOAD_ON_INIT], Boolean(captcha.loadOnInit), StateEnum.CAPTCHA); + setState([StateEnum.CAPTCHA_HIDE_BADGE], Boolean(captcha.hideBadge), StateEnum.CAPTCHA); + } } // Geolocation. diff --git a/src/Blocks/components/form/assets/state.js b/src/Blocks/components/form/assets/state.js index 097b0f583..9a9177f60 100644 --- a/src/Blocks/components/form/assets/state.js +++ b/src/Blocks/components/form/assets/state.js @@ -430,19 +430,11 @@ export class State { getStateCaptchaHideBadge = () => { return getState([StateEnum.CAPTCHA_HIDE_BADGE], StateEnum.CAPTCHA); }; - - //////////////////////////////////////////////////////////////// - // Friendly Captcha getters. - //////////////////////////////////////////////////////////////// - - getStateFriendlyCaptchaIsUsed = () => { - return getState([StateEnum.IS_USED], StateEnum.FRIENDLY_CAPTCHA); - }; - getStateFriendlyCaptchaSiteKey = () => { - return getState([StateEnum.FRIENDLY_CAPTCHA_SITE_KEY], StateEnum.FRIENDLY_CAPTCHA); + getStateCaptchaType = () => { + return getState([StateEnum.CAPTCHA_TYPE], StateEnum.CAPTCHA); }; - getStateFriendlyCaptchaEndpoint = () => { - return getState([StateEnum.FRIENDLY_CAPTCHA_ENDPOINT], StateEnum.FRIENDLY_CAPTCHA); + getStateCaptchaEndpoint = () => { + return getState([StateEnum.CAPTCHA_ENDPOINT], StateEnum.CAPTCHA); }; //////////////////////////////////////////////////////////////// diff --git a/src/Blocks/manifest.json b/src/Blocks/manifest.json index 83f045f79..4ef4febae 100644 --- a/src/Blocks/manifest.json +++ b/src/Blocks/manifest.json @@ -594,7 +594,6 @@ "hubspotPageUrl": "es-form-hubspot-page-url", "mailchimpTags": "es-form-mailchimp-tags", "captcha": "es-form-captcha", - "friendlyCaptcha": "es-form-friendly-captcha", "direct": "es-form-direct", "itemId": "es-form-item-id", "innerId": "es-form-inner-id", @@ -772,7 +771,6 @@ "general": "", "validation": "", "captcha": "", - "friendlyCaptcha": "", "geolocation": "", "enrichment": "", "blocks": "", diff --git a/src/Captcha/FriendlyCaptcha.php b/src/Captcha/FriendlyCaptcha.php index e0ebbb235..ed7ec5153 100644 --- a/src/Captcha/FriendlyCaptcha.php +++ b/src/Captcha/FriendlyCaptcha.php @@ -11,12 +11,11 @@ namespace EightshiftForms\Captcha; use EightshiftForms\Exception\BadRequestException; -use EightshiftForms\Hooks\Variables; use EightshiftForms\Helpers\SettingsHelpers; +use EightshiftForms\Hooks\Variables; use EightshiftForms\Labels\LabelsInterface; use EightshiftForms\Rest\Routes\AbstractBaseRoute; use EightshiftForms\Troubleshooting\SettingsFallback; -use WP_Error; /** * FriendlyCaptcha class. @@ -24,16 +23,22 @@ class FriendlyCaptcha implements CaptchaInterface { /** - * Labels service. + * Friendly Captcha API endpoint URLs. + */ + public const FRIENDLY_CAPTCHA_ENDPOINT_GLOBAL_URL = 'https://global.frcapi.com/api/v2/captcha/siteverify'; + public const FRIENDLY_CAPTCHA_ENDPOINT_EU_URL = 'https://eu.frcapi.com/api/v2/captcha/siteverify'; + + /** + * Instance variable of LabelsInterface data. * * @var LabelsInterface */ - private $labels; + protected $labels; /** - * Constructor. + * Create a new instance that injects classes * - * @param LabelsInterface $labels Labels service. + * @param LabelsInterface $labels Inject labels methods. */ public function __construct(LabelsInterface $labels) { @@ -47,16 +52,23 @@ public function __construct(LabelsInterface $labels) * accepted only for interface compatibility with Google reCAPTCHA. * * @param string $token Token from frontend. - * @param string $action Action to check (unused). - * @param boolean $isEnterprise Type of captcha (unused). + * @param string $action Action to check. + * @param boolean $isEnterprise Type of captcha. * @param array $formDetails Form details. * + * @throws BadRequestException If captcha is not valid. + * * @return array */ public function check(string $token, string $action, bool $isEnterprise, array $formDetails = []): array { if (!\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - return $this->buildSuccess(SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_FEATURE_DISABLED, []); + return [ + AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), + AbstractBaseRoute::R_DEBUG => [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_FEATURE_DISABLED, + ], + ]; } $debug = [ @@ -65,52 +77,22 @@ public function check(string $token, string $action, bool $isEnterprise, array $ ]; if (!$token) { - $this->throwError( + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( $this->labels->getLabel('friendlyCaptchaBadRequest'), - SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_MISSING_TOKEN, - $debug + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_MISSING_TOKEN, + AbstractBaseRoute::R_DEBUG => $debug, + ] ); + // phpcs:enable } - $response = $this->remoteCall($token); - - if (\is_wp_error($response)) { - $this->throwError( - $this->labels->getLabel('submitWpError'), - SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_WP_ERROR, - $debug - ); - } - - $body = \json_decode(\wp_remote_retrieve_body($response), true) ?? []; - - $debug['responseBody'] = $body; - - if (empty($body['success'])) { - $this->throwError( - $this->labels->getLabel('friendlyCaptchaError'), - SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR, - $debug - ); - } - - return $this->buildSuccess(SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_SUCCESS, $debug); - } - - /** - * Make the remote verification call to Friendly Captcha. - * - * @param string $token Verification token from the frontend widget. - * - * @return array|WP_Error - */ - private function remoteCall(string $token): array|WP_Error - { $siteKey = SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaSiteKey(), SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY); $apiKey = SettingsHelpers::getOptionWithConstant(Variables::getFriendlyCaptchaApiKey(), SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_API_KEY); - return \wp_remote_post( - SettingsFriendlyCaptcha::getEndpointUrl(), + $response = \wp_remote_post( + self::getEndpointUrl(), [ 'headers' => [ 'Content-Type' => 'application/json; charset=utf-8', @@ -123,57 +105,62 @@ private function remoteCall(string $token): array|WP_Error ]), ] ); - } - /** - * Build the success response envelope. - * - * @param string $flag Fallback flag constant. - * @param array $debug Debug payload. - * @param array $data Extra R_DATA payload. - * - * @return array - */ - private function buildSuccess(string $flag, array $debug, array $data = []): array - { - $output = [ + // Generic error msg from WP. + if (\is_wp_error($response)) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('submitWpError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_WP_ERROR, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + $responseBody = \json_decode(\wp_remote_retrieve_body($response), true) ?? []; + + $debug['responseBody'] = $responseBody; + + if (empty($responseBody['success'])) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('friendlyCaptchaError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + return [ AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), AbstractBaseRoute::R_DEBUG => [ - AbstractBaseRoute::R_DEBUG_KEY => $flag, + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_SUCCESS, AbstractBaseRoute::R_DEBUG => $debug, ], ]; - - if ($data) { - $output[AbstractBaseRoute::R_DATA] = $data; - } - - return $output; } /** - * Throw a BadRequestException with the project's debug envelope. + * Get the selected endpoint value. * - * @param string $message Localized error message. - * @param string $flag Fallback flag constant. - * @param array $debug Debug payload. - * @param array $extraData Optional extra data. - * - * @throws BadRequestException Always. + * @return string + */ + public static function getEndpoint(): string + { + return SettingsHelpers::isOptionCheckboxChecked(SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY) ? 'eu' : 'global'; + } + + /** + * Get the siteverify URL for the selected endpoint. * - * @return void + * @return string */ - private function throwError(string $message, string $flag, array $debug, array $extraData = []): void + public static function getEndpointUrl(): string { - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $message, - [ - AbstractBaseRoute::R_DEBUG_KEY => $flag, - AbstractBaseRoute::R_DEBUG => $debug, - ], - $extraData - ); - // phpcs:enable + return self::getEndpoint() === 'eu' ? self::FRIENDLY_CAPTCHA_ENDPOINT_EU_URL : self::FRIENDLY_CAPTCHA_ENDPOINT_GLOBAL_URL; } } diff --git a/src/Captcha/SettingsCaptcha.php b/src/Captcha/SettingsCaptcha.php index a0f283637..790b8765f 100644 --- a/src/Captcha/SettingsCaptcha.php +++ b/src/Captcha/SettingsCaptcha.php @@ -32,6 +32,11 @@ class SettingsCaptcha implements SettingGlobalInterface, ServiceInterface */ public const FILTER_SETTINGS_GLOBAL_IS_VALID_NAME = 'es_forms_settings_global_is_valid_captcha'; + /** + * Settings key. + */ + public const SETTINGS_TYPE_KEY = 'captcha'; + /** * Master use key (shared across all providers). */ diff --git a/src/Captcha/SettingsFriendlyCaptcha.php b/src/Captcha/SettingsFriendlyCaptcha.php index b04fd9656..6a0266133 100644 --- a/src/Captcha/SettingsFriendlyCaptcha.php +++ b/src/Captcha/SettingsFriendlyCaptcha.php @@ -37,11 +37,6 @@ class SettingsFriendlyCaptcha implements SettingGlobalInterface, ServiceInterfac */ public const SETTINGS_TYPE_KEY = 'friendly-captcha'; - /** - * Friendly Captcha Use key. - */ - public const SETTINGS_FRIENDLY_CAPTCHA_USE_KEY = 'friendly-captcha-use'; - /** * Friendly Captcha site key. */ @@ -57,12 +52,6 @@ class SettingsFriendlyCaptcha implements SettingGlobalInterface, ServiceInterfac */ public const SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY = 'friendly-captcha-use-eu-endpoint'; - /** - * Friendly Captcha API endpoint URLs. - */ - public const FRIENDLY_CAPTCHA_ENDPOINT_GLOBAL_URL = 'https://global.frcapi.com/api/v2/captcha/siteverify'; - public const FRIENDLY_CAPTCHA_ENDPOINT_EU_URL = 'https://eu.frcapi.com/api/v2/captcha/siteverify'; - /** * Instance variable for labels data. * @@ -207,24 +196,4 @@ public static function getProviderTabs(): array ], ]; } - - /** - * Get the selected endpoint value. - * - * @return string - */ - public static function getEndpoint(): string - { - return SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY) ? 'eu' : 'global'; - } - - /** - * Get the siteverify URL for the selected endpoint. - * - * @return string - */ - public static function getEndpointUrl(): string - { - return self::getEndpoint() === 'eu' ? self::FRIENDLY_CAPTCHA_ENDPOINT_EU_URL : self::FRIENDLY_CAPTCHA_ENDPOINT_GLOBAL_URL; - } } diff --git a/src/Captcha/SettingsRecaptcha.php b/src/Captcha/SettingsRecaptcha.php index 216a8ffe4..92b1a3cb4 100644 --- a/src/Captcha/SettingsRecaptcha.php +++ b/src/Captcha/SettingsRecaptcha.php @@ -35,7 +35,7 @@ class SettingsRecaptcha implements SettingGlobalInterface, ServiceInterface /** * Settings key. */ - public const SETTINGS_TYPE_KEY = 'captcha'; + public const SETTINGS_TYPE_KEY = 'recaptcha'; /** * Google reCaptcha site key. diff --git a/src/Enqueue/Blocks/EnqueueBlocks.php b/src/Enqueue/Blocks/EnqueueBlocks.php index c0403ceb5..342caa933 100644 --- a/src/Enqueue/Blocks/EnqueueBlocks.php +++ b/src/Enqueue/Blocks/EnqueueBlocks.php @@ -15,6 +15,7 @@ use EightshiftForms\Enrichment\EnrichmentInterface; use EightshiftForms\Enrichment\SettingsEnrichment; use EightshiftForms\Settings\SettingsSettings; +use EightshiftForms\Captcha\FriendlyCaptcha; use EightshiftForms\Captcha\SettingsCaptcha; use EightshiftForms\Captcha\SettingsFriendlyCaptcha; use EightshiftForms\Captcha\SettingsRecaptcha; @@ -332,35 +333,35 @@ public function enqueueBlockFrontendScript(): void 'location' => $this->geolocation->getUsersGeolocation(), ]; - // Check if Captcha data is set and valid. - if (\apply_filters(SettingsRecaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - $output['captcha'] = [ - 'isUsed' => true, - 'isEnterprise' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY), - 'siteKey' => SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), SettingsRecaptcha::SETTINGS_CAPTCHA_SITE_KEY), - 'submitAction' => SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found - 'initAction' => SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_INIT_ACTION_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found - 'loadOnInit' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), - 'hideBadge' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), - ]; - } else { - $output['captcha'] = [ - 'isUsed' => false, - ]; - } + // Build the single captcha payload. `type` discriminates the provider so the JS + // layer can render the right widget without probing multiple top-level keys. + if (\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + $provider = SettingsCaptcha::getActiveProvider(); - // Check if Friendly Captcha data is set and valid. - if (\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - $output['friendlyCaptcha'] = [ - 'isUsed' => true, - 'siteKey' => SettingsHelpers::getOptionWithConstant( - Variables::getFriendlyCaptchaSiteKey(), - SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY - ), - 'endpoint' => SettingsFriendlyCaptcha::getEndpoint(), - ]; + if ($provider === SettingsCaptcha::PROVIDER_FRIENDLY) { + $output['captcha'] = [ + 'isUsed' => true, + 'type' => SettingsCaptcha::PROVIDER_FRIENDLY, + 'siteKey' => SettingsHelpers::getOptionWithConstant( + Variables::getFriendlyCaptchaSiteKey(), + SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY + ), + 'endpoint' => FriendlyCaptcha::getEndpoint(), + ]; + } else { + $output['captcha'] = [ + 'isUsed' => true, + 'type' => SettingsCaptcha::PROVIDER_GOOGLE, + 'isEnterprise' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY), + 'siteKey' => SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), SettingsRecaptcha::SETTINGS_CAPTCHA_SITE_KEY), + 'submitAction' => SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found + 'initAction' => SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_INIT_ACTION_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found + 'loadOnInit' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), + 'hideBadge' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), + ]; + } } else { - $output['friendlyCaptcha'] = [ + $output['captcha'] = [ 'isUsed' => false, ]; } diff --git a/src/Hooks/Filters.php b/src/Hooks/Filters.php index 487b818aa..68237cd6b 100644 --- a/src/Hooks/Filters.php +++ b/src/Hooks/Filters.php @@ -432,7 +432,6 @@ private static function getSettingsNonTranslatableNames(): array SettingsRecaptcha::SETTINGS_CAPTCHA_PROJECT_ID_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_API_KEY, - SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_USE_KEY, SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY, SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_API_KEY, SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, diff --git a/src/Hooks/FiltersSettingsBuilder.php b/src/Hooks/FiltersSettingsBuilder.php index e18f18bfb..0bf3484bc 100644 --- a/src/Hooks/FiltersSettingsBuilder.php +++ b/src/Hooks/FiltersSettingsBuilder.php @@ -150,7 +150,7 @@ public function getSettingsFiltersData(): array 'title' => \__('Advanced', 'eightshift-forms'), ], ], - 'captcha' => [ + SettingsCaptcha::SETTINGS_TYPE_KEY => [ 'settingsGlobal' => SettingsCaptcha::FILTER_SETTINGS_GLOBAL_NAME, 'type' => Config::SETTINGS_INTERNAL_TYPE_ADVANCED, 'use' => SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, diff --git a/src/Rest/Routes/AbstractIntegrationFormSubmit.php b/src/Rest/Routes/AbstractIntegrationFormSubmit.php index 0969e3314..fa5893686 100644 --- a/src/Rest/Routes/AbstractIntegrationFormSubmit.php +++ b/src/Rest/Routes/AbstractIntegrationFormSubmit.php @@ -1278,10 +1278,6 @@ protected function prepareApiParams(WP_REST_Request $request, string $type = sel $output[Config::FD_CAPTCHA] = $value['value']; $output[Config::FD_PARAMS][$key] = $value; break; - case UtilsHelper::getStateParam('friendlyCaptcha'): - $output[Config::FD_CAPTCHA] = $value['value']; - $output[Config::FD_PARAMS][$key] = $value; - break; case UtilsHelper::getStateParam('actionExternal'): $output[Config::FD_ACTION_EXTERNAL] = $value['value']; $output[Config::FD_PARAMS][$key] = $value; diff --git a/src/Rest/Routes/General/CaptchaValidateRoute.php b/src/Rest/Routes/General/CaptchaValidateRoute.php index 8a9ec3ba0..42046fd7f 100644 --- a/src/Rest/Routes/General/CaptchaValidateRoute.php +++ b/src/Rest/Routes/General/CaptchaValidateRoute.php @@ -11,7 +11,6 @@ namespace EightshiftForms\Rest\Routes\General; use EightshiftForms\Captcha\CaptchaInterface; -use EightshiftForms\Captcha\SettingsCaptcha; use EightshiftForms\Labels\LabelsInterface; use EightshiftForms\Helpers\DeveloperHelpers; use EightshiftForms\Rest\Routes\AbstractBaseRoute; @@ -57,23 +56,6 @@ public function __construct( $this->captcha = $captcha; } - /** - * Register the route only when Google reCAPTCHA is the active provider. - * - * Friendly Captcha solves entirely in the browser and has no pre-submission - * verification endpoint — exposing this route for it would misroute tokens. - * - * @return void - */ - public function register(): void - { - if (SettingsCaptcha::getActiveProvider() !== SettingsCaptcha::PROVIDER_GOOGLE) { - return; - } - - parent::register(); - } - /** * Get the base url of the route * From 40af7ba739566edc5e82c23e90daa720b1d197c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Wed, 22 Apr 2026 10:35:37 +0200 Subject: [PATCH 12/20] Rework captcha settings page to follow the Corvus flat-tab pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nest-free tab layout per @iruzevic's feedback: a single tab bar at the top with the provider select as the Corvus-style "driver field" inside the first tab, and downstream tabs conditionally present via the spread idiom `...($cond ? [tab] : [])`. - SettingsCaptcha (dispatcher): one `tabs` component. Tab "Settings" hosts the provider select + provider general content. Tab "Advanced" is present only when the active provider is Google. Tab "Help" always present, content per-provider. - SettingsRecaptcha: split getProviderTabs() into getGeneralContent(), getAdvancedContent(), getHelpContent() — each returns a field array only. The dispatcher composes them into tabs. - SettingsFriendlyCaptcha: split into getGeneralContent() and getHelpContent(). - Drop SettingGlobalInterface and the BC getSettingsGlobalData() on both provider settings classes — the dispatcher owns the global page now. Only the validity filter stays registered on each provider. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Captcha/SettingsCaptcha.php | 93 ++++--- src/Captcha/SettingsFriendlyCaptcha.php | 127 ++++----- src/Captcha/SettingsRecaptcha.php | 352 +++++++++++------------- 3 files changed, 278 insertions(+), 294 deletions(-) diff --git a/src/Captcha/SettingsCaptcha.php b/src/Captcha/SettingsCaptcha.php index 790b8765f..ce3534ef4 100644 --- a/src/Captcha/SettingsCaptcha.php +++ b/src/Captcha/SettingsCaptcha.php @@ -96,6 +96,10 @@ public function isSettingsGlobalValid(): bool /** * Build the merged settings page. * + * Lays the page out Corvus-style: a flat tab bar with the provider select + * as the driver field at the top of the first tab, and downstream tabs + * conditionally present based on the active provider. + * * @return array> */ public function getSettingsGlobalData(): array @@ -106,47 +110,70 @@ public function getSettingsGlobalData(): array $provider = self::getActiveProvider(); - $output = [ + $providerSelect = [ + 'component' => 'select', + 'selectName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_PROVIDER_KEY), + 'selectFieldLabel' => \__('Provider', 'eightshift-forms'), + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'selectFieldHelp' => \__('Pick which captcha service validates submissions. Switching the provider reloads the fields below.', 'eightshift-forms'), + 'selectSingleSubmit' => true, + 'selectContent' => [ + [ + 'component' => 'select-option', + 'selectOptionLabel' => \__('Google reCAPTCHA', 'eightshift-forms'), + 'selectOptionValue' => self::PROVIDER_GOOGLE, + 'selectOptionIsSelected' => $provider === self::PROVIDER_GOOGLE, + ], + [ + 'component' => 'select-option', + 'selectOptionLabel' => \__('Friendly Captcha', 'eightshift-forms'), + 'selectOptionValue' => self::PROVIDER_FRIENDLY, + 'selectOptionIsSelected' => $provider === self::PROVIDER_FRIENDLY, + ], + ], + ]; + + $divider = [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ]; + + $providerGeneralContent = $provider === self::PROVIDER_FRIENDLY + ? SettingsFriendlyCaptcha::getGeneralContent() + : SettingsRecaptcha::getGeneralContent(); + + $providerHelpContent = $provider === self::PROVIDER_FRIENDLY + ? SettingsFriendlyCaptcha::getHelpContent() + : SettingsRecaptcha::getHelpContent(); + + return [ SettingsOutputHelpers::getIntro('captcha'), [ - 'component' => 'select', - 'selectName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_PROVIDER_KEY), - 'selectFieldLabel' => \__('Provider', 'eightshift-forms'), - // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings - 'selectFieldHelp' => \__('Pick which captcha service validates submissions. Switching the provider reloads the fields below.', 'eightshift-forms'), - 'selectSingleSubmit' => true, - 'selectContent' => [ + 'component' => 'tabs', + 'tabsContent' => [ [ - 'component' => 'select-option', - 'selectOptionLabel' => \__('Google reCAPTCHA', 'eightshift-forms'), - 'selectOptionValue' => self::PROVIDER_GOOGLE, - 'selectOptionIsSelected' => $provider === self::PROVIDER_GOOGLE, + 'component' => 'tab', + 'tabLabel' => \__('Settings', 'eightshift-forms'), + 'tabContent' => [ + $providerSelect, + $divider, + ...$providerGeneralContent, + ], ], + ...($provider === self::PROVIDER_GOOGLE ? [ + [ + 'component' => 'tab', + 'tabLabel' => \__('Advanced', 'eightshift-forms'), + 'tabContent' => SettingsRecaptcha::getAdvancedContent(), + ], + ] : []), [ - 'component' => 'select-option', - 'selectOptionLabel' => \__('Friendly Captcha', 'eightshift-forms'), - 'selectOptionValue' => self::PROVIDER_FRIENDLY, - 'selectOptionIsSelected' => $provider === self::PROVIDER_FRIENDLY, + 'component' => 'tab', + 'tabLabel' => \__('Help', 'eightshift-forms'), + 'tabContent' => $providerHelpContent, ], ], ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], ]; - - $providerTabs = $provider === self::PROVIDER_FRIENDLY - ? SettingsFriendlyCaptcha::getProviderTabs() - : SettingsRecaptcha::getProviderTabs(); - - if ($providerTabs) { - $output[] = [ - 'component' => 'tabs', - 'tabsContent' => $providerTabs, - ]; - } - - return $output; } } diff --git a/src/Captcha/SettingsFriendlyCaptcha.php b/src/Captcha/SettingsFriendlyCaptcha.php index 6a0266133..bb3fe4084 100644 --- a/src/Captcha/SettingsFriendlyCaptcha.php +++ b/src/Captcha/SettingsFriendlyCaptcha.php @@ -14,13 +14,12 @@ use EightshiftForms\Hooks\Variables; use EightshiftForms\Helpers\SettingsHelpers; use EightshiftForms\Labels\LabelsInterface; -use EightshiftForms\Settings\SettingGlobalInterface; use EightshiftFormsVendor\EightshiftLibs\Services\ServiceInterface; /** * SettingsFriendlyCaptcha class. */ -class SettingsFriendlyCaptcha implements SettingGlobalInterface, ServiceInterface +class SettingsFriendlyCaptcha implements ServiceInterface { /** * Filter global settings key. @@ -76,7 +75,6 @@ public function __construct(LabelsInterface $labels) */ public function register(): void { - \add_filter(self::FILTER_SETTINGS_GLOBAL_NAME, [$this, 'getSettingsGlobalData']); \add_filter(self::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, [$this, 'isSettingsGlobalValid']); } @@ -102,96 +100,71 @@ public function isSettingsGlobalValid(): bool } /** - * Get global settings array for building settings page. + * Field list for the "Settings" tab — API keys and EU endpoint toggle. * - * Retained for BC in case the `FILTER_SETTINGS_GLOBAL_NAME` filter is still - * consulted; the menu entry is now handled by `SettingsCaptcha`. - * - * @return array> + * @return array> */ - public function getSettingsGlobalData(): array + public static function getGeneralContent(): array { - if (!SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY)) { - return SettingsOutputHelpers::getNoActiveFeature(); - } - return [ - SettingsOutputHelpers::getIntro(self::SETTINGS_TYPE_KEY), [ - 'component' => 'tabs', - 'tabsContent' => self::getProviderTabs(), + 'component' => 'intro', + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'introSubtitle' => \__('Protect your forms from spam and abuse using Friendly Captcha.
A privacy-focused, GDPR-compliant alternative to Google reCAPTCHA.', 'eightshift-forms'), + ], + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getFriendlyCaptchaSiteKey(), + self::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY, + 'ES_FRIENDLY_CAPTCHA_SITE_KEY', + \__('Site key', 'eightshift-forms'), + ), + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getFriendlyCaptchaApiKey(), + self::SETTINGS_FRIENDLY_CAPTCHA_API_KEY, + 'ES_FRIENDLY_CAPTCHA_API_KEY', + \__('API key', 'eightshift-forms'), + ), + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'checkboxes', + 'checkboxesFieldLabel' => '', + 'checkboxesName' => SettingsHelpers::getSettingName(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY), + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'checkboxesFieldHelp' => \__('The EU endpoint is hosted in Germany and ensures visitor data never leaves the EU.
Requires a Friendly Captcha Advanced or Enterprise plan.', 'eightshift-forms'), + 'checkboxesContent' => [ + [ + 'component' => 'checkbox', + 'checkboxLabel' => \__('Use EU endpoint', 'eightshift-forms'), + 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY), + 'checkboxValue' => self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, + 'checkboxAsToggle' => true, + ], + ], ], ]; } /** - * Tab definitions for the Friendly Captcha provider, composed into the - * merged captcha page by `SettingsCaptcha`. + * Field list for the "Help" tab — setup steps. * * @return array> */ - public static function getProviderTabs(): array + public static function getHelpContent(): array { return [ [ - 'component' => 'tab', - 'tabLabel' => \__('General', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'intro', - // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings - 'introSubtitle' => \__('Protect your forms from spam and abuse using Friendly Captcha.
A privacy-focused, GDPR-compliant alternative to Google reCAPTCHA.', 'eightshift-forms'), - ], - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getFriendlyCaptchaSiteKey(), - self::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY, - 'ES_FRIENDLY_CAPTCHA_SITE_KEY', - \__('Site key', 'eightshift-forms'), - ), - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getFriendlyCaptchaApiKey(), - self::SETTINGS_FRIENDLY_CAPTCHA_API_KEY, - 'ES_FRIENDLY_CAPTCHA_API_KEY', - \__('API key', 'eightshift-forms'), - ), - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - [ - 'component' => 'checkboxes', - 'checkboxesFieldLabel' => '', - 'checkboxesName' => SettingsHelpers::getSettingName(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY), - // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings - 'checkboxesFieldHelp' => \__('The EU endpoint is hosted in Germany and ensures visitor data never leaves the EU.
Requires a Friendly Captcha Advanced or Enterprise plan.', 'eightshift-forms'), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Use EU endpoint', 'eightshift-forms'), - 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY), - 'checkboxValue' => self::SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY, - 'checkboxAsToggle' => true, - ], - ], - ], - ], - ], - [ - 'component' => 'tab', - 'tabLabel' => \__('Help', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'steps', - 'stepsTitle' => \__('How to get the Friendly Captcha API keys?', 'eightshift-forms'), - 'stepsContent' => [ - // translators: %s will be replaced with the external link. - \sprintf(\__('Visit the Friendly Captcha dashboard.', 'eightshift-forms'), 'https://app.friendlycaptcha.eu/dashboard'), - \__('Create a new application and copy the Site key.', 'eightshift-forms'), - \__('Go to API Keys and create a new API key.', 'eightshift-forms'), - \__('Copy both keys into the fields under the General tab or use the global constants.', 'eightshift-forms'), - \__('In the Friendly Captcha dashboard, set the widget mode to Non-interactive for an invisible captcha experience.', 'eightshift-forms'), - ], - ], + 'component' => 'steps', + 'stepsTitle' => \__('How to get the Friendly Captcha API keys?', 'eightshift-forms'), + 'stepsContent' => [ + // translators: %s will be replaced with the external link. + \sprintf(\__('Visit the Friendly Captcha dashboard.', 'eightshift-forms'), 'https://app.friendlycaptcha.eu/dashboard'), + \__('Create a new application and copy the Site key.', 'eightshift-forms'), + \__('Go to API Keys and create a new API key.', 'eightshift-forms'), + \__('Copy both keys into the fields under the General tab or use the global constants.', 'eightshift-forms'), + \__('In the Friendly Captcha dashboard, set the widget mode to Non-interactive for an invisible captcha experience.', 'eightshift-forms'), ], ], ]; diff --git a/src/Captcha/SettingsRecaptcha.php b/src/Captcha/SettingsRecaptcha.php index 92b1a3cb4..a8ecb38a8 100644 --- a/src/Captcha/SettingsRecaptcha.php +++ b/src/Captcha/SettingsRecaptcha.php @@ -14,13 +14,12 @@ use EightshiftForms\Hooks\Variables; use EightshiftForms\Helpers\SettingsHelpers; use EightshiftForms\Labels\LabelsInterface; -use EightshiftForms\Settings\SettingGlobalInterface; use EightshiftFormsVendor\EightshiftLibs\Services\ServiceInterface; /** * SettingsRecaptcha class. */ -class SettingsRecaptcha implements SettingGlobalInterface, ServiceInterface +class SettingsRecaptcha implements ServiceInterface { /** * Filter global settings key. @@ -117,7 +116,6 @@ public function __construct(LabelsInterface $labels) */ public function register(): void { - \add_filter(self::FILTER_SETTINGS_GLOBAL_NAME, [$this, 'getSettingsGlobalData']); \add_filter(self::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, [$this, 'isSettingsGlobalValid']); } @@ -154,217 +152,203 @@ public function isSettingsGlobalValid(): bool } /** - * Get global settings array for building settings page. + * Field list for the "Settings" tab — API keys + enterprise toggle. * - * Retained for BC in case the `FILTER_SETTINGS_GLOBAL_NAME` filter is still - * consulted; the menu entry is now handled by `SettingsCaptcha`. + * The dispatcher prepends the provider select above these; the fields + * below it swap based on `SETTINGS_CAPTCHA_ENTERPRISE_KEY` in the same + * Corvus-style driver-field pattern. * - * @return array> + * @return array> */ - public function getSettingsGlobalData(): array + public static function getGeneralContent(): array { - if (!SettingsHelpers::isOptionCheckboxChecked(SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY, SettingsCaptcha::SETTINGS_CAPTCHA_USE_KEY)) { - return SettingsOutputHelpers::getNoActiveFeature(); - } + $isEnterprise = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, self::SETTINGS_CAPTCHA_ENTERPRISE_KEY); return [ - SettingsOutputHelpers::getIntro(self::SETTINGS_TYPE_KEY), [ - 'component' => 'tabs', - 'tabsContent' => self::getProviderTabs(), + 'component' => 'intro', + // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings + 'introSubtitle' => \__('Protect your website from spam and abuse using Google\'s reCAPTCHA.
A captcha is a simple task that is easy for humans to do, but difficult for bots.', 'eightshift-forms'), + ], + [ + 'component' => 'checkboxes', + 'checkboxesFieldHideLabel' => true, + 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY), + 'checkboxesContent' => [ + [ + 'component' => 'checkbox', + 'checkboxLabel' => \__('Use reCAPTCHA Enterprise', 'eightshift-forms'), + 'checkboxIsChecked' => $isEnterprise, + 'checkboxValue' => self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, + 'checkboxSingleSubmit' => true, + 'checkboxAsToggle' => true, + ], + ], ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getGoogleReCaptchaSiteKey(), + self::SETTINGS_CAPTCHA_SITE_KEY, + 'ES_GOOGLE_RECAPTCHA_SITE_KEY', + \__('Site key', 'eightshift-forms'), + ), + ...(!$isEnterprise ? [ + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getGoogleReCaptchaSecretKey(), + self::SETTINGS_CAPTCHA_SECRET_KEY, + 'ES_GOOGLE_RECAPTCHA_SECRET_KEY', + \__('Secret key', 'eightshift-forms'), + ), + ] : [ + SettingsOutputHelpers::getInputFieldWithGlobalVariable( + Variables::getGoogleReCaptchaProjectIdKey(), + self::SETTINGS_CAPTCHA_PROJECT_ID_KEY, + 'ES_GOOGLE_RECAPTCHA_PROJECT_ID_KEY', + \__('Project ID', 'eightshift-forms'), + ), + SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( + Variables::getGoogleReCaptchaApiKey(), + self::SETTINGS_CAPTCHA_API_KEY, + 'ES_GOOGLE_RECAPTCHA_API_KEY', + \__('API key', 'eightshift-forms'), + ), + ]), ]; } /** - * Tab definitions for the Google reCAPTCHA provider, composed into the - * merged captcha page by `SettingsCaptcha`. + * Field list for the "Advanced" tab — score, actions, badge, init toggle. * * @return array> */ - public static function getProviderTabs(): array + public static function getAdvancedContent(): array { - $isEnterprise = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, self::SETTINGS_CAPTCHA_ENTERPRISE_KEY); $isInit = SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY); return [ [ - 'component' => 'tab', - 'tabLabel' => \__('General', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'intro', - // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings - 'introSubtitle' => \__('Protect your website from spam and abuse using Google\'s reCAPTCHA.
A captcha is a simple task that is easy for humans to do, but difficult for bots.', 'eightshift-forms'), - ], - [ - 'component' => 'checkboxes', - 'checkboxesFieldHideLabel' => true, - 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_ENTERPRISE_KEY), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Use reCAPTCHA Enterprise', 'eightshift-forms'), - 'checkboxIsChecked' => $isEnterprise, - 'checkboxValue' => self::SETTINGS_CAPTCHA_ENTERPRISE_KEY, - 'checkboxSingleSubmit' => true, - 'checkboxAsToggle' => true, - ], - ], - ], + 'component' => 'checkboxes', + 'checkboxesFieldHideLabel' => true, + 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), + 'checkboxesContent' => [ [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, + 'component' => 'checkbox', + 'checkboxLabel' => \__('Hide badge', 'eightshift-forms'), + 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), + 'checkboxValue' => self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, + 'checkboxSingleSubmit' => true, + 'checkboxAsToggle' => true, + 'checkboxHelp' => \__('Not recommended, as it is against Google\'s terms of use.', 'eightshift-forms'), ], - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getGoogleReCaptchaSiteKey(), - self::SETTINGS_CAPTCHA_SITE_KEY, - 'ES_GOOGLE_RECAPTCHA_SITE_KEY', - \__('Site key', 'eightshift-forms'), - ), - - ...(!$isEnterprise ? [ - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getGoogleReCaptchaSecretKey(), - self::SETTINGS_CAPTCHA_SECRET_KEY, - 'ES_GOOGLE_RECAPTCHA_SECRET_KEY', - \__('Secret key', 'eightshift-forms'), - ), - ] : [ - SettingsOutputHelpers::getInputFieldWithGlobalVariable( - Variables::getGoogleReCaptchaProjectIdKey(), - self::SETTINGS_CAPTCHA_PROJECT_ID_KEY, - 'ES_GOOGLE_RECAPTCHA_PROJECT_ID_KEY', - \__('Project ID', 'eightshift-forms'), - ), - SettingsOutputHelpers::getPasswordFieldWithGlobalVariable( - Variables::getGoogleReCaptchaApiKey(), - self::SETTINGS_CAPTCHA_API_KEY, - 'ES_GOOGLE_RECAPTCHA_API_KEY', - \__('API key', 'eightshift-forms'), - ), - ]), ], ], [ - 'component' => 'tab', - 'tabLabel' => \__('Advanced', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'checkboxes', - 'checkboxesFieldHideLabel' => true, - 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Hide badge', 'eightshift-forms'), - 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), - 'checkboxValue' => self::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, - 'checkboxSingleSubmit' => true, - 'checkboxAsToggle' => true, - 'checkboxHelp' => \__('Not recommended, as it is against Google\'s terms of use.', 'eightshift-forms'), - ], - ], - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - [ - 'component' => 'input', - 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SCORE_KEY), - 'inputFieldLabel' => \__('"Spam unlikely" threshold', 'eightshift-forms'), - 'inputFieldHelp' => \__('The level above which a submission is not considered spam. Should be between 0.1 and 1.0.
In most cases, a user will receive as core between 0.8 and 0.9.', 'eightshift-forms'), - 'inputType' => 'number', - 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SCORE_KEY), - 'inputMin' => 0.1, - 'inputMax' => 1, - 'inputStep' => 0.1, - 'inputIsNumber' => true, - 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SCORE_DEFAULT_KEY, - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - [ - 'component' => 'input', - 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), - 'inputFieldLabel' => \__('"On submit" action name', 'eightshift-forms'), - 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA on form submission.', 'eightshift-forms'), - 'inputType' => 'text', - 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), - 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'input', + 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SCORE_KEY), + 'inputFieldLabel' => \__('"Spam unlikely" threshold', 'eightshift-forms'), + 'inputFieldHelp' => \__('The level above which a submission is not considered spam. Should be between 0.1 and 1.0.
In most cases, a user will receive as core between 0.8 and 0.9.', 'eightshift-forms'), + 'inputType' => 'number', + 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SCORE_KEY), + 'inputMin' => 0.1, + 'inputMax' => 1, + 'inputStep' => 0.1, + 'inputIsNumber' => true, + 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SCORE_DEFAULT_KEY, + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'input', + 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), + 'inputFieldLabel' => \__('"On submit" action name', 'eightshift-forms'), + 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA on form submission.', 'eightshift-forms'), + 'inputType' => 'text', + 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY), + 'inputPlaceholder' => self::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'checkboxes', + 'checkboxesFieldHideLabel' => true, + 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), + 'checkboxesContent' => [ [ - 'component' => 'checkboxes', - 'checkboxesFieldHideLabel' => true, - 'checkboxesName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), - 'checkboxesContent' => [ - [ - 'component' => 'checkbox', - 'checkboxLabel' => \__('Load Captcha on website load', 'eightshift-forms'), - 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), - 'checkboxValue' => self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, - 'checkboxHelp' => \__('By default, Captcha is only loaded on pages that contain forms. However, with this option, you can load Captcha on every page.', 'eightshift-forms'), - 'checkboxSingleSubmit' => true, - 'checkboxAsToggle' => true, - 'checkboxAsToggleSize' => 'medium', - ], - ], + 'component' => 'checkbox', + 'checkboxLabel' => \__('Load Captcha on website load', 'eightshift-forms'), + 'checkboxIsChecked' => $isInit, + 'checkboxValue' => self::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, + 'checkboxHelp' => \__('By default, Captcha is only loaded on pages that contain forms. However, with this option, you can load Captcha on every page.', 'eightshift-forms'), + 'checkboxSingleSubmit' => true, + 'checkboxAsToggle' => true, + 'checkboxAsToggleSize' => 'medium', ], - $isInit ? [ - 'component' => 'input', - 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), - 'inputFieldLabel' => \__('Action name', 'eightshift-forms'), - 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA when Captcha is loaded on every page.', 'eightshift-forms'), - 'inputType' => 'text', - 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), - 'inputPlaceholder' => self::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, - ] : [], ], ], + ...($isInit ? [ + [ + 'component' => 'input', + 'inputName' => SettingsHelpers::getOptionName(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), + 'inputFieldLabel' => \__('Action name', 'eightshift-forms'), + 'inputFieldHelp' => \__('Name of the action sent to reCAPTCHA when Captcha is loaded on every page.', 'eightshift-forms'), + 'inputType' => 'text', + 'inputValue' => SettingsHelpers::getOptionValue(self::SETTINGS_CAPTCHA_INIT_ACTION_KEY), + 'inputPlaceholder' => self::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, + ], + ] : []), + ]; + } + + /** + * Field list for the "Help" tab — setup steps for free and enterprise keys. + * + * @return array> + */ + public static function getHelpContent(): array + { + return [ [ - 'component' => 'tab', - 'tabLabel' => \__('Help', 'eightshift-forms'), - 'tabContent' => [ - [ - 'component' => 'steps', - 'stepsTitle' => \__('How to get the Free reCAPTCHA API key?', 'eightshift-forms'), - 'stepsContent' => [ - // translators: %s will be replaced with the external link. - \sprintf(\__('Visit this link.', 'eightshift-forms'), 'https://www.google.com/recaptcha/admin/create'), - \__('Configure all the options. Make sure to select reCaptcha version 3!', 'eightshift-forms'), - \__('Copy the API key into the field under the API tab or use the global constant.', 'eightshift-forms'), - ], - ], - [ - 'component' => 'divider', - 'dividerExtraVSpacing' => true, - ], - [ - 'component' => 'steps', - 'stepsTitle' => \__('How to get the Enterprise reCAPTCHA API key?', 'eightshift-forms'), - 'stepsContent' => [ - // translators: %s will be replaced with the external link. - \sprintf(\__('Visit Google Cloud Console.', 'eightshift-forms'), 'https://console.cloud.google.com/'), - \__('Create a new project and set that project as Project ID.', 'eightshift-forms'), - \__('Search and go to reCAPTCHA product.', 'eightshift-forms'), - \__('You will probably need to set billing service for this product.', 'eightshift-forms'), - \__('Create a new key and set that key as Site key.', 'eightshift-forms'), - // translators: %s will be replaced with the website domain. - \sprintf(\__('Limit the key to your website domain. Domain: %s (exact, no trailing slash and protocol).', 'eightshift-forms'), \preg_replace("(^https?://)", "", \site_url())), - \__('Search and go to API & Services product.', 'eightshift-forms'), - \__('Go to Credentials section and create a new API key.', 'eightshift-forms'), - // translators: %s will be replaced with the website domain. - \sprintf(\__('Create a new key for Website, add restrictions to your website domain %s (exact, no trailing slash, with protocol) and set API restrictions to reCAPTCHA Enterprise.', 'eightshift-forms'), \esc_url(\site_url())), - \__('Set that key as API key.', 'eightshift-forms'), - ], - ], + 'component' => 'steps', + 'stepsTitle' => \__('How to get the Free reCAPTCHA API key?', 'eightshift-forms'), + 'stepsContent' => [ + // translators: %s will be replaced with the external link. + \sprintf(\__('Visit this link.', 'eightshift-forms'), 'https://www.google.com/recaptcha/admin/create'), + \__('Configure all the options. Make sure to select reCaptcha version 3!', 'eightshift-forms'), + \__('Copy the API key into the field under the API tab or use the global constant.', 'eightshift-forms'), + ], + ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'steps', + 'stepsTitle' => \__('How to get the Enterprise reCAPTCHA API key?', 'eightshift-forms'), + 'stepsContent' => [ + // translators: %s will be replaced with the external link. + \sprintf(\__('Visit Google Cloud Console.', 'eightshift-forms'), 'https://console.cloud.google.com/'), + \__('Create a new project and set that project as Project ID.', 'eightshift-forms'), + \__('Search and go to reCAPTCHA product.', 'eightshift-forms'), + \__('You will probably need to set billing service for this product.', 'eightshift-forms'), + \__('Create a new key and set that key as Site key.', 'eightshift-forms'), + // translators: %s will be replaced with the website domain. + \sprintf(\__('Limit the key to your website domain. Domain: %s (exact, no trailing slash and protocol).', 'eightshift-forms'), \preg_replace("(^https?://)", "", \site_url())), + \__('Search and go to API & Services product.', 'eightshift-forms'), + \__('Go to Credentials section and create a new API key.', 'eightshift-forms'), + // translators: %s will be replaced with the website domain. + \sprintf(\__('Create a new key for Website, add restrictions to your website domain %s (exact, no trailing slash, with protocol) and set API restrictions to reCAPTCHA Enterprise.', 'eightshift-forms'), \esc_url(\site_url())), + \__('Set that key as API key.', 'eightshift-forms'), ], ], ]; From 242c9e28630419937fd6be6ec7adc50a24aaf139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Fri, 24 Apr 2026 08:39:20 +0200 Subject: [PATCH 13/20] Misc fixes --- bun.lock | 8 +++--- src/Blocks/components/form/assets/form.js | 24 +++++++++--------- src/Blocks/components/form/assets/index.js | 25 +++++++++++-------- .../components/form/assets/state-init.js | 19 ++++++++------ .../components/select/select-admin.scss | 1 - src/Captcha/Captcha.php | 11 ++++---- src/Troubleshooting/SettingsFallback.php | 6 ----- 7 files changed, 46 insertions(+), 48 deletions(-) diff --git a/bun.lock b/bun.lock index 6c1df3cec..19a738acd 100644 --- a/bun.lock +++ b/bun.lock @@ -8,18 +8,18 @@ "@eightshift/frontend-libs": "^12.1.9", "@eightshift/ui-components": "^5.6.1", "autosize": "^6.0.1", - "baseline-browser-mapping": "^2.10.10", - "caniuse-lite": "^1.0.30001780", + "baseline-browser-mapping": "^2.10.18", + "caniuse-lite": "^1.0.30001787", "choices.js": "^11.2.1", "dropzone": "^6.0.0-beta.2", "flatpickr": "^4.6.13", "reactflow": "^11.11.4", }, "devDependencies": { - "@playwright/test": "^1.58.2", + "@playwright/test": "^1.59.1", "husky": "^9.1.7", "lint-staged": "^16.4.0", - "webpack": "^5.105.4", + "webpack": "^5.106.1", "webpack-cli": "^6.0.1", }, }, diff --git a/src/Blocks/components/form/assets/form.js b/src/Blocks/components/form/assets/form.js index 9e966503b..bcb2a33e1 100644 --- a/src/Blocks/components/form/assets/form.js +++ b/src/Blocks/components/form/assets/form.js @@ -1970,10 +1970,10 @@ export class Form { // Used only on frontend for single submit. if (!this.state.getStateConfigIsAdmin() && this.state.getStateFormConfigUseSingleSubmit(formId)) { if (this.state.getStateCaptchaIsUsed()) { - this.runFormCaptcha(formId); - } else { - this.formSubmit(formId); - } + this.runFormCaptcha(formId); + } else { + this.formSubmit(formId); + } } }; @@ -2053,10 +2053,10 @@ export class Form { (typeCustom === 'range' || typeCustom === 'number' || typeCustom === 'checkbox' || typeCustom === 'radio' || typeCustom === 'rating') ) { if (this.state.getStateCaptchaIsUsed()) { - this.runFormCaptcha(formId); - } else { - this.formSubmit(formId); - } + this.runFormCaptcha(formId); + } else { + this.formSubmit(formId); + } } }; @@ -2099,10 +2099,10 @@ export class Form { // Used only on frontend for single submit. if (!this.state.getStateConfigIsAdmin() && this.state.getStateFormConfigUseSingleSubmit(formId)) { if (this.state.getStateCaptchaIsUsed()) { - this.runFormCaptcha(formId); - } else { - this.formSubmit(formId); - } + this.runFormCaptcha(formId); + } else { + this.formSubmit(formId); + } } }; diff --git a/src/Blocks/components/form/assets/index.js b/src/Blocks/components/form/assets/index.js index 84f837233..5f90f0802 100644 --- a/src/Blocks/components/form/assets/index.js +++ b/src/Blocks/components/form/assets/index.js @@ -18,18 +18,21 @@ const state = utils.getState(); domReady(() => { if (state.getStateCaptchaIsUsed()) { - if (state.getStateCaptchaType() === StateEnum.CAPTCHA_TYPE_FRIENDLY) { - // Friendly Captcha renders its widget inside the form, so only load - // it on pages that actually contain one. - if (document.querySelectorAll(state.getStateSelector('form', true))?.length) { - import('./friendly-captcha').then(({ FriendlyCaptcha }) => { - new FriendlyCaptcha(utils).init(); + switch (state.getStateCaptchaType()) { + case StateEnum.CAPTCHA_TYPE_FRIENDLY: + // Friendly Captcha renders its widget inside the form, so only load + // it on pages that actually contain one. + if (document.querySelectorAll(state.getStateSelector('form', true))?.length) { + import('./friendly-captcha').then(({ FriendlyCaptcha }) => { + new FriendlyCaptcha(utils).init(); + }); + } + break; + default: + import('./captcha').then(({ Captcha }) => { + new Captcha(utils).init(); }); - } - } else { - import('./captcha').then(({ Captcha }) => { - new Captcha(utils).init(); - }); + break; } } diff --git a/src/Blocks/components/form/assets/state-init.js b/src/Blocks/components/form/assets/state-init.js index f5fa048f1..9ad95e768 100644 --- a/src/Blocks/components/form/assets/state-init.js +++ b/src/Blocks/components/form/assets/state-init.js @@ -268,14 +268,17 @@ export function setStateInitial() { setState([StateEnum.CAPTCHA_TYPE], captcha.type, StateEnum.CAPTCHA); setState([StateEnum.CAPTCHA_SITE_KEY], captcha.siteKey, StateEnum.CAPTCHA); - if (captcha.type === StateEnum.CAPTCHA_TYPE_FRIENDLY) { - setState([StateEnum.CAPTCHA_ENDPOINT], captcha.endpoint, StateEnum.CAPTCHA); - } else { - setState([StateEnum.CAPTCHA_IS_ENTERPRISE], Boolean(captcha.isEnterprise), StateEnum.CAPTCHA); - setState([StateEnum.CAPTCHA_SUBMIT_ACTION], captcha.submitAction, StateEnum.CAPTCHA); - setState([StateEnum.CAPTCHA_INIT_ACTION], captcha.initAction, StateEnum.CAPTCHA); - setState([StateEnum.CAPTCHA_LOAD_ON_INIT], Boolean(captcha.loadOnInit), StateEnum.CAPTCHA); - setState([StateEnum.CAPTCHA_HIDE_BADGE], Boolean(captcha.hideBadge), StateEnum.CAPTCHA); + switch (captcha.type) { + case StateEnum.CAPTCHA_TYPE_FRIENDLY: + setState([StateEnum.CAPTCHA_ENDPOINT], captcha.endpoint, StateEnum.CAPTCHA); + break; + default: + setState([StateEnum.CAPTCHA_IS_ENTERPRISE], Boolean(captcha.isEnterprise), StateEnum.CAPTCHA); + setState([StateEnum.CAPTCHA_SUBMIT_ACTION], captcha.submitAction, StateEnum.CAPTCHA); + setState([StateEnum.CAPTCHA_INIT_ACTION], captcha.initAction, StateEnum.CAPTCHA); + setState([StateEnum.CAPTCHA_LOAD_ON_INIT], Boolean(captcha.loadOnInit), StateEnum.CAPTCHA); + setState([StateEnum.CAPTCHA_HIDE_BADGE], Boolean(captcha.hideBadge), StateEnum.CAPTCHA); + break; } } diff --git a/src/Blocks/components/select/select-admin.scss b/src/Blocks/components/select/select-admin.scss index c39101ebe..69619affe 100644 --- a/src/Blocks/components/select/select-admin.scss +++ b/src/Blocks/components/select/select-admin.scss @@ -43,6 +43,5 @@ div.es-select { --es-input-radius: var(--global-es-radius-8) var(--global-es-radius-8) 0 0; box-shadow: var(--global-esf-box-shadow-l); - z-index: 2; } } diff --git a/src/Captcha/Captcha.php b/src/Captcha/Captcha.php index 3520e7b61..bd9835e8e 100644 --- a/src/Captcha/Captcha.php +++ b/src/Captcha/Captcha.php @@ -10,7 +10,6 @@ namespace EightshiftForms\Captcha; - /** * Captcha class. * @@ -57,11 +56,11 @@ public function __construct(Recaptcha $recaptcha, FriendlyCaptcha $friendlyCaptc */ public function check(string $token, string $action, bool $isEnterprise, array $formDetails = []): array { - if (SettingsCaptcha::getActiveProvider() === SettingsCaptcha::PROVIDER_FRIENDLY) { - return $this->friendlyCaptcha->check($token, $action, $isEnterprise, $formDetails); + switch (SettingsCaptcha::getActiveProvider()) { + case SettingsCaptcha::PROVIDER_FRIENDLY: + return $this->friendlyCaptcha->check($token, $action, $isEnterprise, $formDetails); + default: + return $this->recaptcha->check($token, $action, $isEnterprise, $formDetails); } - - return $this->recaptcha->check($token, $action, $isEnterprise, $formDetails); } } - diff --git a/src/Troubleshooting/SettingsFallback.php b/src/Troubleshooting/SettingsFallback.php index 7b3f72877..04a270c47 100644 --- a/src/Troubleshooting/SettingsFallback.php +++ b/src/Troubleshooting/SettingsFallback.php @@ -97,12 +97,6 @@ class SettingsFallback implements ServiceInterface, SettingsFallbackDataInterfac public const SETTINGS_FALLBACK_FLAG_CAPTCHA_SUCCESS = 'captchaSuccess'; public const SETTINGS_FALLBACK_FLAG_CAPTCHA_DEBUG_SKIP_CHECK = 'captchaDebugSkipCheck'; - public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_FEATURE_DISABLED = 'friendlyCaptchaFeatureDisabled'; - public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_MISSING_TOKEN = 'friendlyCaptchaRequestMissingToken'; - public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_WP_ERROR = 'friendlyCaptchaRequestWpError'; - public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR = 'friendlyCaptchaOutputError'; - public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_SUCCESS = 'friendlyCaptchaSuccess'; - public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_FEATURE_DISABLED = 'geolocationFeatureDisabled'; public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_MALFORMED_DECRYPT_DATA = 'geolocationMalformedDecryptData'; public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_DETECTION_FAILED = 'geolocationDetectionFailed'; From 80b94cfa9a9d797da306ed1cc82d17a87086e34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Fri, 24 Apr 2026 09:13:43 +0200 Subject: [PATCH 14/20] Update SettingsFallback constants --- src/Captcha/FriendlyCaptcha.php | 8 ++++---- src/Troubleshooting/SettingsFallback.php | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Captcha/FriendlyCaptcha.php b/src/Captcha/FriendlyCaptcha.php index ed7ec5153..e3b0e9920 100644 --- a/src/Captcha/FriendlyCaptcha.php +++ b/src/Captcha/FriendlyCaptcha.php @@ -66,7 +66,7 @@ public function check(string $token, string $action, bool $isEnterprise, array $ return [ AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), AbstractBaseRoute::R_DEBUG => [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_FEATURE_DISABLED, + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_FEATURE_DISABLED, ], ]; } @@ -81,7 +81,7 @@ public function check(string $token, string $action, bool $isEnterprise, array $ throw new BadRequestException( $this->labels->getLabel('friendlyCaptchaBadRequest'), [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_MISSING_TOKEN, + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_REQUEST_MISSING_TOKEN, AbstractBaseRoute::R_DEBUG => $debug, ] ); @@ -112,7 +112,7 @@ public function check(string $token, string $action, bool $isEnterprise, array $ throw new BadRequestException( $this->labels->getLabel('submitWpError'), [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_REQUEST_WP_ERROR, + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_REQUEST_WP_ERROR, AbstractBaseRoute::R_DEBUG => $debug, ] ); @@ -138,7 +138,7 @@ public function check(string $token, string $action, bool $isEnterprise, array $ return [ AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), AbstractBaseRoute::R_DEBUG => [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_SUCCESS, + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_SUCCESS, AbstractBaseRoute::R_DEBUG => $debug, ], ]; diff --git a/src/Troubleshooting/SettingsFallback.php b/src/Troubleshooting/SettingsFallback.php index 04a270c47..7205fafb4 100644 --- a/src/Troubleshooting/SettingsFallback.php +++ b/src/Troubleshooting/SettingsFallback.php @@ -97,6 +97,8 @@ class SettingsFallback implements ServiceInterface, SettingsFallbackDataInterfac public const SETTINGS_FALLBACK_FLAG_CAPTCHA_SUCCESS = 'captchaSuccess'; public const SETTINGS_FALLBACK_FLAG_CAPTCHA_DEBUG_SKIP_CHECK = 'captchaDebugSkipCheck'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR = 'friendlyCaptchaOutputError'; + public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_FEATURE_DISABLED = 'geolocationFeatureDisabled'; public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_MALFORMED_DECRYPT_DATA = 'geolocationMalformedDecryptData'; public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_DETECTION_FAILED = 'geolocationDetectionFailed'; @@ -554,6 +556,10 @@ private function getFlagsList(): array 'label' => \__('Captcha debug skip check is active.', 'eightshift-forms'), 'isRecommended' => true, ], + self::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR => [ + 'label' => \__('Friendly Captcha returned an error response.', 'eightshift-forms'), + 'isRecommended' => true, + ], // Geolocation. self::SETTINGS_FALLBACK_FLAG_GEOLOCATION_FEATURE_DISABLED => [ From 55d9d63883e1283a83bc219d2231cfefaca815cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Fri, 24 Apr 2026 10:01:21 +0200 Subject: [PATCH 15/20] Add switch-case for Captcha in EnqueueBlocks --- src/Enqueue/Blocks/EnqueueBlocks.php | 45 +++++++++++++++------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/Enqueue/Blocks/EnqueueBlocks.php b/src/Enqueue/Blocks/EnqueueBlocks.php index 342caa933..ed6512a31 100644 --- a/src/Enqueue/Blocks/EnqueueBlocks.php +++ b/src/Enqueue/Blocks/EnqueueBlocks.php @@ -338,27 +338,30 @@ public function enqueueBlockFrontendScript(): void if (\apply_filters(SettingsCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { $provider = SettingsCaptcha::getActiveProvider(); - if ($provider === SettingsCaptcha::PROVIDER_FRIENDLY) { - $output['captcha'] = [ - 'isUsed' => true, - 'type' => SettingsCaptcha::PROVIDER_FRIENDLY, - 'siteKey' => SettingsHelpers::getOptionWithConstant( - Variables::getFriendlyCaptchaSiteKey(), - SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY - ), - 'endpoint' => FriendlyCaptcha::getEndpoint(), - ]; - } else { - $output['captcha'] = [ - 'isUsed' => true, - 'type' => SettingsCaptcha::PROVIDER_GOOGLE, - 'isEnterprise' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY), - 'siteKey' => SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), SettingsRecaptcha::SETTINGS_CAPTCHA_SITE_KEY), - 'submitAction' => SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found - 'initAction' => SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_INIT_ACTION_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found - 'loadOnInit' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), - 'hideBadge' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), - ]; + switch ($provider) { + case SettingsCaptcha::PROVIDER_FRIENDLY: + $output['captcha'] = [ + 'isUsed' => true, + 'type' => SettingsCaptcha::PROVIDER_FRIENDLY, + 'siteKey' => SettingsHelpers::getOptionWithConstant( + Variables::getFriendlyCaptchaSiteKey(), + SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY + ), + 'endpoint' => FriendlyCaptcha::getEndpoint(), + ]; + break; + default: + $output['captcha'] = [ + 'isUsed' => true, + 'type' => SettingsCaptcha::PROVIDER_GOOGLE, + 'isEnterprise' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_ENTERPRISE_KEY), + 'siteKey' => SettingsHelpers::getOptionWithConstant(Variables::getGoogleReCaptchaSiteKey(), SettingsRecaptcha::SETTINGS_CAPTCHA_SITE_KEY), + 'submitAction' => SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_SUBMIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found + 'initAction' => SettingsHelpers::getOptionValue(SettingsRecaptcha::SETTINGS_CAPTCHA_INIT_ACTION_KEY) ?: SettingsRecaptcha::SETTINGS_CAPTCHA_INIT_ACTION_DEFAULT_KEY, // phpcs:ignore WordPress.PHP.DisallowShortTernary.Found + 'loadOnInit' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_LOAD_ON_INIT_KEY), + 'hideBadge' => SettingsHelpers::isOptionCheckboxChecked(SettingsRecaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY, SettingsRecaptcha::SETTINGS_CAPTCHA_HIDE_BADGE_KEY), + ]; + break; } } else { $output['captcha'] = [ From 50e6a0d632f42fec4aaebe601ce962ae5b9fa937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Mon, 27 Apr 2026 08:57:41 +0200 Subject: [PATCH 16/20] Rename EnqueueCaptcha to EnqueueRecaptcha --- src/Captcha/SettingsFriendlyCaptcha.php | 4 ++-- src/Enqueue/Blocks/EnqueueBlocks.php | 19 ++++++++++++------- ...nqueueCaptcha.php => EnqueueRecaptcha.php} | 4 ++-- 3 files changed, 16 insertions(+), 11 deletions(-) rename src/Enqueue/Captcha/{EnqueueCaptcha.php => EnqueueRecaptcha.php} (97%) diff --git a/src/Captcha/SettingsFriendlyCaptcha.php b/src/Captcha/SettingsFriendlyCaptcha.php index bb3fe4084..e235f88cb 100644 --- a/src/Captcha/SettingsFriendlyCaptcha.php +++ b/src/Captcha/SettingsFriendlyCaptcha.php @@ -163,8 +163,8 @@ public static function getHelpContent(): array \sprintf(\__('Visit the Friendly Captcha dashboard.', 'eightshift-forms'), 'https://app.friendlycaptcha.eu/dashboard'), \__('Create a new application and copy the Site key.', 'eightshift-forms'), \__('Go to API Keys and create a new API key.', 'eightshift-forms'), - \__('Copy both keys into the fields under the General tab or use the global constants.', 'eightshift-forms'), - \__('In the Friendly Captcha dashboard, set the widget mode to Non-interactive for an invisible captcha experience.', 'eightshift-forms'), + \__('Copy both keys into the fields under the Settings tab or use the global constants.', 'eightshift-forms'), + \__('In the Friendly Captcha dashboard, open your application settings, go to the Protection tab, and set the Widget Mode to Smart.', 'eightshift-forms'), ], ], ]; diff --git a/src/Enqueue/Blocks/EnqueueBlocks.php b/src/Enqueue/Blocks/EnqueueBlocks.php index ed6512a31..4820d73a0 100644 --- a/src/Enqueue/Blocks/EnqueueBlocks.php +++ b/src/Enqueue/Blocks/EnqueueBlocks.php @@ -22,7 +22,7 @@ use EightshiftForms\CustomPostType\Result; use EightshiftForms\CustomPostType\Forms; use EightshiftForms\Enqueue\SharedEnqueue; -use EightshiftForms\Enqueue\Captcha\EnqueueCaptcha; +use EightshiftForms\Enqueue\Captcha\EnqueueRecaptcha; use EightshiftForms\Enqueue\Captcha\EnqueueFriendlyCaptcha; use EightshiftForms\Geolocation\GeolocationInterface; use EightshiftForms\Geolocation\SettingsGeolocation; @@ -395,12 +395,17 @@ protected function getFrontendScriptDependencies(): array $output = \apply_filters($scriptsDependency, []); } - if (SettingsCaptcha::getActiveProvider() === SettingsCaptcha::PROVIDER_FRIENDLY) { - if (\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - $output[] = "{$this->getAssetsPrefix()}-" . EnqueueFriendlyCaptcha::FRIENDLY_CAPTCHA_ENQUEUE_HANDLE; - } - } elseif (\apply_filters(SettingsRecaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { - $output[] = "{$this->getAssetsPrefix()}-" . EnqueueCaptcha::CAPTCHA_ENQUEUE_HANDLE; + switch (SettingsCaptcha::getActiveProvider()) { + case SettingsCaptcha::PROVIDER_FRIENDLY: + if (\apply_filters(SettingsFriendlyCaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + $output[] = "{$this->getAssetsPrefix()}-" . EnqueueFriendlyCaptcha::FRIENDLY_CAPTCHA_ENQUEUE_HANDLE; + } + break; + default: + if (\apply_filters(SettingsRecaptcha::FILTER_SETTINGS_GLOBAL_IS_VALID_NAME, false)) { + $output[] = "{$this->getAssetsPrefix()}-" . EnqueueRecaptcha::CAPTCHA_ENQUEUE_HANDLE; + } + break; } return $output; diff --git a/src/Enqueue/Captcha/EnqueueCaptcha.php b/src/Enqueue/Captcha/EnqueueRecaptcha.php similarity index 97% rename from src/Enqueue/Captcha/EnqueueCaptcha.php rename to src/Enqueue/Captcha/EnqueueRecaptcha.php index 7d1d4cafd..8813e6e3c 100644 --- a/src/Enqueue/Captcha/EnqueueCaptcha.php +++ b/src/Enqueue/Captcha/EnqueueRecaptcha.php @@ -19,9 +19,9 @@ use EightshiftFormsVendor\EightshiftLibs\Helpers\Helpers; /** - * Class EnqueueCaptcha + * Class EnqueueRecaptcha */ -class EnqueueCaptcha extends AbstractEnqueueTheme +class EnqueueRecaptcha extends AbstractEnqueueTheme { /** * Captcha enqueue handle. From bb5a954ce154d6137d346cab4ee43f995b1d3a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Mon, 27 Apr 2026 10:18:02 +0200 Subject: [PATCH 17/20] Add loadOnInit option for FriendlyCaptcha and refactor captcha JS - Add SETTINGS_FRIENDLY_CAPTCHA_LOAD_ON_INIT_KEY constant and UI toggle - Pass loadOnInit in FC captcha payload from EnqueueBlocks - Promote CAPTCHA_LOAD_ON_INIT state above switch (shared by both providers) - Remove querySelector guard from index.js for FC (mirrors reCAPTCHA pattern) - Add loadOnInit/querySelector guard inside FriendlyCaptcha::init() - Add initResetOnSubmit() to reset widget on afterFormSubmit window event - Refactor runFormCaptcha() to switch/case based on captcha type Co-Authored-By: Claude Sonnet 4.6 --- src/Blocks/components/form/assets/form.js | 41 +++++++++---------- .../form/assets/friendly-captcha.js | 19 +++++++++ src/Blocks/components/form/assets/index.js | 10 ++--- .../components/form/assets/state-init.js | 2 +- src/Captcha/SettingsFriendlyCaptcha.php | 26 ++++++++++++ src/Enqueue/Blocks/EnqueueBlocks.php | 1 + 6 files changed, 70 insertions(+), 29 deletions(-) diff --git a/src/Blocks/components/form/assets/form.js b/src/Blocks/components/form/assets/form.js index bcb2a33e1..5670d6a5b 100644 --- a/src/Blocks/components/form/assets/form.js +++ b/src/Blocks/components/form/assets/form.js @@ -578,30 +578,29 @@ export class Form { return; } - if (this.state.getStateCaptchaType() === StateEnum.CAPTCHA_TYPE_FRIENDLY) { - const widget = window[prefix]?.friendlyCaptcha; - const token = widget?.getResponse() ?? ''; + switch (this.state.getStateCaptchaType()) { + case StateEnum.CAPTCHA_TYPE_FRIENDLY: { + const widget = window[prefix]?.friendlyCaptcha; + const token = widget?.getResponse() ?? ''; - this.setFormDataCaptcha({ - token, - }); - - await this.formSubmit(formId, filter); - - // Reset the widget after every server response so a fresh single-use - // token is ready for the next submission attempt. - widget?.reset(); - - return; - } + this.setFormDataCaptcha({ + token, + }); - const actionName = this.state.getStateCaptchaSubmitAction(); - const siteKey = this.state.getStateCaptchaSiteKey(); + await this.formSubmit(formId, filter); + break; + } + default: { + const actionName = this.state.getStateCaptchaSubmitAction(); + const siteKey = this.state.getStateCaptchaSiteKey(); - if (this.state.getStateCaptchaIsEnterprise()) { - this.executeEnterpriseCaptcha(actionName, siteKey, formId, false, filter); - } else { - this.executeFreeCaptcha(actionName, siteKey, formId, false, filter); + if (this.state.getStateCaptchaIsEnterprise()) { + this.executeEnterpriseCaptcha(actionName, siteKey, formId, false, filter); + } else { + this.executeFreeCaptcha(actionName, siteKey, formId, false, filter); + } + break; + } } } diff --git a/src/Blocks/components/form/assets/friendly-captcha.js b/src/Blocks/components/form/assets/friendly-captcha.js index 0acdf938c..f42aa6eba 100644 --- a/src/Blocks/components/form/assets/friendly-captcha.js +++ b/src/Blocks/components/form/assets/friendly-captcha.js @@ -36,7 +36,23 @@ export class FriendlyCaptcha { return; } + if (!this.state.getStateCaptchaLoadOnInit() && !document.querySelectorAll(this.state.getStateSelector('form', true))?.length) { + return; + } + this.initWidget(); + this.initResetOnSubmit(); + } + + /** + * Reset widget after each form submission so a fresh token is ready for the next attempt. + * + * @returns {void} + */ + initResetOnSubmit() { + window.addEventListener(this.state.getStateEvent('afterFormSubmit'), () => { + this.widget?.reset(); + }); } /** @@ -87,6 +103,9 @@ export class FriendlyCaptcha { initWidget: () => { this.initWidget(); }, + initResetOnSubmit: () => { + this.initResetOnSubmit(); + }, getResponse: () => this.widget?.getResponse() ?? '', reset: () => { this.widget?.reset(); diff --git a/src/Blocks/components/form/assets/index.js b/src/Blocks/components/form/assets/index.js index 5f90f0802..b94f807ce 100644 --- a/src/Blocks/components/form/assets/index.js +++ b/src/Blocks/components/form/assets/index.js @@ -20,13 +20,9 @@ domReady(() => { if (state.getStateCaptchaIsUsed()) { switch (state.getStateCaptchaType()) { case StateEnum.CAPTCHA_TYPE_FRIENDLY: - // Friendly Captcha renders its widget inside the form, so only load - // it on pages that actually contain one. - if (document.querySelectorAll(state.getStateSelector('form', true))?.length) { - import('./friendly-captcha').then(({ FriendlyCaptcha }) => { - new FriendlyCaptcha(utils).init(); - }); - } + import('./friendly-captcha').then(({ FriendlyCaptcha }) => { + new FriendlyCaptcha(utils).init(); + }); break; default: import('./captcha').then(({ Captcha }) => { diff --git a/src/Blocks/components/form/assets/state-init.js b/src/Blocks/components/form/assets/state-init.js index 9ad95e768..172e801a5 100644 --- a/src/Blocks/components/form/assets/state-init.js +++ b/src/Blocks/components/form/assets/state-init.js @@ -267,6 +267,7 @@ export function setStateInitial() { if (captcha.isUsed) { setState([StateEnum.CAPTCHA_TYPE], captcha.type, StateEnum.CAPTCHA); setState([StateEnum.CAPTCHA_SITE_KEY], captcha.siteKey, StateEnum.CAPTCHA); + setState([StateEnum.CAPTCHA_LOAD_ON_INIT], Boolean(captcha.loadOnInit), StateEnum.CAPTCHA); switch (captcha.type) { case StateEnum.CAPTCHA_TYPE_FRIENDLY: @@ -276,7 +277,6 @@ export function setStateInitial() { setState([StateEnum.CAPTCHA_IS_ENTERPRISE], Boolean(captcha.isEnterprise), StateEnum.CAPTCHA); setState([StateEnum.CAPTCHA_SUBMIT_ACTION], captcha.submitAction, StateEnum.CAPTCHA); setState([StateEnum.CAPTCHA_INIT_ACTION], captcha.initAction, StateEnum.CAPTCHA); - setState([StateEnum.CAPTCHA_LOAD_ON_INIT], Boolean(captcha.loadOnInit), StateEnum.CAPTCHA); setState([StateEnum.CAPTCHA_HIDE_BADGE], Boolean(captcha.hideBadge), StateEnum.CAPTCHA); break; } diff --git a/src/Captcha/SettingsFriendlyCaptcha.php b/src/Captcha/SettingsFriendlyCaptcha.php index e235f88cb..c5fa4122b 100644 --- a/src/Captcha/SettingsFriendlyCaptcha.php +++ b/src/Captcha/SettingsFriendlyCaptcha.php @@ -51,6 +51,11 @@ class SettingsFriendlyCaptcha implements ServiceInterface */ public const SETTINGS_FRIENDLY_CAPTCHA_USE_EU_ENDPOINT_KEY = 'friendly-captcha-use-eu-endpoint'; + /** + * Friendly Captcha load on init (global load) key. + */ + public const SETTINGS_FRIENDLY_CAPTCHA_LOAD_ON_INIT_KEY = 'friendly-captcha-load-on-init'; + /** * Instance variable for labels data. * @@ -144,6 +149,27 @@ public static function getGeneralContent(): array ], ], ], + [ + 'component' => 'divider', + 'dividerExtraVSpacing' => true, + ], + [ + 'component' => 'checkboxes', + 'checkboxesFieldHideLabel' => true, + 'checkboxesName' => SettingsHelpers::getSettingName(self::SETTINGS_FRIENDLY_CAPTCHA_LOAD_ON_INIT_KEY), + 'checkboxesContent' => [ + [ + 'component' => 'checkbox', + 'checkboxLabel' => \__('Load widget on website load', 'eightshift-forms'), + 'checkboxIsChecked' => SettingsHelpers::isOptionCheckboxChecked(self::SETTINGS_FRIENDLY_CAPTCHA_LOAD_ON_INIT_KEY, self::SETTINGS_FRIENDLY_CAPTCHA_LOAD_ON_INIT_KEY), + 'checkboxValue' => self::SETTINGS_FRIENDLY_CAPTCHA_LOAD_ON_INIT_KEY, + 'checkboxHelp' => \__('By default, the widget is only loaded on pages that contain forms. Enable this to load it on every page.', 'eightshift-forms'), + 'checkboxSingleSubmit' => true, + 'checkboxAsToggle' => true, + 'checkboxAsToggleSize' => 'medium', + ], + ], + ], ]; } diff --git a/src/Enqueue/Blocks/EnqueueBlocks.php b/src/Enqueue/Blocks/EnqueueBlocks.php index 4820d73a0..7a2a57801 100644 --- a/src/Enqueue/Blocks/EnqueueBlocks.php +++ b/src/Enqueue/Blocks/EnqueueBlocks.php @@ -348,6 +348,7 @@ public function enqueueBlockFrontendScript(): void SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_SITE_KEY ), 'endpoint' => FriendlyCaptcha::getEndpoint(), + 'loadOnInit' => SettingsHelpers::isOptionCheckboxChecked(SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_LOAD_ON_INIT_KEY, SettingsFriendlyCaptcha::SETTINGS_FRIENDLY_CAPTCHA_LOAD_ON_INIT_KEY), ]; break; default: From 888a27029cd37a6bf7639420715669a81b9625fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Mon, 27 Apr 2026 10:39:30 +0200 Subject: [PATCH 18/20] Add granular error handling for Friendly Captcha siteverify responses - Distinguish auth errors, bad request, timeout/duplicate, invalid solution, HTTP errors - Add 4 new labels and 5 new fallback flags for each scenario --- src/Captcha/FriendlyCaptcha.php | 85 ++++++++++++++++++++++++ src/Labels/Labels.php | 4 ++ src/Troubleshooting/SettingsFallback.php | 25 +++++++ 3 files changed, 114 insertions(+) diff --git a/src/Captcha/FriendlyCaptcha.php b/src/Captcha/FriendlyCaptcha.php index e3b0e9920..971f0a96f 100644 --- a/src/Captcha/FriendlyCaptcha.php +++ b/src/Captcha/FriendlyCaptcha.php @@ -28,6 +28,21 @@ class FriendlyCaptcha implements CaptchaInterface public const FRIENDLY_CAPTCHA_ENDPOINT_GLOBAL_URL = 'https://global.frcapi.com/api/v2/captcha/siteverify'; public const FRIENDLY_CAPTCHA_ENDPOINT_EU_URL = 'https://eu.frcapi.com/api/v2/captcha/siteverify'; + /** + * Friendly Captcha siteverify error codes that indicate a missing or invalid API key. + */ + private const ERROR_CODES_AUTH = ['secret_missing', 'secret_invalid', 'auth_invalid', 'auth_required']; + + /** + * Friendly Captcha siteverify error codes that indicate a malformed request. + */ + private const ERROR_CODES_BAD_REQUEST = ['bad_request', 'solution_missing', 'sitekey_invalid', 'sitekey_missing']; + + /** + * Friendly Captcha siteverify error codes recoverable by a fresh widget solution. + */ + private const ERROR_CODES_TIMEOUT_OR_DUPLICATE = ['solution_timeout_or_duplicate', 'solution_expired', 'solution_already_used']; + /** * Instance variable of LabelsInterface data. * @@ -119,10 +134,80 @@ public function check(string $token, string $action, bool $isEnterprise, array $ // phpcs:enable } + $responseCode = (int) \wp_remote_retrieve_response_code($response); $responseBody = \json_decode(\wp_remote_retrieve_body($response), true) ?? []; + $errorCode = (string) ($responseBody['error']['error_code'] ?? $responseBody['error_code'] ?? ''); + $debug['responseCode'] = $responseCode; $debug['responseBody'] = $responseBody; + $debug['errorCode'] = $errorCode; + + // Auth issues — bad/missing API key. Status 401/403 or specific error codes. + if (\in_array($responseCode, [401, 403], true) || \in_array($errorCode, self::ERROR_CODES_AUTH, true)) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('friendlyCaptchaAuthError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_AUTH_ERROR, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + // Malformed request rejected by Friendly Captcha (sitekey/payload issue). + if (\in_array($errorCode, self::ERROR_CODES_BAD_REQUEST, true)) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('friendlyCaptchaBadRequest'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_BAD_REQUEST, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + // Solution expired or replayed — recoverable by requesting a fresh widget solution. + if (\in_array($errorCode, self::ERROR_CODES_TIMEOUT_OR_DUPLICATE, true)) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('friendlyCaptchaTimeoutOrDuplicate'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_TIMEOUT_OR_DUPLICATE, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + // Solution failed validation — likely a bot or tampered token. + if ($errorCode === 'solution_invalid') { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('friendlyCaptchaInvalidSolution'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_INVALID_SOLUTION, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + + // Non-success HTTP status with no recognised error code (e.g. 5xx). + if ($responseCode < 200 || $responseCode >= 300) { + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('friendlyCaptchaHttpError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_HTTP_ERROR, + AbstractBaseRoute::R_DEBUG => $debug, + ] + ); + // phpcs:enable + } + // Generic catch-all for any other unsuccessful response. if (empty($responseBody['success'])) { // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped throw new BadRequestException( diff --git a/src/Labels/Labels.php b/src/Labels/Labels.php index 1d4aa8579..9cfc500c4 100644 --- a/src/Labels/Labels.php +++ b/src/Labels/Labels.php @@ -428,6 +428,10 @@ private function getCaptchaLabels(): array 'friendlyCaptchaBadRequest' => \__('Spam prevention system encountered an error. Friendly Captcha request is invalid or malformed.', 'eightshift-forms'), 'friendlyCaptchaError' => \__('Spam prevention system encountered an error. Please try again.', 'eightshift-forms'), 'friendlyCaptchaSuccess' => \__('Success', 'eightshift-forms'), + 'friendlyCaptchaAuthError' => \__('Spam prevention system is not configured correctly. Please get in touch with the website administrator to resolve this issue.', 'eightshift-forms'), + 'friendlyCaptchaInvalidSolution' => \__('The request was marked as a potential spam request. Please try again.', 'eightshift-forms'), + 'friendlyCaptchaTimeoutOrDuplicate' => \__('Spam prevention check timed out or was reused. Please try again.', 'eightshift-forms'), + 'friendlyCaptchaHttpError' => \__('Spam prevention service is currently unavailable. Please try again in a moment.', 'eightshift-forms'), ]; } diff --git a/src/Troubleshooting/SettingsFallback.php b/src/Troubleshooting/SettingsFallback.php index 7205fafb4..df7a91c1a 100644 --- a/src/Troubleshooting/SettingsFallback.php +++ b/src/Troubleshooting/SettingsFallback.php @@ -98,6 +98,11 @@ class SettingsFallback implements ServiceInterface, SettingsFallbackDataInterfac public const SETTINGS_FALLBACK_FLAG_CAPTCHA_DEBUG_SKIP_CHECK = 'captchaDebugSkipCheck'; public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR = 'friendlyCaptchaOutputError'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_HTTP_ERROR = 'friendlyCaptchaHttpError'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_AUTH_ERROR = 'friendlyCaptchaAuthError'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_BAD_REQUEST = 'friendlyCaptchaBadRequest'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_INVALID_SOLUTION = 'friendlyCaptchaInvalidSolution'; + public const SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_TIMEOUT_OR_DUPLICATE = 'friendlyCaptchaTimeoutOrDuplicate'; public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_FEATURE_DISABLED = 'geolocationFeatureDisabled'; public const SETTINGS_FALLBACK_FLAG_GEOLOCATION_MALFORMED_DECRYPT_DATA = 'geolocationMalformedDecryptData'; @@ -560,6 +565,26 @@ private function getFlagsList(): array 'label' => \__('Friendly Captcha returned an error response.', 'eightshift-forms'), 'isRecommended' => true, ], + self::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_HTTP_ERROR => [ + 'label' => \__('Friendly Captcha siteverify request returned a non-success HTTP status.', 'eightshift-forms'), + 'isRecommended' => true, + ], + self::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_AUTH_ERROR => [ + 'label' => \__('Friendly Captcha API key is missing or invalid.', 'eightshift-forms'), + 'isRecommended' => true, + ], + self::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_BAD_REQUEST => [ + 'label' => \__('Friendly Captcha rejected the request as malformed.', 'eightshift-forms'), + 'isRecommended' => true, + ], + self::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_INVALID_SOLUTION => [ + 'label' => \__('Friendly Captcha solution failed validation.', 'eightshift-forms'), + 'isRecommended' => false, + ], + self::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_TIMEOUT_OR_DUPLICATE => [ + 'label' => \__('Friendly Captcha solution expired or was already used.', 'eightshift-forms'), + 'isRecommended' => false, + ], // Geolocation. self::SETTINGS_FALLBACK_FLAG_GEOLOCATION_FEATURE_DISABLED => [ From 9507ad3b030307f77c80462106799bfdb01996c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Mon, 27 Apr 2026 13:26:17 +0200 Subject: [PATCH 19/20] Refactor FriendlyCaptcha::check() response handling - Fix error code extraction to use FC v2 format (errors[0].error_code) - Early-return on success, flatten error checks to remove nesting --- src/Captcha/FriendlyCaptcha.php | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Captcha/FriendlyCaptcha.php b/src/Captcha/FriendlyCaptcha.php index 971f0a96f..2f0a67529 100644 --- a/src/Captcha/FriendlyCaptcha.php +++ b/src/Captcha/FriendlyCaptcha.php @@ -136,12 +136,22 @@ public function check(string $token, string $action, bool $isEnterprise, array $ $responseCode = (int) \wp_remote_retrieve_response_code($response); $responseBody = \json_decode(\wp_remote_retrieve_body($response), true) ?? []; - $errorCode = (string) ($responseBody['error']['error_code'] ?? $responseBody['error_code'] ?? ''); + $errorCode = (string) ($responseBody['errors'][0]['error_code'] ?? ''); $debug['responseCode'] = $responseCode; $debug['responseBody'] = $responseBody; $debug['errorCode'] = $errorCode; + if (!empty($responseBody['success'])) { + return [ + AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), + AbstractBaseRoute::R_DEBUG => [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_SUCCESS, + AbstractBaseRoute::R_DEBUG => $debug, + ], + ]; + } + // Auth issues — bad/missing API key. Status 401/403 or specific error codes. if (\in_array($responseCode, [401, 403], true) || \in_array($errorCode, self::ERROR_CODES_AUTH, true)) { // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped @@ -208,25 +218,15 @@ public function check(string $token, string $action, bool $isEnterprise, array $ } // Generic catch-all for any other unsuccessful response. - if (empty($responseBody['success'])) { - // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped - throw new BadRequestException( - $this->labels->getLabel('friendlyCaptchaError'), - [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR, - AbstractBaseRoute::R_DEBUG => $debug, - ] - ); - // phpcs:enable - } - - return [ - AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), - AbstractBaseRoute::R_DEBUG => [ - AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_CAPTCHA_SUCCESS, + // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped + throw new BadRequestException( + $this->labels->getLabel('friendlyCaptchaError'), + [ + AbstractBaseRoute::R_DEBUG_KEY => SettingsFallback::SETTINGS_FALLBACK_FLAG_FRIENDLY_CAPTCHA_OUTPUT_ERROR, AbstractBaseRoute::R_DEBUG => $debug, - ], - ]; + ] + ); + // phpcs:enable } /** From 01c9092778a3335ad4892648b1657adcf442d3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Obradovi=C4=87?= Date: Wed, 29 Apr 2026 13:17:46 +0200 Subject: [PATCH 20/20] Address PR review comments on FriendlyCaptcha MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove redundant captcha type check in friendly-captcha.js init() — class is only loaded when type is FRIENDLY - Use ApiHelpers::isSuccessResponse/isErrorResponse instead of manual HTTP code checks - Narrow auth error check to errorCode match only, dropping 401/403 HTTP code check --- src/Blocks/components/form/assets/friendly-captcha.js | 6 +----- src/Captcha/FriendlyCaptcha.php | 9 +++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Blocks/components/form/assets/friendly-captcha.js b/src/Blocks/components/form/assets/friendly-captcha.js index f42aa6eba..674f8ad6d 100644 --- a/src/Blocks/components/form/assets/friendly-captcha.js +++ b/src/Blocks/components/form/assets/friendly-captcha.js @@ -1,6 +1,6 @@ /* global frcaptcha */ -import { StateEnum, prefix, setStateWindow } from './state-init'; +import { prefix, setStateWindow } from './state-init'; /** * FriendlyCaptcha class. @@ -32,10 +32,6 @@ export class FriendlyCaptcha { return; } - if (this.state.getStateCaptchaType() !== StateEnum.CAPTCHA_TYPE_FRIENDLY) { - return; - } - if (!this.state.getStateCaptchaLoadOnInit() && !document.querySelectorAll(this.state.getStateSelector('form', true))?.length) { return; } diff --git a/src/Captcha/FriendlyCaptcha.php b/src/Captcha/FriendlyCaptcha.php index 2f0a67529..0bf84a733 100644 --- a/src/Captcha/FriendlyCaptcha.php +++ b/src/Captcha/FriendlyCaptcha.php @@ -11,6 +11,7 @@ namespace EightshiftForms\Captcha; use EightshiftForms\Exception\BadRequestException; +use EightshiftForms\Helpers\ApiHelpers; use EightshiftForms\Helpers\SettingsHelpers; use EightshiftForms\Hooks\Variables; use EightshiftForms\Labels\LabelsInterface; @@ -142,7 +143,7 @@ public function check(string $token, string $action, bool $isEnterprise, array $ $debug['responseBody'] = $responseBody; $debug['errorCode'] = $errorCode; - if (!empty($responseBody['success'])) { + if (ApiHelpers::isSuccessResponse($responseCode)) { return [ AbstractBaseRoute::R_MSG => $this->labels->getLabel('friendlyCaptchaSuccess'), AbstractBaseRoute::R_DEBUG => [ @@ -152,8 +153,8 @@ public function check(string $token, string $action, bool $isEnterprise, array $ ]; } - // Auth issues — bad/missing API key. Status 401/403 or specific error codes. - if (\in_array($responseCode, [401, 403], true) || \in_array($errorCode, self::ERROR_CODES_AUTH, true)) { + // Auth issues — bad/missing API key. + if (\in_array($errorCode, self::ERROR_CODES_AUTH, true)) { // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped throw new BadRequestException( $this->labels->getLabel('friendlyCaptchaAuthError'), @@ -205,7 +206,7 @@ public function check(string $token, string $action, bool $isEnterprise, array $ } // Non-success HTTP status with no recognised error code (e.g. 5xx). - if ($responseCode < 200 || $responseCode >= 300) { + if (ApiHelpers::isErrorResponse($responseCode)) { // phpcs:disable Eightshift.Security.HelpersEscape.ExceptionNotEscaped throw new BadRequestException( $this->labels->getLabel('friendlyCaptchaHttpError'),