Skip to content

M12: XFA scripts' event.target finds the document's fields, their signature state, and resets them - #4762

Draft
mtlcl410 wants to merge 23 commits into
storytold:mainfrom
mtlcl410:fix/xfa-event-target-document
Draft

mtlcl410 wants to merge 23 commits into
storytold:mainfrom
mtlcl410:fix/xfa-event-target-document

Conversation

@mtlcl410

@mtlcl410 mtlcl410 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Draft. Depends on #3575 (event.target as the document object); this PR adds getField, signatureInfo, signatureValidate and resetForm on it. It will be rebased onto main once #3575 merges. Until then #3575's commits are included through one merge commit; this PR's own change is the last commit.

What this changes

Fixes #4761
Related #4743

In a dynamic XFA form's JavaScript, the bare event's target is the document (#3575). Forms ask that document for a field, and for a signature field's state. On main there is no such method, so these scripts stop with TypeError: not a callable function and the rest of the script doesn't run. In a scan of 82 public XFA forms:

call forms
event.target.getField(<full SOM name>) 9
getField(...).signatureInfo().status 2 (one of them is the form in #4743)
getField(...).signatureValidate() 7
event.target.resetForm(<name or array>) 2

After this PR:

  • event.target.getField(name) finds a laid-out field by the SOM expression of its XFA field with every index written (form1[0].sbfrmFooter[0].SignatureField1[0]). That is how form tools name the PDF fields of an XFA form (XFA 3.3, ch. 2, "Field Names", pp. 72-74), and it is the only spelling that finds the field:
    • other spellings (form.form1.foot.sig, form1.foot.sig, xfa[0].form[0]..., a missing last index) and a field's short name give null, as do an unknown name and "";
    • a subform's SOM expression gives an object whose name is that expression (its type is undefined);
    • getField() with no argument throws;
    • a signature field is not found while the form initializes (null in an initialize script); later events (a click, preSave) find it.
  • What getField returns is the form object model's own field object (field_object in crates/js/src/lib.rs), the same one AcroForm scripts get, over the document's fields named by their SOM expressions. A value set through it sets the XFA field the way rawValue does, so the script reads it back there. Other properties set through it (readonly, display, colours) are noted in the console and left alone.
  • signatureInfo().status and signatureValidate() give a signature field's state as a number: 0 unsigned, 1 unknown, 2 invalid, 4 valid. Signed or not is ISO 32000-2 §12.7.5.5 (a signature dictionary as the field's value); validity is what PdfCraft found when it read the document's signatures (pdfcraft_sign::pdf::list). Nothing is checked again and nothing goes to the network. In XFA scripts only signature fields have these methods. AcroForm scripts get them on every field, and a field that isn't a signature field gives -1.
  • event.target.resetForm(names) resets the named fields (one name or an array; missing and empty entries are skipped; full SOM expressions or dotted names such as form1.body.t), or every field when no names are given. A list that names nothing resets nothing. It uses the same reset as xfa.host.resetData (ISO 32000-2 §12.7.6.3), and the script reads the defaults back right after the call.

The API (Doc.getField, Doc.resetForm, Field.signatureInfo, Field.signatureValidate, the SignatureInfo.status codes) is defined by the public JavaScript for Acrobat API Reference (https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/), sections "Doc methods", "Field methods" and "SignatureInfo properties". XFA 3.3 itself defines only xfa.event (ch. 10, "Properties", pp. 398-399), whose target is the form node; that is unchanged.

Where the reference doesn't settle a case (which spellings getField takes, a subform's name, the initialize-time null for signature fields, methods on non-signature fields, when resetForm takes effect, which names it takes), the behaviour follows most readers that lay out dynamic XFA forms, observed on 13 synthetic forms.

The form in #4743. Its form initialize calls event.target.getField("form1[0].sbfrmFooter[0].SignatureField1[0]"), tests the result against null, and reads .signatureInfo().status. With this PR the initialize script runs to the end. Its other scripts also need the script objects in its <variables> (planned in the #3432 series). Built together with those locally, the next call that fails is event.target.removeDataObject(...), when its validation button attaches its error list to the document. That is a separate feature, not part of this PR.

