mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
M6: a document script that throws no longer stops the form's other scripts (#3940)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
75207dc8ed
commit
e76ebb9ea3
@@ -669,7 +669,9 @@ impl Automation {
|
||||
let id = self.doc(a)?.id;
|
||||
let o = self.session.run_javascript(id, a.str("script")?, a.opt_str("field")?).map_err(failed)?;
|
||||
let reqs: Vec<Value> = o.requests.iter().map(request_json).collect();
|
||||
Ok(json!({ "alerts": o.alerts, "console": o.console, "requests": reqs, "error": o.error, "result": o.result }))
|
||||
Ok(
|
||||
json!({ "alerts": o.alerts, "console": o.console, "requests": reqs, "error": o.error, "document_errors": o.doc_errors, "result": o.result }),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn js_document_scripts(&self, a: &Args) -> Result<Value> {
|
||||
|
||||
@@ -1094,7 +1094,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
t("form_merge_data", "Merge data files into spreadsheet", "Collect the field values of form data files (FDF, XFDF) or filled-in PDF forms into one CSV file at path: a column per field name, a row per file. Returns the row and column counts.")
|
||||
.cmd("form.merge_data")
|
||||
.with(schema(json!({ "paths": { "type": "array", "items": { "type": "string" }, "minItems": 1 }, "path": { "type": "string" } }), &["paths", "path"])),
|
||||
t("js_run", "Run JavaScript", "Run Acrobat JavaScript in the document, as the JavaScript console does (or as push button `field`'s Mouse Up script when field is given; on a laid-out XFA form, `field` runs that button's XFA click script instead, JavaScript or FormCalc, which can add and remove rows and show or hide subforms). The form object model is available: this/getField, event, app, util, console, display, color, and the document-level scripts. Field changes and resetForm are applied as one undoable step; returns the script's alerts, console output, requests (print, page, url, submit) and error.")
|
||||
t("js_run", "Run JavaScript", "Run Acrobat JavaScript in the document, as the JavaScript console does (or as push button `field`'s Mouse Up script when field is given; on a laid-out XFA form, `field` runs that button's XFA click script instead, JavaScript or FormCalc, which can add and remove rows and show or hide subforms). The form object model is available: this/getField, event, app, util, console, display, color, and the document-level scripts. Field changes and resetForm are applied as one undoable step; returns the script's alerts, console output, requests (print, page, url, submit), error, and the errors of document-level scripts that failed (the rest still ran).")
|
||||
.cmd("tools.js_console")
|
||||
.with(schema(
|
||||
json!({ "doc": doc(), "script": { "type": "string" }, "field": { "type": "string", "description": "Run as this button's Mouse Up event." } }),
|
||||
|
||||
@@ -31,6 +31,13 @@ impl JsOutput {
|
||||
self.console.extend(o.console.iter().cloned());
|
||||
self.requests.extend(o.requests.iter().cloned());
|
||||
self.errors.extend(o.error.iter().cloned());
|
||||
// The document scripts run before every field script, so a failing one would repeat
|
||||
// once per calculation; it is reported once.
|
||||
for e in &o.doc_errors {
|
||||
if !self.errors.contains(e) {
|
||||
self.errors.push(e.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn append(&mut self, other: JsOutput) {
|
||||
|
||||
@@ -4015,6 +4015,11 @@ fn recognize_text_reads_a_picture_on_a_page_that_has_text() {
|
||||
/// A form whose scripts are custom JavaScript: total = price × qty (calculate, through a
|
||||
/// document-level function), shown with a custom format; qty is validated; a button script.
|
||||
fn scripted_form() -> Vec<u8> {
|
||||
scripted_form_with("function times\\(a, b\\) { console.println('times'); return a * b; }")
|
||||
}
|
||||
|
||||
/// [`scripted_form`] with `helpers` (PDF string syntax) as its document-level script.
|
||||
fn scripted_form_with(helpers: &str) -> Vec<u8> {
|
||||
let text = |name: &str, y: u32, aa: &str| {
|
||||
format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}) /Rect [20 {y} 180 {}] /P 3 0 R /F 4 {aa} >>", y + 20)
|
||||
};
|
||||
@@ -4028,7 +4033,7 @@ fn scripted_form() -> Vec<u8> {
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (go) /Rect [20 150 80 170] /P 3 0 R /F 4 >>".to_string(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".to_string(),
|
||||
"<< >>".to_string(),
|
||||
"<< /S /JavaScript /JS (function times\\(a, b\\) { console.println('times'); return a * b; }) >>".to_string(),
|
||||
format!("<< /S /JavaScript /JS ({helpers}) >>"),
|
||||
];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
@@ -4090,6 +4095,27 @@ fn scripts_in_an_xfa_form_see_the_xfa_plugin_installed() {
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("undefined number 3.5")));
|
||||
}
|
||||
|
||||
/// #3940: a document-level script that defines the helper and then calls a method PdfCraft lacks
|
||||
/// (an Acrobat form copying a button icon at open) left every calculated field blank. The
|
||||
/// helper stays defined, the form calculates, and the error is reported once.
|
||||
#[test]
|
||||
fn a_document_script_that_throws_still_lets_the_form_calculate() {
|
||||
let helpers =
|
||||
"function times\\(a, b\\) { return a * b; } this.getField\\('go'\\).buttonSetIcon\\(this.getField\\('go'\\).buttonGetIcon\\(\\)\\);";
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000);
|
||||
let id = s.open("order.pdf", None, Arc::new(scripted_form_with(helpers)), None).unwrap();
|
||||
let _ = s.take_js_output(id);
|
||||
s.apply(id, Edit::SetFieldValue { name: "qty".into(), value: FieldValue::Text("4".into()) }).unwrap();
|
||||
let total = s.get(id).unwrap().form.iter().find(|f| f.name == "total").unwrap().value.clone();
|
||||
assert_eq!(total, ["10"], "calculated through the helper the failing script defined");
|
||||
let out = s.take_js_output(id);
|
||||
assert_eq!(out.errors.len(), 1, "reported once, not once per script: {out:?}");
|
||||
assert!(out.errors[0].starts_with("document script 1: TypeError"), "{out:?}");
|
||||
// The console runs too, and says which document script failed.
|
||||
let o = s.run_javascript(id, "times(2, 3)", None).unwrap();
|
||||
assert_eq!((o.error, o.result.as_deref(), o.doc_errors.len()), (None, Some("6"), 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_history_does_not_replay_field_script_side_effects() {
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000);
|
||||
|
||||
+10
-2
@@ -181,6 +181,9 @@ pub struct Outcome {
|
||||
pub requests: Vec<Request>,
|
||||
/// The script's error (syntax error, exception, or a limit reached), if it failed.
|
||||
pub error: Option<String>,
|
||||
/// Errors in the document-level scripts that ran first. Each failing one stops where it
|
||||
/// failed; the others and the script itself still run, as in Acrobat (#3940).
|
||||
pub doc_errors: Vec<String>,
|
||||
/// The script's completion value (what the console prints), unless undefined.
|
||||
pub result: Option<String>,
|
||||
}
|
||||
@@ -1281,8 +1284,13 @@ fn run_here(script: &str, event: &Event, doc: &DocInfo, fields: Arc<Vec<FieldSta
|
||||
install(&mut ctx)?;
|
||||
let ev = event_object(&mut ctx, event);
|
||||
ctx.register_global_property(js_string!("event"), ev.clone(), Attribute::all())?;
|
||||
for d in doc_scripts {
|
||||
ctx.eval(Source::from_bytes(d))?;
|
||||
// One document script that throws (calling a method PdfCraft lacks, say) must not keep
|
||||
// every field script in the form from running: what it defined before the error, and
|
||||
// the other scripts, stay available (#3940).
|
||||
for (i, d) in doc_scripts.iter().enumerate() {
|
||||
if let Err(e) = ctx.eval(Source::from_bytes(d)) {
|
||||
out.doc_errors.push(format!("document script {}: {e}", i.saturating_add(1)));
|
||||
}
|
||||
}
|
||||
let r = ctx.eval(Source::from_bytes(script))?;
|
||||
if !r.is_undefined() {
|
||||
|
||||
@@ -297,6 +297,34 @@ fn document_level_functions_are_available() {
|
||||
assert_eq!(o.value, "8");
|
||||
}
|
||||
|
||||
/// #3940: a document script that throws after defining a helper (here calling an Acrobat method
|
||||
/// PdfCraft lacks) still leaves the helper, and the later scripts, to the field script.
|
||||
#[test]
|
||||
fn a_failing_document_script_does_not_stop_the_field_script() {
|
||||
let docs = [
|
||||
"function double(x) { return x * 2; }\nthis.getField('qty').buttonSetIcon(null);\nvar after = 1;".to_string(),
|
||||
"function plus(x) { return x + 1; }".to_string(),
|
||||
"this is not javascript".to_string(),
|
||||
];
|
||||
let o = run(
|
||||
"event.value = plus(double(getField('qty').value));",
|
||||
&Event::field("Calculate", "total", ""),
|
||||
&doc(),
|
||||
&fields(),
|
||||
&docs,
|
||||
Limits::default(),
|
||||
);
|
||||
assert_eq!((o.error.as_deref(), o.value.as_str()), (None, "9"));
|
||||
assert_eq!(o.doc_errors.len(), 2, "{:?}", o.doc_errors);
|
||||
assert!(o.doc_errors[0].starts_with("document script 1: TypeError"), "{:?}", o.doc_errors);
|
||||
assert!(o.doc_errors[1].starts_with("document script 3: SyntaxError"), "{:?}", o.doc_errors);
|
||||
// The failing script stopped where it failed.
|
||||
let o = run("event.value = typeof after;", &Event::field("Calculate", "total", ""), &doc(), &fields(), &docs, Limits::default());
|
||||
assert_eq!(o.value, "undefined");
|
||||
// Working document scripts report nothing.
|
||||
assert!(go("event.value = 1;", &Event::field("Calculate", "total", "")).doc_errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_and_runaway_scripts_are_contained() {
|
||||
let o = go("event.value = nosuch.thing;", &Event::field("Calculate", "total", "keep"));
|
||||
|
||||
Reference in New Issue
Block a user