Skip to content

feat(captcha): settings screen for the captcha, with guardrails (#1053 PR4) - #1066

Merged
rpgmem merged 1 commit into
developfrom
claude/altcha-captcha-integration-4vvuer
Sep 5, 2026
Merged

feat(captcha): settings screen for the captcha, with guardrails (#1053 PR4)#1066
rpgmem merged 1 commit into
developfrom
claude/altcha-captcha-integration-4vvuer

Conversation

@rpgmem

@rpgmem rpgmem commented Sep 5, 2026

Copy link
Copy Markdown
Owner

PR4 do épico #1053, e a última — a PR5 foi cortada (ver Sobre a PR5 abaixo). Entrega a tela que torna o ALTCHA da PR3 selecionável.

Nada muda para instalações existentes: o padrão segue o desafio matemático, a única estratégia que roda sem JavaScript e sem contexto seguro — a escolha que não tranca ninguém fora de um formulário que usava antes.

Summary

  • Configurações → Captcha, no grupo Segurança, sem capability nova.
  • Seletor de modo com a consequência dita ao lado da opção, não num parágrafo acima do grupo. São consequências, não preferências: o matemático é o mais fraco; o composto é alcance e não força (o servidor aceita qualquer uma das provas, então o atacante pega a mais barata); o só-ALTCHA não pode ser enviado sem JavaScript.
  • O modo só-ALTCHA é recusado no salvamento sem HTTPS, preservando o que estava configurado. Não é cautela: o widget lança Secure context (HTTPS) required em vez de degradar, então aceitar entregaria a todo visitante um formulário que ele não consegue enviar. O modo composto é permitido ali — sua metade em <noscript> é exatamente para isso.
  • Core\Captcha\CaptchaSettings — o único lugar que sabe o que uma configuração de captcha pode ser. Valores aceitos por atributo, e os dois números com limite.
  • Aviso dispensável sugerindo o modo mais forte, só sob HTTPS e oculto se já estiver nele.
  • Uso do fallback registrado no Activity Log, com IP hasheado e truncado.

Duas decisões que valem a revisão

O fator de trabalho é limitado na leitura, não só no salvamento. Um valor gravado antes de um limite mudar — ou por qualquer coisa que não seja este formulário — ainda precisa cair onde o widget consegue operar. Os limites são reais dos dois lados: abaixo do piso a prova não custa nada, acima do teto um celular lento mói até o widget desistir aos 90 segundos, que é uma falha sem causa visível para o visitante.

humanInteractionSignature e setCookie ficam desligados e deliberadamente não são configuráveis. São formulários de serviço público sob a LGPD; o proof-of-work já carrega a carga anti-automação, e um administrador religando a coleta de temporização de ponteiro e teclado mudaria o que o site tem de declarar sem ser avisado disso.

Três desvios do plano, todos deliberados

  1. algorithm não foi exposto. Só SHA-256 vem no bundle; SHA-384/512 exigiriam vendorizar workers à parte, que o épico já pôs fora de escopo. Um seletor de um item só é ruído.
  2. O bloqueio de HTTPS usa add_settings_error(), não WP_Error. É o idioma que todo o resto do SettingsSaveHandler usa para retorno de salvamento; um WP_Error precisaria de plumbing próprio para chegar à tela e não chegaria mais claro.
  3. O leitor tipado é CaptchaSettings, não acessores em SettingsReader. Estas chaves carregam limites, e limites precisam morar em algum lugar de qualquer forma — espalhar SettingsReader::get_int() deixaria o clamp do lado de fora, para cada leitor repetir. É a forma que o Declarative settings registry: declare each ffc_settings key once #993 defende, construída onde já se pagava por ela.

Sobre a PR5

A PR5 previa forçar o modo só-ALTCHA como padrão numa release posterior. Cortada. A premissa era que ele é estritamente melhor, e não é: exige HTTPS e JavaScript. Trocar o padrão mudaria o comportamento justamente das instalações que nunca abriram esta aba — as menos propensas a perceber que um formulário parou de aceitar envios.

O que esta PR entrega torna o flip desnecessário: a escolha fica visível com a consequência ao lado, o save recusa o modo onde ele não funciona, e o aviso empurra para ele só onde é possível. Escolha informada em vez de mudança silenciosa — mesmo raciocínio que estacionou o flip de IP do cliente no #902. Registrado no épico com o gatilho para revisitar.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would alter existing behavior)
  • Refactor / chore (no functional change)
  • Documentation only

Test plan

  • composer test7584 testes / 21778 asserções, verde
  • composer lint (PHPStan) — nível 8 limpo nos arquivos novos e tocados
  • WPCS, phpcs-tests.xml.dist, ESLint e Stylelint limpos
  • Vitest — 117 arquivos, 1750 testes
  • Manual smoke test in WordPress admin / frontend — pendente do merge: é o único item aberto do épico, e só agora existe interface para trocar de modo
  • Updated or added unit tests where relevant

Cobertura nova. CaptchaSettingsTest cobre os limites nas duas pontas, entradas não numéricas, valores de atributo desconhecidos caindo para o seguro, e que as escolhas de privacidade não são alcançáveis por configuração. Em SettingsSaveHandlerTest, oito casos: a seção intocada quando outra aba é salva, modo desconhecido caindo para matemático, a recusa sem HTTPS preservando o valor anterior e registrando o erro, o modo composto permitido sem HTTPS, clamp no salvamento, e caixas desmarcadas lendo como zero.

Um guarda pegou algo

ActivityLogActionLabelsTest falhou porque a ação nova do log de fallback chegou sem rótulo traduzido — sem ele renderizaria como inglês não traduzido em todos os locales, na tabela, no resumo por ação e no export CSV. Corrigido.

Checklist

  • Source CSS/JS changes were re-minified (npm run build) and the resulting *.min.* files are committed
  • CHANGELOG.md updated — [Unreleased] → Added; sem bump de FFC_VERSION
  • No new PHPStan baseline entries unless explicitly justified
  • No secrets, tokens, or personally identifiable information in the diff

uninstall.php declara a option nova do aviso dispensável, mantendo-o um manifesto honesto para o gate de fresh-install.

CLAUDE.md ganhou uma seção sobre a arquitetura de captcha — o contrato, a regra "conferir ≠ gastar", os cinco sites que emitem desafio, e as especificidades do ALTCHA que custam tempo para redescobrir (os nove atributos, o maxnumber inexistente no 3.x, o contexto seguro obrigatório, o build UMD). A justificativa do PublicCsvExporter foi emendada: "funciona com JS desabilitado" passa a ser propriedade dos modos matemático e composto apenas.

Nota sobre o PHPStan local: rodo o PHAR, que não carrega szepeviktor/phpstan-wordpress, então ele reporta 3 erros em arquivos que esta PR não toca. Artefato do meu ambiente; a CI é a fonte de verdade.

🤖 Generated with Claude Code

https://claude.ai/code/session_012XWx9qJdjZdAq8crxM9GCU


Generated by Claude Code

Adds Settings → Captcha: the mode chooser, the widget options, and the two
bounded numbers. Nothing changes for an existing install — the default stays
the math challenge, which is the only strategy that runs without JavaScript
and without a secure context, so it is the choice that cannot lock anyone out
of a form they could use before.

Each mode's consequence is stated beside the option rather than in a paragraph
above the group, because they are consequences and not preferences: the math
challenge is the weakest, the composite mode is reach rather than strength
(the server accepts either proof, so an attacker takes the cheaper one), and
ALTCHA-only cannot be submitted without JavaScript.

The ALTCHA-only mode is refused at save time without HTTPS, keeping whatever
was configured before. This is not caution: the widget throws "Secure context
(HTTPS) required" rather than degrading, so accepting it would hand every
visitor a form they cannot submit. The composite mode is allowed there — its
<noscript> half is exactly what it is for.

`CaptchaSettings` is the one place that knows what a captcha setting may be,
and it bounds the work factor on READ as well as on save: a value written
before a bound moved, or by anything other than this form, still has to land
somewhere the widget can cope with. The bounds are real on both sides — below
the floor the proof costs nothing, above the ceiling a slow phone grinds into
the widget's own 90-second timeout, which is a failure with no visible cause.

`humanInteractionSignature` and `setCookie` are forced off and deliberately
not settings. These are public-sector forms under the LGPD; the proof of work
already carries the anti-automation load, and an administrator toggling
pointer and keyboard collection back on would change what the site has to
disclose without being told so.

Three deviations from the plan, each deliberate. `algorithm` is not exposed:
only SHA-256 ships in the bundle, and a one-item selector is noise. The HTTPS
refusal uses `add_settings_error()` rather than `WP_Error`, which is the idiom
the rest of this handler uses for save feedback. And the typed reader is
`CaptchaSettings` rather than accessors on `SettingsReader`: these keys carry
bounds, and bounds have to live somewhere regardless — spreading
`SettingsReader::get_int()` would leave the clamp outside, for every reader to
repeat.

`ActivityLogActionLabelsTest` caught the fallback-usage action arriving with
no translated label, which would have rendered as untranslated English in
every locale across the table, the summary and the CSV export.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XWx9qJdjZdAq8crxM9GCU
@rpgmem
rpgmem marked this pull request as ready for review September 5, 2026 18:48
@rpgmem
rpgmem enabled auto-merge (squash) September 5, 2026 18:48
@rpgmem
rpgmem merged commit 22f6074 into develop Sep 5, 2026
19 checks passed
@rpgmem
rpgmem deleted the claude/altcha-captcha-integration-4vvuer branch September 5, 2026 19:00
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 33985124700

Warning

No base build found for commit 6da63da on develop.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 89.856%

Details

  • Patch coverage: 52 uncovered changes across 6 files (101 of 153 lines covered, 66.01%).

Uncovered Changes

File Changed Covered %
includes/admin/class-ffc-captcha-mode-notice.php 29 0 0.0%
includes/settings/tabs/class-ffc-tab-captcha.php 17 0 0.0%
includes/core/captcha/class-ffc-altcha-captcha.php 6 4 66.67%
includes/core/captcha/class-ffc-captcha-settings.php 39 37 94.87%
includes/admin/class-ffc-settings-save-handler.php 34 33 97.06%
includes/core/captcha/class-ffc-composite-captcha.php 16 15 93.75%
Total (9 files) 153 101 66.01%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 53843
Covered Lines: 48381
Line Coverage: 89.86%
Coverage Strength: 4.84 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants