diff --git a/crates/engine/src/plugins.rs b/crates/engine/src/plugins.rs index f1c4075e..ce5aa0c6 100644 --- a/crates/engine/src/plugins.rs +++ b/crates/engine/src/plugins.rs @@ -111,6 +111,10 @@ const DENIED: &[&str] = &[ "edit.redo", "edit.repeat", "command.batch", + // Network sessions, online files and their recovery copies on disk. + "collab.", + // Opens files in new windows. + "window.", "view.theme", ]; /// Read-only exceptions to [`DENIED`]. @@ -128,6 +132,17 @@ fn refusal(m: &Manifest, command: &str, params: &Value) -> Option { if command == "image.place" && get(params, "path").is_some() { return Some("plugins can't read files: pass the image as `data`".into()); } + // `path` here is a file on disk (elsewhere it is a vector path or an action's place). + if matches!(command, "variable.export" | "codeConnect.export" | "component.playground") && get(params, "path").is_some() { + return Some("plugins can't write files: leave out `path` to get the result back".into()); + } + if command == "codeConnect.import" && get(params, "path").is_some() { + return Some("plugins can't read files: pass the connections as `json`".into()); + } + // Variables JSON may be given as a path: only objects and JSON text for plugins. + if command == "variable.import" && get(params, "variables").and_then(Value::as_str).is_some_and(|t| !t.trim_start().starts_with('{')) { + return Some("plugins can't read files: pass the variables JSON itself".into()); + } if !m.permissions.allows(command) { return Some(format!("`{command}` isn't in the commands {} asked permission for", m.name)); } diff --git a/crates/engine/src/plugins_tests.rs b/crates/engine/src/plugins_tests.rs index 102d675a..657b6825 100644 --- a/crates/engine/src/plugins_tests.rs +++ b/crates/engine/src/plugins_tests.rs @@ -169,6 +169,13 @@ fn files_dialogs_history_and_plugins_are_off_limits() { ("plugin.remove", json!({"id": "org.test.deny"})), ("edit.undo", json!({})), ("command.batch", json!({"commands": [{"command": "file.saveAs", "params": {"path": target}}]})), + ("variable.export", json!({"format": "css", "path": target})), + ("codeConnect.export", json!({"path": target})), + ("codeConnect.import", json!({"path": "/etc/passwd"})), + ("component.playground", json!({"id": a, "path": target})), + ("variable.import", json!({"variables": "/etc/passwd"})), + ("collab.recoverFile", json!({"path": "copy.figcraft", "file": "x", "session": 1, "client": 1, "seq": 0})), + ("window.new", json!({"path": "C:/Windows/win.ini"})), ("doc.switch", json!({"index": 0})), ("pref.keepTool", json!({})), // Without a name it would open inline renaming in the layers panel: a UI request. diff --git a/docs/plugins.md b/docs/plugins.md index 6f23e6ec..5867bd25 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -237,7 +237,11 @@ Whatever its manifest says, FigCraft refuses these calls (the plugin gets the re it can handle): - files and the app: `file.*`, `app.*`, `pref.*`, `help.*`, `library.add`, `libraries.*`, - `image.place` with a `path` (pass the image as base64 `data` instead), `view.theme*`; + `image.place` with a `path` (pass the image as base64 `data` instead), `variable.export`, + `codeConnect.export` and `component.playground` with a `path` (leave it out to get the result + back), `codeConnect.import` with a `path` and `variable.import` with a file path instead of JSON, + `window.*` (new windows open files), `view.theme*`; +- live sessions and online files: `collab.*`; - other documents and the version history: `doc.*` and `version.*` (except `doc.list` and `version.list`); - the undo history and nesting: `edit.undo`, `edit.redo`, `edit.repeat`, `command.batch` (a run is