From ef694eaf8355570641fbcd389991609a98f527a7 Mon Sep 17 00:00:00 2001 From: Harshit Date: Wed, 19 Nov 2025 17:08:45 +0530 Subject: [PATCH 1/2] Fix clipboard paste validation accepting invalid notes --- package.json | 1 + src/components/EditorHeader/ControlPanel.jsx | 37 ++++++----- src/utils/clipboard.js | 25 ++++++++ tests/clipboard.test.js | 67 ++++++++++++++++++++ 4 files changed, 114 insertions(+), 16 deletions(-) create mode 100644 src/utils/clipboard.js create mode 100644 tests/clipboard.test.js diff --git a/package.json b/package.json index bddd6175..84aabcfb 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", + "test": "node --test", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index cc1968d7..effed40a 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -47,8 +47,6 @@ import { } from "../../data/constants"; import jsPDF from "jspdf"; import { useHotkeys } from "react-hotkeys-hook"; -import { Validator } from "jsonschema"; -import { areaSchema, noteSchema, tableSchema } from "../../data/schemas"; import { db } from "../../data/db"; import { useLayout, @@ -67,6 +65,7 @@ import { } from "../../hooks"; import { enterFullscreen, exitFullscreen } from "../../utils/fullscreen"; import { dataURItoBlob } from "../../utils/utils"; +import { classifyClipboardPayload } from "../../utils/clipboard"; import { IconAddArea, IconAddNote, IconAddTable } from "../../icons"; import LayoutDropdown from "./LayoutDropdown"; import Sidesheet from "./SideSheet/Sidesheet"; @@ -710,29 +709,35 @@ export default function ControlPanel({ } catch (error) { return; } - const v = new Validator(); - console.log(obj); - if (v.validate(obj, tableSchema).valid) { + + const clipboardEntity = classifyClipboardPayload(obj); + if (!clipboardEntity) { + return; + } + + const { payload } = clipboardEntity; + + if (clipboardEntity.type === "table") { addTable({ table: { - ...obj, - x: obj.x + 20, - y: obj.y + 20, + ...payload, + x: payload.x + 20, + y: payload.y + 20, id: nanoid(), }, }); - } else if (v.validate(obj, areaSchema).valid) { + } else if (clipboardEntity.type === "area") { addArea({ - ...obj, - x: obj.x + 20, - y: obj.y + 20, + ...payload, + x: payload.x + 20, + y: payload.y + 20, id: areas.length, }); - } else if (v.validate(obj, noteSchema)) { + } else if (clipboardEntity.type === "note") { addNote({ - ...obj, - x: obj.x + 20, - y: obj.y + 20, + ...payload, + x: payload.x + 20, + y: payload.y + 20, id: notes.length, }); } diff --git a/src/utils/clipboard.js b/src/utils/clipboard.js new file mode 100644 index 00000000..21f0bbcf --- /dev/null +++ b/src/utils/clipboard.js @@ -0,0 +1,25 @@ +import { Validator } from "jsonschema"; +import { tableSchema, areaSchema, noteSchema } from "../data/schemas"; + +const validator = new Validator(); + +const schemaMap = [ + { type: "table", schema: tableSchema }, + { type: "area", schema: areaSchema }, + { type: "note", schema: noteSchema }, +]; + +export function classifyClipboardPayload(payload) { + if (!payload || typeof payload !== "object") { + return null; + } + + for (const { type, schema } of schemaMap) { + if (validator.validate(payload, schema).valid) { + return { type, payload }; + } + } + + return null; +} + diff --git a/tests/clipboard.test.js b/tests/clipboard.test.js new file mode 100644 index 00000000..b9761b94 --- /dev/null +++ b/tests/clipboard.test.js @@ -0,0 +1,67 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { classifyClipboardPayload } from "../src/utils/clipboard.js"; + +const baseField = { + id: "fld_1", + name: "id", + type: "INTEGER", + default: "", + check: "", + primary: true, + unique: true, + notNull: true, + increment: true, + comment: "", +}; + +test("classifies a valid table payload", () => { + const payload = { + id: "tbl_1", + name: "users", + x: 0, + y: 0, + fields: [baseField], + comment: "", + indices: [], + color: "#000000", + }; + + const result = classifyClipboardPayload(payload); + assert.ok(result); + assert.equal(result.type, "table"); + assert.equal(result.payload, payload); +}); + +test("classifies a valid note payload", () => { + const payload = { + id: 0, + x: 10, + y: 10, + title: "Note", + content: "", + color: "#ffffff", + height: 80, + }; + + const result = classifyClipboardPayload(payload); + assert.ok(result); + assert.equal(result.type, "note"); +}); + +test("rejects invalid note payloads", () => { + const payload = { + id: 0, + title: "Broken note", + }; + + const result = classifyClipboardPayload(payload); + assert.equal(result, null); +}); + +test("returns null for non-object values", () => { + assert.equal(classifyClipboardPayload(null), null); + assert.equal(classifyClipboardPayload("string"), null); + assert.equal(classifyClipboardPayload(42), null); +}); + From d1d550518e6b3c8f9e3ba14e7a01189b6dd1889b Mon Sep 17 00:00:00 2001 From: Harshit Date: Wed, 19 Nov 2025 18:06:03 +0530 Subject: [PATCH 2/2] Fix toolbar read-only guards --- src/components/EditorHeader/ControlPanel.jsx | 9 +++++---- src/utils/clipboard.js | 2 +- src/utils/permissions.js | 9 +++++++++ tests/permissions.test.js | 17 +++++++++++++++++ 4 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 src/utils/permissions.js create mode 100644 tests/permissions.test.js diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index effed40a..b4858133 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -66,6 +66,7 @@ import { import { enterFullscreen, exitFullscreen } from "../../utils/fullscreen"; import { dataURItoBlob } from "../../utils/utils"; import { classifyClipboardPayload } from "../../utils/clipboard"; +import { canMutateDiagram } from "../../utils/permissions"; import { IconAddArea, IconAddNote, IconAddTable } from "../../icons"; import LayoutDropdown from "./LayoutDropdown"; import Sidesheet from "./SideSheet/Sidesheet"; @@ -621,7 +622,7 @@ export default function ControlPanel({ } }; const del = () => { - if (layout.readonly) { + if (!canMutateDiagram(layout)) { return; } switch (selectedElement.element) { @@ -639,7 +640,7 @@ export default function ControlPanel({ } }; const duplicate = () => { - if (layout.readonly) { + if (!canMutateDiagram(layout)) { return; } switch (selectedElement.element) { @@ -699,7 +700,7 @@ export default function ControlPanel({ } }; const paste = () => { - if (layout.readonly) { + if (!canMutateDiagram(layout)) { return; } navigator.clipboard.readText().then((text) => { @@ -744,7 +745,7 @@ export default function ControlPanel({ }); }; const cut = () => { - if (layout.readonly) { + if (!canMutateDiagram(layout)) { return; } copy(); diff --git a/src/utils/clipboard.js b/src/utils/clipboard.js index 21f0bbcf..47cfae6d 100644 --- a/src/utils/clipboard.js +++ b/src/utils/clipboard.js @@ -1,5 +1,5 @@ import { Validator } from "jsonschema"; -import { tableSchema, areaSchema, noteSchema } from "../data/schemas"; +import { tableSchema, areaSchema, noteSchema } from "../data/schemas.js"; const validator = new Validator(); diff --git a/src/utils/permissions.js b/src/utils/permissions.js new file mode 100644 index 00000000..2e5dfc30 --- /dev/null +++ b/src/utils/permissions.js @@ -0,0 +1,9 @@ +export function canMutateDiagram(layout) { + if (!layout) { + return true; + } + + return !layout.readOnly; +} + + diff --git a/tests/permissions.test.js b/tests/permissions.test.js new file mode 100644 index 00000000..3510d7ab --- /dev/null +++ b/tests/permissions.test.js @@ -0,0 +1,17 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { canMutateDiagram } from "../src/utils/permissions.js"; + +test("allows mutations when layout is undefined", () => { + assert.equal(canMutateDiagram(undefined), true); +}); + +test("blocks mutations when readOnly flag is true", () => { + assert.equal(canMutateDiagram({ readOnly: true }), false); +}); + +test("allows mutations when readOnly flag is false", () => { + assert.equal(canMutateDiagram({ readOnly: false }), true); +}); + +