mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-29 05:06:51 -06:00
Engine: document trust for workbooks opened from files
Active content such as macros must not run only because a workbook was opened. DocState gets a trust state: Local for workbooks made in the session (new workbooks and samples) and FromFile for anything opened with file.open. Active content in a FromFile workbook asks before it runs. A second window on a workbook shares its trust. The clipboard carries the trust of its source document. Pasting formulas, formats (conditional formats hold formulas) or validation from a FromFile workbook into another workbook makes that workbook FromFile too; pasting values, column widths or notes carries nothing active and changes nothing. Sheet moves and copies stay inside one workbook, so its trust is unchanged. document.inspect reports the trust.
This commit is contained in:
@@ -676,7 +676,7 @@ fn copy_impl(s: &mut Session, p: &Json, cut: bool) -> Result<Json> {
|
||||
// The HTML rendering is for the host clipboard (UI); it can reach 4 MB, so agents and
|
||||
// scripts get it only when they ask for it.
|
||||
let html = if p.get("html").and_then(Json::as_bool) == Some(true) { crate::io::range_html(&wb, sheet, range, &text) } else { None };
|
||||
let clip = Clipboard { wb, sheet, range, cut, doc_uid: d.uid, text: text.clone(), areas, selected };
|
||||
let clip = Clipboard { wb, sheet, range, cut, doc_uid: d.uid, text: text.clone(), areas, selected, trust: d.trust };
|
||||
s.clipboard = Some(clip);
|
||||
let mut result = json!({"range": range.a1(), "text": text});
|
||||
if let Some(html) = html {
|
||||
@@ -1183,6 +1183,12 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
*cx.sel = crate::Selection { active: at, anchor: at, ranges: vec![RangeRef::new(at, end)] };
|
||||
Ok(())
|
||||
})?;
|
||||
// Formulas, formats (conditional formats hold formulas) and validation from an untrusted file
|
||||
// leave the destination untrusted; plain values, column widths and notes carry nothing active.
|
||||
if !same_doc && !matches!(what, What::Values | What::ValuesAndNumberFormats | What::ColumnWidths | What::Comments) {
|
||||
let d = s.doc_mut()?;
|
||||
d.trust = d.trust.receive(clip.trust);
|
||||
}
|
||||
if cut_move {
|
||||
s.clipboard = None;
|
||||
}
|
||||
|
||||
@@ -782,6 +782,7 @@ fn new_window(s: &mut Session, _: &Json) -> Result<Json> {
|
||||
let d = s.doc()?.clone();
|
||||
let mut copy = crate::DocState::new((*d.wb).clone(), d.path.clone(), format!("{}:2", d.display_title()));
|
||||
copy.selection = d.selection.clone();
|
||||
copy.trust = d.trust;
|
||||
let i = s.add_document(copy);
|
||||
Ok(json!({"index": i}))
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ fn open(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
if crate::io::FileKind::from_path(&name).is_none() && gridcraft_xlsx::sniff(&bytes) == gridcraft_xlsx::Format::Xlsx {
|
||||
doc.content_kind = Some(crate::io::FileKind::Xlsx);
|
||||
}
|
||||
// Someone else may have made the file: its active content asks before it runs.
|
||||
doc.trust = crate::Trust::FromFile;
|
||||
let i = s.add_document(doc);
|
||||
// The decrypted-workbook notice is for the info bar, not a message box.
|
||||
let shown: Vec<&str> = warnings.iter().map(String::as_str).filter(|w| *w != crate::io::DECRYPTED_NOTICE).collect();
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::*;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(query "document.inspect", "Inspect Workbook", [], None, "{} → sheets, selection, names, tables, charts, dirty, history", has_doc, inspect),
|
||||
cmd!(query "document.inspect", "Inspect Workbook", [], None, "{} → sheets, selection, names, tables, charts, dirty, trust (local|fromFile), history", has_doc, inspect),
|
||||
cmd!(query "sheet.read", "Read Range", [], None, "{range?: \"A1:D10\" (default: used range), sheet?, formulas?: bool, formatted?: bool} → rows of values", has_doc, read),
|
||||
cmd!(query "cell.get", "Get Cell", [], None, "{cell?, sheet?} → value, formula, display text, style, comment, link", has_doc, cell_get),
|
||||
cmd!(query "selection.stats", "Selection Statistics", [], None, "{} → count, sum, average, min, max", has_doc, sel_stats),
|
||||
@@ -63,6 +63,7 @@ fn inspect(s: &mut Session, _: &Json) -> Result<Json> {
|
||||
"title": d.display_title(),
|
||||
"path": d.path,
|
||||
"dirty": d.is_dirty(),
|
||||
"trust": d.trust,
|
||||
"activeSheet": wb.active_sheet,
|
||||
"sheets": sheets,
|
||||
"selection": d.selection.a1(),
|
||||
|
||||
@@ -112,6 +112,33 @@ pub struct Clipboard {
|
||||
/// A multi-area copy: the areas as selected (for the marquee). `wb` then holds them slid
|
||||
/// together into one block at `range`. Empty for a single-range copy.
|
||||
pub areas: Vec<RangeRef>,
|
||||
/// Trust of the source document: pasting content from a file someone else made carries it.
|
||||
pub trust: Trust,
|
||||
}
|
||||
|
||||
/// Whether a document's active content (macros) may run without asking the person first.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Trust {
|
||||
/// Made in this session: a new workbook or a sample.
|
||||
#[default]
|
||||
Local,
|
||||
/// Opened from a file, or holding content pasted from one: active content asks before it
|
||||
/// runs.
|
||||
FromFile,
|
||||
}
|
||||
|
||||
impl Trust {
|
||||
/// Whether active content may run without asking.
|
||||
pub fn allows_code(self) -> bool {
|
||||
self == Trust::Local
|
||||
}
|
||||
|
||||
/// The trust of a document after it receives content from a document with trust `source`:
|
||||
/// content from an untrusted file leaves the receiver untrusted.
|
||||
pub fn receive(self, source: Trust) -> Trust {
|
||||
if source == Trust::FromFile { Trust::FromFile } else { self }
|
||||
}
|
||||
}
|
||||
|
||||
/// In-cell edit mode as shown in the status bar.
|
||||
@@ -143,6 +170,8 @@ pub struct DocState {
|
||||
/// The format `path`'s content was recognised as when its extension doesn't name one (an
|
||||
/// XLSX called `budget.dat`): Save writes it back in that format.
|
||||
pub content_kind: Option<io::FileKind>,
|
||||
/// Whether the workbook's active content may run without asking.
|
||||
pub trust: Trust,
|
||||
}
|
||||
|
||||
static NEXT_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
@@ -168,6 +197,7 @@ impl DocState {
|
||||
sheet_selections: vec![],
|
||||
unlocked_ranges: vec![],
|
||||
content_kind: None,
|
||||
trust: Trust::Local,
|
||||
}
|
||||
}
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use serde_json::json;
|
||||
|
||||
use super::{s, v};
|
||||
use crate::{EngineError, Session};
|
||||
use crate::{EngineError, Session, Trust};
|
||||
|
||||
fn undo_labels(s: &Session) -> Vec<String> {
|
||||
s.doc().unwrap().undo.iter().map(|e| e.label.clone()).collect()
|
||||
@@ -293,3 +293,68 @@ fn record_actions_is_unchanged_by_the_buffer() {
|
||||
assert_eq!(r["steps"], 1);
|
||||
assert!(s.recorder.is_none());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- trust
|
||||
|
||||
fn open_json(s: &mut Session, wb: &gridcraft_model::Workbook) {
|
||||
let bytes = serde_json::to_vec(wb).unwrap();
|
||||
s.execute("file.open", json!({"name": "theirs.json", "base64": crate::io::base64_encode(&bytes)})).unwrap();
|
||||
}
|
||||
|
||||
fn trust(s: &Session) -> Trust {
|
||||
s.doc().unwrap().trust
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documents_know_where_they_came_from() {
|
||||
let mut s = s();
|
||||
assert_eq!(trust(&s), Trust::Local);
|
||||
assert!(Trust::Local.allows_code() && !Trust::FromFile.allows_code());
|
||||
s.execute("file.new", json!({"sample": "sales"})).unwrap();
|
||||
assert_eq!(trust(&s), Trust::Local);
|
||||
assert_eq!(s.execute("document.inspect", json!({})).unwrap()["trust"], "local");
|
||||
open_json(&mut s, &gridcraft_model::Workbook::new());
|
||||
assert_eq!(trust(&s), Trust::FromFile);
|
||||
assert_eq!(s.execute("document.inspect", json!({})).unwrap()["trust"], "fromFile");
|
||||
// A second window on the same workbook is the same workbook.
|
||||
s.execute("view.newWindow", json!({})).unwrap();
|
||||
assert_eq!(trust(&s), Trust::FromFile);
|
||||
// Moving and copying sheets inside it changes nothing.
|
||||
s.execute("sheet.move", json!({"to": 0, "copy": true})).unwrap();
|
||||
assert_eq!(trust(&s), Trust::FromFile);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pasting_from_a_file_carries_its_trust() {
|
||||
let mut s = s(); // document 0: Local
|
||||
open_json(&mut s, &gridcraft_model::Workbook::new()); // document 1: FromFile
|
||||
let theirs = s.active_index();
|
||||
set(&mut s, "A1", "=1+1");
|
||||
s.execute("edit.copy", json!({"range": "A1"})).unwrap();
|
||||
s.new_workbook();
|
||||
let mine = s.active_index();
|
||||
// Values carry nothing that runs.
|
||||
s.execute("edit.pasteSpecial", json!({"what": "values", "at": "B1"})).unwrap();
|
||||
assert_eq!(trust(&s), Trust::Local);
|
||||
s.execute("edit.paste", json!({"at": "C1"})).unwrap();
|
||||
assert_eq!(trust(&s), Trust::FromFile);
|
||||
assert_eq!(v(&s, "C1").display(), "2");
|
||||
|
||||
// Pasting within a workbook changes nothing, and content from a local workbook leaves the
|
||||
// destination as it was.
|
||||
s.set_active(theirs);
|
||||
s.execute("edit.paste", json!({"at": "A3"})).unwrap();
|
||||
assert_eq!(trust(&s), Trust::FromFile, "pasting within a workbook");
|
||||
s.new_workbook();
|
||||
set(&mut s, "A1", "=3");
|
||||
s.execute("edit.copy", json!({"range": "A1"})).unwrap();
|
||||
s.set_active(mine);
|
||||
s.execute("edit.paste", json!({"at": "D1"})).unwrap();
|
||||
assert_eq!(trust(&s), Trust::FromFile, "still untrusted from the earlier paste");
|
||||
s.new_workbook();
|
||||
s.execute("edit.paste", json!({"at": "D1"})).unwrap();
|
||||
assert_eq!(trust(&s), Trust::Local);
|
||||
assert_eq!(Trust::Local.receive(Trust::FromFile), Trust::FromFile);
|
||||
assert_eq!(Trust::FromFile.receive(Trust::Local), Trust::FromFile);
|
||||
assert_eq!(Trust::Local.receive(Trust::Local), Trust::Local);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user