Scripting: export commands a script runs directly respect Allow Scripts to Write Files (#2921)

* Scripting: export commands a script runs directly respect Allow Scripts to Write Files

CompItem.saveFrameToPng and exportAsMotionGraphicsTemplate checked the
file-write preference only in their JavaScript wrappers. A script could call
comp.saveFrameAs or essential.exportTemplate itself (app.run, __exec) and the
native handlers wrote the file with the preference off.

Session::refuse_export and write_output_with now refuse when the running
command was issued by a script and the preference is off, so every command
that writes a path it is given is gated the same way. The wrappers check the
gate with a new side-effect-free __file("checkWrite") instead of writing an
empty file at the destination.

* address review: clear stale export test dir before use

* test: allow script writes in script_imports_footage_and_saves
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-11 08:24:28 -05:00
1 parent be5c98506f
commit 33a15e1d86
5 files changed
+72 -3

No files matched your search

+50
View File
@@ -700,6 +700,56 @@ fn save_frame_to_png_writes_behind_the_file_gate() {
let _ = std::fs::remove_dir_all(&dir);
}
/// A script that runs the export commands itself (`app.run`, `__exec`) meets the same file-write
/// preference as the wrappers; with the preference off nothing is created or replaced.
#[test]
fn export_commands_run_directly_by_a_script_respect_the_file_gate() {
let dir = std::env::temp_dir().join(format!("ec-direct-export-{}", std::process::id()));
// A leftover dir from an interrupted run (same pid) must not feed stale absence checks.
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let png = dir.join("frame.png");
let tpl = dir.join("card.ectemplate");
let kept = dir.join("kept.png");
std::fs::write(&kept, b"original").unwrap();
let mut s = session();
s.execute("comp.new", json!({"name": "Card", "width": 64, "height": 64, "duration": 1})).unwrap();
s.execute("layer.newText", json!({"text": "Hi"})).unwrap();
ok(&mut s, "var c = app.project.activeItem; c.layer(1).property('ADBE Transform Group').property('ADBE Opacity').addToMotionGraphicsTemplate(c);");
let q = |p: &std::path::Path| serde_json::to_string(&p.to_string_lossy()).unwrap();
let comp = s.active_comp_id().unwrap().0;
for (code, target) in [
(format!("app.run('comp.saveFrameAs', {{comp: {comp}, path: {}}})", q(&png)), &png),
(format!("__exec('comp.saveFrameAs', JSON.stringify({{comp: {comp}, path: {}}}))", q(&png)), &png),
(format!("app.run('essential.exportTemplate', {{comp: {comp}, path: {}}})", q(&tpl)), &tpl),
(format!("app.run('comp.saveFrameAs', {{comp: {comp}, path: {}}})", q(&kept)), &kept),
] {
let o = run_code(&mut s, &code, "direct.jsx");
let e = o.error.unwrap_or_else(|| panic!("`{code}` was not refused"));
assert!(e.message.contains("Allow Scripts to Write Files"), "{e:?}");
if target == &kept {
assert_eq!(std::fs::read(&kept).unwrap(), b"original");
} else {
assert!(!target.exists(), "{} was written", target.display());
}
}
// The wrappers check the gate without touching the file: an existing one is left as it was.
let o = run_code(&mut s, &format!("app.project.activeItem.saveFrameToPng(0, new File({}))", q(&kept)), "wrap.jsx");
assert!(o.error.is_some());
assert_eq!(std::fs::read(&kept).unwrap(), b"original");
let o = run_code(&mut s, &format!("app.project.activeItem.exportAsMotionGraphicsTemplate(true, {})", q(&tpl)), "wrap.jsx");
assert!(o.error.is_some());
assert!(!tpl.exists());
// The user, from the UI, is not a script: the same command writes.
s.execute("comp.saveFrameAs", json!({"comp": comp, "path": png.to_string_lossy()})).unwrap();
assert!(png.exists());
// With the preference on, the script's direct call writes too.
s.prefs.scripting.allow_scripts_write_files = true;
ok(&mut s, &format!("app.run('essential.exportTemplate', {{comp: {comp}, path: {}}})", q(&tpl)));
assert!(std::fs::metadata(&tpl).unwrap().len() > 100);
let _ = std::fs::remove_dir_all(&dir);
}
/// #1246: with Warn User When Executing Files on, a script that asserts `confirmed` itself still
/// gets the question; only the user's answer opens the file.
#[test]