M6: scripts see xfa_version, and xfa_installed in XFA forms (#4142)

This commit is contained in:
Eric Kaden
2026-10-23 10:19:09 -04:00
committed by GitHub
parent 1b247a4812
commit 8f292c66ae
5 changed files with 67 additions and 6 deletions
+9 -1
View File
@@ -129,8 +129,16 @@ pub struct JsRunner {
}
impl JsRunner {
/// A runner for `cos`: what scripts read about the document (pages, info, whether it is
/// an XFA form) and its document-level scripts, read once.
pub fn new(cos: &pdfcraft_cos::Document, file_name: &str) -> JsRunner {
let doc = pdfcraft_js::DocInfo { file_name: file_name.to_string(), num_pages: pdfcraft_model::pages(cos).len(), page: 0, info: info(cos) };
let doc = pdfcraft_js::DocInfo {
file_name: file_name.to_string(),
num_pages: pdfcraft_model::pages(cos).len(),
page: 0,
info: info(cos),
xfa: pdfcraft_xfa::xfa_shape(cos).is_some(),
};
JsRunner {
doc,
doc_scripts: pdfcraft_forms::document_scripts(cos),
+14
View File
@@ -4076,6 +4076,20 @@ fn form_javascript_validates_calculates_and_formats() {
assert!(s.run_javascript(id, "1", None).is_err());
}
/// XFA forms check `xfa_installed` and `xfa_version` in a document script that runs before
/// each of their scripts, and alert when either is missing.
#[test]
fn scripts_in_an_xfa_form_see_the_xfa_plugin_installed() {
let check = "typeof xfa_installed + ' ' + typeof xfa_version + ' ' + xfa_version";
let mut s = Session::new().with_clock(|| 1_700_000_000);
let xfa = s.open("form.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::template(3))), None).unwrap();
let o = s.run_javascript(xfa, check, None).unwrap();
assert_eq!((o.error, o.result.as_deref()), (None, Some("boolean number 3.5")));
let plain = s.open("order.pdf", None, Arc::new(scripted_form()), None).unwrap();
let o = s.run_javascript(plain, check, None).unwrap();
assert_eq!((o.error, o.result.as_deref()), (None, Some("undefined number 3.5")));
}
#[test]
fn edit_history_does_not_replay_field_script_side_effects() {
let mut s = Session::new().with_clock(|| 1_700_000_000);
+15 -2
View File
@@ -144,6 +144,8 @@ pub struct DocInfo {
pub page: usize,
/// Document information (`this.info.title` etc.): lower-case key, value.
pub info: Vec<(String, String)>,
/// The document has an XFA form, so scripts see `xfa_installed`.
pub xfa: bool,
}
/// Something a script asked the viewer to do.
@@ -971,6 +973,11 @@ fn color_object(ctx: &mut Context) -> JsObject {
o.build()
}
/// The XFA version scripts see as `xfa_version`.
const XFA_VERSION: f64 = 3.5;
/// Define the globals every script sees: the document's functions and properties (`getField`,
/// `numPages`, `info`, the XFA version), and `app`, `util`, `console`, `display` and `color`.
fn install(ctx: &mut Context) -> JsResult<()> {
let globals: [(&str, usize, NativeFunction); 9] = [
("getField", 1, NativeFunction::from_fn_ptr(get_field)),
@@ -991,9 +998,9 @@ fn install(ctx: &mut Context) -> JsResult<()> {
let nf = function(ctx, NativeFunction::from_fn_ptr(num_fields));
let pg = function(ctx, NativeFunction::from_fn_ptr(page_num_get));
let ps = function(ctx, NativeFunction::from_fn_ptr(page_num_set));
let (file, pages, info) = {
let (file, pages, info, xfa) = {
let h = host(ctx)?.borrow();
(h.doc.file_name.clone(), h.doc.num_pages, h.doc.info.clone())
(h.doc.file_name.clone(), h.doc.num_pages, h.doc.info.clone(), h.doc.xfa)
};
let mut io = ObjectInitializer::new(ctx);
for (k, v) in &info {
@@ -1006,6 +1013,12 @@ fn install(ctx: &mut Context) -> JsResult<()> {
ctx.register_global_property(js_string!("numPages"), JsValue::from(pages as f64), rw)?;
ctx.register_global_property(js_string!("documentFileName"), s(&file), rw)?;
ctx.register_global_property(js_string!("info"), info, rw)?;
// XFA forms check both before they run: every document sees the XFA version, and only an
// XFA form sees `xfa_installed`.
ctx.register_global_property(js_string!("xfa_version"), JsValue::from(XFA_VERSION), rw)?;
if xfa {
ctx.register_global_property(js_string!("xfa_installed"), JsValue::from(true), rw)?;
}
let app = ObjectInitializer::new(ctx)
.function(NativeFunction::from_fn_ptr(alert), js_string!("alert"), 1)
+27 -1
View File
@@ -16,7 +16,7 @@ fn fields() -> Vec<FieldState> {
}
fn doc() -> DocInfo {
DocInfo { file_name: "order.pdf".into(), num_pages: 3, page: 0, info: vec![("title".into(), "Order".into())] }
DocInfo { file_name: "order.pdf".into(), num_pages: 3, page: 0, info: vec![("title".into(), "Order".into())], xfa: false }
}
fn go(script: &str, event: &Event) -> Outcome {
@@ -1454,3 +1454,29 @@ fn shared_field_snapshots_isolate_writes_errors_and_script_globals() {
assert_eq!(read.value, "undefined:4:false");
assert!(read.changed.is_empty());
}
/// The check XFA forms run on opening: without both globals, or with a version below 2.8, it
/// alerts that the viewer can't show the form.
const XFA_CHECK: &str = "if (typeof(xfa_installed) == 'undefined' || typeof(xfa_version) == 'undefined' || xfa_version < 2.8) app.alert('old viewer'); \
event.value = typeof xfa_installed + ' ' + typeof xfa_version + ' ' + xfa_version;";
/// An XFA form sees `xfa_installed` (true) and `xfa_version` (3.5), so the check passes.
#[test]
fn an_xfa_form_sees_the_xfa_plugin_installed() {
let xfa = DocInfo { xfa: true, ..doc() };
let o = run(XFA_CHECK, &Event::doc("Open"), &xfa, &fields(), &[], Limits::default());
assert_eq!(o.error, None);
assert_eq!(o.value, "boolean number 3.5");
assert!(o.alerts.is_empty(), "{:?}", o.alerts);
let o = run("event.value = String(xfa_installed);", &Event::doc("Open"), &xfa, &fields(), &[], Limits::default());
assert_eq!(o.value, "true");
}
/// A form without XFA sees `xfa_version` but no `xfa_installed`, so the check alerts.
#[test]
fn a_plain_form_sees_the_xfa_version_but_not_installed() {
let o = go(XFA_CHECK, &Event::doc("Open"));
assert_eq!(o.error, None);
assert_eq!(o.value, "undefined number 3.5");
assert_eq!(o.alerts, ["old viewer"]);
}
+2 -2
View File
@@ -5165,8 +5165,8 @@ milestone = "M6"
status = "partial"
commands = ["tools.js_console"]
tools = ["js_run"]
evidence = ["crates/js/src/tests.rs::calculate_scripts_read_fields_and_set_the_value", "crates/engine/src/tests.rs::form_javascript_validates_calculates_and_formats", "crates/automation/tests/it/automation.rs::javascript_through_tools"]
notes = "boa (pure Rust) with the forms object model: event, getField/Field (value, readonly, display, colours, items, check boxes), app, util.printf/printd/printx, console, display, color; no annotations, layers or dialogs (app.response returns null) yet."
evidence = ["crates/js/src/tests.rs::calculate_scripts_read_fields_and_set_the_value", "crates/engine/src/tests.rs::form_javascript_validates_calculates_and_formats", "crates/automation/tests/it/automation.rs::javascript_through_tools", "crates/js/src/tests.rs::an_xfa_form_sees_the_xfa_plugin_installed", "crates/js/src/tests.rs::a_plain_form_sees_the_xfa_version_but_not_installed", "crates/engine/src/tests.rs::scripts_in_an_xfa_form_see_the_xfa_plugin_installed"]
notes = "boa (pure Rust) with the forms object model: event, getField/Field (value, readonly, display, colours, items, check boxes), app, util.printf/printd/printx, console, display, color; xfa_version (3.5) in every document and xfa_installed in XFA forms, so their version check passes; no annotations, layers or dialogs (app.response returns null) yet, and no app.findComponent."
[[feature]]
id = "form.js-console"