🐛 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
This commit is contained in:
Alonso Torres 2026-10-26 12:21:58 +01:00 committed by GitHub
parent 55ad7d1da8
commit 4fe73011ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 25 additions and 7 deletions

View File

@ -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")

View File

@ -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})]

View File

@ -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.

View File

@ -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.
*