mirror of
https://github.com/penpot/penpot.git
synced 2026-08-14 16:58:35 +00:00
* 🐛 Fix nil getData crash dropping ZIP without manifest.json Add nil-guard in read-as-text to raise typed :invalid-entry error instead of calling (.getData nil writer) which produced a raw TypeError. Made read-zip-manifest public (was defn-) with explicit detection of missing manifest.json, raising typed :invalid-penpot-file validation error. The existing catch path surfaces this hint as a friendly user error instead of the raw TypeError text. Add regression tests for both paths. 374 users were affected, 704 occurrences across 2.17.0-RC2/RC3/RC4. Fixes #10709. AI-assisted-by: minimax-m3 * 🐛 Harden dnd/get-data against missing dataTransfer When the sortable hook or any caller passes a synthetic event without a dataTransfer property (e.g. a dragend fired after a drop that has already cleared the transfer), the previous implementation called .getData directly on the nil/undefined result and threw "Cannot read properties of undefined (reading 'getData')". Wrap the body in when-let so get-data returns nil cleanly when dataTransfer is missing. All three current callers (hooks.cljs:164, viewport/actions.cljs:531 and :562) already treat the return value as optional via when-let / when, so no caller breaks. Add a regression test covering both the missing-dataTransfer case and a real dataTransfer roundtrip. AI-assisted-by: minimax-m3 * 🐛 Harden paste handler against missing clipboardData in forms When a paste event arrives without a clipboardData property (e.g. a programmatically dispatched ClipboardEvent in some browsers, or edge cases like dragging a file with no text content), the previous implementation called .getData directly on the nil/undefined clipboardData and threw "Cannot read properties of undefined (reading 'getData')". Wrap the body in when-let so the paste logic is skipped entirely when clipboardData is missing. The existing (string? paste-data) guard in the inner when already tolerates nil; no other caller behavior changes. AI-assisted-by: minimax-m3 * 🐛 Harden paste handler against missing clipboardData in components/forms Same defensive pattern as the main/ui/forms.cljs paste handler: wrap the body in when-let so the .getData call is skipped when the clipboardData property is missing on the paste event. Prevents the raw "Cannot read properties of undefined (reading 'getData')" TypeError for programmatic / edge-case paste events. AI-assisted-by: minimax-m3 * 🐛 Harden v3 text editor paste and styles-fn against undefined receivers Two related fixes for the "Cannot read properties of undefined (reading 'getData')" family of bugs in the workspace text editor: - v3_editor.cljs: wrap the paste body in when-let on clipboardData so .getData("text/plain") is never called on a nil receiver. The existing (when (and text (seq text))) guard already tolerates nil text; only the outer .getData call was unprotected. - editor.cljs: add (and content ...) to the if branch in styles-fn so .getText and .getData are never called on a nil content. The else branch (legacy.txt/styles-to-attrs) is already the correct fallback for missing content. Add a regression test that mirrors the fixed patterns and verifies they no longer throw on synthetic events with no clipboardData or nil content. AI-assisted-by: minimax-m3 * 🐛 Harden get-editor-block-data and get-editor-block-type against nil block getCurrentBlock from Draft.js can return undefined for an empty selection (e.g. before any block is created). The previous implementations called .getData / .getType directly on the result and threw "Cannot read properties of undefined (reading 'getData')" / "...reading 'getType')". Wrap both functions in (when (some? block) ...) so they return nil cleanly. Callers in editor.cljs and text_editor.cljs already handle nil results (render-block short-circuits via the case on type; the text-data caller in text_editor.cljs lets nil flow up), so no upstream change is required. Add a regression test covering both functions with nil and js/undefined input. AI-assisted-by: minimax-m3 * 🐛 Harden draft-js block-data helpers against nil block Three related fixes in the vendored draft-js package: - mergeBlockData: early-return undefined when block is falsy. Without this, the first line (block.getData()) throws for callers that pass a nil block. - splitBlockPreservingData: guard the blockMap.get(...) lookup. If the start key is stale (e.g. after a Modifier.splitBlock that doesn't actually produce the expected key), .get() returns undefined and the subsequent .getData() throws. Fall back to an empty Immutable Map for the block data. - updateBlockData: short-circuit (return state unchanged) when mergeBlockData returns undefined. Without this, the chain newBlock.getData() would throw on the same nil-block case that mergeBlockData now guards. These match the defensive nil-handling pattern used elsewhere in the frontend (.getData callers) and protect against stale selection keys in the Draft.js content state. AI-assisted-by: minimax-m3
243 lines
9.8 KiB
Clojure
243 lines
9.8 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
|
|
|
(ns app.worker.import
|
|
(:refer-clojure :exclude [resolve])
|
|
(:require
|
|
[app.common.exceptions :as ex]
|
|
[app.common.json :as json]
|
|
[app.common.logging :as log]
|
|
[app.common.schema :as sm]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.uploads :as uploads]
|
|
[app.main.repo :as rp]
|
|
[app.util.http :as http]
|
|
[app.util.i18n :as i18n :refer [tr]]
|
|
[app.util.sse :as sse]
|
|
[app.util.zip :as uz]
|
|
[app.worker.impl :as impl]
|
|
[beicon.v2.core :as rx]
|
|
[cuerdas.core :as str]))
|
|
|
|
(log/set-level! :warn)
|
|
|
|
(defn- import-cause-message
|
|
"Prefer the server `:hint` (full text, e.g. SSE error payload), then `:explain`
|
|
when present; avoid the generic `stream exception` wrapper when a payload exists."
|
|
[cause default-msg]
|
|
(let [data (ex-data cause)
|
|
hint (some-> data :hint str/trim)
|
|
explain (some-> data :explain str/trim)]
|
|
(cond
|
|
(not (str/blank? hint)) hint
|
|
(not (str/blank? explain)) explain
|
|
:else
|
|
(let [msg (some-> (ex-message cause) str/trim)]
|
|
(if (or (str/blank? msg) (= msg "stream exception"))
|
|
default-msg
|
|
msg)))))
|
|
|
|
;; Upload changes batches size
|
|
(def ^:const change-batch-size 100)
|
|
|
|
(def conjv (fnil conj []))
|
|
|
|
(defn read-zip-manifest
|
|
[zip-reader]
|
|
(->> (rx/from (uz/get-entry zip-reader "manifest.json"))
|
|
(rx/mapcat (fn [entry]
|
|
(if (nil? entry)
|
|
(rx/throw (ex/error :type :validation
|
|
:code :invalid-penpot-file
|
|
:hint "Not a valid Penpot file: manifest.json is missing"))
|
|
(uz/read-as-text entry))))
|
|
(rx/map json/decode)))
|
|
|
|
(defn slurp-uri
|
|
([uri] (slurp-uri uri :text))
|
|
([uri response-type]
|
|
(->> (http/send!
|
|
{:uri uri
|
|
:response-type response-type
|
|
:method :get})
|
|
(rx/map :body))))
|
|
|
|
(defn parse-mtype [ba]
|
|
(let [u8 (js/Uint8Array. ba 0 4)
|
|
sg (areduce u8 i ret "" (str ret (if (zero? i) "" " ") (.toString (aget u8 i) 8)))]
|
|
(case sg
|
|
"120 113 3 4" "application/zip"
|
|
"1 13 32 206" "application/octet-stream"
|
|
"other")))
|
|
|
|
;; NOTE: this is a limited subset schema for the manifest file of
|
|
;; binfile-v3 format; is used for partially parse it and read the
|
|
;; files referenced inside the exported file
|
|
|
|
(def ^:private schema:manifest
|
|
[:map {:title "Manifest"}
|
|
[:type :string]
|
|
[:files
|
|
[:vector
|
|
[:map
|
|
[:id ::sm/uuid]
|
|
[:name :string]]]]])
|
|
|
|
(def ^:private decode-manifest
|
|
(sm/decoder schema:manifest sm/json-transformer))
|
|
|
|
(defn analyze-file
|
|
[{:keys [uri] :as file}]
|
|
(let [stream (->> (slurp-uri uri :buffer)
|
|
(rx/merge-map
|
|
(fn [body]
|
|
(let [mtype (parse-mtype body)]
|
|
(cond
|
|
(= "application/zip" mtype)
|
|
(let [zip-reader (uz/reader body)]
|
|
(->> (read-zip-manifest zip-reader)
|
|
(rx/map
|
|
(fn [manifest]
|
|
(if (= (:type manifest) "penpot/export-files")
|
|
(let [manifest (decode-manifest manifest)]
|
|
(assoc file :type :binfile-v3 :files (:files manifest)))
|
|
(assoc file :type :unknown))))
|
|
(rx/finalize (partial uz/close zip-reader))))
|
|
|
|
(= "application/octet-stream" mtype)
|
|
(rx/of (assoc file :type :binfile-v1))
|
|
|
|
:else
|
|
(rx/of (assoc file :type :unknown))))))
|
|
|
|
(rx/share))]
|
|
|
|
(->> (rx/merge
|
|
(->> stream
|
|
(rx/filter (fn [entry] (= :binfile-v1 (:type entry))))
|
|
(rx/map (fn [entry]
|
|
(let [file-id (uuid/next)]
|
|
(-> entry
|
|
(assoc :file-id file-id)
|
|
(assoc :name (:name file))
|
|
(assoc :status :success))))))
|
|
|
|
(->> stream
|
|
(rx/filter (fn [entry] (= :binfile-v3 (:type entry))))
|
|
(rx/merge-map (fn [{:keys [files] :as entry}]
|
|
(->> (rx/from files)
|
|
(rx/map (fn [file]
|
|
(-> entry
|
|
(dissoc :files)
|
|
(assoc :name (:name file))
|
|
(assoc :file-id (:id file))
|
|
(assoc :status :success))))))))
|
|
|
|
(->> stream
|
|
(rx/filter (fn [data] (= :unknown (:type data))))
|
|
(rx/map (fn [_]
|
|
{:uri (:uri file)
|
|
:status :error
|
|
:error (tr "dashboard.import.analyze-error")}))))
|
|
|
|
(rx/catch (fn [cause]
|
|
(let [error (import-cause-message cause (tr "dashboard.import.analyze-error"))]
|
|
(rx/of (assoc file :error error :status :error))))))))
|
|
|
|
(defmethod impl/handler :analyze-import
|
|
[{:keys [files]}]
|
|
(->> (rx/from files)
|
|
(rx/merge-map analyze-file)))
|
|
|
|
(defn- import-blob-via-upload
|
|
"Fetches `uri` as a Blob, uploads it using the generic chunked-upload
|
|
session API and calls `import-binfile` with the resulting upload-id.
|
|
Returns an observable of SSE events from the import stream."
|
|
[uri {:keys [name version project-id]}]
|
|
(->> (slurp-uri uri :blob)
|
|
(rx/mapcat
|
|
(fn [blob]
|
|
(->> (uploads/upload-blob-chunked blob)
|
|
(rx/mapcat
|
|
(fn [{:keys [session-id]}]
|
|
(rp/cmd! ::sse/import-binfile
|
|
{:name name
|
|
:upload-id session-id
|
|
:version version
|
|
:project-id project-id}))))))))
|
|
|
|
(defmethod impl/handler :import-files
|
|
[{:keys [project-id files]}]
|
|
(let [binfile-v1 (filter #(= :binfile-v1 (:type %)) files)
|
|
binfile-v3 (filter #(= :binfile-v3 (:type %)) files)]
|
|
|
|
(rx/merge
|
|
(->> (rx/from binfile-v1)
|
|
(rx/merge-map
|
|
(fn [data]
|
|
(->> (import-blob-via-upload (:uri data)
|
|
{:name (str/replace (:name data) #".penpot$" "")
|
|
:version 1
|
|
:project-id project-id})
|
|
(rx/tap (fn [event]
|
|
(let [payload (sse/get-payload event)
|
|
type (sse/get-type event)]
|
|
(if (= type "progress")
|
|
(log/dbg :hint "import-binfile: progress"
|
|
:section (:section payload)
|
|
:name (:name payload))
|
|
(log/dbg :hint "import-binfile: end")))))
|
|
(rx/filter sse/end-of-stream?)
|
|
(rx/map (fn [_]
|
|
{:status :finish
|
|
:file-id (:file-id data)}))
|
|
(rx/catch
|
|
(fn [cause]
|
|
(log/error :hint "unexpected error on import process"
|
|
:project-id project-id
|
|
:cause cause)
|
|
(rx/of {:status :error
|
|
:error (import-cause-message cause (tr "labels.error"))
|
|
:file-id (:file-id data)})))))))
|
|
|
|
(->> (rx/from binfile-v3)
|
|
(rx/reduce (fn [result file]
|
|
(update result (:uri file) (fnil conj []) file))
|
|
{})
|
|
(rx/mapcat identity)
|
|
(rx/merge-map
|
|
(fn [[uri entries]]
|
|
(->> (import-blob-via-upload uri
|
|
{:name (-> entries first :name)
|
|
:version 3
|
|
:project-id project-id})
|
|
(rx/tap (fn [event]
|
|
(let [payload (sse/get-payload event)
|
|
type (sse/get-type event)]
|
|
(if (= type "progress")
|
|
(log/dbg :hint "import-binfile: progress"
|
|
:section (:section payload)
|
|
:name (:name payload))
|
|
(log/dbg :hint "import-binfile: end")))))
|
|
(rx/filter sse/end-of-stream?)
|
|
(rx/mapcat (fn [_]
|
|
(->> (rx/from entries)
|
|
(rx/map (fn [entry]
|
|
{:status :finish
|
|
:file-id (:file-id entry)})))))
|
|
(rx/catch
|
|
(fn [cause]
|
|
(log/error :hint "unexpected error on import process"
|
|
:project-id project-id
|
|
::log/sync? true
|
|
:cause cause)
|
|
(let [err (import-cause-message cause (tr "labels.error"))]
|
|
(->> (rx/from entries)
|
|
(rx/map (fn [entry]
|
|
{:status :error
|
|
:error err
|
|
:file-id (:file-id entry)})))))))))))))
|