mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-31 22:26:57 -06:00
Merge g3 into g4
# Conflicts: # ROADMAP.md # crates/engine/src/cmd/edit.rs # crates/engine/src/cmd/formulas.rs # crates/engine/src/cmd/inspect.rs # crates/engine/src/cmd/pivot.rs # crates/ui-egui/src/lib.rs # crates/ui-egui/src/tabs.rs # docs/gaps.md # docs/ui-parity.md
This commit is contained in:
@@ -209,8 +209,34 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
new_script
|
||||
),
|
||||
cmd!(query "automate.allScripts", "All Scripts", ["Automate", "Scripting Tools"], None, "{} → saved scripts", has_doc, all_scripts),
|
||||
cmd!("automate.runScript", "Run Script", [], None, "{name}", has_doc, run_script),
|
||||
cmd!(
|
||||
"automate.runScript",
|
||||
"Run Script",
|
||||
[],
|
||||
None,
|
||||
"{name, trusted?: bool (runs a script from an untrusted workbook this once)} — refuses scripts in a workbook opened from a file until it is trusted, and steps that reach outside the workbook (file.*, link.*, any `path` param)",
|
||||
has_doc,
|
||||
run_script
|
||||
),
|
||||
cmd!(
|
||||
noundo "automate.trustScripts",
|
||||
"Trust Scripts",
|
||||
["Automate", "Scripting Tools"],
|
||||
None,
|
||||
"{trusted?: bool (default true)} → {trusted}: lets this workbook's scripts run for the rest of the session",
|
||||
has_doc,
|
||||
trust_scripts
|
||||
),
|
||||
cmd!("automate.deleteScript", "Delete Script", [], None, "{name}", has_doc, delete_script),
|
||||
cmd!(
|
||||
noundo "automate.stopAction",
|
||||
"Stop Action",
|
||||
[],
|
||||
None,
|
||||
"{} → {stopped: bool}: stops an action left running on the active workbook (an open undo group, such as a debugger's) and puts back the workbook and its undo history as they were when it began",
|
||||
has_doc,
|
||||
stop_action
|
||||
),
|
||||
// Cell controls, themes, borders, styles
|
||||
cmd!(
|
||||
"insert.checkbox",
|
||||
@@ -955,7 +981,7 @@ fn from_csv(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
return Err(EngineError::Other("Nothing to import.".into()));
|
||||
}
|
||||
let name = str_param(p, "sheet").map(str::to_string).or_else(|| {
|
||||
str_param(p, "path").and_then(|x| std::path::Path::new(x).file_stem().and_then(|f| f.to_str()).map(|f| f.chars().take(31).collect()))
|
||||
str_param(p, "path").and_then(|x| std::path::Path::new(x).file_stem().and_then(std::ffi::OsStr::to_str).map(|f| f.chars().take(31).collect()))
|
||||
});
|
||||
let r = s.execute("home.insertSheet", json!({"name": name})).or_else(|_| s.execute("home.insertSheet", json!({})))?;
|
||||
let w = rows.iter().map(Vec::len).max().unwrap_or(1).max(1);
|
||||
@@ -1017,10 +1043,13 @@ fn new_script(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
}
|
||||
|
||||
fn all_scripts(s: &mut Session, _: &Json) -> Result<Json> {
|
||||
let wb = &s.doc()?.wb;
|
||||
let d = s.doc()?;
|
||||
let (wb, trusted) = (&d.wb, d.scripts_trusted);
|
||||
let all = stash_get(wb, SCRIPTS_NAME);
|
||||
let mut list: Vec<Json> =
|
||||
all.as_object().map(|o| o.iter().map(|(k, v)| json!({"name": k, "steps": v.as_array().map_or(0, Vec::len)})).collect()).unwrap_or_default();
|
||||
let mut list: Vec<Json> = all
|
||||
.as_object()
|
||||
.map(|o| o.iter().map(|(k, v)| json!({"name": k, "steps": v.as_array().map_or(0, Vec::len), "trusted": trusted})).collect())
|
||||
.unwrap_or_default();
|
||||
// A preserved VBA project is reported alongside GridCraft's own scripts; it is carried on
|
||||
// save but never executed here, so it has no step count.
|
||||
if let Some(bytes) = &wb.vba_project {
|
||||
@@ -1029,22 +1058,96 @@ fn all_scripts(s: &mut Session, _: &Json) -> Result<Json> {
|
||||
Ok(Json::Array(list))
|
||||
}
|
||||
|
||||
/// Whether `wb` carries GridCraft scripts (a workbook opened with them starts untrusted).
|
||||
pub(crate) fn has_scripts(wb: &Workbook) -> bool {
|
||||
stash_map(wb, SCRIPTS_NAME).as_object().is_some_and(|o| !o.is_empty())
|
||||
}
|
||||
|
||||
/// Why a script step is refused, whatever the workbook's trust: scripts stay inside the
|
||||
/// workbook. Anything that reads or writes files (`file.*`, or any `path` parameter: Get Data,
|
||||
/// From Text/CSV, Insert Picture) or leaves the app (`link.*` opens URLs) is the caller's to run
|
||||
/// directly, not a stored script's. (Nested `automate.*` steps are skipped.) This check goes by
|
||||
/// command id and parameter name, so it is only the up-front refusal: the steps also run inside
|
||||
/// an [`crate::io::ScriptSandbox`] (file access refused whatever the parameter is called) and
|
||||
/// [`run_script`] drops any `OpenUrl` request a step raises.
|
||||
fn denied_script_step(id: &str, params: &Json) -> Option<String> {
|
||||
if id.starts_with("file.") || id.starts_with("link.") {
|
||||
return Some(format!("scripts can't run `{id}`: it reaches outside the workbook"));
|
||||
}
|
||||
// Scripts are scoped to their workbook: a step that switches the active window would
|
||||
// point every later step at another open document, so those are refused too.
|
||||
if id.starts_with("window.") || matches!(id, "view.switchWindows" | "view.newWindow" | "view.hideWindow" | "view.unhideWindow") {
|
||||
return Some(format!("scripts can't run `{id}`: it reaches outside the workbook"));
|
||||
}
|
||||
if params.get("path").is_some() {
|
||||
return Some(format!("scripts can't pass a file `path` to `{id}`"));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn trust_scripts(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
// Only a missing `trusted` means "trust"; `"false"`, `0` or `null` are mistakes, not consent.
|
||||
let trusted = match p.get("trusted") {
|
||||
None => true,
|
||||
Some(Json::Bool(b)) => *b,
|
||||
Some(_) => return Err(bad("automate.trustScripts", "`trusted` must be true or false")),
|
||||
};
|
||||
s.doc_mut()?.scripts_trusted = trusted;
|
||||
Ok(json!({"trusted": trusted}))
|
||||
}
|
||||
|
||||
/// Runs a stored script. Scripts in a workbook opened from a file run only once the workbook is
|
||||
/// trusted for this session (`automate.trustScripts`, or `trusted: true` on this call); steps that
|
||||
/// reach outside the workbook ([`denied_script_step`]) are refused before any step runs.
|
||||
fn run_script(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("automate.runScript", "missing `name`"))?;
|
||||
let all = stash_get(&s.doc()?.wb, SCRIPTS_NAME);
|
||||
let d = s.doc()?;
|
||||
let all = stash_get(&d.wb, SCRIPTS_NAME);
|
||||
let steps = all.get(name).and_then(Json::as_array).cloned().ok_or_else(|| bad("automate.runScript", "no such script"))?;
|
||||
if !d.scripts_trusted && !bool_param(p, "trusted").unwrap_or(false) {
|
||||
return Err(bad(
|
||||
"automate.runScript",
|
||||
"scripts in this workbook came from a file and are disabled; trust the workbook first (Trust Scripts in View › Macros, Alt+F8; `automate.trustScripts`; or `trusted: true`)",
|
||||
));
|
||||
}
|
||||
let steps: Vec<Json> = steps.into_iter().take(100_000).collect();
|
||||
for st in &steps {
|
||||
let id = st["command"].as_str().unwrap_or("");
|
||||
if let Some(why) = denied_script_step(id, &st["params"]) {
|
||||
return Err(bad("automate.runScript", why));
|
||||
}
|
||||
}
|
||||
let _sandbox = crate::io::ScriptSandbox::enter();
|
||||
let mut n = 0;
|
||||
for st in steps.iter().take(100_000) {
|
||||
for st in &steps {
|
||||
let id = st["command"].as_str().unwrap_or("");
|
||||
if id.starts_with("automate.") {
|
||||
continue;
|
||||
}
|
||||
s.execute(id, st["params"].clone())?;
|
||||
let before = s.ui_requests.len();
|
||||
let ran = s.execute(id, st["params"].clone());
|
||||
// A step that asks the host to open a URL leaves the app: drop the request and stop.
|
||||
if s.ui_requests.get(before..).is_some_and(|new| new.iter().any(|r| matches!(r, crate::UiRequest::OpenUrl(_)))) {
|
||||
let mut i = 0;
|
||||
s.ui_requests.retain(|r| {
|
||||
i += 1;
|
||||
i <= before || !matches!(r, crate::UiRequest::OpenUrl(_))
|
||||
});
|
||||
return Err(bad("automate.runScript", format!("scripts can't open links (`{id}`)")));
|
||||
}
|
||||
ran?;
|
||||
n += 1;
|
||||
}
|
||||
Ok(json!({"ran": n}))
|
||||
}
|
||||
|
||||
/// Automate › Stop Action: rolls back the undo groups open on the active workbook, so Undo and
|
||||
/// Redo work there again.
|
||||
fn stop_action(s: &mut Session, _: &Json) -> Result<Json> {
|
||||
let uid = s.doc()?.uid;
|
||||
Ok(json!({"stopped": s.abort_groups(uid)}))
|
||||
}
|
||||
|
||||
fn delete_script(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("automate.deleteScript", "missing `name`"))?.to_string();
|
||||
edit(s, |cx| {
|
||||
@@ -1207,7 +1310,7 @@ fn recently_used(s: &mut Session, _: &Json) -> Result<Json> {
|
||||
|
||||
fn use_in_formula(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("formulas.useInFormula", "missing `name`"))?;
|
||||
s.ui_requests.push(crate::UiRequest::EditCell(Some(format!("={name}"))));
|
||||
s.ui_requests.push(crate::UiRequest::InsertName(name.to_string()));
|
||||
ok()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user