Encontrado ao reescrever o cliente do endpoint de fragmentos (#1053, PR do DynamicFragments). Pré-existente — a reescrita preserva o comportamento de propósito, para não alargar aquela PR.
Sintoma
Numa página que combine um formulário de certificado com outro bloco de captcha — [ffc_form id=X] + [ffc_self_scheduling], ou [ffc_form id=X] + [ffc_csv_download] — o refresh de fragmentos escreve o mesmo token nos dois blocos. Como o token é de uso único desde o #1054, quem submeter primeiro queima o do outro: o segundo formulário recebe "a resposta matemática está incorreta" para uma resposta que está certa.
É a mesma classe do #1056, um nível acima: lá o refresh do agendamento trocava a pergunta em todos os formulários e o token só no primeiro; aqui os dois recebem pergunta e token idênticos.
Causa
O endpoint só emite o mapa por formulário quando há mais de um form_id:
// includes/frontend/class-ffc-dynamic-fragments.php
if ( count( $form_ids ) > 1 ) {
// ... $fragments['captchas'][ $fid ] = ...
}
E form_ids vem de um seletor que enxerga só formulários de certificado:
// assets/js/ffc-dynamic-fragments.js
var formWrappers = document.querySelectorAll('.ffc-form-wrapper[id^="ffc-form-"]');
O bloco de segurança do agendamento e o do download de CSV não vivem dentro de um .ffc-form-wrapper, então não entram na contagem. Com um formulário de certificado + um deles: count($form_ids) === 1, nenhum mapa por formulário, e os dois blocos caem no data.captcha padrão.
O descompasso de fundo é conceitual: o cliente conta formulários de certificado, o servidor emite um desafio por formulário contado, mas quem consome um desafio é o bloco de segurança — e esses três números não são o mesmo.
Correção proposta
Contar o que de fato consome. O cliente já sabe quantos blocos existem (securityBlocks(), introduzida na PR do DynamicFragments), então:
- O cliente envia a quantidade de blocos, não só os
form_ids.
- O servidor devolve essa quantidade de desafios, chamando
CaptchaProvider::resolve()->challenge_payload() uma vez por bloco.
- O cliente distribui um por bloco, na mesma ordem, mantendo a preferência atual pelo payload específico do formulário quando houver.
Alternativa mais simples, se a mudança de protocolo não se pagar: aplicar o desafio padrão a no máximo um bloco e deixar os demais com o token renderizado pelo servidor. Um token possivelmente vencido é menos danoso que dois blocos compartilhando um — o vencido falha sozinho e o with_fresh_challenge() já devolve um novo na rejeição, enquanto o compartilhado faz um formulário derrubar o outro.
Prefiro a primeira: a segunda deixa o refresh sem fazer aquilo para que existe em parte da página.
Alcance
Precisa das duas metades — servidor e cliente — na mesma PR, senão um emite o que o outro não sabe consumir.
Cobertura
O teste JS gives each form on the page its own challenge já cobre o caso de dois formulários de certificado. Falta o caso misto: um .ffc-form-wrapper mais um bloco fora de wrapper, provando que os dois recebem tokens distintos.
Prioridade
Baixa. Exige uma página que misture os shortcodes, o que não é a configuração comum, e degrada para uma mensagem de erro com desafio novo — não perde dados nem abre acesso.
Encontrado ao reescrever o cliente do endpoint de fragmentos (#1053, PR do
DynamicFragments). Pré-existente — a reescrita preserva o comportamento de propósito, para não alargar aquela PR.Sintoma
Numa página que combine um formulário de certificado com outro bloco de captcha —
[ffc_form id=X]+[ffc_self_scheduling], ou[ffc_form id=X]+[ffc_csv_download]— o refresh de fragmentos escreve o mesmo token nos dois blocos. Como o token é de uso único desde o #1054, quem submeter primeiro queima o do outro: o segundo formulário recebe "a resposta matemática está incorreta" para uma resposta que está certa.É a mesma classe do #1056, um nível acima: lá o refresh do agendamento trocava a pergunta em todos os formulários e o token só no primeiro; aqui os dois recebem pergunta e token idênticos.
Causa
O endpoint só emite o mapa por formulário quando há mais de um
form_id:E
form_idsvem de um seletor que enxerga só formulários de certificado:O bloco de segurança do agendamento e o do download de CSV não vivem dentro de um
.ffc-form-wrapper, então não entram na contagem. Com um formulário de certificado + um deles:count($form_ids) === 1, nenhum mapa por formulário, e os dois blocos caem nodata.captchapadrão.O descompasso de fundo é conceitual: o cliente conta formulários de certificado, o servidor emite um desafio por formulário contado, mas quem consome um desafio é o bloco de segurança — e esses três números não são o mesmo.
Correção proposta
Contar o que de fato consome. O cliente já sabe quantos blocos existem (
securityBlocks(), introduzida na PR doDynamicFragments), então:form_ids.CaptchaProvider::resolve()->challenge_payload()uma vez por bloco.Alternativa mais simples, se a mudança de protocolo não se pagar: aplicar o desafio padrão a no máximo um bloco e deixar os demais com o token renderizado pelo servidor. Um token possivelmente vencido é menos danoso que dois blocos compartilhando um — o vencido falha sozinho e o
with_fresh_challenge()já devolve um novo na rejeição, enquanto o compartilhado faz um formulário derrubar o outro.Prefiro a primeira: a segunda deixa o refresh sem fazer aquilo para que existe em parte da página.
Alcance
Precisa das duas metades — servidor e cliente — na mesma PR, senão um emite o que o outro não sabe consumir.
Cobertura
O teste JS
gives each form on the page its own challengejá cobre o caso de dois formulários de certificado. Falta o caso misto: um.ffc-form-wrappermais um bloco fora de wrapper, provando que os dois recebem tokens distintos.Prioridade
Baixa. Exige uma página que misture os shortcodes, o que não é a configuração comum, e degrada para uma mensagem de erro com desafio novo — não perde dados nem abre acesso.