mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-06 02:21:45 +00:00
fix(xfa): scripts come only from the XDP's own packets and quote the date pattern (#5987)
The template (and the saved form state) was found anywhere under the XDP root, so a template-shaped element inside imported datasets could supply the form and its event scripts. A date field's pattern went into AFDate_FormatEx/KeystrokeEx string literals unescaped.
This commit is contained in:
1 parent
ecdd1e658c
commit
76260bb6a5
4 files changed
+52
-5
No files matched your search
@@ -746,10 +746,12 @@ pub fn parse(xml: &str) -> Result<(Template, Vec<String>), XfaError> {
|
||||
&& n.tag_name().name() == "template"
|
||||
&& (n.tag_name().namespace().is_some_and(|ns| ns.contains("xfa-template")) || children(*n, "subform").next().is_some())
|
||||
};
|
||||
// The template is a packet of the XDP itself, a child of its root: one nested deeper is
|
||||
// data (an imported dataset, say), which must not supply the form's scripts.
|
||||
let template = if is_template(&root) {
|
||||
root
|
||||
} else {
|
||||
root.descendants().find(is_template).ok_or_else(|| XfaError::Malformed("no <template> element".into()))?
|
||||
root.children().find(is_template).ok_or_else(|| XfaError::Malformed("no <template> element".into()))?
|
||||
};
|
||||
let mut warnings = Vec::new();
|
||||
let sf = children(template, "subform").next().ok_or_else(|| XfaError::Malformed("the template has no root subform".into()))?;
|
||||
@@ -760,7 +762,7 @@ pub fn parse(xml: &str) -> Result<(Template, Vec<String>), XfaError> {
|
||||
}
|
||||
// The form packet beside the template holds the state the form was saved in.
|
||||
let saved = root
|
||||
.descendants()
|
||||
.children()
|
||||
.find(|n| n.is_element() && n.tag_name().name() == "form" && n.tag_name().namespace().is_some_and(|ns| ns.contains("xfa-form")));
|
||||
if let Some(saved_root) = saved.and_then(|f| children(f, "subform").next()) {
|
||||
if let Some(p) = saved_root.attribute("presence") {
|
||||
|
||||
@@ -17,7 +17,9 @@ mod strip;
|
||||
|
||||
pub use common::jpeg_size;
|
||||
pub(crate) use common::rounded;
|
||||
use common::{all_faces, content_hash, empty_form, hex32, jpeg_info, js_action, n, parse_hex32, rgb, rgb_array, standard_font, state_name};
|
||||
use common::{
|
||||
all_faces, content_hash, empty_form, hex32, jpeg_info, js_action, js_string, n, parse_hex32, rgb, rgb_array, standard_font, state_name,
|
||||
};
|
||||
pub(crate) use strip::USAGE_RIGHTS_REMOVED;
|
||||
use strip::drop_usage_rights;
|
||||
pub use strip::{Stripped, XfaShape, existing_layout, strip, xfa_shape};
|
||||
@@ -205,8 +207,9 @@ impl Emitter<'_> {
|
||||
}
|
||||
if let WidgetKind::Date(pattern) = &w.kind {
|
||||
let mut aa = Dict::new();
|
||||
aa.set(b"F".to_vec(), js_action(&format!("AFDate_FormatEx(\"{pattern}\");")));
|
||||
aa.set(b"K".to_vec(), js_action(&format!("AFDate_KeystrokeEx(\"{pattern}\");")));
|
||||
let pattern = js_string(pattern);
|
||||
aa.set(b"F".to_vec(), js_action(&format!("AFDate_FormatEx({pattern});")));
|
||||
aa.set(b"K".to_vec(), js_action(&format!("AFDate_KeystrokeEx({pattern});")));
|
||||
d.set(b"AA".to_vec(), Object::Dict(aa));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,9 +192,41 @@ pub(super) fn empty_form(doc: &mut Document, w: f64, h: f64) -> Object {
|
||||
Object::Ref(doc.add(Object::Stream(Stream::from_raw(d, Vec::new()))))
|
||||
}
|
||||
|
||||
/// `s` as a JavaScript string literal, quotes included: a quote, a backslash or a line break in
|
||||
/// it stays text instead of ending the string and running as code.
|
||||
pub(super) fn js_string(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len() + 2);
|
||||
out.push('"');
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' | '\\' => {
|
||||
out.push('\\');
|
||||
out.push(c);
|
||||
}
|
||||
c if c.is_control() || c == '\u{2028}' || c == '\u{2029}' => out.push_str(&format!("\\u{:04x}", u32::from(c))),
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
out
|
||||
}
|
||||
|
||||
pub(super) fn js_action(js: &str) -> Object {
|
||||
let mut a = Dict::new();
|
||||
a.set(b"S".to_vec(), Object::name("JavaScript"));
|
||||
a.set(b"JS".to_vec(), Object::String(PdfString::literal(js.as_bytes().to_vec())));
|
||||
Object::Dict(a)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod js_string_tests {
|
||||
use super::js_string;
|
||||
|
||||
/// A date pattern that tries to close the string and run code stays one string literal.
|
||||
#[test]
|
||||
fn a_pattern_cannot_leave_its_string() {
|
||||
assert_eq!(js_string("yyyy-mm-dd"), "\"yyyy-mm-dd\"");
|
||||
assert_eq!(js_string("x\");app.alert(1);//"), "\"x\\\");app.alert(1);//\"");
|
||||
assert_eq!(js_string("a\\\nb\u{2028}"), "\"a\\\\\\u000ab\\u2028\"");
|
||||
}
|
||||
}
|
||||
@@ -4531,3 +4531,13 @@ fn a_container_is_as_wide_as_a_turned_draw_stands() {
|
||||
assert!((got - right).abs() < 0.01, "{rotate}: the neighbour stands {got} right of the anchor, not {right}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Only a template that is a packet of the XDP is the form's: one inside imported data (a
|
||||
/// dataset holding XML that looks like a template) is not read, and neither are its scripts.
|
||||
#[test]
|
||||
fn a_template_inside_the_data_is_not_the_form() {
|
||||
let nested = "<xdp:xdp xmlns:xdp=\"http://ns.adobe.com/xdp/\"><xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data><template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"evil\"><event activity=\"initialize\"><script>xfa.host.messageBox(1)</script></event></subform></template></xfa:data></xfa:datasets></xdp:xdp>";
|
||||
assert!(parse(nested).is_err(), "no template packet");
|
||||
let real = "<xdp:xdp xmlns:xdp=\"http://ns.adobe.com/xdp/\"><template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"form1\"/></template></xdp:xdp>";
|
||||
assert_eq!(parse(real).unwrap().0.root.common.name.as_deref(), Some("form1"));
|
||||
}
|
||||
Reference in new issue
Block a user