mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 00:41:56 -06:00
Protection: authorize the sheet a column move changes, and refuse edits that unlock locked cells (#3196)
- sheet.moveColumns resolved its target sheet from `sheet`, or a `Sheet!` range or cell, but its two protection checks were passed only the `sheet` parameter, so a qualified range moved columns of a protected sheet while the checks authorized the active one. Both checks now get the resolved sheet index. - check_protection compared only cell content, so a sort or duplicate removal moving an unlocked cell with equal content onto a locked one left the content as it was but made the cell editable. The commit check now also refuses a change that leaves a previously locked cell unlocked (effective lock, row and column formats included).
This commit is contained in:
@@ -459,8 +459,11 @@ pub(crate) const TOUCH_LOG_LIMIT: usize = 1_000_000;
|
||||
pub(crate) const PROTECTED: &str = "The cell or chart you're trying to change is on a protected sheet.";
|
||||
|
||||
/// Refuses an edit that changed what a locked cell on a protected sheet holds (its value or
|
||||
/// formula), whichever command made it. Sheets are matched by name, so moving, renaming or
|
||||
/// deleting a sheet isn't an edit of its cells.
|
||||
/// formula), or that unlocked it, whichever command made it. Sheets are matched by name, so
|
||||
/// moving, renaming or deleting a sheet isn't an edit of its cells. A sort or duplicate removal
|
||||
/// that moves an unlocked cell with equal content onto a locked one changes no content but would
|
||||
/// leave the cell editable, so the effective lock (row and column formats included) is compared
|
||||
/// as well.
|
||||
fn check_protection(old: &Workbook, new: &Workbook, unlocked: &[(String, String)]) -> Result<()> {
|
||||
for sh in &old.sheets {
|
||||
if !sh.is_protected() {
|
||||
@@ -471,7 +474,8 @@ fn check_protection(old: &Workbook, new: &Workbook, unlocked: &[(String, String)
|
||||
continue;
|
||||
}
|
||||
for c in sh.cells.diff(&after.cells) {
|
||||
if !same_content(sh.cell(c), after.cell(c))
|
||||
let unlocked_now = !new.styles.get(after.style_id(c)).protection.locked;
|
||||
if (unlocked_now || !same_content(sh.cell(c), after.cell(c)))
|
||||
&& old.styles.get(sh.style_id(c)).protection.locked
|
||||
&& let Some(m) = locked_cell_error(sh, c, unlocked)
|
||||
{
|
||||
|
||||
@@ -398,10 +398,10 @@ fn move_columns(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
{
|
||||
return Err(EngineError::Other("To prevent possible loss of data, columns cannot be shifted off of the worksheet.".into()));
|
||||
}
|
||||
// The checks must authorize the sheet this command modifies (`target_sheet`), not whatever
|
||||
// sheet is active: both carry the command's own `sheet` parameter on.
|
||||
check_lines_protection(s, &json!({"cols": RangeRef::cols(before, before).a1(), "sheet": p.get("sheet")}), Axis::Cols, before, count, true)?;
|
||||
check_lines_protection(s, &json!({"cols": RangeRef::cols(from, end - 1).a1(), "sheet": p.get("sheet")}), Axis::Cols, from, count, false)?;
|
||||
// The checks must authorize the sheet this command modifies, not whatever sheet is active:
|
||||
// both get the index `target_sheet` resolved (from `sheet`, or a `Sheet!` range or cell).
|
||||
check_lines_protection(s, &json!({"cols": RangeRef::cols(before, before).a1(), "sheet": sheet}), Axis::Cols, before, count, true)?;
|
||||
check_lines_protection(s, &json!({"cols": RangeRef::cols(from, end - 1).a1(), "sheet": sheet}), Axis::Cols, from, count, false)?;
|
||||
|
||||
let moved_from = if before <= from { from + count } else { from };
|
||||
if moved_from.checked_add(count).is_none_or(|end| end > MAX_COLS) {
|
||||
|
||||
@@ -5297,6 +5297,42 @@ fn move_columns_checks_the_protection_of_the_named_sheet() {
|
||||
assert_eq!(v(&s, "A1"), Value::text("A"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_columns_checks_the_protection_of_a_sheet_named_by_its_range() {
|
||||
// A `Sheet!` range or cell picks the sheet the move modifies; the protection checks must
|
||||
// authorize that sheet too, not the active one.
|
||||
let mut s = s();
|
||||
s.execute("home.insertSheet", json!({"name": "P"})).unwrap();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1, 2, 3, 4]]})).unwrap();
|
||||
s.execute("review.protectSheet", json!({})).unwrap();
|
||||
s.execute("sheet.activate", json!({"sheet": 0})).unwrap();
|
||||
for key in ["range", "cell"] {
|
||||
let err = s.execute("sheet.moveColumns", json!({key: "P!A1", "cols": "B:C", "before": 4})).expect_err("moving columns on a protected sheet");
|
||||
assert!(err.to_string().contains("protected"), "{err}");
|
||||
}
|
||||
let d = s.doc().unwrap();
|
||||
let sh = &d.wb.sheets[d.wb.sheet_index("P").unwrap()];
|
||||
let pv: Vec<Value> = (0..4).map(|c| sh.value(CellRef::new(0, c))).collect();
|
||||
assert_eq!(pv, vec![Value::number(1.0), Value::number(2.0), Value::number(3.0), Value::number(4.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sorting_cannot_unlock_a_locked_cell_by_moving_equal_content_onto_it() {
|
||||
// An unlocked 2 sorted onto a locked 2 leaves the content as it was, but would make the cell
|
||||
// editable; the sort is refused instead.
|
||||
let mut s = s();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[2], [2], [1]]})).unwrap();
|
||||
s.execute("home.lockCell", json!({"range": "A1", "on": false})).unwrap();
|
||||
s.execute("home.lockCell", json!({"range": "A3", "on": false})).unwrap();
|
||||
s.execute("review.protectSheet", json!({"sort": true})).unwrap();
|
||||
let before = s.doc().unwrap().wb.clone();
|
||||
let err =
|
||||
s.execute("data.sort", json!({"range": "A1:A3", "header": false, "keys": [{"column": "A", "order": "asc"}]})).expect_err("sort unlocking A2");
|
||||
assert!(err.to_string().contains("protected sheet"), "{err}");
|
||||
assert_eq!(*s.doc().unwrap().wb, *before);
|
||||
assert!(s.execute("cell.set", json!({"cell": "A2", "input": "9"})).is_err(), "A2 stays locked");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_script_cannot_switch_windows() {
|
||||
// Stored scripts are scoped to their workbook: a step that activates another window would
|
||||
|
||||
Reference in New Issue
Block a user