From 2ed39e43c30a2a11d3aa3f5a369185405f086c6e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 11 Dec 2025 23:42:41 +0100 Subject: [PATCH 1/9] :bug: Fix issue on reading rlimit config --- backend/src/app/rpc/rlimit.clj | 64 +++++++++++++-------------- backend/src/app/rpc/rlimit/bucket.lua | 2 +- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/backend/src/app/rpc/rlimit.clj b/backend/src/app/rpc/rlimit.clj index d181b843fe..03ae35831f 100644 --- a/backend/src/app/rpc/rlimit.clj +++ b/backend/src/app/rpc/rlimit.clj @@ -104,28 +104,29 @@ (def ^:private schema:limit [:and [:map - [::name :any] + [::name :keyword] [::strategy schema:strategy] [::key :string] - [::opts :string]] - [:or - [:map - [::capacity ::sm/int] - [::rate ::sm/int] - [::internal ::ct/duration] - [::params [::sm/vec :any]]] - [:map - [::nreq ::sm/int] - [::unit [:enum :days :hours :minutes :seconds :weeks]]]]]) + [::opts :string] + [::capacity {:optional true} ::sm/int] + [::rate {:optional true} ::sm/int] + [::interval {:optional true} ::ct/duration] + [::params {:optional true} [::sm/vec :any]] + [::permits {:optional true} ::sm/int] + [::unit {:optional true} [:enum :days :hours :minutes :seconds :weeks]]] + [:fn (fn [attrs] + (let [contains-fn (partial contains? attrs)] + (or (every? contains-fn [::capacity ::rate ::interval]) + (every? contains-fn [::permits ::unit]))))]]) (def ^:private schema:limits [:map-of :keyword [::sm/vec schema:limit]]) (def ^:private valid-limit-tuple? - (sm/lazy-validator schema:limit-tuple)) + (sm/validator schema:limit-tuple)) (def ^:private valid-rlimit-instance? - (sm/lazy-validator ::rpc/rlimit)) + (sm/validator ::rpc/rlimit)) (defmethod parse-limit :window [[name strategy opts :as vlimit]] @@ -134,16 +135,16 @@ (merge {::name name ::strategy strategy} - (if-let [[_ nreq unit] (re-find window-opts-re opts)] - (let [nreq (parse-long nreq)] - {::nreq nreq + (if-let [[_ permits unit] (re-find window-opts-re opts)] + (let [permits (parse-long permits)] + {::permits permits ::unit (case unit "d" :days "h" :hours "m" :minutes "s" :seconds "w" :weeks) - ::key (str "ratelimit.window." (d/name name)) + ::key (str "penpot.rlimit." (cf/get :tenant) ".window." (d/name name)) ::opts opts}) (ex/raise :type :validation :code :invalid-window-limit-opts @@ -164,15 +165,15 @@ ::interval interval ::opts opts ::params [(->seconds interval) rate capacity] - ::key (str "ratelimit.bucket." (d/name name))}) + ::key (str "penpot.rlimit." (cf/get :tenant) ".bucket." (d/name name))}) (ex/raise :type :validation :code :invalid-bucket-limit-opts :hint (str/ffmt "looks like '%' does not have a valid format" opts)))) (defmethod process-limit :bucket - [rconn user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}] + [rconn profile-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}] (let [script (-> bucket-rate-limit-script - (assoc ::rscript/keys [(str key "." service "." user-id)]) + (assoc ::rscript/keys [(str key "." service "." profile-id)]) (assoc ::rscript/vals (conj params (->seconds now)))) result (rds/eval rconn script) allowed? (boolean (nth result 0)) @@ -192,18 +193,18 @@ (assoc ::lresult/remaining remaining)))) (defmethod process-limit :window - [rconn user-id now {:keys [::nreq ::unit ::key ::service] :as limit}] + [rconn profile-id now {:keys [::permits ::unit ::key ::service] :as limit}] (let [ts (ct/truncate now unit) ttl (ct/diff now (ct/plus ts {unit 1})) script (-> window-rate-limit-script - (assoc ::rscript/keys [(str key "." service "." user-id "." (ct/format-inst ts))]) - (assoc ::rscript/vals [nreq (->seconds ttl)])) + (assoc ::rscript/keys [(str key "." service "." profile-id "." (ct/format-inst ts))]) + (assoc ::rscript/vals [permits (->seconds ttl)])) result (rds/eval rconn script) allowed? (boolean (nth result 0)) remaining (nth result 1)] (l/trace :hint "limit processed" :service service - :limit (name (::name limit)) + :name (name (::name limit)) :strategy (name (::strategy limit)) :opts (::opts limit) :allowed allowed? @@ -214,8 +215,8 @@ (assoc ::lresult/reset (ct/plus ts {unit 1}))))) (defn- process-limits - [rconn user-id limits now] - (let [results (into [] (map (partial process-limit rconn user-id now)) limits) + [rconn profile-id limits now] + (let [results (into [] (map (partial process-limit rconn profile-id now)) limits) remaining (->> results (d/index-by ::name ::lresult/remaining) (uri/map->query-string)) @@ -227,7 +228,7 @@ (when rejected (l/warn :hint "rejected rate limit" - :user-id (str user-id) + :profile-id (str profile-id) :limit-service (-> rejected ::service name) :limit-name (-> rejected ::name name) :limit-strategy (-> rejected ::strategy name))) @@ -371,12 +372,9 @@ (defn- on-refresh-error [_ cause] (when-not (instance? java.util.concurrent.RejectedExecutionException cause) - (if-let [explain (-> cause ex-data ex/explain)] - (l/warn ::l/raw (str "unable to refresh config, invalid format:\n" explain) - ::l/sync? true) - (l/warn :hint "unexpected exception on loading config" - :cause cause - ::l/sync? true)))) + (l/warn :hint "unexpected exception on loading config" + :cause cause + ::l/sync? true))) (defn- get-config-path [] diff --git a/backend/src/app/rpc/rlimit/bucket.lua b/backend/src/app/rpc/rlimit/bucket.lua index 4200dec4d1..32512d3506 100644 --- a/backend/src/app/rpc/rlimit/bucket.lua +++ b/backend/src/app/rpc/rlimit/bucket.lua @@ -25,9 +25,9 @@ local allowed = filled >= requested local newTokens = filled if allowed then newTokens = filled - requested + redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp) end -redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp) redis.call("expire", tokensKey, ttl) return { allowed, newTokens } From 974495e08f0725d9b49104c3cddbfa83e676df04 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 12 Dec 2025 08:15:54 +0100 Subject: [PATCH 2/9] :sparkles: Reduce log level for profile picture download error Because it is not blocking operation and does not provents user to proceed. --- backend/src/app/rpc/commands/auth.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 742077e875..fb5db45ef7 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -307,7 +307,8 @@ :content-type (:mtype input)})] (:id sobject)) (catch Throwable cause - (l/err :hint "unable to import profile picture" + (l/wrn :hint "unable to import profile picture" + :uri uri :cause cause) nil))) From 81b72c5acd526a282be11747b4a08951ce5ea4b9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 12 Dec 2025 11:24:53 +0100 Subject: [PATCH 3/9] :bug: Fix exception on assinging gradient to shadow on multiple selection --- .../src/app/main/data/workspace/colors.cljs | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 38cfe651ac..5856cec029 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -14,7 +14,7 @@ [app.common.types.fills :as types.fills] [app.common.types.library :as ctl] [app.common.types.shape :as shp] - [app.common.types.shape.shadow :refer [check-shadow]] + [app.common.types.shape.shadow :as types.shadow] [app.common.types.text :as txt] [app.main.broadcast :as mbc] [app.main.data.helpers :as dsh] @@ -406,30 +406,30 @@ (defn change-shadow [ids attrs index] - (ptk/reify ::change-shadow - ptk/WatchEvent - (watch [_ _ _] - (rx/of (dwsh/update-shapes - ids - (fn [shape] - (let [;; If we try to set a gradient to a shadow (for - ;; example using the color selection from - ;; multiple shapes) let's use the first stop - ;; color - attrs (cond-> attrs - (:gradient attrs) - (dm/get-in [:gradient :stops 0])) + (letfn [(update-shadow [shape] + (let [;; If we try to set a gradient to a shadow (for + ;; example using the color selection from + ;; multiple shapes) let's use the first stop + ;; color + attrs (cond-> attrs + (:gradient attrs) + (-> (dm/get-in [:gradient :stops 0]) + (select-keys types.shadow/color-attrs))) - attrs' (-> (dm/get-in shape [:shadow index :color]) - (merge attrs) - (d/without-nils))] - (assoc-in shape [:shadow index :color] attrs')))))))) + attrs' (-> (dm/get-in shape [:shadow index :color]) + (merge attrs) + (d/without-nils))] + (assoc-in shape [:shadow index :color] attrs')))] + (ptk/reify ::change-shadow + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dwsh/update-shapes ids update-shadow)))))) (defn add-shadow [ids shadow] (assert - (check-shadow shadow) + (types.shadow/check-shadow shadow) "expected a valid shadow struct") (assert @@ -1146,16 +1146,16 @@ (defn- shadow->color-attr "Given a stroke map enriched with :shape-id, :index, and optionally :has-token-applied / :token-name, returns a color attribute map. - + If :has-token-applied is true, adds token metadata to :attrs: {:has-token-applied true :token-name } - + Args: - stroke: map with stroke info, including :shape-id and :index - file-id: current file UUID - libraries: map of shared color libraries - + Returns: A map like: {:attrs {...color data...} @@ -1260,12 +1260,12 @@ will include extra attributes in its :attrs map: {:has-token-applied true :token-name } - + Args: - shapes: vector of shape maps - file-id: current file UUID - libraries: map of shared color libraries - + Returns: A vector of color attribute maps with metadata for each shape." [shapes file-id libraries] From 507bf7445bd6eb19ff7cef0acf9a1e1191fcbb54 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 12 Dec 2025 09:52:33 +0100 Subject: [PATCH 4/9] :bug: Fix tokens-lib encoding when value is nilable --- common/src/app/common/types/tokens_lib.cljc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 70e36bfc2f..90fd5a52ec 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -1410,8 +1410,8 @@ Will return a value that matches this schema: ;; NOTE: we can't assign statically at eval time the value of a ;; function that is declared but not defined; so we need to pass ;; an anonymous function and delegate the resolution to runtime - {:encode/json #(export-dtcg-json %) - :decode/json #(read-multi-set-dtcg %) + {:encode/json #(some-> % export-dtcg-json) + :decode/json #(some-> % read-multi-set-dtcg) ;; FIXME: add better, more reallistic generator :gen/gen (->> (sg/small-int) (sg/fmap (fn [_] From 94f95ca6b8f0ab8cd6c9f7853d24a7cdcbca51d0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 12 Dec 2025 12:33:38 +0100 Subject: [PATCH 5/9] :bug: Fix incorrect redis connection error handling --- backend/src/app/worker/dispatcher.clj | 49 ++++++++++++++------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/backend/src/app/worker/dispatcher.clj b/backend/src/app/worker/dispatcher.clj index f95ade8e1c..818c50c18c 100644 --- a/backend/src/app/worker/dispatcher.clj +++ b/backend/src/app/worker/dispatcher.clj @@ -137,33 +137,34 @@ RETURNING task.id, task.queue") ::wait))) (run-batch [] - (let [rconn (rds/connect cfg)] - (try - (-> cfg - (assoc ::rds/conn rconn) - (db/tx-run! run-batch')) + (try + (let [rconn (rds/connect cfg)] + (try + (-> cfg + (assoc ::rds/conn rconn) + (db/tx-run! run-batch')) + (finally + (.close ^AutoCloseable rconn)))) - (catch InterruptedException cause - (throw cause)) - (catch Exception cause - (cond - (rds/exception? cause) - (do - (l/wrn :hint "redis exception (will retry in an instant)" :cause cause) - (px/sleep timeout)) + (catch InterruptedException cause + (throw cause)) - (db/sql-exception? cause) - (do - (l/wrn :hint "database exception (will retry in an instant)" :cause cause) - (px/sleep timeout)) + (catch Exception cause + (cond + (rds/exception? cause) + (do + (l/wrn :hint "redis exception (will retry in an instant)" :cause cause) + (px/sleep timeout)) - :else - (do - (l/err :hint "unhandled exception (will retry in an instant)" :cause cause) - (px/sleep timeout)))) + (db/sql-exception? cause) + (do + (l/wrn :hint "database exception (will retry in an instant)" :cause cause) + (px/sleep timeout)) - (finally - (.close ^AutoCloseable rconn))))) + :else + (do + (l/err :hint "unhandled exception (will retry in an instant)" :cause cause) + (px/sleep timeout)))))) (dispatcher [] (l/inf :hint "started") @@ -176,7 +177,7 @@ RETURNING task.id, task.queue") (catch InterruptedException _ (l/trc :hint "interrupted")) (catch Throwable cause - (l/err :hint " unexpected exception" :cause cause)) + (l/err :hint "unexpected exception" :cause cause)) (finally (l/inf :hint "terminated"))))] From 84415476d087987cd89162b2513e121aadbe3a8a Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 11 Dec 2025 16:13:38 +0100 Subject: [PATCH 6/9] :bug: Fix problem with reflow layout --- render-wasm/src/shapes/modifiers.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 02e7911aaa..f8e0c6428a 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -277,7 +277,6 @@ fn propagate_reflow( }; let shapes = &state.shapes; - let mut reflow_parent = false; if reflown.contains(id) { return; @@ -294,15 +293,10 @@ fn propagate_reflow( // If this is a fill layout but the parent has not been reflown yet // we wait for the next iteration for reflow skip_reflow = true; - reflow_parent = true; } } } - if shape.is_layout_vertical_auto() || shape.is_layout_horizontal_auto() { - reflow_parent = true; - } - if !skip_reflow { layout_reflows.push(*id); } @@ -312,32 +306,26 @@ fn propagate_reflow( if let Some(child) = shapes.get(&children_ids[0]) { let child_bounds = bounds.find(child); bounds.insert(shape.id, child_bounds); - reflow_parent = true; } reflown.insert(*id); } Type::Group(_) => { if let Some(shape_bounds) = calculate_group_bounds(shape, shapes, bounds) { bounds.insert(shape.id, shape_bounds); - reflow_parent = true; } reflown.insert(*id); } Type::Bool(_) => { if let Some(shape_bounds) = calculate_bool_bounds(shape, shapes, bounds, modifiers) { bounds.insert(shape.id, shape_bounds); - reflow_parent = true; } reflown.insert(*id); } - _ => { - // Other shapes don't have to be reflown - reflow_parent = true; - } + _ => {} } if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) { - if reflow_parent && (parent.has_layout() || parent.is_group_like()) { + if parent.has_layout() || parent.is_group_like() { entries.push_back(Modifier::reflow(parent.id)); } } From 63325ec7966d795c306c7325a0feeca09569519a Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 11 Dec 2025 17:05:51 +0100 Subject: [PATCH 7/9] :bug: Fix problem with flex fill size distribution --- render-wasm/src/shapes/modifiers/flex_layout.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index 7246d167e6..b35ed401fa 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -344,6 +344,7 @@ fn distribute_fill_across_space(layout_axis: &LayoutAxis, tracks: &mut [TrackDat let mut size = track.across_size - child.margin_across_start - child.margin_across_end; size = size.clamp(child.min_across_size, child.max_across_size); + size = f32::min(size, layout_axis.across_space()); child.across_size = size; } } From 8fde6b28ed3eae6f4c6a86c356ac81384f9f5525 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 11 Dec 2025 17:06:06 +0100 Subject: [PATCH 8/9] :bug: Fix problems with alignments and margins --- .../src/shapes/modifiers/flex_layout.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index b35ed401fa..3a7c9929f2 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -546,14 +546,22 @@ fn child_position( align_self: Some(align_self), .. }) => match align_self { - AlignSelf::Center => (track.across_size - child_axis.across_size) / 2.0, + AlignSelf::Center => { + (track.across_size - child_axis.across_size + child_axis.margin_across_start + - child_axis.margin_across_end) + / 2.0 + } AlignSelf::End => { track.across_size - child_axis.across_size - child_axis.margin_across_end } _ => child_axis.margin_across_start, }, _ => match layout_data.align_items { - AlignItems::Center => (track.across_size - child_axis.across_size) / 2.0, + AlignItems::Center => { + (track.across_size - child_axis.across_size + child_axis.margin_across_start + - child_axis.margin_across_end) + / 2.0 + } AlignItems::End => { track.across_size - child_axis.across_size - child_axis.margin_across_end } @@ -579,7 +587,11 @@ pub fn reflow_flex_layout( let tracks = calculate_track_data(shape, layout_data, flex_data, layout_bounds, shapes, bounds); for track in tracks.iter() { - let total_shapes_size = track.shapes.iter().map(|s| s.main_size).sum::(); + let total_shapes_size = track + .shapes + .iter() + .map(|s| s.main_size + s.margin_main_start + s.margin_main_end) + .sum::(); let mut shape_anchor = first_anchor(layout_data, &layout_axis, track, total_shapes_size); for child_axis in track.shapes.iter() { From 9aa387a4732ae9a431933edfb76c1a6e3c3e2f89 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 11 Dec 2025 18:40:13 +0100 Subject: [PATCH 9/9] :bug: Fix incorrect string truncation with abbreviate template filter --- CHANGES.md | 1 + backend/resources/app/email/invite-to-team/en.html | 2 +- backend/src/app/util/template.clj | 8 ++++++++ common/deps.edn | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 011361113a..6b941393c3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -95,6 +95,7 @@ example. It's still usable as before, we just removed the example. - Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841) - Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492) - Fix horizontal scroll on layer panel [Taiga #12843](https://tree.taiga.io/project/penpot/issue/12843) +- Fix unicode handling on email template abbreviation filter [Github #7966](https://github.com/penpot/penpot/pull/7966) ## 2.11.1 diff --git a/backend/resources/app/email/invite-to-team/en.html b/backend/resources/app/email/invite-to-team/en.html index 5e61f1f675..337593902d 100644 --- a/backend/resources/app/email/invite-to-team/en.html +++ b/backend/resources/app/email/invite-to-team/en.html @@ -240,4 +240,4 @@ - \ No newline at end of file + diff --git a/backend/src/app/util/template.clj b/backend/src/app/util/template.clj index 5c7a0b8c6e..2365423349 100644 --- a/backend/src/app/util/template.clj +++ b/backend/src/app/util/template.clj @@ -7,10 +7,18 @@ (ns app.util.template (:require [app.common.exceptions :as ex] + [cuerdas.core :as str] + [selmer.filters :as sf] [selmer.parser :as sp])) ;; (sp/cache-off!) +(sf/add-filter! :abbreviate + (fn [s n] + (let [n (parse-long n)] + (str/abbreviate s n)))) + + (defn render [path context] (try diff --git a/common/deps.edn b/common/deps.edn index a2d9a1b1ec..e7e73645a5 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -30,7 +30,7 @@ integrant/integrant {:mvn/version "1.0.0"} funcool/tubax {:mvn/version "2021.05.20-0"} - funcool/cuerdas {:mvn/version "2025.06.16-414"} + funcool/cuerdas {:mvn/version "2026.415"} funcool/promesa {:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8" :git/url "https://github.com/funcool/promesa"}