;; 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 (ns app.config (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.flags :as flags] [app.common.logging :as log] [app.common.time :as ct] [app.common.uri :as u] [app.common.uuid :as uuid] [app.common.version :as v] [app.util.avatars :as avatars] [app.util.extends] [app.util.globals :refer [global location]] [app.util.navigator :as nav] [app.util.object :as obj] [app.util.storage :as sto] [app.util.timers :as ts] [cuerdas.core :as str])) (set! *assert* js/goog.DEBUG) ;; --- Auxiliar Functions (def valid-browsers #{:chrome :firefox :safari :safari-16 :safari-17 :safari-18 :safari-26 :edge :other}) (def valid-platforms #{:windows :linux :macos :other}) (defn- parse-browser [] (let [user-agent (-> (nav/get-user-agent) str/lower) check-chrome? (fn [] (str/includes? user-agent "chrom")) check-firefox? (fn [] (str/includes? user-agent "firefox")) check-edge? (fn [] (str/includes? user-agent "edg")) check-safari? (fn [] (str/includes? user-agent "safari")) check-safari-16? (fn [] (and (check-safari?) (str/includes? user-agent "version/16"))) check-safari-17? (fn [] (and (check-safari?) (str/includes? user-agent "version/17"))) check-safari-18? (fn [] (and (check-safari?) (str/includes? user-agent "version/18"))) check-safari-26? (fn [] (and (check-safari?) (str/includes? user-agent "version/26")))] (cond ^boolean (check-edge?) :edge ^boolean (check-chrome?) :chrome ^boolean (check-firefox?) :firefox ^boolean (check-safari-16?) :safari-16 ^boolean (check-safari-17?) :safari-17 ^boolean (check-safari-18?) :safari-18 ^boolean (check-safari-26?) :safari-26 ^boolean (check-safari?) :safari :else :unknown))) (defn- parse-platform [] (let [user-agent (str/lower (nav/get-user-agent)) check-windows? (fn [] (str/includes? user-agent "windows")) check-linux? (fn [] (str/includes? user-agent "linux")) check-macos? (fn [] (str/includes? user-agent "mac os"))] (cond ^boolean (check-windows?) :windows ^boolean (check-linux?) :linux ^boolean (check-macos?) :macos :else :other))) (defn- parse-target [global] (if (some? (obj/get global "document")) :browser :webworker)) (defn- parse-flags [global] (let [flags (obj/get global "penpotFlags" "") flags (sequence (map keyword) (str/words flags))] (flags/parse flags/default flags))) (defn- parse-version [global] (-> (obj/get global "penpotVersion") (v/parse))) (defn parse-build-date [global] (let [date (obj/get global "penpotBuildDate")] (if (= date "%buildDate%") "unknown" date))) ;; --- Compile-time version tag ;; ;; This value is baked into the compiled JS at build time via closure-defines, ;; so it travels with the JS bundle. In contrast, `version-tag` (below) is read ;; at runtime from globalThis.penpotVersionTag which is set by the always-fresh ;; index.html. Comparing the two lets us detect when the browser has loaded ;; stale cached JS files. (goog-define compiled-version-tag "develop") ;; --- Global Config Vars (def default-theme "default") (def default-language "en") (def themes (obj/get global "penpotThemes")) (def build-date (parse-build-date global)) (def flags (parse-flags global)) (def target (parse-target global)) (def browser (parse-browser)) (def platform (parse-platform)) (def session-id (uuid/next)) (def version (parse-version global)) (def version-tag (obj/get global "penpotVersionTag")) (defn stale-build? "Returns true when the compiled JS was built with a different version tag than the one present in the current index.html. This indicates the browser has cached JS from a previous deployment." ^boolean [] (not= compiled-version-tag version-tag)) ;; --- Throttled reload ;; ;; A generic reload mechanism with loop protection via sessionStorage. ;; Used by both the boot-time stale-build check and the runtime ;; stale-asset error handler. (def ^:private reload-storage-key "penpot-last-reload-timestamp") (def ^:private reload-cooldown-ms 30000) (defn throttled-reload "Force a hard page reload unless one was already triggered within the last 30 seconds (tracked in sessionStorage). Returns true when a reload is initiated, false when suppressed." [& {:keys [reason]}] (let [now (ct/now) prev-ts (-> (sto/get-item sto/session-storage reload-storage-key) (d/parse-integer))] (if (and (some? prev-ts) (< (- now prev-ts) reload-cooldown-ms)) (do (log/wrn :hint "reload suppressed (cooldown active)" :reason reason) false) (do (log/wrn :hint "forcing page reload" :reason reason) (sto/set-item sto/session-storage reload-storage-key (str now)) (ts/asap #(.reload ^js location true)) true)))) (def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI")) (def oidc-name (obj/get global "penpotOIDCName")) (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI")) (def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (def plugins-list-uri (obj/get global "penpotPluginsListURI" "https://penpot.app/penpothub/plugins")) (def plugins-whitelist (into #{} (obj/get global "penpotPluginsWhitelist" []))) (def templates-uri (obj/get global "penpotTemplatesURI" "https://penpot.github.io/penpot-files/")) (def upload-chunk-size (obj/get global "penpotUploadChunkSize" (* 1024 1024 25))) ;; 25 MiB ;; We set the current parsed flags under common for make ;; it available for common code without the need to pass ;; the flags all arround on parameters. (set! app.common.flags/*current* flags) (defn- normalize-uri [uri-str] ;; Ensure that the path always ends with "/"; this ensures that ;; all path join operations works as expected. (u/ensure-path-slash uri-str)) (def public-uri (normalize-uri (or (obj/get global "penpotPublicURI") (obj/get location "origin")))) (def mcp-ws-uri (or (some-> (obj/get global "penpotMcpServerURI") u/uri) (u/join public-uri "mcp/ws"))) (def rasterizer-uri (or (some-> (obj/get global "penpotRasterizerURI") normalize-uri) public-uri)) (def worker-uri (-> public-uri (u/join "js/worker/main.js") (get :path) (str "?version=" version-tag))) (defn external-feature-flag [flag value] (let [f (obj/get global "externalFeatureFlag")] (when (fn? f) (f flag value)))) (defn external-session-id [] (let [f (obj/get global "externalSessionId")] (when (fn? f) (f)))) (defn external-context-info [] (let [f (obj/get global "externalContextInfo")] (when (fn? f) (f)))) (defn external-notify-register-success [profile-id] (let [f (obj/get global "externalNotifyRegisterSuccess")] (when (fn? f) (f (str profile-id))))) (defn initialize-external-context-info [] (let [f (obj/get global "initializeExternalConfigInfo")] (when (fn? f) (f)))) (def mcp-server-url (-> public-uri u/ensure-path-slash (u/join "mcp/stream") str)) (def mcp-help-center-uri "https://help.penpot.app/mcp/") ;; --- Helper Functions (defn ^boolean check-browser? [candidate] (dm/assert! (contains? valid-browsers candidate)) (if (= candidate :safari) (contains? #{:safari :safari-16 :safari-17 :safari-18 :safari-26} browser) (= candidate browser))) (defn ^boolean check-platform? [candidate] (dm/assert! (contains? valid-platforms candidate)) (= candidate platform)) (defn resolve-profile-photo-url [{:keys [photo-id fullname name color] :as profile}] (if (nil? photo-id) (avatars/generate {:name (or fullname name) :color color}) (dm/str (u/join public-uri "assets/by-id/" photo-id)))) (defn resolve-team-photo-url [{:keys [photo-id name] :as team}] (if (nil? photo-id) (avatars/generate {:name name}) (dm/str (u/join public-uri "assets/by-id/" photo-id)))) (defn resolve-media [id] (dm/str (u/join public-uri "assets/by-id/" (str id)))) (defn resolve-file-media ([media] (resolve-file-media media false)) ([{:keys [id data-uri] :as media} thumbnail?] (or data-uri (dm/str (cond-> (u/join public-uri "assets/by-file-media-id/") (true? thumbnail?) (u/join (dm/str id "/thumbnail")) (false? thumbnail?) (u/join (dm/str id))))))) (defn resolve-href [resource] (let [href (-> public-uri (u/ensure-path-slash) (u/join resource) (get :path))] (str href "?version=" version-tag)))