Code

  • crates/js/src/xfa.rs: getField and resetForm on M6: form buttons open the files attached to their document (exportDataObject) #3575's document object; field_written (a value set through the field object goes to the XFA field); som_names, the name matching resetData already used, moved here from the engine so both resets match names the same way.
  • crates/js/src/lib.rs: SignatureState, FieldState::signature, and signatureInfo/signatureValidate on the field object. In XFA scripts the field object writes through to the form.
  • crates/js/src/xfa_session.rs: Script::fields, the document's fields as they are for each script.
  • crates/engine/src/xfa.rs, js.rs, lib.rs: the runner builds the fields once per layout, named by SOM (fields_by_som), with each signature field's state, and keeps their values up to date as scripts change them. On opening, the signatures are read with pdfcraft_sign::pdf::list; in an edit, they come from the document's list.

On a real form

The form in #4743 is the Romanian Ministry of Finance's electronic payment order (OPME, form 1129, public: https://mfinante.gov.ro/documents/2552173/2552377/31.OrdinPlataElectronic_2026_06_18_A2.0.46.pdf/86e41cfc-210e-023a-ab73-dd758954b420?t=1781789905297&download=true). Its root subform's initialize script reads the state of its two signature boxes:

var TheSignature1 = event.target.getField("form1[0].sbfrmFooter[0].SignatureField1[0]");
if (TheSignature1 == null) { var TheStatus1 = 0; }
else { var TheStatus1 = TheSignature1.signatureInfo().status; }

It then makes the second signature box read-only or open depending on that state. Opened with pdfcraft-cli, then NrOp and IbanPlatitor filled in and the validation button pressed:

