Skip to content

refactor(captcha): serve the cached-page refresh from the provider (#1053) - #1064

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

refactor(captcha): serve the cached-page refresh from the provider (#1053)#1064
rpgmem merged 1 commit into
developfrom
claude/altcha-captcha-integration-4vvuer

Conversation

@rpgmem

@rpgmem rpgmem commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Fecha os dois itens que a auditoria pós-merge da PR2 (#1055) encontrou em aberto, listados em #1053. Pré-requisito da PR3: sem isto, o ALTCHA nasceria com um endpoint que devolve desafio matemático.

Summary

  • DynamicFragments::handle() passa a servir o provider configurado. Chamava SecurityService::generate_simple_captcha() direto, em dois pontos. É o quinto site de refresh — a PR2 roteou os quatro de retry por with_fresh_challenge() e não viu este, deixando o docblock de challenge_payload() afirmando um consumidor que ele não tinha.
  • O endpoint não nomeia mais os campos. Encaminha o payload verbatim. Mapear label/hash só valeria para o math: um desafio de proof-of-work não tem pergunta nem hash de resposta. O cliente despacha pelo campo provider do payload e ignora o que não reconhece, em vez de aplicar meio payload — que apagaria um desafio que o visitante talvez já tenha resolvido.
  • O escopo do cliente mudou junto. Antes ele varria o documento inteiro atrás de labels, tokens e respostas separadamente; agora percorre um bloco de segurança por vez. É isso que mantém dois desafios na mesma página independentes, e casa com onde os campos de fato vivem — templates/captcha/math-fields.php põe label e os dois inputs dentro da .ffc-captcha-row.
  • Remove Shortcodes::get_new_captcha_data(), método público cujo único consumidor era o próprio teste.

Sobre markup legado

securityBlocks() coleta .ffc-security-container e qualquer .ffc-captcha-row que esteja fora de uma — markup renderizado antes desse invólucro existir. Páginas em cache são a razão de ser deste endpoint, então ignorar a forma antiga seria justamente falhar no caso que ele existe para atender.

Decisão de compatibilidade que vale registrar

O formato do fio mudou (label/hashprovider/new_label/new_hash). Um navegador que segure uma cópia em cache do ffc-dynamic-fragments.js antigo encontrando o servidor novo escreveria undefined no label. Optei por não emitir as duas formas. Em produção o ?ver= gira no bump da release e o script antigo não sobrevive; só no ambiente de testes, onde FFC_VERSION não muda entre deploys de develop, a janela existe — e um refresh forçado a fecha. Aliases permanentes para proteger um transitório seriam exatamente a indireção que não se paga. Se preferir a rede de proteção, digo e adiciono.

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

Sem mudança funcional observável: math é o único provider registrado, então o payload que trafega hoje descreve o mesmo desafio de sempre.

Test plan

  • composer test passes locally — suíte completa, 7526 testes / 21677 asserções, verde
  • composer lint (PHPStan) passes locally — nível 8 sem erros; WPCS limpo; ESLint zero
  • Vitest completo — 116 arquivos, 1738 testes, verde
  • Manual smoke test in WordPress admin / frontend — pendente do deploy em testes
  • Updated or added unit tests where relevant

PHP. O endpoint passou a ser verificado contra um CaptchaProvider mockado, não contra generate_simple_captcha() mockado — este último passa igualmente bem com o endpoint furando o contrato, que é justamente o defeito corrigido. A asserção compara o payload inteiro, provando que nada é reformatado no caminho. Dois testes novos cobrem a ramificação por formulário, que não tinha nenhum: um prova que dois formulários recebem tokens distintos, outro que um formulário só não gera mapa.

JS. As seis provas de captcha foram verificadas falhando contra o script anterior. Três casos novos: provider desconhecido não toca em nada; markup legado sem .ffc-security-container continua sendo atualizado; dois formulários na página recebem desafios distintos.

Duas armadilhas nas fixtures, que a revisão vale a pena conhecer

  1. As fixtures punham os inputs fora da .ffc-captcha-row — coisa que nenhum site de render faz. Como o script agora escopa por bloco, uma fixture infiel testaria um DOM que o plugin nunca emite. Passaram a espelhar o template.
  2. As respostas-semente eram não numéricas ('abc', 'seed'), e o input real é type="number" — cujo .value devolve '' para valor não numérico. Ou seja, toda asserção de "limpa a resposta" passava vacuamente. Agora são numéricas, e o motivo está comentado no helper para não voltar.

Checklist

  • Source CSS/JS changes were re-minified (npm run build) and the resulting *.min.* files are committed
  • CHANGELOG.md updated (and readme.txt == Changelog == section if this is a release-bound change) — [Unreleased], em Changed e Removed; sem bump de FFC_VERSION
  • No new PHPStan baseline entries unless explicitly justified
  • No secrets, tokens, or personally identifiable information in the diff

ModuleBoundaryTest segue verde sem regenerar baseline: Core\Captcha é sub-namespace de Core, e a aresta Frontend → Core já existia.

Achado registrado à parte

A reescrita expôs um defeito pré-existente, aberto como #1063 e deliberadamente não corrigido aqui: o mapa por formulário só é emitido quando count($form_ids) > 1, e form_ids conta apenas formulários de certificado. Numa página que misture [ffc_form] com [ffc_self_scheduling] ou [ffc_csv_download] há dois blocos de segurança e um form_id só, então os dois recebem o mesmo token — e o uso único faz o primeiro envio queimar o do outro. Corrigir exige mudar o protocolo, o que é PR própria; esta preserva o comportamento como estava.

🤖 Generated with Claude Code

https://claude.ai/code/session_012XWx9qJdjZdAq8crxM9GCU


Generated by Claude Code

…1053)

`DynamicFragments::handle()` called `SecurityService::generate_simple_captcha()`
directly, in two places. It is the fifth refresh site: PR2 routed the four
retry sites through `with_fresh_challenge()` and missed this one, leaving
`challenge_payload()`'s own docblock claiming a consumer it did not have.

The endpoint now forwards `CaptchaProvider::resolve()->challenge_payload()`
verbatim and names none of the fields. Mapping them would only ever hold for
the math challenge — a proof-of-work challenge has no question and no answer
hash — so the client dispatches on the payload's `provider` key instead, and
leaves a provider it does not recognise alone rather than half-applying it,
which would blank a challenge the visitor may already have solved.

Scoping moved with it. The client used to query the whole document for
labels, tokens and answers separately; it now walks one security block at a
time (`.ffc-security-container`, plus any bare `.ffc-captcha-row` from markup
cached before that wrapper existed — cached pages being this endpoint's whole
audience). That is what keeps two challenges on one page independent, and it
matches where the fields actually live: `templates/captcha/math-fields.php`
puts the label and both inputs inside the row.

Also removes `Shortcodes::get_new_captcha_data()`, a public method whose only
consumer was its own test.

Test notes. The JS fixtures placed the inputs outside the row, which no
render site does; they now mirror the template. Their seed answers were
non-numeric, and the real input is `type="number"` — which reads back as ''
— so every "blanks the answer" assertion was passing without proving
anything. All six captcha tests were verified failing against the previous
script. On the PHP side the endpoint is now checked against a mocked
`CaptchaProvider` rather than a mocked `generate_simple_captcha()`: the
latter passes just as well with the endpoint bypassing the contract, which
is the bug being fixed.

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 16:30
@rpgmem
rpgmem enabled auto-merge (squash) September 5, 2026 16:30
@rpgmem
rpgmem merged commit bb34774 into develop Sep 5, 2026
19 checks passed
@rpgmem
rpgmem deleted the claude/altcha-captcha-integration-4vvuer branch September 5, 2026 16:38
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 33977777323

Warning

No base build found for commit 3845329 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.95%

Details

  • Patch coverage: 2 of 2 lines across 1 file are fully covered (100%).

Uncovered Changes

No uncovered changes found.

Coverage Regressions

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


Coverage Stats

Coverage Status
Relevant Lines: 53521
Covered Lines: 48142
Line Coverage: 89.95%
Coverage Strength: 4.84 hits per line

💛 - Coveralls

rpgmem added a commit that referenced this pull request Sep 5, 2026
#1053) (#1067)

Smoke-testing the ALTCHA-only mode found the widget's container rendering
empty on the public CSV download. The bundle was never loaded there: the
enqueue sat inside `if ( $has_form || $has_verification )` in
`Frontend::enqueue_scripts()`, and that page is neither. The self-scheduling
booking form would have been the same — a different class enqueues it — so
two of the four surfaces that render the security block were broken. A custom
element that never upgrades reports nothing, so the symptom was an empty box
with a clean console.

This is the trap CLAUDE.md already documents for the batched-export click
handler (#783), in enqueue form.

Adding `$has_csv_download` to the list would fix the screenshot and leave the
defect: the list is what drifts. The provider that renders the widget now
enqueues it. `Loader::register_frontend_assets()` registers both scripts —
the place whose own docblock says "only registers; actual enqueue happens
when shortcodes load their dependencies" — and `AltchaCaptcha::render_fields()`
enqueues at render time. Shortcodes render during `the_content`, well before
`wp_footer`, and both are footer scripts, so a late enqueue lands. The
localisation moved with them, so the `language` attribute and the i18n store
key are now produced by one method instead of two that could disagree —
a disagreement the widget answers by falling back to English in silence.

Both new guards were verified failing against the broken code: the render
test reports `Failed asserting that an array contains 'ffc-captcha'`, the
registration test the same for 'ffc-altcha'.

Test-isolation fallout, and a note for the next person. Stubbing `get_locale`
in `LoaderTest` broke 14 tests in two unrelated classes — the same
`LabelSorter::locale()` `function_exists` guard that bit in #1064, whose
branch depends on whether any earlier test in the process defined that
function. Defining it in `tests/bootstrap.php` is not the fix: Patchwork
cannot redefine a function declared in an uninstrumented file, so every
`Functions\when( 'get_locale' )` starts throwing instead. The two classes now
stub it explicitly, and CLAUDE.md records the trap with that dead end.


Claude-Session: https://claude.ai/code/session_012XWx9qJdjZdAq8crxM9GCU

Co-authored-by: Claude <noreply@anthropic.com>
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