mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-08 03:41:47 +00:00
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.
This commit is contained in:
@@ -509,12 +509,27 @@ impl Session {
|
||||
/// is not for an export to replace. For callers that write the file themselves; call it
|
||||
/// before the work, not after it.
|
||||
pub fn refuse_export(&self, path: &str) -> crate::Result<()> {
|
||||
self.refuse_script_write(path)?;
|
||||
match self.blocking(path) {
|
||||
Some(source) => Err(crate::EngineError::Other(source.refusal(path, Use::Export))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// `Err` when a script issued the running command ([`Session::running_for_script`]) and
|
||||
/// Settings ▸ Scripting & Expressions ▸ Allow Scripts to Write Files is off. The scripting
|
||||
/// wrappers check this too, but a script can run any command directly (`app.run`, `__exec`),
|
||||
/// so the commands that write a path they are given check here, through
|
||||
/// [`Session::refuse_export`] and [`Session::write_output`].
|
||||
pub fn refuse_script_write(&self, path: &str) -> crate::Result<()> {
|
||||
if self.running_for_script() && !self.prefs.scripting.allow_scripts_write_files {
|
||||
return Err(crate::EngineError::Other(format!(
|
||||
"Unable to write {path}: scripts can't write files unless Settings ▸ Scripting & Expressions ▸ Allow Scripts to Write Files and Access Network is on"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write `bytes` to `path` through the host's services, unless the project reads the file
|
||||
/// there ([`Session::refuse_export`]). Every command that writes to a path it is given
|
||||
/// writes through this; Save and Save As (the project's own file) do not.
|
||||
@@ -525,6 +540,7 @@ impl Session {
|
||||
/// [`Session::write_output`] against files already found ([`Session::protected_files`]),
|
||||
/// for a command that writes many.
|
||||
pub(crate) fn write_output_with(&self, protected: &Protected, path: &str, bytes: &[u8]) -> crate::Result<()> {
|
||||
self.refuse_script_write(path)?;
|
||||
if let Some(source) = protected.find(Path::new(path)) {
|
||||
return Err(crate::EngineError::Other(source.refusal(path, Use::Export)));
|
||||
}
|
||||
|
||||
@@ -1652,7 +1652,7 @@ CompItem.prototype.constructor = CompItem;
|
||||
P.saveFrameToPng = function (time, file) {
|
||||
var f = __str(file);
|
||||
if (!f) throw __err("saveFrameToPng: missing file");
|
||||
__file("write", f, "");
|
||||
__file("checkWrite", f);
|
||||
__call("comp.saveFrameAs", { comp: this.__id, path: f, time: __num(time, "time") });
|
||||
};
|
||||
P.openInEssentialGraphics = function () { __call("comp.openInEssentialGraphics", { comp: this.__id }); };
|
||||
@@ -1662,8 +1662,8 @@ CompItem.prototype.constructor = CompItem;
|
||||
if (!/\.[A-Za-z0-9]+$/.test(f)) f += ".ectemplate";
|
||||
if (!overwrite && JSON.parse(__file("exists", f)) === true) return false;
|
||||
if (this.__controllers().length === 0) return false;
|
||||
// The file gate applies to templates too.
|
||||
__file("write", f, "");
|
||||
// The file gate applies to templates too (checked without touching the file).
|
||||
__file("checkWrite", f);
|
||||
__call("essential.exportTemplate", { comp: this.__id, path: f });
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -409,6 +409,8 @@ fn native_file(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsV
|
||||
let p = p.to_string_lossy();
|
||||
Ok(json!(if op == "exists" { s.services.exists(&p) } else { s.services.is_dir(&p) }))
|
||||
}
|
||||
// The write gate alone, with no side effect on the file (export wrappers).
|
||||
"checkWrite" => check_access(s, &path, true).map(|_| json!(true)),
|
||||
"read" | "readBinary" => {
|
||||
let p = check_access(s, &path, false)?;
|
||||
let bytes = s.services.read_file(&p.to_string_lossy()).map_err(|e| format!("Unable to read {path}: {e}"))?;
|
||||
|
||||
@@ -662,6 +662,54 @@ 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()));
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user