diff --git a/crates/engine/src/js.rs b/crates/engine/src/js.rs index c80537fba..a59743dbb 100644 --- a/crates/engine/src/js.rs +++ b/crates/engine/src/js.rs @@ -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), diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index afe6fd06c..c8f0eb886 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -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); diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index f2221f750..13bdc4ba3 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -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) diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs index 7d63b4998..b2e866fae 100644 --- a/crates/js/src/tests.rs +++ b/crates/js/src/tests.rs @@ -16,7 +16,7 @@ fn fields() -> Vec { } 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"]); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 6200bac36..973e0cc69 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -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"