main this PR
form initialize stops at line 6: TypeError: not a callable function (the getField call), so the signature-state checks after it never run runs to the end: getField gives null for the unsigned box during initialize, so the script takes its own == null branch, as the form expects
first script error after that — NrOp (exit): ReferenceError: csDataTool is not defined, from the form's script objects in <variables>, which main doesn't support yet (part of the #3432 series, not this PR)

So this PR removes the first failure in that form; #4743 stays open until the script objects land too.

Before and after

evtarget-01-getfield-som

Synthetic form 01, getField by the full SOM name: typeof event.target.getField is function, the text field is found and its value read; the signature box gives null because this runs in an initialize script. Main on the left, this PR on the right.

evtarget-04-getfield-unknown

Synthetic form 04, names the form doesn't have: null (so == null and === null are true), null for "", and an error for no argument. Main on the left, this PR on the right.

evtarget-05-getfield-subform

Synthetic form 05, a subform's SOM name: an object whose name is form1[0].foot[0] and whose type is undefined. Main on the left, this PR on the right.

evtarget-12-resetform-all

Synthetic form 12 after pressing Run (sets t and u to changed, then event.target.resetForm()), saved and rendered: both fields are back to default. Main on the left, this PR on the right.

evtarget-13-event-target-members

Synthetic form 13, event.target in an initialize script: it is no longer the node (=== this is false), and getField and resetForm are functions next to #3575's exportDataObject. The other members are still undefined. Main on the left, this PR on the right.

The signature methods need a signed box, so they aren't pictured. On the synthetic forms, a click script gets 0 from both methods for the unsigned box; the engine test below gets 2 for a stand-in signature that fails its check. The other forms (02 other spellings, 03 short names, 06/07 signature methods, 08 signature methods on a text field, 09 a value set through getField, 10/11 resetForm with one name and with an array that has a hole) are in the table below.

form case main this PR
02 getField("form.form1.foot.sig"), "form1.foot.sig", "xfa[0].form[0]...sig[0]", "form1[0].foot[0].sig" TypeError null each
03 getField("sig"), getField("t") TypeError null, null
06 click: signatureInfo().status of the unsigned box TypeError 0
07 click: signatureValidate() of the unsigned box TypeError 0
08 signatureInfo() / signatureValidate() on a text field TypeError TypeError (not on text fields)
09 initialize: getField(t).value = "after", then t.rawValue TypeError after
10 click: resetForm("form1[0].body[0].t[0]"), then t.rawValue TypeError, t stays changed default, t is default
11 click: f = new Array(1); f[1] = "form1.body.t"; resetForm(f) TypeError t reset, u unchanged

How you verified it

On main at 13bfbd0 with #3575 merged in, branch head 5b01d59:

  • cargo fmt --check
  • clippy with -D warnings on the crates I changed (pdfcraft-js, pdfcraft-xfa, pdfcraft-engine, --all-targets)
  • tests pass on the crates I changed: pdfcraft-js 92 passed (5 new), pdfcraft-xfa 128 passed, pdfcraft-engine 375 + 16 passed (3 new), 0 failed
  • cargo check --workspace --all-targets
  • cargo xtask layers, cargo xtask assets and cargo xtask parity

Tests

  • crates/js/src/tests.rs
    • event_target_get_field_finds_a_field_by_its_full_som_expression: the full SOM name finds the field (name, value, type). Four other spellings, the short name, an unknown name and "" give null. A subform's name gives an object naming it. No argument is an error. Without the document's fields nothing is found and nothing fails.
    • signature_info_and_validate_report_the_signature_state: 0/1/2/4 for the four states. A text field has neither method. An initialize script doesn't find a signature field but does find a text field.
    • a_value_set_through_get_field_sets_the_xfa_field: the value becomes a SetValue effect on the SOM, rawValue reads it back, and readonly is noted, not applied.
    • event_target_reset_form_resets_the_named_fields_or_all: one name, an array with a hole and a dotted name, no argument (all), [] and [''] (nothing); the script reads the defaults back at once.
    • signature_fields_report_their_signature_state: AcroForm scripts get 4 for a valid signature and -1 for a text field.
  • crates/engine/src/tests.rs (synthetic dynamic forms opened through Session)
    • xfa_scripts_reach_the_document_fields_through_the_event_target: on opening, the signature box is null and an unknown name is null, and a value set through getField reaches the field. A click then reads the box as form1[0].foot[0].sig[0]|0|0, and another click's resetForm puts t back to default and reads it back at once.
    • xfa_scripts_see_a_signed_box_through_the_event_target: pdfcraft_xfa::fixtures::signed_shell_with (the existing signed fixture, with a button added) reads 2 for the signed box (its stand-in signature has no byte range) and 0 for the other.
    • form_scripts_see_a_signature_fields_state: the console's AcroForm context reads the same 2 from the document's checked signatures.

Each test was seen failing with its part of the change reverted (one part at a time, only in the non-test code, files restored after each run):

reverted failing tests
getField/resetForm not on the document object all 4 XFA unit tests, both XFA engine tests
signatureInfo/signatureValidate not on the field object both signature unit tests, all 3 engine tests
no write-through to the XFA field value unit test, engine reach test
signature states not taken from the document's signatures signed-box engine test, AcroForm engine test
AcroForm scripts not given the checked states AcroForm engine test
signature fields found while initializing signature unit test, engine reach test
resetForm not seen at once by the script reset unit test, engine reach test
subform name gives null getField unit test
getField() without a name not an error getField unit test
signature methods on every field in XFA scripts signature unit test
a list naming nothing resets everything reset unit test

Never-crash

  • No new unwrap/expect/panic!/unreachable!/todo! outside tests
  • No indexing or slicing with input-derived positions. Names are cut to a message's length (clip). resetForm reads arrays through the existing bounded strings and keeps at most as many names as a snapshot has nodes. Unapplied-property notes are capped at 20. getField is a lookup over the fields the engine handed in.
  • No unsafe
  • Any crash this fixes has a regression test: does not apply

Provenance

  • No asset from an Adobe product
  • No font files
  • No GPL/AGPL code
  • Behaviour comes from XFA 3.3, ISO 32000-2, the public JavaScript API reference cited above, and black-box observation of readers on synthetic forms. No other implementation's source was read; no real form's script is copied (the forms in the scan were only counted).
  • No new assets

Security

  • No paths touched
  • MCP and the control channel are unchanged
  • No network access: signature states come from what was read on opening; nothing is validated again

Parity and reach

  • parity/acrobat-features.toml, form.js-host: the notes describe getField, resetForm and the signature methods with what is still missing (status 3 isn't told apart from 1; signatureInfo has only status; the other document members on the XFA event target). The new tests are listed as evidence.
  • Headless: XFA scripts already run through doc_open, form_fill and js_run; no new tool is needed.
  • Docs: crates/xfa/README.md, the scripting paragraph.

Anything a reviewer should look at first

  • One behaviour not mirrored. In the observed reader, an initialize script's getField(t).value = "after" didn't stick: the field kept its value, and even getField(t).value read the old one back. Here the write sticks, as it does from any other event. Dropping a script's write without saying so would be a silent loss. No form in the scan sets values through getField, and only the initialize case was observed.
  • The initialize-time null for signature fields follows the observation. The form in error when filling out PDF forms #4743 handles it (if (TheSignature1 == null) ... TheStatus1 = 0). A form that doesn't check will throw there, as it does in that reader.
  • Status 3 (valid, signer's identity not verified) isn't told apart: PdfCraft's Status::Unknown covers both an untrusted signer and an unchecked signature, so both give 1.
  • Cost on opening. On opening, the signatures are now read once more (pdfcraft_sign::pdf::list) before the scripts run; for an unsigned form that is cheap. The document's own list is built later from a separate cache.
  • Conflicts with XFA scripts: the bare event is named as the field event, of type Field, and its target has numPages #4546. It adds numPages to the same document object initializer, so the two PRs touch adjacent lines.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK

# Conflicts:
#	crates/engine/src/tests.rs
#	crates/js/src/tests.rs
#	crates/ui-egui/tests/forms.rs
… back to the tab it opened; nLaunch 1 and 2 only; button click scripts only
# Conflicts:
#	crates/js/src/lib.rs
#	crates/js/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	crates/automation/src/a11y.rs
#	crates/automation/src/tools.rs
#	crates/engine/src/js.rs
#	crates/engine/src/tests.rs
#	crates/engine/src/xfa.rs
#	crates/js/src/lib.rs
#	crates/js/src/tests.rs
#	crates/js/src/xfa.rs
#	crates/ui-egui/src/js_ui.rs
#	crates/ui-egui/src/lib.rs
#	crates/ui-egui/tests/it/forms.rs
#	parity/acrobat-features.toml
# Conflicts:
#	crates/ui-egui/tests/it/forms.rs
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

EricKaden and others added 6 commits October 26, 2026 22:44
# Conflicts:
#	crates/automation/src/a11y.rs
#	crates/js/src/lib.rs
#	crates/js/src/xfa.rs
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/lib.rs
# Conflicts:
#	crates/automation/src/tools.rs
#	crates/js/src/globals.rs
#	crates/js/src/xfa.rs
#	crates/js/src/xfa/globals.rs
#	crates/js/src/xfa/model.rs
#	crates/ui-egui/src/workflows/js_ui.rs
#	parity/acrobat-features.toml
…nature state, and resets them

In an XFA form's JavaScript, the bare event's target is the document (storytold#3575).
It now has getField and resetForm, and the field objects getField returns have
signatureInfo().status and signatureValidate().

- getField(name) finds a laid-out field by the SOM expression of its XFA field
  with every index written, how form tools name an XFA form's PDF fields
  (XFA 3.3 ch. 2, "Field Names", pp. 72-74). It returns the form object
  model's own field object; a value set through it sets the XFA field.
  A subform's SOM expression gives an object naming it; other names give
  null; no name is an error. A signature field isn't found while the form
  initializes.
- signatureInfo().status and signatureValidate() give a signature field's
  state as PdfCraft read the document's signatures (ISO 32000-2 12.7.5.5):
  0 unsigned, 1 unknown, 2 invalid, 4 valid. Nothing is checked again and
  nothing goes to the network. AcroForm scripts get them too (-1 for a field
  that isn't a signature field); in XFA scripts only signature fields have them.
- resetForm(names) resets the named fields (one name or an array; full SOM
  expressions or dotted names), or all of them, through the same reset as
  xfa.host.resetData (ISO 32000-2 12.7.6.3), and the script reads the
  defaults back at once.

Related: storytold#4743

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
@mtlcl410
mtlcl410 force-pushed the fix/xfa-event-target-document branch from 5b01d59 to 40e0e54 Compare October 31, 2026 06:26
@mtlcl410

Copy link
Copy Markdown
Contributor Author

Rebased onto current main. Main has since split pdfcraft-js (lib.rs into model.rs, host.rs, document.rs, run.rs; xfa.rs into xfa/model.rs, xfa/host.rs, xfa/globals.rs, xfa/run.rs) and the engine's lib.rs (edit/run.rs, open.rs, session/editing.rs, xfa_form.rs), so this PR's hunks were moved into those modules. The behaviour is unchanged. #3575 is merged in again at its current head.

This branch has not been deployed

No deployments
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.

XFA scripts: event.target has no getField() or resetForm(), so a form's signature-status checks stop with "not a callable function"

2 participants