makesomethingshit 4a5c6fce7f
Fetch only caller share-link in view-only bundle (#11657)
*  Fetch only caller share-link in view-only bundle

Share-link callers now resolve a single row with a composite
(id, file-id) predicate instead of loading all sibling rows
and filtering in memory. Membership path keeps full query.

Related #11633

AI-assisted-by: muse-spark-1.3-contributor

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

*  Add DB-access regression test for share-link bundle

The share-link path must resolve the caller row with a composite
(id, file-id) single-row lookup and never run the full
file-id query. Keep cross-file replay coverage.

Related #11633

AI-assisted-by: muse-spark-1.3-contributor
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

---------

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-15 17:40:52 +02:00

154 lines
5.6 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.rpc.commands.viewer
(:require
[app.binfile.common :as bfc]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.schema :as sm]
[app.config :as cf]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc]
[app.rpc.permissions :as perms]
[app.util.services :as sv]
[cuerdas.core :as str]))
;; --- QUERY: View Only Bundle
(defn- remove-not-allowed-pages
[data allowed]
(-> data
(update :pages (fn [pages] (filterv #(contains? allowed %) pages)))
(update :pages-index select-keys allowed)))
(defn obfuscate-email
"Obfuscate the `email` for share-link members so the viewer only sees a
partially redacted address. Accepts any string shape (including nil,
missing `@`, or a domain with no `.`) and falls back to a fully-masked
result rather than throwing — the function is called while building the
view-only bundle for anonymous viewers, so an NPE here would abort the
entire share-link response."
[email]
(let [[name domain]
(str/split (or email "") "@" 2)
[_ rest]
(str/split (or domain "") "." 2)
name
(if (> (count name) 3)
(str (subs name 0 1) (apply str (take (dec (count name)) (repeat "*"))))
"****")]
(str name "@****" (when rest (str "." rest)))))
(defn anonymize-member
[member]
(-> (select-keys member [:id :email :name :fullname :photo-id])
(update :email obfuscate-email)
(assoc :can-read true)))
(defn- get-view-only-bundle
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id share-id ::perms] :as params}]
(let [file (bfc/get-file cfg file-id)
project (db/get conn :project
{:id (:project-id file)}
{:columns [:id :name :team-id]})
team (-> (db/get conn :team {:id (:team-id project)})
(teams/decode-row))
members (cond->> (teams/get-team-members conn (:team-id project))
(= :share-link (:type perms))
(mapv anonymize-member))
member-ids (into #{} (map :id) members)
perms (assoc perms :in-team (contains? member-ids profile-id))
_ (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file)))
file (cond-> file
(= :share-link (:type perms))
(update :data remove-not-allowed-pages (:pages perms))
:always
(update :data select-keys [:id :options :pages :pages-index :components]))
libs (->> (bfc/get-file-libraries conn file-id)
(mapv (fn [{:keys [id] :as lib}]
(merge lib (bfc/get-file cfg id)))))
decode-link
(fn [row]
(-> row
(update :pages db/decode-pgarray #{})
;; NOTE: the flags are deprecated but are still present
;; on the table on old rows. The flags are pgarray and
;; for avoid decoding it (because they are no longer used
;; on frontend) we just dissoc the column attribute from
;; row.
(dissoc :flags)))
;; NOTE: on the share-link path we fetch at most the caller's own
;; row with a composite (id, file-id) predicate, so sibling tokens
;; never leave postgres. The membership path keeps the full list
;; the share-management UI needs. A nil share-id never falls back
;; to the full query; it simply resolves to an empty vector.
links (if (= :share-link (:type perms))
(if-some [row (db/get* conn :share-link {:id share-id :file-id file-id})]
[(decode-link row)]
[])
(mapv decode-link (db/query conn :share-link {:file-id file-id})))
fonts (db/query conn :team-font-variant
{:team-id (:id team)
:deleted-at nil})]
{:users members
:profiles members
:fonts fonts
:project project
:share-links links
:libraries libs
:file file
:team (assoc team :permissions perms)
:permissions perms}))
(def schema:get-view-only-bundle
[:map {:title "get-view-only-bundle"}
[:file-id ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]
[:features {:optional true} ::cfeat/features]])
(sv/defmethod ::get-view-only-bundle
{::rpc/auth false
::doc/added "1.17"
::sm/params schema:get-view-only-bundle}
[system {:keys [::rpc/profile-id file-id share-id] :as params}]
(db/run! system
(fn [system]
(let [perms (perms/get-file-read-permissions system profile-id file-id share-id)
params (-> params
(assoc ::perms perms)
(assoc :profile-id profile-id))]
;; When we have neither profile nor share, we just return a not
;; found response to the user.
(when-not perms
(ex/raise :type :not-found
:code :object-not-found
:hint "object not found"))
(get-view-only-bundle system params)))))