Compare commits

..

1 Commits

Author SHA1 Message Date
Andrés Moya
adc6af129c wip 2023-06-22 11:32:31 +02:00
259 changed files with 9632 additions and 17338 deletions

View File

@@ -1,13 +0,0 @@
root = true
[*.{cljs,cljc,clj,js,css,scss,html,yml,yaml,json,mustache}]
charset = utf-8
indent_size = 2
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -1,9 +0,0 @@
{
"files.exclude": {
"**/.clj-kondo": true,
"**/.cpcache": true,
"**/.lsp": true,
"**/.shadow-cljs": true,
"**/node_modules": true
}
}

View File

@@ -1,112 +1,23 @@
# CHANGELOG
## 1.19.0
## :rocket: 1.19.0
### :boom: Breaking changes & Deprecations
### :sparkles: New features
- Default naming of text layers [Taiga #2836](https://tree.taiga.io/project/penpot/us/2836)
- Create typography style from a selected text layer [Taiga #3041](https://tree.taiga.io/project/penpot/us/3041)
- Create typography style from a selected text layer[Taiga #3041](https://tree.taiga.io/project/penpot/us/3041)
- Board as ruler origin [Taiga #4833](https://tree.taiga.io/project/penpot/us/4833)
- Access tokens support [Taiga #4460](https://tree.taiga.io/project/penpot/us/4460)
- Show interactions setting at the view mode [Taiga #1330](https://tree.taiga.io/project/penpot/issue/1330)
- Improve dashboard performance related to thumbnails; now the thumbnails are
rendered as bitmap images.
- Add the ability to disable google fonts provider with the `disable-google-fonts-provider` flag
- Add the ability to disable dashboard templates section with the `disable-dashboard-templates-section` flag
- Add the ability to use the registration whitelist with OICD [Github #3348](https://github.com/penpot/penpot/issues/3348)
- Add support for local caching of google fonts (this avoids exposing the final user IP to
goolge and reduces the amount of request sent to google)
- Set smooth/instant autoscroll depending on distance [GitHub #3377](https://github.com/penpot/penpot/issues/3377)
### :bug: Bugs fixed
- Fix files can be opened from multiple urls [Taiga #5310](https://tree.taiga.io/project/penpot/issue/5310)
- Fix asset color item was created from the selected layer [Taiga #5180](https://tree.taiga.io/project/penpot/issue/5180)
- Fix unpublish more than one library at the same time [Taiga #5532](https://tree.taiga.io/project/penpot/issue/5532)
- Fix drag projects on dahsboard [Taiga #5531](https://tree.taiga.io/project/penpot/issue/5531)
- Fix allow team name to be all blank [Taiga #5527](https://tree.taiga.io/project/penpot/issue/5527)
- Fix search font visualitation [Taiga #5523](https://tree.taiga.io/project/penpot/issue/5523)
- Fix create and account only with spaces [Taiga #5518](https://tree.taiga.io/project/penpot/issue/5518)
- Fix context menu outside screen [Taiga #5524](https://tree.taiga.io/project/penpot/issue/5524)
- Fix graphic item rename on assets pannel [Taiga #5556](https://tree.taiga.io/project/penpot/issue/5556)
- Fix component and media name validation on assets panel [Taiga #5555](https://tree.taiga.io/project/penpot/issue/5555)
- Fix problem with selection shortcuts [Taiga #5492](https://tree.taiga.io/project/penpot/issue/5492)
- Fix issue with paths line to curve and concurrent editing [Taiga #5191](https://tree.taiga.io/project/penpot/issue/5191)
- Fix problems with locked layers [Taiga #5139](https://tree.taiga.io/project/penpot/issue/5139)
- Fix export from shared prototype [Taiga #5565](https://tree.taiga.io/project/penpot/issue/5565)
- Fix email change: validation error displaying even after both fields are identical [Taiga #5514](https://tree.taiga.io/project/penpot/issue/5514)
- Fix scroll on viewer comment list [Taiga #5563](https://tree.taiga.io/project/penpot/issue/5563)
- Fix context menu z-index [Taiga #5561](https://tree.taiga.io/project/penpot/issue/5561)
- Fix select all checkbox on shared link config [Taiga #5566](https://tree.taiga.io/project/penpot/issue/5566)
- Fix validation on full name input on account creation [Taiga #5516](https://tree.taiga.io/project/penpot/issue/5516)
- Fix validation on team name input [Taiga #5510](https://tree.taiga.io/project/penpot/issue/5510)
- Fix incorrect uri generation issues on share-link modal [Taiga #5564](https://tree.taiga.io/project/penpot/issue/5564)
- Fix cache issues with share-links [Taiga #5559](https://tree.taiga.io/project/penpot/issue/5559)
- Makes height priority for the rows/columns grids [#2774](https://github.com/penpot/penpot/issues/2774)
- Fix problem with comments mode not staying [#3363](https://github.com/penpot/penpot/issues/3363)
- Fix problem with comments when user left the team [Taiga #5562](https://tree.taiga.io/project/penpot/issue/5562)
- Fix problem with images patterns repeating [#3372](https://github.com/penpot/penpot/issues/3372)
- Fix grid not being clipped in frames [#3365](https://github.com/penpot/penpot/issues/3365)
- Fix cut/delete text layer when while creating text [Taiga #5602](https://tree.taiga.io/project/penpot/issue/5602)
- Fix picking a gradient color in recent colors for a new color in the assets tab [Taiga #5601](https://tree.taiga.io/project/penpot/issue/5601)
- Fix problem with importation process [Taiga #5597](https://tree.taiga.io/project/penpot/issue/5597)
- Fix problem with HSV color picker [#3317](https://github.com/penpot/penpot/issues/3317)
- Fix problem with slashes in layers names for exporter [#3276](https://github.com/penpot/penpot/issues/3276)
- Fix incorrect modified data on moving files on dashboard [Taiga #5530](https://tree.taiga.io/project/penpot/issue/5530)
- Fix focus handling on comments edition [Taiga #5560](https://tree.taiga.io/project/penpot/issue/5560)
- Fix incorrect fullname use on registring user after OIDC authentication [Taiga #5517](https://tree.taiga.io/project/penpot/issue/5517)
- Fix incorrect modified-at on project after import file [Taiga #5268](https://tree.taiga.io/project/penpot/issue/5268)
- Fix incorrect message after sending invitation to already member [Taiga 5599](https://tree.taiga.io/project/penpot/issue/5599)
- Fix text decoration on button [Taiga #5301](https://tree.taiga.io/project/penpot/issue/5301)
- Fix menu order on design tab [Taiga #5195](https://tree.taiga.io/project/penpot/issue/5195)
- Fix search bar width on layer tab [Taiga #5445](https://tree.taiga.io/project/penpot/issue/5445)
- Fix border radius values with decimals [Taiga #5283](https://tree.taiga.io/project/penpot/issue/5283)
- Fix shortcuts translations not homogenized [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
- Fix overlay manual position in nested boards [Taiga #5135](https://tree.taiga.io/project/penpot/issue/5135)
- Fix close overlay from a nested board [Taiga #5587](https://tree.taiga.io/project/penpot/issue/5587)
- Fix overlay position when it has shadow or blur [Taiga #4752](https://tree.taiga.io/project/penpot/issue/4752)
- Fix overlay position when there are elements fixed when scrolling [Taiga #4383](https://tree.taiga.io/project/penpot/issue/4383)
- Fix problem when sliding color picker in selected-colors [#3150](https://github.com/penpot/penpot/issues/3150)
- Fix error screen on upload image error [Taiga #5608](https://tree.taiga.io/project/penpot/issue/5608)
- Fix bad frame-id for certain componentes [#3205](https://github.com/penpot/penpot/issues/3205)
- Fix paste elements at bottom of frame [Taig #5253](https://tree.taiga.io/project/penpot/issue/5253)
- Fix new-file button on project not redirecting to the new file [Taiga #5610](https://tree.taiga.io/project/penpot/issue/5610)
- Fix retrieve user comments in dashboard [Taiga #5607](https://tree.taiga.io/project/penpot/issue/5607)
- Locks shapes when moved inside a locked parent [Taiga #5252](https://tree.taiga.io/project/penpot/issue/5252)
- Fix rotate several elements in bulk [Taiga #5165](https://tree.taiga.io/project/penpot/issue/5165)
- Fix onboarding slides height [Taiga #5373](https://tree.taiga.io/project/penpot/issue/5373)
- Fix create typography with section closed [Taiga #5574](https://tree.taiga.io/project/penpot/issue/5574)
- Fix exports menu on viewer mode [Taiga #5568](https://tree.taiga.io/project/penpot/issue/5568)
- Fix create empty comments [Taiga #5536](https://tree.taiga.io/project/penpot/issue/5536)
- Fix position of text cursor is a bit too high in Invitations section [Taiga #5511](https://tree.taiga.io/project/penpot/issue/5511)
- Fix undo when updating several texts [Taiga #5197](https://tree.taiga.io/project/penpot/issue/5197)
- Fix assets right click button for multiple selection [Taiga #5545](https://tree.taiga.io/project/penpot/issue/5545)
- Fix problem with precision in resizes [Taiga #5623](https://tree.taiga.io/project/penpot/issue/5623)
- Fix absolute positioned layouts not showing flex properties [Taiga #5630](https://tree.taiga.io/project/penpot/issue/5630)
- Fix text gradient handlers [Taiga #4047](https://tree.taiga.io/project/penpot/issue/4047)
- Fix when user deletes one file during import it is impossible to finish importing of second file [Taiga #5656](https://tree.taiga.io/project/penpot/issue/5656)
- Fix export multiple images when only one of them has export settings [Taiga #5649](https://tree.taiga.io/project/penpot/issue/5649)
- Fix error when a user different than the thread creator edits a comment [Taiga #5647](https://tree.taiga.io/project/penpot/issue/5647)
- Fix unnecessary button [Taiga #3312](https://tree.taiga.io/project/penpot/issue/3312)
- Fix copy color information in several formats [Taiga #4723](https://tree.taiga.io/project/penpot/issue/4723)
- Fix dropdown width [Taiga #5541](https://tree.taiga.io/project/penpot/issue/5541)
- Fix enable comment mode and insert image keeps on comment mode [Taiga #5678](https://tree.taiga.io/project/penpot/issue/5678)
- Fix enable undo just after using pencil [Taiga #5674](https://tree.taiga.io/project/penpot/issue/5674)
- Fix 400 error when user changes password [Taiga #5643](https://tree.taiga.io/project/penpot/issue/5643)
- Fix cannot undo layer styles [Taiga #5676](https://tree.taiga.io/project/penpot/issue/5676)
- Fix unexpected exception on boolean shapes [Taiga #5685](https://tree.taiga.io/project/penpot/issue/5685)
- Fix ctrl+z on select not working [Taiga #5677](https://tree.taiga.io/project/penpot/issue/5677)
- Fix thubmnail rendering flashing [Taiga #5675](https://tree.taiga.io/project/penpot/issue/5675)
### :arrow_up: Deps updates
- Update google fonts catalog (at 2023/07/06) [Taiga #5592](https://tree.taiga.io/project/penpot/issue/5592)
### :heart: Community contributions by (Thank you!)
- Update Typography palette order (by @akshay-gupta7) [Github #3156](https://github.com/penpot/penpot/pull/3156)
- Palettes (color, typographies) empty state (by @akshay-gupta7) [Github #3160](https://github.com/penpot/penpot/pull/3160)
- Duplicate objects via drag + alt (by @akshay-gupta7) [Github #3147](https://github.com/penpot/penpot/pull/3147)
@@ -120,7 +31,6 @@
- Open project in new tab from workspace (by @akshay-gupta7) [Github #3246](https://github.com/penpot/penpot/pull/3246)
- Distribute fix enabled when two elements were selected (by @dfelinto) [Github #3266](https://github.com/penpot/penpot/pull/3266)
- Distribute vertical spacing failing for overlapped text (by @dfelinto) [Github #3267](https://github.com/penpot/penpot/pull/3267)
- bug Change independent corner radius input tooltips #3332 (by @astudentinearth) [Github #3332](https://github.com/penpot/penpot/pull/3332)
## 1.18.6

View File

@@ -6,7 +6,7 @@
org.clojure/clojure {:mvn/version "1.11.1"}
org.clojure/core.async {:mvn/version "1.6.673"}
com.github.luben/zstd-jni {:mvn/version "1.5.5-4"}
com.github.luben/zstd-jni {:mvn/version "1.5.2-5"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@@ -17,7 +17,7 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.2.4.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.2.2.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti
@@ -26,8 +26,8 @@
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.883"}
metosin/reitit-core {:mvn/version "0.6.0"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
metosin/reitit-core {:mvn/version "0.5.18"}
org.postgresql/postgresql {:mvn/version "42.6.0"}
@@ -35,12 +35,12 @@
io.whitfin/siphash {:mvn/version "2.0.0"}
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.5.351"}
buddy/buddy-hashers {:mvn/version "1.8.158"}
buddy/buddy-sign {:mvn/version "3.4.333"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.6"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.5"}
org.jsoup/jsoup {:mvn/version "1.16.1"}
org.jsoup/jsoup {:mvn/version "1.15.3"}
org.im4java/im4java
{:git/tag "1.4.0-penpot-2"
:git/sha "e2b3e16"
@@ -49,14 +49,14 @@
org.lz4/lz4-java {:mvn/version "1.8.0"}
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
integrant/integrant {:mvn/version "0.8.1"}
integrant/integrant {:mvn/version "0.8.0"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.11.4"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.20.96"}
software.amazon.awssdk/s3 {:mvn/version "2.19.29"}
}
:paths ["src" "resources" "target/classes"]

View File

@@ -1,30 +1,36 @@
[{:id "material-design-3"
:name "Material Design 3"
:thumbnail-uri "https://penpot.app/images/libraries/cover-md3.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/main/Material%20Design%203.penpot"}
{:id "tutorial-for-beginners"
:name "Tutorial for beginners"
:thumbnail-uri "https://penpot.app/images/libraries/tutorial-for-beginners.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"}
{:id "penpot-design-system"
:name "Penpot Design System"
:thumbnail-uri "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}
{:id "flex-layout-playground"
:name "Flex Layout Playground"
:file-uri "https://github.com/penpot/penpot-files/raw/main/Flex%20Layout%20Playground.penpot"}
{:id "wireframing-kit"
:name "Wireframing Kit"
:thumbnail-uri "https://penpot.app/images/libraries/cover-wireframes.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
{:id "ant-design"
:name "Ant Design UI Kit (lite)"
:thumbnail-uri "https://penpot.app/images/libraries/cover-ant-design.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Ant-Design-UI-Kit-Lite.penpot"}
{:id "cocomaterial"
:name "Cocomaterial"
:thumbnail-uri "https://penpot.app/images/libraries/cover-cocomaterial.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Cocomaterial.penpot"}
{:id "circum-icons"
:name "Circum Icons pack"
:thumbnail-uri "https://penpot.app/images/libraries/cover-circum.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/CircumIcons.penpot"}
{:id "coreui"
:name "CoreUI"
:thumbnail-uri "https://penpot.app/images/libraries/cover-coreui.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/main/CoreUI%20DesignSystem%20(DEMO).penpot"}
{:id "whiteboarding-kit"
:name "Whiteboarding Kit"
:thumbnail-uri "https://penpot.app/images/libraries/cover-whiteboards.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}]

View File

@@ -18,8 +18,6 @@ cp scripts/manage.py target/dist/manage.py
chmod +x target/dist/run.sh;
chmod +x target/dist/manage.py
# Prefetch templates
rm -rf builtin-templates;
mkdir builtin-templates;
# Prefetch
bb ./scripts/prefetch-templates.clj resources/app/onboarding.edn builtin-templates/
cp -r builtin-templates target/dist/

View File

@@ -6,15 +6,13 @@
(ns app.auth
(:require
[app.config :as cf]
[buddy.hashers :as hashers]
[cuerdas.core :as str]
[promesa.exec :as px]))
(def default-params
{:alg :argon2id
:memory (* 32768 2) ;; 64 MiB
:iterations 7
:memory (* 32768 2)
:iterations 5
:parallelism (px/get-available-processors)})
(defn derive-password
@@ -29,16 +27,3 @@
{:update false
:valid false})))
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if
given whitelist is an empty string."
([email]
(let [domains (cf/get :registration-domain-whitelist)]
(email-domain-in-whitelist? domains email)))
([domains email]
(if (or (nil? domains) (empty? domains))
true
(let [[_ candidate] (-> (str/lower email)
(str/split #"@" 2))]
(contains? domains candidate)))))

View File

@@ -7,7 +7,6 @@
(ns app.auth.oidc
"OIDC client implementation."
(:require
[app.auth :as auth]
[app.auth.oidc.providers :as-alias providers]
[app.common.data :as d]
[app.common.data.macros :as dm]
@@ -25,8 +24,6 @@
[app.tokens :as tokens]
[app.util.json :as json]
[app.util.time :as dt]
[buddy.sign.jwk :as jwk]
[buddy.sign.jwt :as jwt]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
@@ -50,29 +47,36 @@
(defn- discover-oidc-config
[cfg {:keys [base-uri] :as opts}]
(let [uri (dm/str (u/join base-uri ".well-known/openid-configuration"))
rsp (http/req! cfg {:method :get :uri uri} {:sync? true})]
(if (= 200 (:status rsp))
(let [data (-> rsp :body json/decode)
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
response (ex/try! (http/req! cfg
{:method :get :uri (str discovery-uri)}
{:sync? true}))]
(cond
(ex/exception? response)
(do
(l/warn :hint "unable to discover oidc configuration"
:discover-uri (str discovery-uri)
:cause response)
nil)
(= 200 (:status response))
(let [data (json/decode (:body response))
token-uri (get data :token_endpoint)
auth-uri (get data :authorization_endpoint)
user-uri (get data :userinfo_endpoint)
jwks-uri (get data :jwks_uri)]
user-uri (get data :userinfo_endpoint)]
(l/debug :hint "oidc uris discovered"
:token-uri token-uri
:auth-uri auth-uri
:user-uri user-uri
:jwks-uri jwks-uri)
:user-uri user-uri)
{:token-uri token-uri
:auth-uri auth-uri
:user-uri user-uri
:jwks-uri jwks-uri})
:user-uri user-uri})
:else
(do
(l/warn :hint "unable to discover OIDC configuration"
:discover-uri uri
:http-status (:status rsp))
:uri (str discovery-uri)
:response-status-code (:status response))
nil))))
(defn- prepare-oidc-opts
@@ -83,7 +87,6 @@
:token-uri (cf/get :oidc-token-uri)
:auth-uri (cf/get :oidc-auth-uri)
:user-uri (cf/get :oidc-user-uri)
:jwks-uri (cf/get :oidc-jwks-uri)
:scopes (cf/get :oidc-scopes #{"openid" "profile" "email"})
:roles-attr (cf/get :oidc-roles-attr)
:roles (cf/get :oidc-roles)
@@ -98,42 +101,8 @@
(string? (:user-uri opts))
(string? (:auth-uri opts)))
opts
(try
(-> (discover-oidc-config cfg opts)
(merge opts {:discover? true}))
(catch Throwable cause
(l/warn :hint "unable to discover OIDC configuration"
:cause cause)))))))
(defn- process-oidc-jwks
[keys]
(reduce (fn [result {:keys [kid] :as kdata}]
(let [pkey (ex/try! (jwk/public-key kdata))]
(if (ex/exception? pkey)
(do
(l/warn :hint "unable to create public key"
:kid (:kid kdata)
:cause pkey)
result)
(assoc result kid pkey))))
{}
keys))
(defn- fetch-oidc-jwks
[cfg {:keys [jwks-uri]}]
(when jwks-uri
(try
(let [{:keys [status body]} (http/req! cfg {:method :get :uri jwks-uri} {:sync? true})]
(if (= 200 status)
(-> body json/decode :keys process-oidc-jwks)
(do
(l/warn :hint "unable to retrieve JWKs (unexpected response status code)"
:http-status status
:http-body body)
nil)))
(catch Throwable cause
(l/warn :hint "unable to retrieve JWKs (unexpected exception)"
:cause cause)))))
(some-> (discover-oidc-config cfg opts)
(merge opts {:discover? true}))))))
(defmethod ig/pre-init-spec ::providers/generic [_]
(s/keys :req [::http/client]))
@@ -142,7 +111,7 @@
[_ cfg]
(when (contains? cf/flags :login-with-oidc)
(if-let [opts (prepare-oidc-opts cfg)]
(let [jwks (fetch-oidc-jwks cfg opts)]
(do
(l/info :hint "provider initialized"
:provider "oidc"
:method (if (:discover? opts) "discover" "manual")
@@ -153,9 +122,8 @@
:user-uri (:user-uri opts)
:token-uri (:token-uri opts)
:roles-attr (:roles-attr opts)
:roles (:roles opts)
:keys (str/join "," (map str (keys jwks))))
(assoc opts :jwks jwks))
:roles (:roles opts))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "oidc")
nil))))
@@ -196,7 +164,7 @@
[cfg tdata props]
(or (some-> props :github/email)
(let [params {:uri "https://api.github.com/user/emails"
:headers {"Authorization" (dm/str (:token/type tdata) " " (:token/access tdata))}
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}
@@ -305,7 +273,7 @@
{}
props))
(defn fetch-access-token
(defn retrieve-access-token
[{:keys [provider] :as cfg} code]
(let [params {:client_id (:client-id provider)
:client_secret (:client-secret provider)
@@ -329,9 +297,8 @@
(l/trace :hint "access token response" :status status :body body)
(if (= status 200)
(let [data (json/decode body)]
{:token/access (get data :access_token)
:token/id (get data :id_token)
:token/type (get data :token_type)})
{:token (get data :access_token)
:type (get data :token_type)})
(ex/raise :type :internal
:code :unable-to-retrieve-token
@@ -339,11 +306,12 @@
:http-status status
:http-body body)))))
(defn- process-user-info
[provider tdata info]
(defn- retrieve-user-info
[{:keys [provider] :as cfg} tdata]
(letfn [(get-email [props]
;; Allow providers hook into this for custom email
;; retrieval method.
(if-let [get-email-fn (:get-email-fn provider)]
(get-email-fn tdata props)
(let [attr-kw (cf/get :oidc-email-attr "email")
@@ -354,53 +322,48 @@
(let [attr-kw (cf/get :oidc-name-attr "name")
attr-ph (parse-attr-path provider attr-kw)]
(get-in props attr-ph)))
]
(let [props (qualify-props provider info)
email (get-email props)]
{:backend (:name provider)
:fullname (or (get-name props) email)
:email email
:props props})))
(process-response [response]
(let [info (-> response :body json/decode)
props (qualify-props provider info)
email (get-email props)]
{:backend (:name provider)
:fullname (or (get-name props) email)
:email email
:props props}))]
(defn- fetch-user-info
[{:keys [provider] :as cfg} tdata]
(l/trace :hint "fetch user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token/access tdata)))
(l/trace :hint "request user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token tdata))
:token-type (:type tdata))
(let [params {:uri (:user-uri provider)
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
:timeout 6000
:method :get}
response (http/req! cfg params {:sync? true})]
(let [request {:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}
response (http/req! cfg request {:sync? true})]
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status response)
:http-body (:body response)))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status response)
:http-body (:body response)))
(-> response :body json/decode)))
(let [info (process-response response)]
(l/trace :hint "authentication info" :info info)
(defn- get-user-info
[{:keys [provider]} tdata]
(try
(let [{:keys [kid alg] :as theader} (jwt/decode-header (:token/id tdata))]
(when-let [key (if (str/starts-with? (name alg) "hs")
(:client-secret provider)
(get-in provider [:jwks kid]))]
(let [claims (jwt/unsign (:token/id tdata) key {:alg alg})]
(dissoc claims :exp :iss :iat :sid :aud :sub))))
(catch Throwable cause
(l/warn :hint "unable to get user info from JWT token (unexpected exception)"
:cause cause))))
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
(ex/raise :type :internal
:code :incomplete-user-info
:hint "inconmplete user info"
:info info))
info))))
(s/def ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string)
@@ -413,7 +376,7 @@
::props]))
(defn get-info
[{:keys [provider ::main/props] :as cfg} {:keys [params] :as request}]
[{:keys [provider] :as cfg} {:keys [params] :as request}]
(when-let [error (get params :error)]
(ex/raise :type :internal
:code :error-on-retrieving-code
@@ -422,24 +385,9 @@
(let [state (get params :state)
code (get params :code)
state (tokens/verify props {:token state :iss :oauth})
tdata (fetch-access-token cfg code)
info (case (cf/get :oidc-user-info-source)
:token (get-user-info cfg tdata)
:userinfo (fetch-user-info cfg tdata)
(or (get-user-info cfg tdata)
(fetch-user-info cfg tdata)))
info (process-user-info provider tdata info)]
(l/trace :hint "user info" :info info)
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
(ex/raise :type :internal
:code :incomplete-user-info
:hint "inconmplete user info"
:info info))
state (tokens/verify (::main/props cfg) {:token state :iss :oauth})
token (retrieve-access-token cfg code)
info (retrieve-user-info cfg token)]
;; If the provider is OIDC, we can proceed to check
;; roles if they are defined.
@@ -482,24 +430,10 @@
::yrs/headers {"location" (str uri)}})
(defn- generate-error-redirect
[_ cause]
(let [data (if (ex/error? cause) (ex-data cause) nil)
code (or (:code data) :unexpected)
type (or (:type data) :internal)
hint (or (:hint data)
(if (ex/exception? cause)
(ex-message cause)
(str cause)))
params {:error "unable-to-auth"
:hint hint
:type type
:code code}
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string params)))]
[_ error]
(let [uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string {:error "unable-to-auth" :hint (ex-message error)})))]
(redirect-response uri)))
(defn- generate-redirect
@@ -529,23 +463,19 @@
(->> (redirect-response uri)
(sxf request)))
(let [info (assoc info
:iss :prepared-register
:is-active true
:exp (dt/in-future {:hours 48}))
token (tokens/generate (::main/props cfg) info)
params (d/without-nils
{:token token
:fullname (:fullname info)})
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params)))]
(if (auth/email-domain-in-whitelist? (:email info))
(let [info (assoc info
:iss :prepared-register
:is-active true
:exp (dt/in-future {:hours 48}))
token (tokens/generate (::main/props cfg) info)
params (d/without-nils
{:token token
:fullname (:fullname info)})
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params)))]
(redirect-response uri))
(generate-error-redirect cfg "email-domain-not-allowed"))))
(redirect-response uri))))
(defn- auth-handler
[cfg {:keys [params] :as request}]

View File

@@ -146,13 +146,11 @@
(s/def ::google-client-id ::us/string)
(s/def ::google-client-secret ::us/string)
(s/def ::oidc-client-id ::us/string)
(s/def ::oidc-user-info-source ::us/keyword)
(s/def ::oidc-client-secret ::us/string)
(s/def ::oidc-base-uri ::us/string)
(s/def ::oidc-token-uri ::us/string)
(s/def ::oidc-auth-uri ::us/string)
(s/def ::oidc-user-uri ::us/string)
(s/def ::oidc-jwks-uri ::us/string)
(s/def ::oidc-scopes ::us/set-of-strings)
(s/def ::oidc-roles ::us/set-of-strings)
(s/def ::oidc-roles-attr ::us/string)
@@ -243,12 +241,10 @@
::google-client-secret
::oidc-client-id
::oidc-client-secret
::oidc-user-info-source
::oidc-base-uri
::oidc-token-uri
::oidc-auth-uri
::oidc-user-uri
::oidc-jwks-uri
::oidc-scopes
::oidc-roles-attr
::oidc-email-attr

View File

@@ -305,7 +305,7 @@
(fn [params]
(when (contains? cf/flags :smtp)
(let [session (create-smtp-session cfg)]
(with-open [transport (.getTransport session (if (::ssl cfg) "smtps" "smtp"))]
(with-open [transport (.getTransport session (if (:ssl cfg) "smtps" "smtp"))]
(.connect ^Transport transport
^String (::username cfg)
^String (::password cfg))
@@ -341,7 +341,7 @@
(map :content)
first)))
(println "******** end email" (:id email) "**********"))]
(l/raw! :info out)))
(l/info ::l/raw out)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EMAIL FACTORIES

View File

@@ -36,10 +36,10 @@
(defmethod ig/init-key ::routes
[_ {:keys [::wrk/executor] :as cfg}]
(letfn [(handler [request]
(letfn [(handler [request respond _]
(let [data (-> request yrq/body slurp)]
(px/run! executor #(handle-request cfg data)))
{::yrs/status 200})]
(respond {::yrs/status 200}))]
["/sns" {:handler handler
:allowed-methods #{:post}}]))

View File

@@ -118,7 +118,8 @@
(t/write! tw data)))
(catch java.io.IOException _)
(catch Throwable cause
(l/error :hint "unexpected error on encoding response" :cause cause))
(l/warn :hint "unexpected error on encoding response"
:cause cause))
(finally
(.close ^OutputStream output-stream))))))
@@ -131,8 +132,8 @@
(catch java.io.IOException _)
(catch Throwable cause
(l/error :hint "unexpected error on encoding response"
:cause cause))
(l/warn :hint "unexpected error on encoding response"
:cause cause))
(finally
(.close ^OutputStream output-stream))))))

View File

@@ -29,7 +29,6 @@
[app.redis :as-alias rds]
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias rpc.doc]
[app.setup :as-alias setup]
[app.srepl :as-alias srepl]
[app.storage :as-alias sto]
[app.storage.fs :as-alias sto.fs]
@@ -221,7 +220,7 @@
{::db/pool (ig/ref ::db/pool)}
::http.awsns/routes
{::props (ig/ref ::setup/props)
{::props (ig/ref :app.setup/props)
::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)
::wrk/executor (ig/ref ::wrk/executor)}
@@ -264,7 +263,7 @@
::oidc/routes
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::props (ig/ref ::setup/props)
::props (ig/ref :app.setup/props)
::oidc/providers {:google (ig/ref ::oidc.providers/google)
:github (ig/ref ::oidc.providers/github)
:gitlab (ig/ref ::oidc.providers/gitlab)
@@ -276,7 +275,7 @@
::db/pool (ig/ref ::db/pool)
::rpc/routes (ig/ref ::rpc/routes)
::rpc.doc/routes (ig/ref ::rpc.doc/routes)
::props (ig/ref ::setup/props)
::props (ig/ref :app.setup/props)
::mtx/routes (ig/ref ::mtx/routes)
::oidc/routes (ig/ref ::oidc/routes)
::http.debug/routes (ig/ref ::http.debug/routes)
@@ -323,10 +322,11 @@
::rpc/climit (ig/ref ::rpc/climit)
::rpc/rlimit (ig/ref ::rpc/rlimit)
::setup/templates (ig/ref ::setup/templates)
::props (ig/ref ::setup/props)
::props (ig/ref :app.setup/props)
:pool (ig/ref ::db/pool)
:templates (ig/ref :app.setup/builtin-templates)
}
:app.rpc.doc/routes
@@ -337,7 +337,7 @@
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::session/manager (ig/ref ::session/manager)
::props (ig/ref ::setup/props)}
::props (ig/ref :app.setup/props)}
::wrk/registry
{::mtx/metrics (ig/ref ::mtx/metrics)
@@ -390,7 +390,7 @@
:app.tasks.telemetry/handler
{::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)
::props (ig/ref ::setup/props)}
::props (ig/ref :app.setup/props)}
[::srepl/urepl ::srepl/server]
{::srepl/port (cf/get :urepl-port 6062)
@@ -400,9 +400,10 @@
{::srepl/port (cf/get :prepl-port 6063)
::srepl/host (cf/get :prepl-host "localhost")}
::setup/templates {}
:app.setup/builtin-templates
{::http.client/client (ig/ref ::http.client/client)}
::setup/props
:app.setup/props
{::db/pool (ig/ref ::db/pool)
::key (cf/get :secret-key)
@@ -411,7 +412,7 @@
::migrations (ig/ref :app.migrations/migrations)}
::audit.tasks/archive
{::props (ig/ref ::setup/props)
{::props (ig/ref :app.setup/props)
::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)}

View File

@@ -9,7 +9,6 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.media :as cm]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
@@ -227,7 +226,6 @@
(defmethod process-error org.im4java.core.InfoException
[error]
(l/error :hint "unexpected error on processing image" :cause error)
(ex/raise :type :validation
:code :invalid-image
:hint "invalid image"

View File

@@ -9,7 +9,7 @@
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -19,7 +19,9 @@
[app.rpc.climit :as-alias climit]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.util.services :as sv]))
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
(defn- event->row [event]
[(uuid/next)
@@ -50,23 +52,25 @@
(when (seq events)
(db/insert-multi! pool :audit-log event-columns events))))
(def schema:event
[:map {:title "Event"}
[:name [:string {:max 250}]]
[:type [:string {:max 250}]]
[:props
[:map-of :keyword :any]]
[:context {:optional true}
[:map-of :keyword :any]]])
(s/def ::name ::us/string)
(s/def ::type ::us/string)
(s/def ::props (s/map-of ::us/keyword any?))
(s/def ::timestamp dt/instant?)
(s/def ::context (s/map-of ::us/keyword any?))
(def schema:push-audit-events
[:map {:title "push-audit-events"}
[:events [:vector schema:event]]])
(s/def ::event
(s/keys :req-un [::type ::name ::props ::timestamp]
:opt-un [::context]))
(s/def ::events (s/every ::event))
(s/def ::push-audit-events
(s/keys :req [::rpc/profile-id]
:req-un [::events]))
(sv/defmethod ::push-audit-events
{::climit/id :submit-audit-events-by-profile
::climit/key-fn ::rpc/profile-id
::sm/params schema:push-audit-events
::audit/skip true
::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} params]

View File

@@ -6,12 +6,10 @@
(ns app.rpc.commands.auth
(:require
[app.auth :as auth]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -27,13 +25,31 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(def schema:password
[::sm/word-string {:max 500}])
(s/def ::email ::us/email)
(s/def ::fullname ::us/not-empty-string)
(s/def ::lang ::us/string)
(s/def ::path ::us/string)
(s/def ::password ::us/not-empty-string)
(s/def ::old-password ::us/not-empty-string)
(s/def ::theme ::us/string)
(s/def ::invitation-token ::us/not-empty-string)
(s/def ::token ::us/not-empty-string)
(def schema:token
[::sm/word-string {:max 6000}])
;; ---- HELPERS
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if
given whitelist is an empty string."
[domains email]
(if (or (empty? domains)
(nil? domains))
true
(let [[_ candidate] (-> (str/lower email)
(str/split #"@" 2))]
(contains? domains candidate))))
;; ---- COMMAND: login with password
@@ -97,22 +113,22 @@
(rph/with-meta {::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
(def schema:login-with-password
[:map {:title "login-with-password"}
[:email ::sm/email]
[:password schema:password]
[:invitation-token {:optional true} schema:token]])
(s/def ::login-with-password
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::login-with-password
"Performs authentication using penpot password."
{::rpc/auth false
::doc/added "1.15"
::sm/params schema:login-with-password}
::doc/added "1.15"}
[cfg params]
(login-with-password cfg params))
;; ---- COMMAND: Logout
(s/def ::logout
(s/keys :opt [::rpc/profile-id]))
(sv/defmethod ::logout
"Clears the authentication cookie and logout the current session."
{::rpc/auth false
@@ -137,15 +153,13 @@
(update-password conn))
nil)))
(def schema:recover-profile
[:map {:title "recover-profile"}
[:token schema:token]
[:password schema:password]])
(s/def ::token ::us/not-empty-string)
(s/def ::recover-profile
(s/keys :req-un [::token ::password]))
(sv/defmethod ::recover-profile
{::rpc/auth false
::doc/added "1.15"
::sm/params schema:recover-profile}
::doc/added "1.15"}
[cfg params]
(recover-profile cfg params))
@@ -166,9 +180,10 @@
:code :email-does-not-match-invitation
:hint "email should match the invitation"))))
(when-not (auth/email-domain-in-whitelist? (:email params))
(ex/raise :type :validation
:code :email-domain-is-not-allowed))
(when-let [domains (cf/get :registration-domain-whitelist)]
(when-not (email-domain-in-whitelist? domains (:email params))
(ex/raise :type :validation
:code :email-domain-is-not-allowed)))
;; Don't allow proceed in preparing registration if the profile is
;; already reported as spammer.
@@ -226,16 +241,13 @@
(with-meta {:token token}
{::audit/profile-id uuid/zero})))
(def schema:prepare-register-profile
[:map {:title "prepare-register-profile"}
[:email ::sm/email]
[:password schema:password]
[:invitation-token {:optional true} schema:token]])
(s/def ::prepare-register-profile
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::prepare-register-profile
{::rpc/auth false
::doc/added "1.15"
::sm/params schema:prepare-register-profile}
::doc/added "1.15"}
[cfg params]
(prepare-register cfg params))
@@ -245,7 +257,7 @@
"Create the profile entry on the database with limited set of input
attrs (all the other attrs are filled with default values)."
[conn {:keys [email] :as params}]
(dm/assert! ::sm/email email)
(us/assert! ::us/email email)
(let [id (or (:id params) (uuid/next))
props (-> (audit/extract-utm-params params)
(merge (:props params))
@@ -323,9 +335,9 @@
:extra-data ptoken})))
(defn register-profile
[{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
params (assoc claims :fullname fullname)
params (merge params claims)
is-active (or (:is-active params)
(not (contains? cf/flags :email-verification)))
@@ -392,16 +404,12 @@
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
(def schema:register-profile
[:map {:title "register-profile"}
[:token schema:token]
[:fullname [::sm/word-string {:max 100}]]])
(s/def ::register-profile
(s/keys :req-un [::token ::fullname]))
(sv/defmethod ::register-profile
{::rpc/auth false
::doc/added "1.15"
::sm/params schema:register-profile}
::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg ::db/conn conn)
@@ -453,15 +461,12 @@
(create-recovery-token)
(send-email-notification conn))))))
(def schema:request-profile-recovery
[:map {:title "request-profile-recovery"}
[:email ::sm/email]])
(s/def ::request-profile-recovery
(s/keys :req-un [::email]))
(sv/defmethod ::request-profile-recovery
{::rpc/auth false
::doc/added "1.15"
::sm/params schema:request-profile-recovery}
::doc/added "1.15"}
[cfg params]
(request-profile-recovery cfg params))

View File

@@ -592,7 +592,7 @@
(let [options (-> options
(assoc ::section section)
(assoc ::input input)
(assoc ::db/conn conn))]
(assoc :conn conn))]
(binding [*options* options]
(read-section options))))
[:v1/metadata :v1/files :v1/rels :v1/sobjects])
@@ -620,7 +620,7 @@
(update :components pmap-wrap))))
(defmethod read-section :v1/files
[{:keys [::db/conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}]
[{:keys [conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}]
(doseq [expected-file-id (-> *state* deref :files)]
(let [file (read-obj! input)
media' (read-obj! input)
@@ -678,7 +678,7 @@
(db/delete! conn :file-thumbnail {:file-id file-id'})))))))
(defmethod read-section :v1/rels
[{:keys [::db/conn ::input ::timestamp]}]
[{:keys [conn ::input ::timestamp]}]
(let [rels (read-obj! input)]
;; Insert all file relations
(doseq [rel rels]
@@ -693,7 +693,7 @@
(db/insert! conn :file-library-rel rel)))))
(defmethod read-section :v1/sobjects
[{:keys [::sto/storage ::db/conn ::input ::overwrite?]}]
[{:keys [::sto/storage conn ::input ::overwrite?]}]
(let [storage (media/configure-assets-storage storage)
ids (read-obj! input)]
@@ -929,10 +929,5 @@
::input (:path file)
::project-id project-id
::ignore-index-errors? true))]
(db/update! conn :project
{:modified-at (dt/now)}
{:id project-id})
(rph/with-meta ids
{::audit/props {:file nil :file-ids ids}}))))

View File

@@ -468,8 +468,8 @@
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::db/for-update? true)
{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)]
(let [{:keys [thread-id] :as comment} (get-comment conn id ::db/for-update? true)
{:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)

View File

@@ -38,11 +38,6 @@
;; --- FEATURES
(defn resolve-public-uri
[media-id]
(when media-id
(str (cf/get :public-uri) "/assets/by-id/" media-id)))
(def supported-features
#{"storage/objects-map"
"storage/pointer-map"
@@ -189,8 +184,6 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn check-features-compatibility!
"Function responsible to check if provided features are supported by
the current backend"
[features]
(let [not-supported (set/difference features supported-features)]
(when (seq not-supported)
@@ -250,53 +243,50 @@
(into #{} (comp (filter pmap/pointer-map?)
(map pmap/get-id)))))
;; FIXME: file locking
(defn- process-components-v2-feature
"A special case handling of the components/v2 feature."
[{:keys [features data] :as file}]
(let [data (ctf/migrate-to-components-v2 data)
features (conj features "components/v2")]
(-> file
(assoc ::pmg/migrated true)
(assoc :features features)
(assoc :data data))))
(defn handle-file-features!
[{:keys [features] :as file} client-features]
;; Check features compatibility between the currently supported features on
;; the current backend instance and the file retrieved from the database
(check-features-compatibility! features)
(cond-> file
(and (contains? features "components/v2")
(not (contains? client-features "components/v2")))
(as-> file (ex/raise :type :restriction
:code :feature-mismatch
:feature "components/v2"
:hint "file has 'components/v2' feature enabled but frontend didn't specifies it"
:file-id (:id file)))
;; This operation is needed because the components migration generates a new
;; page with random id which is returned to the client; without persisting
;; the migration this can cause that two simultaneous clients can have a
;; different view of the file data and end persisting two pages with main
;; components and breaking the whole file."
(and (contains? client-features "components/v2")
(not (contains? features "components/v2")))
(as-> file (process-components-v2-feature file))
;; This operation is needed for backward comapatibility with frontends that
;; does not support pointer-map resolution mechanism; this just resolves the
;; pointers on backend and return a complete file.
(and (contains? features "storage/pointer-map")
(not (contains? client-features "storage/pointer-map")))
(process-pointers deref)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUERY COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn handle-file-features!
[conn {:keys [id features data] :as file} client-features]
(when (and (contains? features "components/v2")
(not (contains? client-features "components/v2")))
(ex/raise :type :restriction
:code :feature-mismatch
:feature "components/v2"
:hint "file has 'components/v2' feature enabled but frontend didn't specifies it"))
;; NOTE: this operation is needed because the components migration
;; generates a new page with random id which is returned to the
;; client; without persisting the migration this can cause that two
;; simultaneous clients can have a different view of the file data
;; and end persisting two pages with main components and breaking
;; the whole file
(let [file (if (and (contains? client-features "components/v2")
(not (contains? features "components/v2")))
(binding [pmap/*tracked* (atom {})]
(let [data (ctf/migrate-to-components-v2 data)
features (conj features "components/v2")
modified-at (dt/now)
features' (db/create-array conn "text" features)]
(db/update! conn :file
{:data (blob/encode data)
:modified-at modified-at
:features features'}
{:id id})
(persist-pointers! conn id)
(-> file
(assoc :modified-at modified-at)
(assoc :features features)
(assoc :data data))))
file)]
(cond-> file
(and (contains? features "storage/pointer-map")
(not (contains? client-features "storage/pointer-map")))
(process-pointers deref))))
;; --- COMMAND QUERY: get-file (by id)
(sm/def! ::features
@@ -339,41 +329,32 @@
([conn id client-features]
(get-file conn id client-features nil))
([conn id client-features project-id]
;; here we check if client requested features are supported
;; here we check if client requested features are supported
(check-features-compatibility! client-features)
(binding [pmap/*load-fn* (partial load-pointer conn id)
pmap/*tracked* (atom {})]
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(let [params (merge {:id id}
(when (some? project-id)
{:project-id project-id}))
(when (some? project-id)
{:project-id project-id}))
file (-> (db/get conn :file params)
(decode-row)
(pmg/migrate-file))
file (handle-file-features! file client-features)]
file (handle-file-features! conn file client-features)]
;; NOTE: when file is migrated, we break the rule of no perform
;; mutations on get operations and update the file with all
;; migrations applied
(when (pmg/migrated? file)
(let [features (db/create-array conn "text" (:features file))]
(db/update! conn :file
{:data (blob/encode (:data file))
:features features}
{:id id})
(persist-pointers! conn id)))
file))))
;; NOTE: if migrations are applied, probably new pointers generated so
;; instead of persiting them on each get-file, we just resolve them until
;; user updates the file and permanently persists the new pointers
(cond-> file
(pmg/migrated? file)
(process-pointers deref))))))
(defn get-minimal-file
[{:keys [::db/pool] :as cfg} id]
(db/get pool :file {:id id} {:columns [:id :modified-at :revn]}))
(defn get-file-etag
[{:keys [::rpc/profile-id]} {:keys [modified-at revn]}]
(str profile-id (dt/format-instant modified-at :iso) revn))
[{:keys [modified-at revn]}]
(str (dt/format-instant modified-at :iso) "-" revn))
(sv/defmethod ::get-file
"Retrieve a file by its ID. Only authenticated users."
@@ -383,12 +364,12 @@
::sm/params ::get-file
::sm/result ::file-with-permissions}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features project-id] :as params}]
(db/with-atomic [conn pool]
(dm/with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id id)]
(check-read-permissions! perms)
(let [file (-> (get-file conn id features project-id)
(assoc :permissions perms))]
(vary-meta file assoc ::cond/key (get-file-etag params file))))))
(vary-meta file assoc ::cond/key (get-file-etag file))))))
;; --- COMMAND QUERY: get-file-fragment (by id)
@@ -432,23 +413,15 @@
f.modified_at,
f.name,
f.revn,
f.is_shared,
ft.media_id
f.is_shared
from file as f
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
where f.project_id = ?
and f.deleted_at is null
order by f.modified_at desc")
(defn get-project-files
[conn project-id]
(->> (db/exec! conn [sql:project-files project-id])
(mapv (fn [row]
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(dissoc row :media-id))))))
(db/exec! conn [sql:project-files project-id]))
(sv/defmethod ::get-project-files
"Get all files for the specified project."
@@ -529,7 +502,6 @@
[:map {:title "GetPage"}
[:file-id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]
[:object-id {:optional true} ::sm/uuid]
[:features {:optional true} ::features]])
@@ -545,12 +517,14 @@
Mainly used for rendering purposes."
{::doc/added "1.17"
::sm/params ::get-page}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(dm/with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id file-id share-id)]
(check-read-permissions! perms)
(binding [pmap/*load-fn* (partial load-pointer conn file-id)]
(get-page conn params)))))
(check-read-permissions! conn profile-id file-id)
(binding [pmap/*load-fn* (partial load-pointer conn file-id)]
(get-page conn params))))
;; --- COMMAND QUERY: get-team-shared-files
@@ -562,11 +536,9 @@
f.created_at,
f.modified_at,
f.name,
f.is_shared,
ft.media_id
f.is_shared
from file as f
inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
where f.is_shared = true
and f.deleted_at is null
and p.deleted_at is null
@@ -597,12 +569,6 @@
(->> (db/exec! conn [sql:team-shared-files team-id])
(into #{} (comp
(map decode-row)
(map (fn [row]
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(dissoc row :media-id))))
(map #(assoc % :library-summary (library-summary %)))
(map #(dissoc % :data)))))))
@@ -702,11 +668,9 @@
f.modified_at,
f.name,
f.is_shared,
ft.media_id,
row_number() over w as row_num
from file as f
inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
join project as p on (p.id = f.project_id)
where p.team_id = ?
and p.deleted_at is null
and f.deleted_at is null
@@ -717,13 +681,7 @@
(defn get-team-recent-files
[conn team-id]
(->> (db/exec! conn [sql:team-recent-files team-id])
(mapv (fn [row]
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(dissoc row :media-id))))))
(db/exec! conn [sql:team-recent-files team-id]))
(s/def ::get-team-recent-files
(s/keys :req [::rpc/profile-id]

View File

@@ -86,16 +86,16 @@
(ex/raise :type :validation
:code :cant-persist-already-persisted-file))
(let [data
(->> revs
(mapcat #(->> % :changes blob/decode))
(cp/process-changes (blob/decode (:data file))))]
(db/update! conn :file
{:deleted-at nil
:revn revn
:data (blob/encode data)}
{:id id}))
(loop [revs (seq revs)
data (blob/decode (:data file))]
(if-let [rev (first revs)]
(recur (rest revs)
(->> rev :changes blob/decode (cp/process-changes data)))
(db/update! conn :file
{:deleted-at nil
:revn revn
:data (blob/encode data)}
{:id id})))
nil))
(s/def ::persist-temp-file

View File

@@ -14,6 +14,7 @@
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.types.shape-tree :as ctt]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.loggers.audit :as-alias audit]
@@ -38,6 +39,10 @@
;; --- COMMAND QUERY: get-file-object-thumbnails
(defn- get-public-uri
[media-id]
(str (cf/get :public-uri) "/assets/by-id/" media-id))
(defn- get-object-thumbnails
([conn file-id]
(let [sql (str/concat
@@ -47,7 +52,7 @@
res (db/exec! conn [sql file-id])]
(->> res
(d/index-by :object-id (fn [row]
(or (some-> row :media-id files/resolve-public-uri)
(or (some-> row :media-id get-public-uri)
(:data row))))
(d/without-nils))))
@@ -60,7 +65,7 @@
res (db/exec! conn [sql file-id ids])]
(d/index-by :object-id
(fn [row]
(or (some-> row :media-id files/resolve-public-uri)
(or (some-> row :media-id get-public-uri)
(:data row)))
res))))
@@ -80,6 +85,8 @@
;; --- COMMAND QUERY: get-file-thumbnail
;; FIXME: refactor to support uploading data to storage
(defn get-file-thumbnail
[conn file-id revn]
(let [sql (sql/select :file-thumbnail
@@ -88,15 +95,10 @@
{:limit 1
:order-by [[:revn :desc]]})
row (db/exec-one! conn sql)]
(when-not row
(ex/raise :type :not-found
:code :file-thumbnail-not-found))
(when-not (:data row)
(ex/raise :type :not-found
:code :file-thumbnail-not-found))
{:data (:data row)
:props (some-> (:props row) db/decode-transit-pgobject)
:revn (:revn row)
@@ -111,16 +113,20 @@
:opt-un [::revn]))
(sv/defmethod ::get-file-thumbnail
{::doc/added "1.17"
::doc/deprecated "1.19"}
"Method used in frontend for obtain the file thumbnail (used in the
dashboard)."
{::doc/added "1.17"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
(dm/with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
(-> (get-file-thumbnail conn file-id revn)
(rph/with-http-cache long-cache-duration))))
;; --- COMMAND QUERY: get-file-data-for-thumbnail
;; FIXME: performance issue, handle new media_id
;;
;; We need to improve how we set frame for thumbnail in order to avoid
;; loading all pages into memory for find the frame set for thumbnail.
@@ -304,17 +310,14 @@
(:id media) (:id media)])))
(def schema:create-file-object-thumbnail
[:map {:title "create-file-object-thumbnail"}
[:file-id ::sm/uuid]
[:object-id :string]
[:media ::media/upload]])
(s/def ::media (s/nilable ::media/upload))
(s/def ::create-file-object-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::object-id ::media]))
(sv/defmethod ::create-file-object-thumbnail
{:doc/added "1.19"
::audit/skip true
::sm/params schema:create-file-object-thumbnail}
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
@@ -377,6 +380,7 @@
(db/exec-one! conn [sql:upsert-file-thumbnail
file-id revn data props data props])))
(s/def ::revn ::us/integer)
(s/def ::props map?)
@@ -423,27 +427,24 @@
:bucket "file-thumbnail"})]
(db/exec-one! conn [sql:create-file-thumbnail file-id revn
(:id media) props
(:id media) props])
media))
(:id media) props])))
(s/def ::media ::media/upload)
(s/def ::create-file-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::revn ::props ::media]))
(sv/defmethod ::create-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails."
{::doc/added "1.19"
::audit/skip true
::sm/params [:map {:title "create-file-thumbnail"}
[:file-id ::sm/uuid]
[:revn :int]
[:media ::media/upload]]
}
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [media (-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(create-file-thumbnail! params))]
{:uri (files/resolve-public-uri (:id media))}))))
(-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(create-file-thumbnail! params))
nil)))

View File

@@ -36,7 +36,6 @@
(s/def ::id ::us/uuid)
(s/def ::name ::us/not-empty-string)
(s/def ::project-id ::us/uuid)
(s/def ::share-id ::us/uuid)
(s/def ::style valid-style)
(s/def ::team-id ::us/uuid)
(s/def ::weight valid-weight)
@@ -48,8 +47,7 @@
(s/keys :req [::rpc/profile-id]
:opt-un [::team-id
::file-id
::project-id
::share-id])
::project-id])
(fn [o]
(or (contains? o :team-id)
(contains? o :file-id)
@@ -57,7 +55,7 @@
(sv/defmethod ::get-font-variants
{::doc/added "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id share-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id] :as params}]
(dm/with-open [conn (db/open pool)]
(cond
(uuid? team-id)
@@ -76,12 +74,11 @@
(uuid? file-id)
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})
perms (files/get-permissions conn profile-id file-id share-id)]
(files/check-read-permissions! perms)
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})]
(files/check-read-permissions! conn profile-id file-id)
(db/query conn :team-font-variant
{:team-id (:team-id project)
:deleted-at nil})))))
{:team-id (:team-id project)
:deleted-at nil})))))
(declare create-font-variant)

View File

@@ -10,7 +10,6 @@
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.pages.migrations :as pmg]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
@@ -21,15 +20,12 @@
[app.rpc.commands.projects :as proj]
[app.rpc.commands.teams :as teams :refer [create-project-role create-project]]
[app.rpc.doc :as-alias doc]
[app.setup :as-alias setup]
[app.setup.templates :as tmpl]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[clojure.walk :as walk]
[promesa.exec :as px]))
[clojure.walk :as walk]))
;; --- COMMAND: Duplicate File
@@ -237,7 +233,7 @@
(let [project (-> (db/get-by-id conn :project project-id)
(assoc :is-pinned false))
files (db/query conn :file
{:project-id (:id project)
:deleted-at nil}
@@ -323,18 +319,6 @@
;; delete possible broken relations on moved files
(db/exec-one! conn [sql:delete-broken-relations pids])
;; Update the modification date of the all affected projects
;; ensuring that the destination project is the most recent one.
(doseq [project-id (into (list project-id) source)]
;; NOTE: as this is executed on virtual thread, sleeping does
;; not causes major issues, and allows an easy way to set a
;; trully different modification date to each file.
(px/sleep 10)
(db/update! conn :project
{:modified-at (dt/now)}
{:id project-id}))
nil))
(s/def ::ids (s/every ::us/uuid :kind set?))
@@ -377,6 +361,7 @@
nil))
(s/def ::move-project
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::project-id]))
@@ -391,54 +376,46 @@
;; --- COMMAND: Clone Template
(defn- clone-template!
[{:keys [::db/conn] :as cfg} {:keys [profile-id template-id project-id]}]
(let [template (tmpl/get-template-stream cfg template-id)
(declare clone-template)
(s/def ::template-id ::us/not-empty-string)
(s/def ::clone-template
(s/keys :req [::rpc/profile-id]
:req-un [::project-id ::template-id]))
(sv/defmethod ::clone-template
"Clone into the specified project the template by its id."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(clone-template (assoc params :profile-id profile-id)))))
(defn- clone-template
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]
(let [template (d/seek #(= (:id %) template-id) templates)
project (db/get-by-id conn :project project-id {:columns [:id :team-id]})]
(teams/check-edition-permissions! conn profile-id (:team-id project))
(when-not template
(ex/raise :type :not-found
:code :template-not-found
:hint "template not found"))
(teams/check-edition-permissions! conn profile-id (:team-id project))
(-> cfg
;; FIXME: maybe reuse the conn instead of creating more
;; connections in the import process?
(dissoc ::db/conn)
(assoc ::binfile/input template)
(assoc ::binfile/input (:path template))
(assoc ::binfile/project-id (:id project))
(assoc ::binfile/ignore-index-errors? true)
(assoc ::binfile/migrate? true)
(binfile/import!))))
(def schema:clone-template
[:map {:title "clone-template"}
[:project-id ::sm/uuid]
[:template-id ::sm/word-string]])
(sv/defmethod ::clone-template
"Clone into the specified project the template by its id."
{::doc/added "1.16"
::webhooks/event? true
::sm/params schema:clone-template}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(-> (assoc cfg ::db/conn conn)
(clone-template! (assoc params :profile-id profile-id)))))
;; --- COMMAND: Get list of builtin templates
;; --- COMMAND: Retrieve list of builtin templates
(s/def ::retrieve-list-of-builtin-templates any?)
(sv/defmethod ::retrieve-list-of-builtin-templates
{::doc/added "1.10"
::doc/deprecated "1.19"}
[cfg _params]
(mapv #(select-keys % [:id :name]) (::setup/templates cfg)))
(sv/defmethod ::get-builtin-templates
{::doc/added "1.19"}
[cfg _params]
(mapv #(select-keys % [:id :name]) (::setup/templates cfg)))
(mapv #(select-keys % [:id :name :thumbnail-uri]) (:templates cfg)))

View File

@@ -27,6 +27,7 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(declare check-profile-existence!)
@@ -40,7 +41,7 @@
(def schema:profile
[:map {:title "Profile"}
[:id ::sm/uuid]
[:fullname [::sm/word-string {:max 250}]]
[:fullname :string]
[:email ::sm/email]
[:is-active {:optional true} :boolean]
[:is-blocked {:optional true} :boolean]
@@ -81,15 +82,12 @@
;; --- MUTATION: Update Profile (own)
(def schema:update-profile
[:map {:title "update-profile"}
[:fullname [::sm/word-string {:max 250}]]
[:lang {:optional true} [:string {:max 5}]]
[:theme {:optional true} [:string {:max 250}]]])
(sv/defmethod ::update-profile
{::doc/added "1.0"
::sm/params schema:update-profile
::sm/params [:map {:title "UpdateProfileParams"}
[:fullname {:min 1} :string]
[:lang {:optional true} :string]
[:theme {:optional true} :string]]
::sm/result schema:profile}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}]
@@ -130,15 +128,11 @@
(declare update-profile-password!)
(declare invalidate-profile-session!)
(def schema:update-profile-password
[:map {:title "update-profile-password"}
[:password [::sm/word-string {:max 500}]]
;; Social registered users don't have old-password
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]])
(sv/defmethod ::update-profile-password
{:doc/added "1.0"
::sm/params schema:update-profile-password
::sm/params [:map {:title "UpdateProfilePasswordParams"}
[:password :string]
[:old-password :string]]
::sm/result :nil}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}]
@@ -184,13 +178,10 @@
(declare upload-photo)
(declare update-profile-photo)
(def schema:update-profile-photo
[:map {:title "update-profile-photo"}
[:file ::media/upload]])
(sv/defmethod ::update-profile-photo
{:doc/added "1.1"
::sm/params schema:update-profile-photo
::sm/params [:map {:title "UpdateProfilePhotoParams"}
[:file ::media/upload]]
::sm/result :nil}
[cfg {:keys [::rpc/profile-id file] :as params}]
;; Validate incoming mime type
@@ -248,13 +239,11 @@
(declare ^:private request-email-change!)
(declare ^:private change-email-immediately!)
(def schema:request-email-change
[:map {:title "request-email-change"}
[:email ::sm/email]])
(s/def ::request-email-change
(s/keys :req [::rpc/profile-id]
:req-un [::email]))
(sv/defmethod ::request-email-change
{::doc/added "1.0"
::sm/params schema:request-email-change}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id email] :as params}]
(db/with-atomic [conn pool]
(let [profile (db/get-by-id conn :profile profile-id)
@@ -315,13 +304,12 @@
;; --- MUTATION: Update Profile Props
(def schema:update-profile-props
[:map {:title "update-profile-props"}
[:props [:map-of :keyword :any]]])
(s/def ::props map?)
(s/def ::update-profile-props
(s/keys :req [::rpc/profile-id]
:req-un [::props]))
(sv/defmethod ::update-profile-props
{::doc/added "1.0"
::sm/params schema:update-profile-props}
[{:keys [::db/pool]} {:keys [::rpc/profile-id props]}]
(db/with-atomic [conn pool]
(let [profile (get-profile conn profile-id ::db/for-update? true)
@@ -341,12 +329,15 @@
(filter-props props))))
;; --- MUTATION: Delete Profile
(declare ^:private get-owned-teams-with-participants)
(s/def ::delete-profile
(s/keys :req [::rpc/profile-id]))
(sv/defmethod ::delete-profile
{::doc/added "1.0"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(let [teams (get-owned-teams-with-participants conn profile-id)

View File

@@ -10,7 +10,6 @@
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -720,22 +719,29 @@
itoken))))
(def ^:private schema:create-team-invitations
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
[:role [::sm/one-of #{:owner :admin :editor}]]
[:emails ::sm/set-of-emails]])
(s/def ::email ::us/email)
(s/def ::emails ::us/set-of-valid-emails)
(s/def ::create-team-invitations
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::role]
:opt-un [::email ::emails]))
(sv/defmethod ::create-team-invitations
"A rpc call that allow to send a single or multiple invitations to
join the team."
{::doc/added "1.17"
::sm/params schema:create-team-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email emails role] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)]
team (db/get-by-id conn :team team-id)
;; Members emails. We don't re-send inviation to already existing members
member? (into #{}
(map :email)
(db/exec! conn [sql:team-members team-id]))
emails (cond-> (or emails #{}) (string? email) (conj email))]
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/invitations-per-team
@@ -758,13 +764,9 @@
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(let [cfg (assoc cfg ::db/conn conn)
members (->> (db/exec! conn [sql:team-members team-id])
(into #{} (map :email)))
invitations (into #{}
invitations (into []
(comp
;; We don't re-send inviation to already existing members
(remove (partial contains? members))
(remove member?)
(map (fn [email]
{:email (str/lower email)
:team team
@@ -772,8 +774,7 @@
:role role}))
(keep (partial create-invitation cfg)))
emails)]
(with-meta {:total (count invitations)
:invitations invitations}
(with-meta invitations
{::audit/props {:invitations (count invitations)}})))))

View File

@@ -87,6 +87,9 @@
(sv/defmethod ::get-view-only-bundle
{::rpc/auth false
::cond/get-object #(files/get-minimal-file %1 (:file-id %2))
::cond/key-fn files/get-file-etag
::cond/reuse-key? true
::doc/added "1.17"
::sm/params ::get-view-only-bundle}
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]

View File

@@ -27,8 +27,6 @@
[app.common.logging :as l]
[app.rpc.helpers :as rph]
[app.util.services :as-alias sv]
[buddy.core.codecs :as bc]
[buddy.core.hash :as bh]
[yetti.response :as yrs]))
(def
@@ -36,16 +34,9 @@
:doc "Runtime flag for enable/disable conditional processing of RPC methods."}
*enabled* false)
(defn- encode
[s]
(-> s
bh/blake2b-256
bc/bytes->b64u
bc/bytes->str))
(defn- fmt-key
[s]
(str "W/\"" (encode s) "\""))
(str "W/\"" s "\""))
(defn wrap
[_ f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}]
@@ -55,8 +46,9 @@
(fn [cfg {:keys [::key] :as params}]
(if *enabled*
(let [key' (when (or key reuse-key?)
(some->> (get-object cfg params) (key-fn params) (fmt-key)))]
(if (and (some? key) (= key key'))
(some-> (get-object cfg params) key-fn fmt-key))]
(if (and (some? key)
(= key key'))
(fn [_] {::yrs/status 304})
(let [result (f cfg params)
etag (or (and reuse-key? key')

View File

@@ -12,8 +12,8 @@
[app.common.uuid :as uuid]
[app.db :as db]
[app.main :as-alias main]
[app.setup.builtin-templates]
[app.setup.keys :as keys]
[app.setup.templates]
[buddy.core.codecs :as bc]
[buddy.core.nonce :as bn]
[clojure.spec.alpha :as s]

View File

@@ -0,0 +1,72 @@
;; 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.setup.builtin-templates
"A service/module that is responsible for download, load & internally
expose a set of builtin penpot file templates."
(:require
[app.common.logging :as l]
[app.common.spec :as us]
[app.http.client :as http]
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[datoteka.fs :as fs]
[integrant.core :as ig]))
(declare download-all!)
(s/def ::id ::us/not-empty-string)
(s/def ::name ::us/not-empty-string)
(s/def ::thumbnail-uri ::us/not-empty-string)
(s/def ::file-uri ::us/not-empty-string)
(s/def ::path fs/path?)
(s/def ::template
(s/keys :req-un [::id ::name ::thumbnail-uri ::file-uri]
:opt-un [::path]))
(defmethod ig/pre-init-spec :app.setup/builtin-templates [_]
(s/keys :req [::http/client]))
(defmethod ig/init-key :app.setup/builtin-templates
[_ cfg]
(let [presets (-> "app/onboarding.edn" io/resource slurp edn/read-string)]
(l/info :hint "loading template files" :total (count presets))
(let [result (download-all! cfg presets)]
(us/conform (s/coll-of ::template) result))))
(defn- download-preset!
[cfg {:keys [path file-uri] :as preset}]
(let [response (http/req! cfg
{:method :get
:uri file-uri}
{:response-type :input-stream
:sync? true})]
(us/verify! (= 200 (:status response)) "unexpected response found on fetching preset")
(with-open [output (io/output-stream path)]
(with-open [input (io/input-stream (:body response))]
(io/copy input output)))))
(defn- download-all!
"Download presets to the default directory, if preset is already
downloaded, no action will be performed."
[cfg presets]
(let [dest (fs/join fs/*cwd* "builtin-templates")]
(when-not (fs/exists? dest)
(fs/create-dir dest))
(doall
(map (fn [item]
(let [path (fs/join dest (:id item))
item (assoc item :path path)]
(if (fs/exists? path)
(l/trace :hint "template file already present" :id (:id item))
(do
(l/trace :hint "downloading template file" :id (:id item) :dest (str path))
(download-preset! cfg item)))
item))
presets))))

View File

@@ -1,64 +0,0 @@
;; 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.setup.templates
"A service/module that is responsible for download, load & internally
expose a set of builtin penpot file templates."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.http.client :as http]
[app.setup :as-alias setup]
[clojure.edn :as edn]
[clojure.java.io :as io]
[datoteka.fs :as fs]
[integrant.core :as ig]))
(def ^:private schema:template
[:map {:title "Template"}
[:id ::sm/word-string]
[:name ::sm/word-string]
[:file-uri ::sm/word-string]])
(def ^:private schema:templates
[:vector schema:template])
(defmethod ig/init-key ::setup/templates
[_ _]
(let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string)
dest (fs/join fs/*cwd* "builtin-templates")]
(dm/verify!
"expected a valid templates file"
(sm/valid? schema:templates templates))
(doseq [{:keys [id path] :as template} templates]
(let [path (or path (fs/join dest id))]
(if (fs/exists? path)
(l/debug :hint "template file" :id id :state "present" :path (dm/str path))
(l/debug :hint "template file" :id id :state "absent"))))
templates))
(defn get-template-stream
[cfg template-id]
(when-let [template (d/seek #(= (:id %) template-id)
(::setup/templates cfg))]
(let [dest (fs/join fs/*cwd* "builtin-templates")
path (or (:path template) (fs/join dest template-id))]
(if (fs/exists? path)
(io/input-stream path)
(let [resp (http/req! cfg
{:method :get :uri (:file-uri template)}
{:response-type :input-stream :sync? true})]
(dm/verify!
"unexpected response found on fetching template"
(= 200 (:status resp)))
(io/input-stream (:body resp)))))))

View File

@@ -184,7 +184,7 @@
(when (seq res)
(doseq [media-id (into #{} (keep :media-id) res)]
;; Mark as deleted the storage object related with the
;; media-id field.
;; photo-id field.
(l/trace :hint "mark storage object as deleted" :id media-id)
(sto/del-object! storage media-id))

View File

@@ -14,7 +14,6 @@
[app.common.pages :as cp]
[app.common.pprint :as pp]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -128,7 +127,7 @@
(assoc-in [::db/pool ::db/uri] (:database-uri config))
(assoc-in [::db/pool ::db/username] (:database-username config))
(assoc-in [::db/pool ::db/password] (:database-password config))
(assoc-in [:app.rpc/methods :app.setup/templates] templates)
(assoc-in [:app.rpc/methods :templates] templates)
(dissoc :app.srepl/server
:app.http/server
:app.http/router
@@ -136,7 +135,7 @@
:app.auth.oidc/gitlab-provider
:app.auth.oidc/github-provider
:app.auth.oidc/generic-provider
:app.setup/templates
:app.setup/builtin-templates
:app.auth.oidc/routes
:app.worker/monitor
:app.http.oauth/handler
@@ -415,14 +414,6 @@
(println
(us/pretty-explain data))
(= :params-validation (:code data))
(app.common.pprint/pprint
(sm/humanize-data (::sm/explain data)))
(= :data-validation (:code data))
(app.common.pprint/pprint
(sm/humanize-data (::sm/explain data)))
(= :service-error (:type data))
(print-error! (.getCause ^Throwable error))

View File

@@ -39,8 +39,8 @@
params {::th/type :push-audit-events
::rpc/profile-id (:id prof)
:events [{:name "navigate"
:props {:project-id (str proj-id)
:team-id (str team-id)
:props {:project-id proj-id
:team-id team-id
:route "dashboard-files"}
:context {:engine "blink"}
:profile-id (:id prof)
@@ -71,8 +71,8 @@
params {::th/type :push-audit-events
::rpc/profile-id (:id prof)
:events [{:name "navigate"
:props {:project-id (str proj-id)
:team-id (str team-id)
:props {:project-id proj-id
:team-id team-id
:route "dashboard-files"}
:context {:engine "blink"}
:profile-id uuid/zero
@@ -91,8 +91,6 @@
(t/is (= 1 (count rows)))
(t/is (= (:id prof) (:profile-id row)))
(t/is (= "navigate" (:name row)))
(t/is (= "frontend" (:source row))))
)))
(t/is (= "frontend" (:source row)))))))

View File

@@ -252,7 +252,6 @@
:components-v2 true
:changes changes}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))]
@@ -279,7 +278,7 @@
[{:type :add-obj
:page-id page-id
:id shid
:parent-id uuid/zero
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj {:id shid
@@ -287,7 +286,7 @@
:frame-id uuid/zero
:parent-id uuid/zero
:type :image
:metadata {:id (:id fmo1) :width 200 :height 200 :mtype "image/jpeg"}}}])
:metadata {:id (:id fmo1)}}}])
;; Check that reference storage objects on filemediaobjects
;; are the same because of deduplication feature.

View File

@@ -141,7 +141,7 @@
)))
(t/deftest create-file-thumbnail
(t/deftest upsert-file-thumbnail
(let [storage (::sto/storage th/*system*)
profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile)
@@ -159,6 +159,7 @@
data2 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:props {}
:revn 2
:media {:filename "sample.jpg"
:size 7923
@@ -168,6 +169,7 @@
data3 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:props {}
:revn 3
:media {:filename "sample.jpg"
:size 312043
@@ -181,11 +183,11 @@
(let [out (th/command! data2)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (contains? (:result out) :uri)))
(t/is (nil? (:result out))))
(let [out (th/command! data3)]
(t/is (nil? (:error out)))
(t/is (contains? (:result out) :uri)))
(t/is (nil? (:result out))))
(let [[row1 row2 row3 :as rows] (th/db-query :file-thumbnail
{:file-id (:id file)}

View File

@@ -10,7 +10,7 @@
[app.config :as cf]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.auth :as auth]
[app.rpc.commands.auth :as cauth]
[app.tokens :as tokens]
[app.util.time :as dt]
[backend-tests.helpers :as th]
@@ -226,11 +226,11 @@
(t/deftest registration-domain-whitelist
(let [whitelist #{"gmail.com" "hey.com" "ya.ru"}]
(t/testing "allowed email domain"
(t/is (true? (auth/email-domain-in-whitelist? whitelist "username@ya.ru")))
(t/is (true? (auth/email-domain-in-whitelist? #{} "username@somedomain.com"))))
(t/is (true? (cauth/email-domain-in-whitelist? whitelist "username@ya.ru")))
(t/is (true? (cauth/email-domain-in-whitelist? #{} "username@somedomain.com"))))
(t/testing "not allowed email domain"
(t/is (false? (auth/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
(t/is (false? (cauth/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
(t/deftest prepare-register-and-register-profile-1
(let [data {::th/type :prepare-register-profile
@@ -278,7 +278,7 @@
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :params-validation))))
(t/is (th/ex-of-code? error :spec-validation))))
;; try correct register
(let [data {::th/type :register-profile

View File

@@ -37,7 +37,7 @@
:role :editor}]
;; invite external user without complaints
(let [data (assoc data :emails ["foo@bar.com"])
(let [data (assoc data :email "foo@bar.com")
out (th/command! data)
;; retrieve the value from the database and check its content
invitation (db/exec-one!
@@ -52,7 +52,7 @@
;; invite internal user without complaints
(th/reset-mock! mock)
(let [data (assoc data :emails [(:email profile2)])
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
@@ -60,7 +60,7 @@
;; invite user with complaint
(th/create-global-complaint-for pool {:type :complaint :email "foo@bar.com"})
(th/reset-mock! mock)
(let [data (assoc data :emails ["foo@bar.com"])
(let [data (assoc data :email "foo@bar.com")
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
@@ -79,7 +79,7 @@
(th/reset-mock! mock)
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
(let [data (assoc data :emails ["foo@bar.com"])
(let [data (assoc data :email "foo@bar.com")
out (th/command! data)]
(t/is (not (th/success? out)))
@@ -92,7 +92,7 @@
;; invite internal user that is muted
(th/reset-mock! mock)
(let [data (assoc data :emails [(:email profile3)])
(let [data (assoc data :email (:email profile3))
out (th/command! data)]
(t/is (not (th/success? out)))
@@ -118,7 +118,7 @@
;; Try to invite a not existing user
(let [data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:emails ["notexisting@example.com"]
:email "notexisting@example.com"
:team-id (:id team)
:role :editor}
out (th/command! data)]
@@ -126,15 +126,15 @@
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock)))
(t/is (= 1 (-> out :result :total)))
(t/is (= 1 (-> out :result count)))
(let [token (-> out :result :invitations first)
(let [token (-> out :result first)
claims (tokens/decode sprops token)]
(t/is (= :team-invitation (:iss claims)))
(t/is (= (:id profile1) (:profile-id claims)))
(t/is (= :editor (:role claims)))
(t/is (= (:id team) (:team-id claims)))
(t/is (= (first (:emails data)) (:member-email claims)))
(t/is (= (:email data) (:member-email claims)))
(t/is (nil? (:member-id claims)))))
(th/reset-mock! mock)
@@ -142,7 +142,7 @@
;; Try to invite existing user
(let [data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:emails [(:email profile2)]
:email (:email profile2)
:team-id (:id team)
:role :editor}
out (th/command! data)]
@@ -150,15 +150,15 @@
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock)))
(t/is (= 1 (-> out :result :total)))
(t/is (= 1 (-> out :result count)))
(let [token (-> out :result :invitations first)
(let [token (-> out :result first)
claims (tokens/decode sprops token)]
(t/is (= :team-invitation (:iss claims)))
(t/is (= (:id profile1) (:profile-id claims)))
(t/is (= :editor (:role claims)))
(t/is (= (:id team) (:team-id claims)))
(t/is (= (first (:emails data)) (:member-email claims)))
(t/is (= (:email data) (:member-email claims)))
(t/is (= (:id profile2) (:member-id claims)))))
)))
@@ -264,7 +264,7 @@
;; invite internal user without complaints
(with-redefs [app.config/flags #{}]
(th/reset-mock! mock)
(let [data (assoc data :emails [(:email profile2)])
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 0 (:call-count (deref mock)))))

View File

@@ -1,37 +1,40 @@
{:deps
{org.clojure/clojure {:mvn/version "1.11.1"}
org.clojure/data.json {:mvn/version "2.4.0"}
org.clojure/tools.cli {:mvn/version "1.0.219"}
org.clojure/tools.cli {:mvn/version "1.0.214"}
org.clojure/clojurescript {:mvn/version "1.11.60"}
org.clojure/test.check {:mvn/version "1.1.1"}
org.clojure/data.fressian {:mvn/version "1.0.0"}
;; Logging
org.apache.logging.log4j/log4j-api {:mvn/version "2.20.0"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.20.0"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.20.0"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.20.0"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.20.0"}
org.slf4j/slf4j-api {:mvn/version "2.0.7"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.30"}
org.apache.logging.log4j/log4j-api {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.19.0"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.19.0"}
org.slf4j/slf4j-api {:mvn/version "2.0.6"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.26"}
selmer/selmer {:mvn/version "1.12.58"}
selmer/selmer {:mvn/version "1.12.55"}
criterium/criterium {:mvn/version "0.4.6"}
metosin/jsonista {:mvn/version "0.3.7"}
metosin/malli {:mvn/version "0.11.0"}
expound/expound {:mvn/version "0.9.0"}
com.cognitect/transit-clj {:mvn/version "1.0.333"}
com.cognitect/transit-clj {:mvn/version "1.0.329"}
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/cuerdas {:mvn/version "2022.06.16-403"}
funcool/promesa {:mvn/version "11.0.671"}
funcool/promesa
{:git/tag "11.0-alpha13"
:git/sha "f6cab38"
:git/url "https://github.com/funcool/promesa.git"}
funcool/datoteka {:mvn/version "3.0.66"
:exclusions [funcool/promesa]}
lambdaisland/uri {:mvn/version "1.15.125"
lambdaisland/uri {:mvn/version "1.13.95"
:exclusions [org.clojure/data.json]}
frankiesardo/linked {:mvn/version "1.3.0"}
@@ -41,7 +44,7 @@
;; exception printing
fipp/fipp {:mvn/version "0.6.26"}
io.aviso/pretty {:mvn/version "1.4.4"}
io.aviso/pretty {:mvn/version "1.3"}
environ/environ {:mvn/version "1.2.0"}}
:paths ["src" "target/classes"]
:aliases

View File

@@ -752,12 +752,6 @@
[key (delay (generator-fn key))]))
keys))
(defn opacity-to-hex [opacity]
(let [opacity (* opacity 255)
value (mth/round opacity)]
(.. value
(toString 16)
(padStart 2 "0"))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; String Functions

View File

@@ -1,211 +0,0 @@
/**
* 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
*/
"use strict";
goog.require("cljs.core");
goog.provide("app.common.encoding_impl");
goog.scope(function() {
const core = cljs.core;
const global = goog.global;
const self = app.common.encoding_impl;
const hexMap = [];
for (let i = 0; i < 256; i++) {
hexMap[i] = (i + 0x100).toString(16).substr(1);
}
function hexToBuffer(input) {
if (typeof input !== "string") {
throw new TypeError("Expected input to be a string");
}
// Accept UUID hex format
input = input.replace(/-/g, "");
if ((input.length % 2) !== 0) {
throw new RangeError("Expected string to be an even number of characters")
}
const view = new Uint8Array(input.length / 2);
for (let i = 0; i < input.length; i += 2) {
view[i / 2] = parseInt(input.substring(i, i + 2), 16);
}
return view.buffer;
}
function bufferToHex(source, isUuid) {
if (source instanceof Uint8Array) {
} else if (ArrayBuffer.isView(source)) {
source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
} else if (Array.isArray(source)) {
source = Uint8Array.from(source);
}
if (source.length != 16) {
throw new RangeError("only 16 bytes array is allowed");
}
const spacer = isUuid ? "-" : "";
let i = 0;
return (hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] + spacer +
hexMap[source[i++]] +
hexMap[source[i++]] + spacer +
hexMap[source[i++]] +
hexMap[source[i++]] + spacer +
hexMap[source[i++]] +
hexMap[source[i++]] + spacer +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]]);
}
self.hexToBuffer = hexToBuffer;
self.bufferToHex = bufferToHex;
// base-x encoding / decoding
// Copyright (c) 2018 base-x contributors
// Copyright (c) 2014-2018 The Bitcoin Core developers (base58.cpp)
// Distributed under the MIT software license, see the accompanying
// file LICENSE or http://www.opensource.org/licenses/mit-license.php.
// WARNING: This module is NOT RFC3548 compliant, it cannot be used
// for base16 (hex), base32, or base64 encoding in a standards
// compliant manner.
function getBaseCodec (ALPHABET) {
if (ALPHABET.length >= 255) { throw new TypeError("Alphabet too long"); }
let BASE_MAP = new Uint8Array(256);
for (let j = 0; j < BASE_MAP.length; j++) {
BASE_MAP[j] = 255;
}
for (let i = 0; i < ALPHABET.length; i++) {
let x = ALPHABET.charAt(i);
let xc = x.charCodeAt(0);
if (BASE_MAP[xc] !== 255) { throw new TypeError(x + " is ambiguous"); }
BASE_MAP[xc] = i;
}
let BASE = ALPHABET.length;
let LEADER = ALPHABET.charAt(0);
let FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up
let iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up
function encode (source) {
if (source instanceof Uint8Array) {
} else if (ArrayBuffer.isView(source)) {
source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
} else if (Array.isArray(source)) {
source = Uint8Array.from(source);
}
if (!(source instanceof Uint8Array)) { throw new TypeError("Expected Uint8Array"); }
if (source.length === 0) { return ""; }
// Skip & count leading zeroes.
let zeroes = 0;
let length = 0;
let pbegin = 0;
let pend = source.length;
while (pbegin !== pend && source[pbegin] === 0) {
pbegin++;
zeroes++;
}
// Allocate enough space in big-endian base58 representation.
let size = ((pend - pbegin) * iFACTOR + 1) >>> 0;
let b58 = new Uint8Array(size);
// Process the bytes.
while (pbegin !== pend) {
let carry = source[pbegin];
// Apply "b58 = b58 * 256 + ch".
let i = 0;
for (let it1 = size - 1; (carry !== 0 || i < length) && (it1 !== -1); it1--, i++) {
carry += (256 * b58[it1]) >>> 0;
b58[it1] = (carry % BASE) >>> 0;
carry = (carry / BASE) >>> 0;
}
if (carry !== 0) { throw new Error("Non-zero carry"); }
length = i;
pbegin++;
}
// Skip leading zeroes in base58 result.
let it2 = size - length;
while (it2 !== size && b58[it2] === 0) {
it2++;
}
// Translate the result into a string.
let str = LEADER.repeat(zeroes);
for (; it2 < size; ++it2) { str += ALPHABET.charAt(b58[it2]); }
return str;
}
function decodeUnsafe (source) {
if (typeof source !== "string") { throw new TypeError("Expected String"); }
if (source.length === 0) { return new Uint8Array(); }
let psz = 0;
// Skip and count leading '1's.
let zeroes = 0;
let length = 0;
while (source[psz] === LEADER) {
zeroes++;
psz++;
}
// Allocate enough space in big-endian base256 representation.
let size = (((source.length - psz) * FACTOR) + 1) >>> 0; // log(58) / log(256), rounded up.
let b256 = new Uint8Array(size);
// Process the characters.
while (source[psz]) {
// Decode character
let carry = BASE_MAP[source.charCodeAt(psz)];
// Invalid character
if (carry === 255) { return; }
let i = 0;
for (let it3 = size - 1; (carry !== 0 || i < length) && (it3 !== -1); it3--, i++) {
carry += (BASE * b256[it3]) >>> 0;
b256[it3] = (carry % 256) >>> 0;
carry = (carry / 256) >>> 0;
}
if (carry !== 0) { throw new Error("Non-zero carry"); }
length = i;
psz++;
}
// Skip leading zeroes in b256.
let it4 = size - length;
while (it4 !== size && b256[it4] === 0) {
it4++;
}
let vch = new Uint8Array(zeroes + (size - it4));
let j = zeroes;
while (it4 !== size) {
vch[j++] = b256[it4++];
}
return vch;
}
function decode (string) {
let buffer = decodeUnsafe(string);
if (buffer) { return buffer; }
throw new Error("Non-base" + BASE + " character");
}
return {
encode: encode,
decodeUnsafe: decodeUnsafe,
decode: decode
};
}
// MORE bases here: https://github.com/cryptocoinjs/base-x/tree/master
const BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
self.bufferToBase62 = getBaseCodec(BASE62).encode;
});

View File

@@ -341,15 +341,13 @@
:else
(let [objects (lookup-objects file)
bool-content (gsh/calc-bool-content bool objects)
bool' (gsh/update-bool-selrect bool children objects)]
(commit-change
file
{:type :mod-obj
:id bool-id
:operations
[{:type :set :attr :bool-content :val bool-content :ignore-touched true}
{:type :set :attr :selrect :val (:selrect bool') :ignore-touched true}
[{:type :set :attr :selrect :val (:selrect bool') :ignore-touched true}
{:type :set :attr :points :val (:points bool') :ignore-touched true}
{:type :set :attr :x :val (-> bool' :selrect :x) :ignore-touched true}
{:type :set :attr :y :val (-> bool' :selrect :y) :ignore-touched true}

View File

@@ -323,13 +323,3 @@
:rfn (fn [^Reader rdr]
(let [^List x (read-object! rdr)]
(Matrix. (.get x 0) (.get x 1) (.get x 2) (.get x 3) (.get x 4) (.get x 5))))})
;; Backward compatibility for 1.19 with v1.20;
(add-handlers!
{:name "penpot/geom/rect"
:rfn read-map-like}
{:name "penpot/shape"
:rfn read-map-like})

View File

@@ -136,7 +136,6 @@
(dm/export gco/center-rect)
(dm/export gco/center-points)
(dm/export gco/transform-points)
(dm/export gco/shape->points)
(dm/export gpr/make-rect)
(dm/export gpr/make-selrect)

View File

@@ -147,7 +147,6 @@
:else
(cph/reduce-objects
objects
(fn [shape]
(and (d/not-empty? (:shapes shape))
(or (not (cph/frame-shape? shape))

View File

@@ -84,15 +84,3 @@
(or (mth/nan? (:x p))
(mth/nan? (:y p))))
points)))
(defn shape->points
[{:keys [transform points]}]
(if (gmt/unit? transform)
;; Fix problem with precision could skew the shape
;; when there are no transforms the points are the selrect shape
(let [p0 (nth points 0) ;; left top
p2 (nth points 2) ;; right bottom
p1 (gpt/point (:x p2) (:y p0))
p3 (gpt/point (:x p0) (:y p2))]
[p0 p1 p2 p3])
points))

View File

@@ -463,7 +463,7 @@
(cond-> modif-tree
snap-pixel? (gpp/adjust-pixel-precision objects snap-precision snap-ignore-axis))
bounds (d/lazy-map (keys objects) #(gco/shape->points (get objects %)))
bounds (d/lazy-map (keys objects) #(dm/get-in objects [% :points]))
bounds (cond-> bounds
(some? old-modif-tree)
(transform-bounds objects old-modif-tree))

View File

@@ -58,7 +58,7 @@
(defn set-pixel-precision
"Adjust modifiers so they adjust to the pixel grid"
[modifiers shape precision ignore-axis]
(let [points (-> shape gco/shape->points (gco/transform-points (ctm/modifiers->transform modifiers)))
(let [points (-> shape :points (gco/transform-points (ctm/modifiers->transform modifiers)))
has-resize? (not (ctm/only-move? modifiers))
[modifiers points]

View File

@@ -218,7 +218,7 @@
(make-selrect (min xp1 xp2) (min yp1 yp2) (abs (- xp1 xp2)) (abs (- yp1 yp2)))))
(defn clip-selrect
[{:keys [x1 y1 x2 y2] :as sr} clip-rect]
[{:keys [x1 y1 x2 y2] :as sr} bounds]
(when (some? sr)
(let [{bx1 :x1 by1 :y1 bx2 :x2 by2 :y2 :as sr2} (rect->selrect clip-rect)]
(let [{bx1 :x1 by1 :y1 bx2 :x2 by2 :y2} (rect->selrect bounds)]
(corners->selrect (max bx1 x1) (max by1 y1) (min bx2 x2) (min by2 y2)))))

View File

@@ -317,7 +317,7 @@
its properties. We adjust de x,y,width,height and create a custom transform"
[shape transform-mtx]
(let [points' (gco/shape->points shape)
(let [points' (:points shape)
points (gco/transform-points points' transform-mtx)
shape (-> shape (adjust-shape-flips points))
bool? (= (:type shape) :bool)

View File

@@ -239,7 +239,7 @@
#?(:clj
(defn slf4j-log-handler
{:no-doc true}
[_ _ _ {:keys [::logger ::level ::trace ::message] }]
[_ _ _ {:keys [::logger ::level ::props ::cause ::trace ::message]}]
(when-let [logger (enabled? logger level)]
(let [message (cond-> @message
(some? trace)
@@ -307,18 +307,6 @@
(l/set-level! logger level)))
config)))
(defmacro raw!
[level message]
(let [cljs? (:ns &env)]
`(do
(~(if cljs?
`(partial console-log-handler nil nil nil)
`(partial slf4j-log-handler nil nil nil))
{::logger ~(str *ns*)
::level ~level
::message (delay ~message)})
nil)))
(defmacro info
[& params]
`(do

View File

@@ -246,8 +246,9 @@
(let [shape-old (dm/get-in data-old [:pages-index page-id :objects id])
shape-new (dm/get-in data-new [:pages-index page-id :objects id])]
;; If object has changed or is new verify is correct
(when (and (some? shape-new)
;; If object has changed verify is correct
(when (and (some? shape-old)
(some? shape-new)
(not= shape-old shape-new))
(dm/verify! (cts/shape? shape-new)))))]

View File

@@ -603,9 +603,7 @@
(apply-changes-local))))
(defn add-component
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page]
(add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil))
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation]
[changes id path name new-shapes updated-shapes main-instance-id main-instance-page]
(assert-page-id changes)
(assert-objects changes)
(let [page-id (::page-id (meta changes))
@@ -643,8 +641,7 @@
:path path
:name name
:main-instance-id main-instance-id
:main-instance-page main-instance-page
:annotation annotation}
:main-instance-page main-instance-page}
(some? new-shapes) ;; this will be null in components-v2
(assoc :shapes (vec new-shapes))))
(into (map mk-change) updated-shapes))))
@@ -658,7 +655,7 @@
(map lookupf)
(map mk-change))
updated-shapes))))
(apply-changes-local)))))
(apply-changes-local))))
(defn update-component
[changes id update-fn]

View File

@@ -12,7 +12,7 @@
[app.common.schema :as sm]
[app.common.uuid :as uuid]))
(def file-version 21)
(def file-version 20)
(def default-color clr/gray-20)
(def root uuid/zero)

View File

@@ -45,9 +45,8 @@
(defn migrated?
[{:keys [data] :as file}]
(or (::migrated file)
(> (:version data)
(::orig-version file))))
(> (:version data)
(::orig-version file)))
;; Default handler, noop
(defmethod migrate :default [data] data)
@@ -437,7 +436,7 @@
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(defmethod migrate 21
(defmethod migrate 20
[data]
(letfn [(update-object [objects object]
(let [frame-id (:frame-id object)

View File

@@ -50,15 +50,12 @@
(defn update-curve-to
[command h1 h2]
(let [params {:x (-> command :params :x)
:y (-> command :params :y)
:c1x (:x h1)
:c1y (:y h1)
:c2x (:x h2)
:c2y (:y h2)}]
(-> command
(assoc :command :curve-to)
(assoc :params params))))
(-> command
(assoc :command :curve-to)
(assoc-in [:params :c1x] (:x h1))
(assoc-in [:params :c1y] (:y h1))
(assoc-in [:params :c2x] (:x h2))
(assoc-in [:params :c2y] (:y h2))))
(defn make-curve-to
[to h1 h2]

View File

@@ -282,9 +282,7 @@
(def! ::email
{:type ::email
:pred (fn [s]
(and (string? s)
(< (count s) 250)
(re-seq email-re s)))
(and (string? s) (re-seq email-re s)))
:type-properties
{:title "email"
:description "string with valid email address"
@@ -466,7 +464,6 @@
(def! ::word-string
{:type ::word-string
:pred #(and (string? %) (not (str/blank? %)))
:property-pred (m/-min-max-pred count)
:type-properties
{:title "string"
:description "string"

View File

@@ -48,7 +48,7 @@
(sm/def! ::gradient
[:map {:title "Gradient"}
[:type [::sm/one-of #{:linear :radial "linear" "radial"}]]
[:type [::sm/one-of #{:linear :radial}]]
[:start-x ::sm/safe-number]
[:start-y ::sm/safe-number]
[:end-x ::sm/safe-number]

View File

@@ -30,12 +30,13 @@
(assoc component :modified-at (dt/now)))
(defn add-component
[fdata {:keys [id name path main-instance-id main-instance-page shapes annotation]}]
[fdata {:keys [id name path main-instance-id main-instance-page shapes]}]
(let [components-v2 (dm/get-in fdata [:options :components-v2])
fdata (update fdata :components assoc id (touch {:id id :name name :path path}))]
(if components-v2
(cond-> (update-in fdata [:components id] assoc :main-instance-id main-instance-id :main-instance-page main-instance-page)
annotation (update-in [:components id] assoc :annotation annotation))
(update-in fdata [:components id] assoc
:main-instance-id main-instance-id
:main-instance-page main-instance-page)
(let [wrap-object-fn feat/*wrap-with-objects-map-fn*]
(assoc-in fdata [:components id :objects]
@@ -115,9 +116,3 @@
[]
[[(:id shape) (:component-id shape) :component]]))
[]))
(defn get-component-annotation
[shape libraries]
(let [library (dm/get-in libraries [(:component-file shape) :data])
component (get-component library (:component-id shape) true)]
(:annotation component)))

View File

@@ -96,25 +96,22 @@
"Get the parent shape linked to a component for this shape, if any"
([objects shape] (get-component-shape objects shape nil))
([objects shape {:keys [allow-main?] :or {allow-main? false} :as options}]
(cond
(nil? shape)
nil
(cond
(nil? shape)
nil
(= uuid/zero (:id shape))
nil
(and (not (ctk/in-component-copy? shape)) (not allow-main?))
nil
(and (not (ctk/in-component-copy? shape)) (not allow-main?))
nil
(ctk/instance-root? shape)
shape
(ctk/instance-root? shape)
shape
:else
(get-component-shape objects (get objects (:parent-id shape)) options))))
:else
(get-component-shape objects (get objects (:parent-id shape)) options))))
(defn in-component-main?
"Check if the shape is inside a component non-main instance.
Note that we must iterate on the parents because non-root shapes in
a main component have not any discriminating attribute."
[objects shape]

View File

@@ -291,7 +291,7 @@
been modified after the given date."
[file-data library since-date]
(letfn [(used-assets-shape [shape]
(concat
(concat
(ctkl/used-components-changed-since shape library since-date)
(ctcl/used-colors-changed-since shape library since-date)
(ctyl/used-typographies-changed-since shape library since-date)))
@@ -299,7 +299,7 @@
(used-assets-container [container]
(->> (mapcat used-assets-shape (ctn/shapes-seq container))
(map #(cons (:id container) %))))]
(mapcat used-assets-container (containers-seq file-data))))
(defn get-or-add-library-page
@@ -407,7 +407,7 @@
(update page :objects update-vals root-to-board))]
(-> file-data
(add-instance-grid (reverse (sort-by :name components)))
(add-instance-grid (sort-by :name components))
(update :pages-index update-vals roots-to-board)
(assoc-in [:options :components-v2] true))))))))

View File

@@ -22,14 +22,14 @@
[:map
[:color ::grid-color]
[:type [::sm/one-of #{:stretch :left :center :right}]]
[:size {:optional true} [:maybe ::sm/safe-number]]
[:size {:optional true} ::sm/safe-number]
[:margin {:optional true} [:maybe ::sm/safe-number]]
[:item-length {:optional true} [:maybe ::sm/safe-number]]
[:gutter {:optional true} [:maybe ::sm/safe-number]]])
(sm/def! ::square-params
[:map
[:size {:optional true} [:maybe ::sm/safe-number]]
[:size ::sm/safe-number]
[:color ::grid-color]])
(sm/def! ::grid

View File

@@ -182,7 +182,7 @@
[:vector {:gen/max 2}
[:map
[:command :keyword]
[:relative {:optional true} :boolean]
[:relative :boolean]
[:params [:map-of {:gen/max 5} :keyword ::sm/safe-number]]]]]])
(sm/def! ::rect-attrs
@@ -225,7 +225,7 @@
[:map {:title "TextAttrs"}
[:id ::sm/uuid]
[:type [:= :text]]
[:content {:optional true} [:maybe ::ctsx/content]]])
[:content ::ctsx/content]])
(sm/def! ::shape
[:multi {:dispatch :type :title "Shape"}

View File

@@ -495,7 +495,8 @@
"expected compatible interaction map"
(has-overlay-opts interaction))
(let [;; When the interactive item is inside a nested frame we need to add to the offset the position
(let [
;; When the interactive item is inside a nested frame we need to add to the offset the position
;; of the parent-frame otherwise the position won't match
shape-frame (cph/get-frame objects shape)
@@ -504,10 +505,10 @@
(cph/root-frame? shape-frame)
(cph/root? shape-frame))
frame-offset
(gpt/add frame-offset (gpt/point shape-frame)))]
(gpt/add frame-offset (gpt/point shape-frame)))
]
(if (nil? dest-frame)
[(gpt/point 0 0) [:top :left]]
(gpt/point 0 0)
(let [overlay-size (gsb/get-object-bounds objects dest-frame)
base-frame-size (:selrect base-frame)
relative-to-shape-size (:selrect relative-to-shape)
@@ -525,46 +526,37 @@
overlay-position
{:x (- (:x overlay-position) (:x relative-to-adjusted-to-base-frame))
:y (- (:y overlay-position) (:y relative-to-adjusted-to-base-frame))})]
(case (:overlay-pos-type interaction)
:center
[(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(+ (:y base-position) (/ (- (:height relative-to-shape-size) (:height overlay-size)) 2)))
[:center :center]]
(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(+ (:y base-position) (/ (- (:height relative-to-shape-size) (:height overlay-size)) 2)))
:top-left
[(gpt/point (:x base-position) (:y base-position))
[:top :left]]
(gpt/point (:x base-position) (:y base-position))
:top-right
[(gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size)))
(:y base-position))
[:top :right]]
(gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size)))
(:y base-position))
:top-center
[(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(:y base-position))
[:top :center]]
(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(:y base-position))
:bottom-left
[(gpt/point (:x base-position)
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
[:bottom :left]]
(gpt/point (:x base-position)
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
:bottom-right
[(gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size)))
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
[:bottom :right]]
(gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size)))
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
:bottom-center
[(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
[:bottom :center]]
(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
:manual
[(gpt/point (+ (:x base-position) (:x overlay-position))
(+ (:y base-position) (:y overlay-position)))
[:top :left]])))))
(gpt/point (+ (:x base-position) (:x overlay-position))
(+ (:y base-position) (:y overlay-position))))))))
(defn has-animation?
[interaction]

View File

@@ -260,11 +260,7 @@
(let [frame-ids (cond->> (all-frames-by-position objects position)
(some? excluded)
(remove excluded)
:always
(remove #(or (dm/get-in objects [% :hidden])
(dm/get-in objects [% :blocked]))))
(remove excluded))
frame-set (set frame-ids)]
@@ -280,10 +276,7 @@
"Search the top nested frame in a list of ids"
[objects ids]
(let [frame-ids (->> ids
(filter #(cph/frame-shape? objects %))
(remove #(or (dm/get-in objects [% :hidden])
(dm/get-in objects [% :blocked]))))
(let [frame-ids (->> ids (filter #(cph/frame-shape? objects %)))
frame-set (set frame-ids)]
(loop [current-id (first frame-ids)]
(let [current-shape (get objects current-id)

View File

@@ -4,11 +4,9 @@
;;
;; Copyright (c) KALEIDOS INC
#_:clj-kondo/ignore
(ns app.common.uuid
(:refer-clojure :exclude [next uuid zero? short])
(:refer-clojure :exclude [next uuid zero?])
(:require
[app.common.data.macros :as dm]
#?(:clj [clojure.core :as c])
#?(:cljs [app.common.uuid-impl :as impl])
#?(:cljs [cljs.core :as c]))
@@ -68,10 +66,3 @@
(let [buf (ByteBuffer/wrap o)]
(UUID. ^long (.getLong buf)
^long (.getLong buf)))))
#?(:cljs
(defn uuid->short-id
"Return a shorter string of a safe subset of bytes of an uuid encoded
with base62. It is only safe to use with uuid v4 and penpot custom v8"
[id]
(impl/short-v8 (dm/str id))))

View File

@@ -8,17 +8,13 @@
"use strict";
goog.require("cljs.core");
goog.require("app.common.encoding_impl");
goog.provide("app.common.uuid_impl");
goog.scope(function() {
const core = cljs.core;
const global = goog.global;
const encoding = app.common.encoding_impl;
const self = app.common.uuid_impl;
const timeRef = 1640995200000; // ms since 2022-01-01T00:00:00
const fill = (() => {
if (typeof global.crypto !== "undefined" &&
typeof global.crypto.getRandomValues !== "undefined") {
@@ -49,8 +45,12 @@ goog.scope(function() {
}
})();
const hexMap = [];
for (let i = 0; i < 256; i++) {
hexMap[i] = (i + 0x100).toString(16).substr(1);
}
function toHexString(buf) {
const hexMap = encoding.hexMap;
let i = 0;
return (hexMap[buf[i++]] +
hexMap[buf[i++]] +
@@ -68,7 +68,18 @@ goog.scope(function() {
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]]);
};
}
self.v4 = (function () {
const buff8 = new Uint8Array(16);
return function v4() {
fill(buff8);
buff8[6] = (buff8[6] & 0x0f) | 0x40;
buff8[8] = (buff8[8] & 0x3f) | 0x80;
return core.uuid(toHexString(buff8));
};
})();
function getBigUint64(view, byteOffset, le) {
const a = view.getUint32(byteOffset, le);
@@ -92,54 +103,17 @@ goog.scope(function() {
}
}
function currentTimestamp(timeRef) {
return BigInt.asUintN(64, "" + (Date.now() - timeRef));
}
const tmpBuff = new ArrayBuffer(8);
const tmpView = new DataView(tmpBuff);
const tmpInt8 = new Uint8Array(tmpBuff);
function nextLong() {
fill(tmpInt8);
return getBigUint64(tmpView, 0, false);
}
self.shortID = (function () {
const buff = new ArrayBuffer(8);
self.v8 = (function () {
const buff = new ArrayBuffer(16);
const int8 = new Uint8Array(buff);
const view = new DataView(buff);
const base = 0x0000_0000_0000_0000n;
const tmpBuff = new ArrayBuffer(8);
const tmpView = new DataView(tmpBuff);
const tmpInt8 = new Uint8Array(tmpBuff);
return function shortID(ts) {
const tss = currentTimestamp(timeRef);
const msb = (base
| (nextLong() & 0xffff_ffff_0000_0000n)
| (tss & 0x0000_0000_ffff_ffffn));
setBigUint64(view, 0, msb, false);
return encoding.toBase62(int8);
};
})();
self.v4 = (function () {
const arr = new Uint8Array(16);
return function v4() {
fill(arr);
arr[6] = (arr[6] & 0x0f) | 0x40;
arr[8] = (arr[8] & 0x3f) | 0x80;
return core.uuid(encoding.bufferToHex(arr, true));
};
})();
self.v8 = (function () {
const buff = new ArrayBuffer(16);
const int8 = new Uint8Array(buff);
const view = new DataView(buff);
const maxCs = 0x0000_0000_0000_3fffn; // 14 bits space
const timeRef = 1640995200000; // ms since 2022-01-01T00:00:00
const maxCs = 0x0000_0000_0000_3fffn; // 14 bits space
let countCs = 0n;
let lastRd = 0n;
@@ -148,6 +122,15 @@ goog.scope(function() {
let baseMsb = 0x0000_0000_0000_8000n;
let baseLsb = 0x8000_0000_0000_0000n;
const currentTimestamp = () => {
return BigInt.asUintN(64, "" + (Date.now() - timeRef));
};
const nextLong = () => {
fill(tmpInt8);
return getBigUint64(tmpView, 0, false);
};
lastRd = nextLong() & 0xffff_ffff_ffff_f0ffn;
lastCs = nextLong() & maxCs;
@@ -162,12 +145,12 @@ goog.scope(function() {
setBigUint64(view, 0, msb, false);
setBigUint64(view, 8, lsb, false);
return core.uuid(encoding.bufferToHex(int8, true));
return core.uuid(toHexString(int8));
};
const factory = function v8() {
while (true) {
let ts = currentTimestamp(timeRef);
let ts = currentTimestamp();
// Protect from clock regression
if ((ts - lastTs) < 0) {
@@ -212,12 +195,6 @@ goog.scope(function() {
})();
self.short_v8 = function(uuid) {
const buff = encoding.hexToBuffer(uuid);
const short = new Uint8Array(buff, 4);
return encoding.bufferToBase62(short);
};
self.custom = function formatAsUUID(mostSigBits, leastSigBits) {
const most = mostSigBits.toString("16").padStart(16, "0");
const least = leastSigBits.toString("16").padStart(16, "0");

View File

@@ -324,275 +324,209 @@
interaction-rect (ctsi/set-position-relative-to interaction (:id rect))]
(t/testing "Overlay top-left relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :top-left base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 0))
(t/is (= (:y overlay-pos) 0))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 0))))
(t/testing "Overlay top-center relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :top-center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 0))
(t/is (= snap-v :top))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 0))))
(t/testing "Overlay top-right relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :top-right base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 70))
(t/is (= (:y overlay-pos) 0))
(t/is (= snap-v :top))
(t/is (= snap-h :right))))
(t/is (= (:y overlay-pos) 0))))
(t/testing "Overlay bottom-left relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :bottom-left base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 0))
(t/is (= (:y overlay-pos) 80))
(t/is (= snap-v :bottom))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 80))))
(t/testing "Overlay bottom-center relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :bottom-center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 80))
(t/is (= snap-v :bottom))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 80))))
(t/testing "Overlay bottom-right relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :bottom-right base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 70))
(t/is (= (:y overlay-pos) 80))
(t/is (= snap-v :bottom))
(t/is (= snap-h :right))))
(t/is (= (:y overlay-pos) 80))))
(t/testing "Overlay center relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 40))
(t/is (= snap-v :center))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 40))))
(t/testing "Overlay manual relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 40))
(t/is (= snap-v :center))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 40))))
(t/testing "Overlay manual relative to auto"
(let [i2 (-> interaction-auto
(ctsi/set-overlay-pos-type :manual base-frame objects)
(ctsi/set-overlay-position (gpt/point 12 62)))
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 17))
(t/is (= (:y overlay-pos) 67))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 67))))
(t/testing "Overlay top-left relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :top-left base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 5))
(t/is (= (:y overlay-pos) 5))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 5))))
(t/testing "Overlay top-center relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :top-center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 40))
(t/is (= (:y overlay-pos) 5))
(t/is (= snap-v :top))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 5))))
(t/testing "Overlay top-right relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :top-right base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 75))
(t/is (= (:y overlay-pos) 5))
(t/is (= snap-v :top))
(t/is (= snap-h :right))))
(t/is (= (:y overlay-pos) 5))))
(t/testing "Overlay bottom-left relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :bottom-left base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 5))
(t/is (= (:y overlay-pos) 85))
(t/is (= snap-v :bottom))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 85))))
(t/testing "Overlay bottom-center relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :bottom-center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 40))
(t/is (= (:y overlay-pos) 85))
(t/is (= snap-v :bottom))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 85))))
(t/testing "Overlay bottom-right relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :bottom-right base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 75))
(t/is (= (:y overlay-pos) 85))
(t/is (= snap-v :bottom))
(t/is (= snap-h :right))))
(t/is (= (:y overlay-pos) 85))))
(t/testing "Overlay center relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 40))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :center))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 45))))
(t/testing "Overlay manual relative to base-frame"
(let [i2 (-> interaction-base-frame
(ctsi/set-overlay-pos-type :manual base-frame objects)
(ctsi/set-overlay-position (gpt/point 12 62)))
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 17))
(t/is (= (:y overlay-pos) 67))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 67))))
(t/testing "Overlay top-left relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-left base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 15))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 15))))
(t/testing "Overlay top-center relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 15))))
(t/testing "Overlay top-right relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-right base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :right))))
(t/is (= (:y overlay-pos) 15))))
(t/testing "Overlay bottom-left relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :bottom-left base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 15))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 45))))
(t/testing "Overlay bottom-center relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :bottom-center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 45))))
(t/testing "Overlay bottom-right relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :bottom-right base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :right))))
(t/is (= (:y overlay-pos) 45))))
(t/testing "Overlay center relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 30))
(t/is (= snap-v :center))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 30))))
(t/testing "Overlay manual relative to popup"
(let [i2 (-> interaction-popup
(ctsi/set-overlay-pos-type :manual base-frame objects)
(ctsi/set-overlay-position (gpt/point 12 62)))
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 27))
(t/is (= (:y overlay-pos) 77))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 77))))
(t/testing "Overlay top-left relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-left base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 15))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 15))))
(t/testing "Overlay top-center relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :top-center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 15))))
(t/testing "Overlay top-right relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :top-right base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :right))))
(t/is (= (:y overlay-pos) 15))))
(t/testing "Overlay bottom-left relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :bottom-left base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 15))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :left))))
(t/is (= (:y overlay-pos) 45))))
(t/testing "Overlay bottom-center relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :bottom-center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 45))))
(t/testing "Overlay bottom-right relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :bottom-right base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :right))))
(t/is (= (:y overlay-pos) 45))))
(t/testing "Overlay center relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :center base-frame objects)
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 30))
(t/is (= snap-v :center))
(t/is (= snap-h :center))))
(t/is (= (:y overlay-pos) 30))))
(t/testing "Overlay manual relative to rect"
(let [i2 (-> interaction-rect
(ctsi/set-overlay-pos-type :manual base-frame objects)
(ctsi/set-overlay-position (gpt/point 12 62)))
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 17))
(t/is (= (:y overlay-pos) 67))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))))
(t/is (= (:y overlay-pos) 67))))))
(t/deftest animation-checks

View File

@@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION=v18.16.1 \
CLOJURE_VERSION=1.11.1.1347 \
CLJKONDO_VERSION=2023.05.26 \
BABASHKA_VERSION=1.3.181 \
ENV NODE_VERSION=v18.15.0 \
CLOJURE_VERSION=1.11.1.1257 \
CLJKONDO_VERSION=2023.03.17 \
BABASHKA_VERSION=1.3.176 \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8
@@ -104,12 +104,16 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='b16c0271899de1f0e277dc0398bfff11b54511765f104fa938929ac484dc926d'; \
BINARY_URL='https://github.com/adoptium/temurin20-binaries/releases/download/jdk-20.0.1%2B9/OpenJDK20U-jdk_aarch64_linux_hotspot_20.0.1_9.tar.gz'; \
ESUM='1c4be9aa173cb0deb0d215643d9509c8900e5497290b29eee4bee335fa57984f'; \
BINARY_URL='https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jdk_aarch64_linux_hotspot_19.0.2_7.tar.gz'; \
;; \
armhf|armv7l) \
ESUM='6a51cb3868b5a3b81848a0d276267230ff3f8639f20ba9ae9ef1d386440bf1fd'; \
BINARY_URL='https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jdk_arm_linux_hotspot_19.0.2_7.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='43ad054f135a7894dc87ad5d10ad45d8e82846186515892acdbc17c2c5cd27e4'; \
BINARY_URL='https://github.com/adoptium/temurin20-binaries/releases/download/jdk-20.0.1%2B9/OpenJDK20U-jdk_x64_linux_hotspot_20.0.1_9.tar.gz'; \
ESUM='3a3ba7a3f8c3a5999e2c91ea1dca843435a0d1c43737bd2f6822b2f02fc52165'; \
BINARY_URL='https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jdk_x64_linux_hotspot_19.0.2_7.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \

View File

@@ -40,10 +40,7 @@ http {
'' close;
}
proxy_cache_path /tmp/cache/ levels=2:2 keys_zone=penpot:20m;
proxy_cache_methods GET HEAD;
proxy_cache_valid any 48h;
proxy_cache_key "$host$request_uri";
# include /etc/nginx/sites-enabled/*;
server {
listen 3449 default_server;
@@ -92,26 +89,14 @@ http {
error_page 301 302 307 = @handle_redirect;
}
location /internal/gfonts/css {
proxy_pass https://fonts.googleapis.com/css?$args;
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Cross-Origin-Resource-Policy;
proxy_hide_header Link;
proxy_hide_header Alt-Svc;
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_ignore_headers Set-Cookie Vary Cache-Control Expires;
proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
proxy_set_header Host "fonts.googleapis.com";
proxy_set_header User-Agent "curl/7.74.0";
proxy_set_header Host "raw.githubusercontent.com";
proxy_set_header Accept "*/*";
proxy_cache penpot;
add_header Access-Control-Allow-Origin $http_origin;
add_header Cache-Control max-age=86400;
add_header X-Cache-Status $upstream_cache_status;
proxy_buffering off;
}
location /internal/assets {
@@ -157,53 +142,10 @@ http {
}
location / {
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
proxy_hide_header Access-Control-Allow-Origin;
proxy_set_header User-Agent "curl/7.74.0";
proxy_set_header Host "raw.githubusercontent.com";
proxy_set_header Accept "*/*";
add_header Access-Control-Allow-Origin $http_origin;
proxy_buffering off;
}
location ~ ^/internal/gfonts/font/(?<font_file>.+) {
proxy_pass https://fonts.gstatic.com/s/$font_file;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Cross-Origin-Resource-Policy;
proxy_hide_header Link;
proxy_hide_header Alt-Svc;
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Cross-Origin-Opener-Policy;
proxy_hide_header Report-To;
proxy_ignore_headers Set-Cookie Vary Cache-Control Expires;
proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
proxy_set_header Host "fonts.gstatic.com";
proxy_set_header Accept "*/*";
proxy_cache penpot;
add_header Access-Control-Allow-Origin $http_origin;
add_header Cache-Control max-age=86400;
add_header X-Cache-Status $upstream_cache_status;
}
location ~ ^/(/|css|fonts|images|js|wasm) {
}
location ~ ^/[^/]+/(.*)$ {
return 301 " /404";
}
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
try_files $uri /index.html$is_args$args =404;
}
}
}

View File

@@ -131,15 +131,6 @@ http {
location / {
add_header Cache-Control "no-cache, max-age=0";
location ~ ^/(/|css|fonts|images|js|wasm) {
}
location ~ ^/[^/]+/(.*)$ {
return 301 " /404";
}
try_files $uri /index.html$is_args$args =404;
}
}
}

View File

@@ -45,11 +45,6 @@ http {
'' close;
}
proxy_cache_path /tmp/cache/ levels=2:2 keys_zone=penpot:20m;
proxy_cache_methods GET HEAD;
proxy_cache_valid any 48h;
proxy_cache_key "$host$request_uri";
server {
listen 80 default_server;
server_name _;
@@ -93,28 +88,6 @@ http {
error_page 301 302 307 = @handle_redirect;
}
location /internal/gfonts/css {
proxy_pass https://fonts.googleapis.com/css?$args;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Cross-Origin-Resource-Policy;
proxy_hide_header Link;
proxy_hide_header Alt-Svc;
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_ignore_headers Set-Cookie Vary Cache-Control Expires;
proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
proxy_set_header Host "fonts.googleapis.com";
proxy_set_header Accept "*/*";
proxy_cache penpot;
add_header Access-Control-Allow-Origin $http_origin;
add_header Cache-Control max-age=86400;
add_header X-Cache-Status $upstream_cache_status;
}
location /internal/assets {
internal;
alias /opt/data/assets;
@@ -136,31 +109,6 @@ http {
}
location / {
location ~ ^/internal/gfonts/font/(?<font_file>.+) {
proxy_pass https://fonts.gstatic.com/s/$font_file;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Cross-Origin-Resource-Policy;
proxy_hide_header Link;
proxy_hide_header Alt-Svc;
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Cross-Origin-Opener-Policy;
proxy_hide_header Report-To;
proxy_ignore_headers Set-Cookie Vary Cache-Control Expires;
proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
proxy_set_header Host "fonts.gstatic.com";
proxy_set_header Accept "*/*";
proxy_cache penpot;
add_header Access-Control-Allow-Origin $http_origin;
add_header Cache-Control max-age=86400;
add_header X-Cache-Status $upstream_cache_status;
}
location ~* \.(js|css).*$ {
add_header Cache-Control "max-age=86400" always; # 24 hours
}
@@ -168,16 +116,7 @@ http {
location ~* \.(html).*$ {
add_header Cache-Control "no-cache, max-age=0" always;
}
location ~ ^/(/|css|fonts|images|js|wasm) {
}
location ~ ^/[^/]+/(.*)$ {
return 301 " /404";
}
root /var/www/app/;
try_files $uri /index.html$is_args$args =404;
}
}
}

View File

@@ -23,15 +23,11 @@
(declare ^:private assoc-file-name)
(declare prepare-exports)
;; Regex to clean namefiles
(def sanitize-file-regex #"[\\/:*?\"<>|]")
(s/def ::file-id ::us/uuid)
(s/def ::filename ::us/string)
(s/def ::name ::us/string)
(s/def ::object-id ::us/uuid)
(s/def ::page-id ::us/uuid)
(s/def ::share-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::scale ::us/number)
(s/def ::suffix ::us/string)
@@ -39,8 +35,7 @@
(s/def ::wait ::us/boolean)
(s/def ::export
(s/keys :req-un [::page-id ::file-id ::object-id ::type ::suffix ::scale ::name]
:opt-un [::share-id]))
(s/keys :req-un [::page-id ::file-id ::object-id ::type ::suffix ::scale ::name]))
(s/def ::exports
(s/coll-of ::export :kind vector? :min-count 1))
@@ -94,6 +89,7 @@
proc (-> (rd/render export on-progress)
(p/then (constantly resource))
(p/catch on-error))]
(if wait
(p/then proc #(assoc exchange :response/body (dissoc % :path)))
(assoc exchange :response/body (dissoc resource :path)))))
@@ -137,7 +133,7 @@
:on-progress on-progress)
append (fn [{:keys [filename path] :as object}]
(rsc/add-to-zip! zip path (str/replace filename sanitize-file-regex "_")))
(rsc/add-to-zip! zip path filename))
proc (-> (p/do
(p/loop [exports (seq exports)]
@@ -147,7 +143,9 @@
(p/recur (rest exports)))))
(.finalize zip))
(p/then (constantly resource))
(p/catch on-error))]
(p/catch on-error))
]
(if wait
(p/then proc #(assoc exchange :response/body (dissoc % :path)))
(assoc exchange :response/body (dissoc resource :path)))))
@@ -190,7 +188,6 @@
(process-partition [[part1 :as part]]
{:file-id (:file-id part1)
:page-id (:page-id part1)
:share-id (:share-id part1)
:name (:name part1)
:token token
:type (:type part1)

View File

@@ -18,14 +18,12 @@
(s/def ::type #{:jpeg :png :pdf :svg})
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::share-id ::us/uuid)
(s/def ::scale ::us/number)
(s/def ::token ::us/string)
(s/def ::filename ::us/string)
(s/def ::object
(s/keys :req-un [::id ::name ::suffix ::filename]
:opt-un [::share-id]))
(s/keys :req-un [::id ::name ::suffix ::filename]))
(s/def ::objects
(s/coll-of ::object :min-count 1))

View File

@@ -17,7 +17,7 @@
[promesa.core :as p]))
(defn render
[{:keys [file-id page-id share-id token scale type objects] :as params} on-object]
[{:keys [file-id page-id token scale type objects] :as params} on-object]
(letfn [(prepare-options [uri]
#js {:screen #js {:width bw/default-viewport-width
:height bw/default-viewport-height}
@@ -48,9 +48,9 @@
;; take the screnshot of requested objects, one by one
(p/run! (partial render-object page) objects)
nil))]
(p/let [params {:file-id file-id
:page-id page-id
:share-id share-id
:object-id (mapv :id objects)
:route "objects"}
uri (-> (cf/get :public-uri)

View File

@@ -17,7 +17,7 @@
[promesa.core :as p]))
(defn render
[{:keys [file-id page-id share-id token scale type objects] :as params} on-object]
[{:keys [file-id page-id token scale type objects] :as params} on-object]
(letfn [(prepare-options [uri]
#js {:screen #js {:width bw/default-viewport-width
:height bw/default-viewport-height}
@@ -31,7 +31,6 @@
(prepare-uri [base-uri object-id]
(let [params {:file-id file-id
:page-id page-id
:share-id share-id
:object-id object-id
:route "objects"}]
(-> base-uri

View File

@@ -108,7 +108,7 @@
:height height}))
(defn render
[{:keys [page-id file-id share-id objects token scale type]} on-object]
[{:keys [page-id file-id objects token scale type]} on-object]
(letfn [(convert-to-ppm [pngpath]
(let [ppmpath (str/concat pngpath "origin.ppm")]
(l/trace :fn :convert-to-ppm :path ppmpath)
@@ -338,9 +338,9 @@
;; take the screnshot of requested objects, one by one
(p/run! (partial render-object page) objects)
nil))]
(p/let [params {:file-id file-id
:page-id page-id
:share-id share-id
:render-embed true
:object-id (mapv :id objects)
:route "objects"}

View File

@@ -131,8 +131,7 @@ function readManifest() {
"polyfills": "js/polyfills.js",
"main": "js/main.js",
"shared": "js/shared.js",
"worker": "js/worker.js",
"thumbnail-renderer": "js/thumbnail-renderer.js"
"worker": "js/worker.js"
};
}
}
@@ -243,17 +242,7 @@ gulp.task("template:render", templatePipeline({
output: paths.output
}));
gulp.task("template:thumbnail-renderer", templatePipeline({
name: "thumbnail-renderer.html",
input: paths.resources + "templates/thumbnail-renderer.mustache",
output: paths.output
}));
gulp.task("templates", gulp.series("svg:sprite:icons",
"svg:sprite:cursors",
"template:main",
"template:render",
"template:thumbnail-renderer"));
gulp.task("templates", gulp.series("svg:sprite:icons", "svg:sprite:cursors", "template:main", "template:render"));
gulp.task("polyfills", function() {
return gulp.src(paths.resources + "polyfills/*.js")

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -199,7 +199,7 @@
}
&.value {
background: linear-gradient(var(--gradient-direction), #000 0%, #fff 100%);
background: linear-gradient(var(--gradient-direction), #fff 0%, #000 100%);
}
.handler {

View File

@@ -246,7 +246,7 @@
pointer-events: auto;
.thread-groups {
height: calc(100% - 34px);
height: 100%;
overflow-y: scroll;
hr {
border: 0;

View File

@@ -60,7 +60,7 @@
font-size: $fs14;
background-color: $color-white;
display: flex;
max-width: 1000px;
min-width: 1000px;
width: 100%;
min-height: 97px;
align-items: center;

View File

@@ -51,10 +51,6 @@
border-radius: $br3;
border: 2px solid lighten($color-gray-20, 15%);
text-align: initial;
img {
object-fit: contain;
}
}
&.dragged {
@@ -363,7 +359,7 @@
// LIBRARY VIEW
.grid-item {
.library {
height: 580px;
height: 556px;
}
&.project-th.library {

View File

@@ -35,7 +35,6 @@
input {
&.no-padding {
padding-top: 12px;
height: 50px;
}
min-height: 40px;
}
@@ -325,7 +324,7 @@
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
z-index: 12;
top: 30px;
left: -151px;
left: 6px;
width: 155px;
hr {

View File

@@ -466,27 +466,20 @@
.dashboard-templates-section {
position: absolute;
display: flex;
flex-direction: column;
justify-content: flex-end;
bottom: 0;
width: 100%;
height: 228px;
height: 285px;
transition: bottom 300ms;
pointer-events: none;
&.collapsed {
bottom: -228px;
transition: bottom 300ms;
}
.title {
pointer-events: all;
width: fit-content;
top: -56px;
right: -28px;
width: 100%;
text-align: right;
height: 56px;
position: absolute;
button {
border: none;
cursor: pointer;
@@ -536,7 +529,6 @@
display: flex;
align-items: center;
justify-content: center;
pointer-events: all;
svg {
width: 12px;
height: 12px;
@@ -558,7 +550,6 @@
}
.content {
pointer-events: all;
background-color: $color-white;
width: 200%;
height: 229px;

View File

@@ -290,7 +290,6 @@
border-radius: $br4;
margin: 0.5rem;
cursor: pointer;
text-decoration: none;
&:hover {
background-color: $color-primary;

View File

@@ -899,8 +899,7 @@
}
&.onboarding-v2 {
min-height: unset;
height: 100%;
min-height: 464px;
min-width: 752px;
.modal-left {
background-color: $color-gray-50;

View File

@@ -316,22 +316,6 @@
&:hover {
border: 1px solid $color-gray-20;
}
&.no-check {
.custom-select-dropdown {
width: 100%;
min-width: unset;
.check-icon {
display: none;
}
li.checked-element {
padding-left: 0.5rem;
&.is-selected {
background-color: $color-primary;
}
}
}
}
}
.opened {
border: 1px solid $color-primary;
@@ -2430,23 +2414,10 @@
.title .icon {
display: flex;
}
textarea {
min-height: 250px;
}
}
&.creating {
textarea {
min-height: 250px;
}
}
.hidden {
display: none;
svg {
display: none;
}
}
.counter {
@@ -2471,6 +2442,7 @@
textarea {
background-color: $color-gray-60;
color: $color-white;
min-height: 250px;
padding: 10px;
border: none;

View File

@@ -355,12 +355,10 @@ span.element-name {
.pages-tool-bar {
display: flex;
justify-content: space-between;
height: 40px;
padding: 0;
height: 32px;
margin-top: 8px;
&.search {
margin-top: 8px;
padding: 8px;
.search-box {
border: 1px solid $color-gray-20;
border-radius: $br4;
@@ -378,8 +376,6 @@ span.element-name {
background-color: $color-gray-50;
color: $color-white;
font-size: $fs12;
flex-grow: 1;
margin: 0;
height: 16px;
&:focus {
outline: none;
@@ -388,16 +384,10 @@ span.element-name {
div {
height: 16px;
overflow: hidden;
width: 100%;
display: flex;
align-items: center;
}
.filter,
.clear {
width: 35px;
display: flex;
justify-content: center;
align-items: center;
&.active {
svg {
fill: $color-primary;
@@ -413,73 +403,6 @@ span.element-name {
margin: 0 2px 0 5px;
cursor: pointer;
}
.page-name {
padding: 8px;
margin-top: 8px;
color: #e3e3e3;
font-size: 0.875rem;
max-width: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon-search {
margin-top: 8px;
}
.focus-title {
width: 100%;
height: 100%;
display: grid;
align-items: center;
grid-template-columns: 16px 1fr auto;
grid-column-gap: 8px;
cursor: pointer;
.back-button {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background: none;
border: none;
padding: 0;
margin: 0;
svg {
width: 12px;
height: 12px;
fill: $color-gray-20;
transform: rotate(180deg);
}
&:hover {
svg {
fill: $color-primary;
}
}
}
.focus-name {
height: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.focus-mode {
color: $color-primary;
border: 1px solid $color-primary;
border-radius: $br3;
font-size: $fs10;
text-transform: uppercase;
padding: 0px 4px;
display: flex;
align-items: center;
}
}
}
}
.active-filters {

Some files were not shown because too many files have changed in this diff Show More