diff --git a/README.md b/README.md index 1bcf7fc..5ae404e 100644 --- a/README.md +++ b/README.md @@ -91,29 +91,31 @@ itself is enabled. ## Logging -Turn on **Debug logging** (Forms → Settings → ALTCHA) to record every decision — -useful for verifying behaviour in staging or chasing a report. Each decision -writes one greppable line to the PHP error log: +Each protection decision is logged through **Gravity Forms' own logging +framework**. Enable the Gravity Forms *Logging* add-on, then under Forms → +Settings → **Logging** set *ALTCHA for Gravity Forms* to "Log errors only" or +"Log all messages" — you get a per-add-on, downloadable log file. Failures +(fail / blocked / spam) log at error level, passes at debug, so "errors only" +surfaces just the blocks. Sample messages: ``` -[gravityforms-altcha] altcha: pass {"form":43} -[gravityforms-altcha] altcha: fail {"form":43,"reason":"replay"} -[gravityforms-altcha] rate_limit: blocked {"form":12} -[gravityforms-altcha] content_filter: spam {"form":7,"score":3,"signals":["keyword"]} -[gravityforms-altcha] email_validation: blocked {"form":7,"status":"undeliverable","disposable":false,"reason":"rejected_email","domain":"exmaple.com"} +altcha: pass {"form":43} +altcha: fail {"form":43,"reason":"replay"} +rate_limit: blocked {"form":12} +content_filter: spam {"form":7,"score":3,"signals":["keyword"]} +email_validation: blocked {"form":7,"status":"undeliverable","disposable":false,"reason":"rejected_email","domain":"example.com"} ``` `reason` for ALTCHA is `missing` / `invalid` / `replay`. **No PII is logged** — IPs only ever appear as a salted hash, and emails as the domain only. -Route the records elsewhere (Sentry, Query Monitor, …) via the -`genero/gravityforms_altcha/log` action — and optionally drop the default -error-log line: +Every decision also fires the `genero/gravityforms_altcha/log` action, so you +can route records anywhere (Sentry, Query Monitor, …) independently of the GF +logger: ```php -remove_action('genero/gravityforms_altcha/log', ['Genero\GravityFormsAltcha\Logger', 'writeToErrorLog']); add_action('genero/gravityforms_altcha/log', function (string $event, string $outcome, array $context) { - // ship $event/$outcome/$context to your sink + // ship $event / $outcome / $context to your sink }, 10, 3); ``` @@ -220,14 +222,6 @@ add_filter('genero/gravityforms_altcha/spam_keywords', fn (array $words) => [... add_filter('genero/gravityforms_altcha/spam_score_threshold', fn () => 4); ``` -### `genero/gravityforms_altcha/logging` - -Force decision logging on or off regardless of the *Debug logging* setting: - -```php -add_filter('genero/gravityforms_altcha/logging', fn () => defined('WP_DEBUG') && WP_DEBUG); -``` - ### `genero/gravityforms_altcha/bouncer_api_key` Provide the Bouncer key in code instead of the `BOUNCER_API_KEY` env var (e.g. diff --git a/gravityforms-altcha.php b/gravityforms-altcha.php index 5f9b658..3dadbdf 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.4.0 + * Version: 0.5.0 * Requires at least: 6.0 * Requires PHP: 8.2 * Author: Genero diff --git a/src/Logger.php b/src/Logger.php index c374f44..732f8d6 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -3,16 +3,18 @@ namespace Genero\GravityFormsAltcha; /** - * Lightweight, opt-in logging so each protection decision can be verified - * through the logs. Off by default; enabled by the "Debug logging" setting (or - * the `genero/gravityforms_altcha/logging` filter). + * Records each protection decision so behaviour can be verified. * - * Every decision fires the `genero/gravityforms_altcha/log` action so it can be - * routed anywhere (Sentry, Query Monitor, …); a default listener writes a - * greppable line to the PHP error log (registered in {@see Plugin}). + * Every decision fires the `genero/gravityforms_altcha/log` action; the default + * listener (wired in {@see Plugin}) routes it into Gravity Forms' own logging + * framework, so it shows up under Forms → Settings → Logging with a per-add-on + * level (off / errors only / all) and a downloadable log file. Failures log at + * ERROR level, everything else at DEBUG, so "log errors only" surfaces just the + * blocks/failures. * - * Privacy: callers pass only non-PII context — a form id, a salted IP hash, an - * email *domain* (never the full address or raw IP), spam signal names, etc. + * The action is also a clean extension point for routing elsewhere (Sentry, + * Query Monitor, …). Privacy: callers pass only non-PII context — a form id, a + * salted IP hash, an email domain — never a raw IP or full address. */ class Logger { @@ -20,36 +22,21 @@ class Logger /** * @param string $event e.g. altcha | rate_limit | content_filter | email_validation - * @param string $outcome e.g. pass | fail | blocked | allowed + * @param string $outcome e.g. pass | fail | blocked | spam | allowed * @param array $context non-PII detail */ public static function record(string $event, string $outcome, array $context = []): void { - if (! self::enabled()) { - return; - } - do_action(self::HOOK, $event, $outcome, $context); } - public static function enabled(): bool - { - /** - * Filters whether decisions are logged. Defaults to the "Debug logging" - * setting (wired in Plugin); return true/false to override. - */ - return (bool) apply_filters('genero/gravityforms_altcha/logging', false); - } - /** - * Default sink — a single greppable line per decision. Other listeners can - * be added on the same hook, or this one removed to fully take over routing. - * - * @param array $context + * Whether an outcome represents a failure/block — logged at error level so + * it surfaces under GF's "log errors only". */ - public static function writeToErrorLog(string $event, string $outcome, array $context = []): void + public static function isFailure(string $outcome): bool { - error_log(self::format($event, $outcome, $context)); + return in_array($outcome, ['fail', 'blocked', 'spam'], true); } /** @@ -59,6 +46,6 @@ public static function format(string $event, string $outcome, array $context = [ { $suffix = $context !== [] ? ' '.json_encode($context) : ''; - return sprintf('[gravityforms-altcha] %s: %s%s', $event, $outcome, $suffix); + return sprintf('%s: %s%s', $event, $outcome, $suffix); } } diff --git a/src/Plugin.php b/src/Plugin.php index 10974bd..1ee365c 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -4,7 +4,7 @@ class Plugin { - public const VERSION = '0.4.0'; + public const VERSION = '0.5.0'; public const SLUG = 'gravityforms-altcha'; @@ -63,9 +63,9 @@ public function registerHooks(): void return; } - // Logging: drive the toggle from settings, and write the default sink. - add_filter('genero/gravityforms_altcha/logging', [Settings::class, 'loggingEnabled']); - add_action(Logger::HOOK, [Logger::class, 'writeToErrorLog'], 10, 3); + // Route decision logs into Gravity Forms' logging framework + // (Forms → Settings → Logging — per-add-on level + downloadable file). + add_action(Logger::HOOK, [Settings::class, 'logToGravityForms'], 10, 3); Integration::register(); ChallengeEndpoint::register(); diff --git a/src/Settings.php b/src/Settings.php index 7cba4d3..e34c2e4 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -97,13 +97,6 @@ public function plugin_settings_fields() 'tooltip' => esc_html__('Asks the visitor to correct a bad email address before submitting. Verifies via Bouncer — requires the BOUNCER_API_KEY environment variable, and sends the email to a third-party service. Fails open (never blocks) if the service is unavailable. Applies to all Gravity Forms.', 'gravityforms-altcha'), 'default_value' => false, ], - [ - 'name' => 'enable_logging', - 'type' => 'toggle', - 'label' => esc_html__('Debug logging', 'gravityforms-altcha'), - 'tooltip' => esc_html__('Logs every ALTCHA, rate-limit, content-filter and email decision (pass or fail) to the PHP error log so you can verify behaviour. No raw IPs or full email addresses are logged. Leave off in normal operation.', 'gravityforms-altcha'), - 'default_value' => false, - ], [ 'name' => 'email_block', 'type' => 'checkbox', @@ -227,9 +220,23 @@ public static function emailValidationEnabled(): bool return (bool) self::get_instance()->get_plugin_setting('enable_email_validation'); } - public static function loggingEnabled(): bool + /** + * Routes a logged decision into Gravity Forms' logging framework, so it + * appears under Forms → Settings → Logging and respects the per-add-on level + * set there. Failures log at error level, everything else at debug. + * + * @param array $context + */ + public static function logToGravityForms(string $event, string $outcome, array $context = []): void { - return (bool) self::get_instance()->get_plugin_setting('enable_logging'); + $addon = self::get_instance(); + $message = Logger::format($event, $outcome, $context); + + if (Logger::isFailure($outcome)) { + $addon->log_error($message); + } else { + $addon->log_debug($message); + } } /** diff --git a/test/mocked/LoggerRecordTest.php b/test/mocked/LoggerRecordTest.php index c0d1125..d37e34f 100644 --- a/test/mocked/LoggerRecordTest.php +++ b/test/mocked/LoggerRecordTest.php @@ -5,22 +5,12 @@ namespace Genero\GravityFormsAltcha\Tests\Mocked; use Brain\Monkey\Actions; -use Brain\Monkey\Functions; use Genero\GravityFormsAltcha\Logger; class LoggerRecordTest extends MockedTestCase { - public function test_records_nothing_when_disabled(): void + public function test_record_fires_the_log_action(): void { - Functions\when('apply_filters')->justReturn(false); - Actions\expectDone(Logger::HOOK)->never(); - - Logger::record('altcha', 'pass', ['form' => 1]); - } - - public function test_fires_the_log_action_when_enabled(): void - { - Functions\when('apply_filters')->justReturn(true); Actions\expectDone(Logger::HOOK)->once()->with('rate_limit', 'blocked', ['form' => 1]); Logger::record('rate_limit', 'blocked', ['form' => 1]); diff --git a/test/unit/LoggerTest.php b/test/unit/LoggerTest.php index 35634c3..ede763a 100644 --- a/test/unit/LoggerTest.php +++ b/test/unit/LoggerTest.php @@ -11,14 +11,24 @@ class LoggerTest extends TestCase { public function test_format_without_context(): void { - $this->assertSame('[gravityforms-altcha] altcha: pass', Logger::format('altcha', 'pass')); + $this->assertSame('altcha: pass', Logger::format('altcha', 'pass')); } public function test_format_with_context(): void { $this->assertSame( - '[gravityforms-altcha] altcha: fail {"form":43,"reason":"invalid"}', + 'altcha: fail {"form":43,"reason":"invalid"}', Logger::format('altcha', 'fail', ['form' => 43, 'reason' => 'invalid']), ); } + + public function test_failures_log_at_error_level(): void + { + foreach (['fail', 'blocked', 'spam'] as $outcome) { + $this->assertTrue(Logger::isFailure($outcome), $outcome); + } + foreach (['pass', 'allowed'] as $outcome) { + $this->assertFalse(Logger::isFailure($outcome), $outcome); + } + } }