diff --git a/README.md b/README.md index 5b3866d..7bcf459 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ a signed PBKDF2 proof that the browser quietly solved in the background. * MIT-licensed end to end ([altcha-org/altcha](https://github.com/altcha-org/altcha-lib-php) for PHP, [altcha](https://www.npmjs.com/package/altcha) widget for JS, this plugin for the glue). * No external API calls — challenges are issued and verified by your own WordPress install. * No license keys, no quotas. -* Drop-in: enables itself automatically for every Gravity Form. +* Opt-in: enable globally for all forms, or per individual form. ## Requirements @@ -34,10 +34,25 @@ wp plugin activate gravityforms-altcha 2. Upload to `wp-content/plugins/` (or via Plugins → Add New → Upload). 3. Activate. +## Settings + +ALTCHA is **off by default**. Enable it one of two ways: + +* **Globally** — Forms → Settings → **ALTCHA** → toggle *Enable for all forms*. + Every Gravity Form is then protected. +* **Per form** — a form's Settings → **ALTCHA** tab → toggle *Enable ALTCHA for + this form*. Use this when you only want protection on selected forms. + +A form is protected when the global toggle is on **or** that form's own toggle +is on. Both are stored in the standard Gravity Forms settings (the global one as +the `gravityformsaddon_gravityforms-altcha_settings` option, the per-form one in +the form meta). The `genero/gravityforms_altcha/should_protect` filter can still +override the saved settings programmatically. + ## How it works -1. **Form render** — the plugin injects a hidden `` web component - above the submit button on every Gravity Form. +1. **Form render** — when enabled for the form, the plugin injects a hidden + `` web component above the submit button. 2. **Browser-side proof-of-work** — the widget fetches a fresh challenge from `/wp-json/genero/gravityforms-altcha/v1/challenge` (signed with a per-site HMAC secret) and brute-forces a PBKDF2/SHA-256 derived-key match. @@ -47,20 +62,21 @@ wp plugin activate gravityforms-altcha reconstructs the challenge, and runs `altcha-org/altcha::verifySolution()`. On failure the submission is rejected with a generic error message. -There is no admin UI, no per-form configuration, and no settings page — -everything customisable lives behind WordPress filters. +Day-to-day configuration lives in the admin UI (see [Settings](#settings)); the +filters below cover advanced overrides. ## Filters ### `genero/gravityforms_altcha/should_protect` -Skip protection on specific forms: +Override the saved settings — force protection on (or off) for specific forms +regardless of the global / per-form toggles: ```php add_filter('genero/gravityforms_altcha/should_protect', function (bool $protect, array $form): bool { - // Don't run ALTCHA on internal preview-only forms. - if (in_array((int) $form['id'], [42, 43], true)) { - return false; + // Always protect the high-value lead form, whatever the toggles say. + if ((int) $form['id'] === 7) { + return true; } return $protect; }, 10, 2); diff --git a/gravityforms-altcha.php b/gravityforms-altcha.php index d81f9ef..1b7fd0a 100644 --- a/gravityforms-altcha.php +++ b/gravityforms-altcha.php @@ -6,7 +6,7 @@ * Plugin Name: ALTCHA for Gravity Forms * Plugin URI: https://github.com/generoi/gravityforms-altcha * Description: Invisible ALTCHA spam protection for Gravity Forms — uses the MIT-licensed altcha-org/altcha PHP library and the ALTCHA widget web component to proof-of-work-verify every form submission with no user interaction. - * Version: 0.1.1 + * Version: 0.2.0 * Requires at least: 6.0 * Requires PHP: 8.2 * Author: Genero diff --git a/src/Integration.php b/src/Integration.php index d10dc4d..6884884 100644 --- a/src/Integration.php +++ b/src/Integration.php @@ -97,12 +97,17 @@ public function enqueueScripts(array $form, bool $is_ajax): void private function shouldProtect(array $form): bool { /** - * Filters whether ALTCHA should protect this form. Default `true` — the - * plugin is opt-out, since the whole point is silent universal coverage. - * Sites that want per-form scoping can flip the default to `false` and - * enable on specific form IDs via this filter. + * Filters whether ALTCHA should protect this form. The default is + * derived from the add-on settings — opt-in via the global "Enable for + * all forms" toggle or the per-form toggle (see {@see Settings}). Pass + * a hard-coded boolean here to override the saved settings, e.g. to + * force protection on a specific form ID regardless of its toggle. */ - return (bool) apply_filters('genero/gravityforms_altcha/should_protect', true, $form); + return (bool) apply_filters( + 'genero/gravityforms_altcha/should_protect', + Settings::isEnabledForForm($form), + $form, + ); } private function challenge(): Challenge diff --git a/src/Plugin.php b/src/Plugin.php index 02422cf..488746e 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -4,7 +4,7 @@ class Plugin { - public const VERSION = '0.1.1'; + public const VERSION = '0.2.0'; public const SLUG = 'gravityforms-altcha'; @@ -39,6 +39,7 @@ public function boot(): void { add_action('plugins_loaded', [$this, 'loadTextdomain']); add_action('plugins_loaded', [$this, 'registerHooks'], 20); + add_action('gform_loaded', [$this, 'registerAddon'], 5); } public function loadTextdomain(): void @@ -66,6 +67,21 @@ public function registerHooks(): void ChallengeEndpoint::register(); } + /** + * Registers the Gravity Forms add-on that powers the global + per-form + * settings UI. Runs on `gform_loaded` so the add-on framework — and thus + * the `\GFAddOn` base class — is guaranteed to be available. + */ + public function registerAddon(): void + { + if (! method_exists('\GFForms', 'include_addon_framework')) { + return; + } + + \GFForms::include_addon_framework(); + \GFAddOn::register(Settings::class); + } + /** * The HMAC key used to sign + verify challenges. Resolved from (in order): * 1. The `genero/gravityforms_altcha/hmac_key` filter (lets consumers point diff --git a/src/Settings.php b/src/Settings.php new file mode 100644 index 0000000..3a841ab --- /dev/null +++ b/src/Settings.php @@ -0,0 +1,119 @@ +_full_path = Plugin::getInstance()->file; + + parent::__construct(); + } + + public static function get_instance(): Settings + { + if (self::$instance === null) { + self::$instance = new self; + } + + return self::$instance; + } + + /** + * Global settings page (Forms → Settings → ALTCHA). + * + * @return array> + */ + public function plugin_settings_fields() + { + return [ + [ + 'title' => esc_html__('ALTCHA spam protection', 'gravityforms-altcha'), + 'description' => esc_html__('Invisible proof-of-work spam protection for Gravity Forms — no captcha puzzles, no third-party requests.', 'gravityforms-altcha'), + 'fields' => [ + [ + 'name' => 'enable_all_forms', + 'type' => 'toggle', + 'label' => esc_html__('Enable for all forms', 'gravityforms-altcha'), + 'tooltip' => esc_html__('When on, ALTCHA protects every Gravity Form on the site. When off, enable it per form from the form\'s ALTCHA settings tab.', 'gravityforms-altcha'), + 'default_value' => false, + ], + ], + ], + ]; + } + + /** + * Per-form settings tab (form → Settings → ALTCHA). + * + * @param array $form + * @return array> + */ + public function form_settings_fields($form) + { + return [ + [ + 'title' => esc_html__('ALTCHA spam protection', 'gravityforms-altcha'), + 'fields' => [ + [ + 'name' => 'enabled', + 'type' => 'toggle', + 'label' => esc_html__('Enable ALTCHA for this form', 'gravityforms-altcha'), + 'tooltip' => esc_html__('Adds invisible ALTCHA spam protection to this form. Has no extra effect when "Enable for all forms" is turned on globally.', 'gravityforms-altcha'), + 'default_value' => false, + ], + ], + ], + ]; + } + + /** + * Whether ALTCHA should protect a given form, based on the saved settings: + * the global "enable for all forms" toggle wins, otherwise the form's own + * per-form toggle decides. Both default off. + * + * @param array $form + */ + public static function isEnabledForForm(array $form): bool + { + $addon = self::get_instance(); + + if ($addon->get_plugin_setting('enable_all_forms')) { + return true; + } + + $formSettings = $addon->get_form_settings($form); + + return is_array($formSettings) && ! empty($formSettings['enabled']); + } +}