From 4fe73011ae287fd52b3baba648d0bd4aa4c2f8de Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Mon, 26 Oct 2026 12:21:58 +0100 Subject: [PATCH] :bug: Reject restoring components of a linked library from plugins (#12611) LibraryComponent.restore() did not check that the component belongs to the current file's library. Called on a deleted component of a connected shared library, it restored that component and saved the change to the library file, while remove(), annotation and duplicate() reject such handles with a validation error. restore() now rejects them the same way: "Can only restore components from the current file's library". Closes #12607 --- frontend/src/app/plugins/library.cljs | 3 +++ .../frontend_tests/plugins/library_test.cljs | 24 +++++++++++++++---- plugins/CHANGELOG.md | 2 +- plugins/libs/plugin-types/index.d.ts | 3 ++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs index 803cca9ffd..aff38a654d 100644 --- a/frontend/src/app/plugins/library.cljs +++ b/frontend/src/app/plugins/library.cljs @@ -917,6 +917,9 @@ (not (r/check-permission plugin-id "library:write")) (u/not-valid plugin-id :restore "Plugin doesn't have 'library:write' permission") + (not= file-id (:current-file-id @st/state)) + (u/not-valid plugin-id :restore "Can only restore components from the current file's library") + (nil? component) (u/not-valid plugin-id :restore "Component does not exist") diff --git a/frontend/test/frontend_tests/plugins/library_test.cljs b/frontend/test/frontend_tests/plugins/library_test.cljs index e7e9efb646..839c25e7a7 100644 --- a/frontend/test/frontend_tests/plugins/library_test.cljs +++ b/frontend/test/frontend_tests/plugins/library_test.cljs @@ -55,13 +55,10 @@ (cthf/apply-changes file changes))) (t/deftest ^:async component-restore-preserves-the-users-view - (doseq [scenario [:same-page :other-page :linked-library]] + (doseq [scenario [:same-page :other-page]] (let [file (cond-> (file-with-deleted-component) (= scenario :other-page) (cthf/add-sample-page :other-page)) - store (if (= scenario :linked-library) - (ths/setup-store (cthf/sample-file :local-file) - {:renderer :svg :libraries [file]}) - (ths/setup-store file {:renderer :svg})) + store (ths/setup-store file {:renderer :svg}) view {:selected #{(thi/id :copy)} :zoom 2 :vport {:x 100 :y 200 :width 800 :height 600} @@ -108,6 +105,23 @@ (t/is (empty? @events)) (t/is (true? (get-in @store [:files (:id file) :data :components (thi/id :restorable) :deleted])))))))) +(t/deftest ^:async component-restore-rejects-linked-libraries + (let [library (file-with-deleted-component) + file (cthf/sample-file :consumer-file) + store (ths/setup-store file {:renderer :svg :libraries [library]}) + before @store + events (atom [])] + (ptk/emit! store #(assoc-in % [:plugins :flags plugin-id :throw-validation-errors] true)) + (await + (mock/with-mocks* + {st/state store + st/emit! (mock/stub (fn [& emitted] (swap! events into emitted)))} + (let [component (library/lib-component-proxy plugin-id (:id library) (thi/id :restorable))] + (t/is (thrown-with-msg? js/Error #"current file" (.restore component))) + (await (async/settle)) + (t/is (empty? @events)) + (t/is (= (:files before) (:files @store)))))))) + (t/deftest ^:async component-restore-rejects-a-missing-component (let [file (cthf/sample-file :restore-file) store (ths/setup-store file {:renderer :svg})] diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 3ea8fd12c9..de0f09ed73 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -2,7 +2,7 @@ ### 🚀 Features -- **plugin-types:** Added `LibraryComponent.restore()` to restore a deleted component and its main instance without changing the current page, selection, zoom, or viewport. Requires `library:write` permission. +- **plugin-types:** Added `LibraryComponent.restore()` to restore a deleted component and its main instance without changing the current page, selection, zoom, or viewport. Requires `library:write` permission and only applies to the current file's library. - **plugin-types:** Added `Shape.updateMain()`, `LibraryComponent.duplicate()`, writable component `annotation`, and `Library.deletedComponents`. Declared the existing `LibraryComponent.remove()` method. - **plugin-types:** Added `LibraryContext.tokensSource` to read or set the current file's token source to a connected library, or restore local tokens. - **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently. diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index 6d5e8c5408..c756cc50b4 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -2791,7 +2791,8 @@ export interface LibraryComponent extends LibraryElement { remove(): void; /** - * Restores this component if it has been deleted, including its main instance. + * Restores this component if it has been deleted, including its main instance, + * in the current file's library. * Leaves the current page, selection, zoom, and viewport unchanged. * Requires `library:write` permission. Does nothing if already restored. *