diff --git a/.carve/ignore b/.carve/ignore index 83ce1fffe4..ac491435a6 100644 --- a/.carve/ignore +++ b/.carve/ignore @@ -15,6 +15,12 @@ frontend.debug/print ;; Lazily loaded frontend.extensions.code/editor ;; Lazily loaded +frontend.extensions.age-encryption/keygen +frontend.extensions.age-encryption/encrypt-with-x25519 +frontend.extensions.age-encryption/decrypt-with-x25519 +frontend.extensions.age-encryption/encrypt-with-user-passphrase +frontend.extensions.age-encryption/decrypt-with-user-passphrase +;; Lazily loaded frontend.extensions.excalidraw/draw ;; Referenced in commented TODO frontend.extensions.pdf.utils/get-page-bounding diff --git a/android/app/build.gradle b/android/app/build.gradle index 3e550442b3..5cfa0617b3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.logseq.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 20 - versionName "0.6.7" + versionCode 21 + versionName "0.6.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/public/index.html b/public/index.html index 3004100a30..bcdaf496f6 100644 --- a/public/index.html +++ b/public/index.html @@ -49,6 +49,7 @@ + + diff --git a/resources/index.html b/resources/index.html index 9696c9d8b0..8dcec0fe61 100644 --- a/resources/index.html +++ b/resources/index.html @@ -52,6 +52,7 @@ const portal = new MagicPortal(worker); + diff --git a/resources/package.json b/resources/package.json index aac476aa9a..8a67f99286 100644 --- a/resources/package.json +++ b/resources/package.json @@ -1,6 +1,6 @@ { "name": "Logseq", - "version": "0.6.7", + "version": "0.6.8", "main": "electron.js", "author": "Logseq", "license": "AGPL-3.0", @@ -36,7 +36,7 @@ "https-proxy-agent": "5.0.0", "@sentry/electron": "2.5.1", "posthog-js": "1.10.2", - "@logseq/rsapi": "0.0.9", + "@logseq/rsapi": "0.0.11", "electron-deeplink": "1.0.9" }, "devDependencies": { diff --git a/shadow-cljs.edn b/shadow-cljs.edn index 21d99e7316..e475782d89 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -12,12 +12,12 @@ :js-options {:ignore-asset-requires true} ;; handle `require(xxx.css)` :modules {:main {:init-fn frontend.core/init} - ;; :graph - ;; {:entries [frontend.extensions.graph.force] - ;; :depends-on #{:main}} :code-editor {:entries [frontend.extensions.code] :depends-on #{:main}} + :age-encryption + {:entries [frontend.extensions.age-encryption] + :depends-on #{:main}} :excalidraw {:entries [frontend.extensions.excalidraw] :depends-on #{:main}}} @@ -69,12 +69,12 @@ :js-options {:ignore-asset-requires true} :modules {:main {:init-fn frontend.publishing/init} - ;; :graph - ;; {:entries [frontend.extensions.graph.force] - ;; :depends-on #{:main}} :code-editor {:entries [frontend.extensions.code] :depends-on #{:main}} + :age-encryption + {:entries [frontend.extensions.age-encryption] + :depends-on #{:main}} :excalidraw {:entries [frontend.extensions.excalidraw] :depends-on #{:main}}} diff --git a/src/electron/electron/core.cljs b/src/electron/electron/core.cljs index b45f07f22d..4383f49e80 100644 --- a/src/electron/electron/core.cljs +++ b/src/electron/electron/core.cljs @@ -127,7 +127,7 @@ ;; TODO: ugly, replace with ls-files and filter with ".map" _ (p/all (map (fn [file] (. fs removeSync (path/join static-dir "js" (str file ".map")))) - ["main.js" "code-editor.js" "excalidraw.js"]))] + ["main.js" "code-editor.js" "excalidraw.js" "age-encryption.js"]))] (. dialog showMessageBox (clj->js {:message (str "Export public pages and publish assets to " root-dir " successfully")}))))))) (defn setup-app-manager! diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 20424cbdb8..1fca222c27 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -391,14 +391,19 @@ :on-mouse-down (fn [e] (util/stop e) - (if (gobj/get e "shiftKey") + (cond + (gobj/get e "shiftKey") (when-let [page-entity (db/entity [:block/name redirect-page-name])] (state/sidebar-add-block! (state/get-current-repo) (:db/id page-entity) - :page - {:page page-entity})) - (state/pub-event! [:page/create redirect-page-name])) + :page)) + + (not= redirect-page-name page-name) + (route-handler/redirect-to-page! redirect-page-name) + + :else + (state/pub-event! [:page/create page-name-in-block])) (when (and contents-page? (util/mobile?) (state/get-left-sidebar-open?)) @@ -642,7 +647,7 @@ (declare block-content) (declare block-container) -(declare block-parents) +(declare breadcrumb) (rum/defc block-reference < rum/reactive db-mixins/query @@ -684,8 +689,7 @@ (state/sidebar-add-block! (state/get-current-repo) (:db/id block) - :block-ref - {:block block}) + :block-ref) (match [block-type (util/electron?)] ;; pdf annotation @@ -700,7 +704,7 @@ {:style {:width 735 :text-align "left" :max-height 600}} - [(block-parents config repo block-id {:indent? true}) + [(breadcrumb config repo block-id {:indent? true}) (blocks-container (db/get-block-and-children repo block-id) (assoc config :id (str id) :preview? true))]]) @@ -1384,8 +1388,7 @@ (state/sidebar-add-block! (state/get-current-repo) (:db/id block) - :block - block) + :block) (util/stop e)) (route-handler/redirect-to-page! uuid))) @@ -1955,24 +1958,27 @@ nil)]])) (rum/defc block-refs-count < rum/static - [block] + [block *hide-block-refs?] (let [block-refs-count (count (:block/_refs block))] (when (> block-refs-count 0) [:div [:a.open-block-ref-link.bg-base-2.text-sm.ml-2.fade-link {:title "Open block references" :style {:margin-top -1} - :on-click (fn [] - (state/sidebar-add-block! - (state/get-current-repo) - (:db/id block) - :block-ref - {:block block}))} + :on-click (fn [e] + (if (gobj/get e "shiftKey") + (state/sidebar-add-block! + (state/get-current-repo) + (:db/id block) + :block-ref) + (swap! *hide-block-refs? not)))} block-refs-count]]))) -(rum/defc block-content-or-editor < rum/reactive - [config {:block/keys [uuid format] :as block} edit-input-id block-id heading-level edit?] - (let [editor-box (get config :editor-box) +(rum/defcs block-content-or-editor < rum/reactive + (rum/local true :hide-block-refs?) + [state config {:block/keys [uuid format] :as block} edit-input-id block-id heading-level edit?] + (let [*hide-block-refs? (get state :hide-block-refs?) + editor-box (get config :editor-box) editor-id (str "editor-" edit-input-id) slide? (:slide? config)] (if (and edit? editor-box) @@ -1991,25 +1997,31 @@ (editor-handler/escape-editing select?))))} edit-input-id config))] - [:div.flex.flex-row.block-content-wrapper - [:div.flex-1.w-full {:style {:display (if (:slide? config) "block" "flex")}} - (ui/catch-error - (ui/block-error "Block Render Error:" - {:content (:block/content block) - :section-attrs - {:on-click #(state/set-editing! edit-input-id (:block/content block) block "")}}) - (block-content config block edit-input-id block-id slide?))] - [:div.flex.flex-row.items-center - (when (and (:embed? config) - (:embed-parent config)) - [:a.opacity-30.hover:opacity-100.svg-small.inline - {:on-mouse-down (fn [e] - (util/stop e) - (when-let [block (:embed-parent config)] - (editor-handler/edit-block! block :max (:block/uuid block))))} - svg/edit]) + (let [refs-count (count (:block/_refs block))] + [:div.flex.flex-col.block-content-wrapper + [:div.flex.flex-row + [:div.flex-1.w-full {:style {:display (if (:slide? config) "block" "flex")}} + (ui/catch-error + (ui/block-error "Block Render Error:" + {:content (:block/content block) + :section-attrs + {:on-click #(state/set-editing! edit-input-id (:block/content block) block "")}}) + (block-content config block edit-input-id block-id slide?))] + [:div.flex.flex-row.items-center + (when (and (:embed? config) + (:embed-parent config)) + [:a.opacity-30.hover:opacity-100.svg-small.inline + {:on-mouse-down (fn [e] + (util/stop e) + (when-let [block (:embed-parent config)] + (editor-handler/edit-block! block :max (:block/uuid block))))} + svg/edit]) - (block-refs-count block)]]))) + (block-refs-count block *hide-block-refs?)]] + + (when (and (not @*hide-block-refs?) (> refs-count 0)) + (let [refs-cp (state/get-component :block/linked-references)] + (refs-cp uuid)))])))) (defn non-dragging? [e] @@ -2019,30 +2031,43 @@ (not @*dragging?))) (rum/defc breadcrumb-fragment - [config block label] - (if (= block :page) ; page - (when label - (let [page (db/entity [:block/name (util/page-name-sanity-lc label)])] - (page-cp config page))) - [:a {:on-mouse-down - (fn [e] - (if (gobj/get e "shiftKey") - (do - (util/stop e) - (state/sidebar-add-block! - (state/get-current-repo) - (:db/id block) - :block-ref - {:block block})) - (route-handler/redirect-to-page! (:block/uuid block))))} - label])) + [config block label opts] + [:a {:on-mouse-down + (fn [e] + (cond + (gobj/get e "shiftKey") + (do + (util/stop e) + (state/sidebar-add-block! + (state/get-current-repo) + (:db/id block) + :block-ref)) + + (util/atom? (:navigating-block opts)) + (do + (util/stop e) + (reset! (:navigating-block opts) (:block/uuid block))) + + (some? (:sidebar-key config)) + (do + (util/stop e) + (state/sidebar-replace-block! + (:sidebar-key config) + [(state/get-current-repo) + (:db/id block) + (if (:block/name block) :page :block)])) + + :else + (route-handler/redirect-to-page! (:block/uuid block))))} + label]) (rum/defc breadcrumb-separator [] [:span.mx-2.opacity-50 "➤"]) -(defn block-parents - [config repo block-id {:keys [show-page? indent? level-limit] +(defn breadcrumb + [config repo block-id {:keys [show-page? indent? level-limit _navigating-block] :or {show-page? true - level-limit 3}}] + level-limit 3} + :as opts}] (let [parents (db/get-block-parents repo block-id (inc level-limit)) page (db/get-block-page repo block-id) page-name (:block/name page) @@ -2055,7 +2080,7 @@ parents (if more? (take-last level-limit parents) parents)] (when show? (let [page-name-props (when show-page? - [:page + [page (or page-original-name page-name)]) parents-props (doall (for [{:block/keys [uuid name content] :as block} parents] @@ -2075,10 +2100,10 @@ (filterv identity) (map (fn [x] (if (vector? x) (let [[block label] x] - (breadcrumb-fragment config block label)) + (breadcrumb-fragment config block label opts)) [:span.opacity-70 "⋯"]))) (interpose (breadcrumb-separator)))] - [:div.block-parents.flex-row.flex-1 + [:div.breadcrumb.block-parents.flex-row.flex-1 {:class (when (seq breadcrumb) (str (when-not (:search? config) " my-2") @@ -2214,7 +2239,9 @@ :else nil) - (assoc state ::control-show? (atom false)))) + (assoc state + ::control-show? (atom false) + ::navigating-block (atom (:block/uuid block))))) :should-update (fn [old-state new-state] (let [compare-keys [:block/uuid :block/content :block/parent :block/collapsed? :block/properties :block/left :block/children :block/_refs :ui/selected?] @@ -2227,8 +2254,20 @@ (not= (select-keys (first (:rum/args old-state)) config-compare-keys) (select-keys (first (:rum/args new-state)) config-compare-keys)))] (boolean result)))} - [state config {:block/keys [uuid children pre-block? top? refs heading-level level type format content] :as block}] + [state config block] (let [repo (state/get-current-repo) + *navigating-block (get state ::navigating-block) + navigating-block (rum/react *navigating-block) + navigated? (and (not= (:block/uuid block) navigating-block) navigating-block) + block (if navigated? + (let [block (db/pull [:block/uuid navigating-block]) + blocks (db/get-paginated-blocks repo (:db/id block) + {:scoped-block-id (:db/id block)}) + tree (tree/blocks->vec-tree blocks (:block/uuid (first blocks)))] + (first tree)) + block) + {:block/keys [uuid children pre-block? top? refs heading-level level type format content]} block + config (if navigated? (assoc config :id (str navigating-block)) config) block (merge block (block/parse-title-and-body uuid format pre-block? content)) blocks-container-id (:blocks-container-id config) config (update config :block merge block) @@ -2295,8 +2334,9 @@ (assoc :data-query true)) (when (and ref? breadcrumb-show?) - (block-parents config repo uuid {:show-page? false - :indent? true})) + (breadcrumb config repo uuid {:show-page? false + :indent? true + :navigating-block *navigating-block})) ;; only render this for the first block in each container (when top? @@ -2956,7 +2996,8 @@ (rum/defcs blocks-container < {:init (fn [state] - (assoc state ::init-blocks-container-id (atom nil)))} + (assoc state + ::init-blocks-container-id (atom nil)))} [state blocks config] (let [*init-blocks-container-id (::init-blocks-container-id state) blocks-container-id (if @*init-blocks-container-id @@ -2974,6 +3015,29 @@ {:class (when doc-mode? "document-mode")} (lazy-blocks config flat-blocks blocks->vec-tree)])))) +(rum/defcs breadcrumb-with-container < rum/reactive + {:init (fn [state] + (assoc state ::navigating-block (atom (:block/uuid (ffirst (:rum/args state))))))} + [state blocks config] + (let [repo (state/get-current-repo) + *navigating-block (::navigating-block state) + navigating-block (rum/react *navigating-block) + block (first blocks) + navigated? (and (not= (:block/uuid block) navigating-block) navigating-block) + blocks (if navigated? + (let [block (db/pull [:block/uuid navigating-block])] + (db/get-paginated-blocks repo (:db/id block) + {:scoped-block-id (:db/id block)})) + blocks)] + [:div + (when (:breadcrumb-show? config) + (breadcrumb config (state/get-current-repo) navigating-block + {:show-page? false + :navigating-block *navigating-block})) + (blocks-container blocks (assoc config + :breadcrumb-show? false + :navigating-block *navigating-block))])) + ;; headers to hiccup (defn ->hiccup [blocks config option] @@ -2997,12 +3061,7 @@ (page-cp config page) (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])] (for [[_parent blocks] parent-blocks] - (let [block (first blocks)] - [:div - (when (:breadcrumb-show? config) - (block-parents config (state/get-current-repo) (:block/uuid block) - {:show-page? false})) - (blocks-container blocks (assoc config :breadcrumb-show? false))])) + (breadcrumb-with-container blocks config)) {})])))] (and (:group-by-page? config) diff --git a/src/main/frontend/components/content.cljs b/src/main/frontend/components/content.cljs index b72a30507e..93ca7beda9 100644 --- a/src/main/frontend/components/content.cljs +++ b/src/main/frontend/components/content.cljs @@ -215,7 +215,7 @@ :on-click (fn [_e] (editor-handler/copy-block-ref! block-id #(util/format "{{embed ((%s))}}" %)))} "Copy block embed") - + ;; TODO Logseq protocol mobile support (when (util/electron?) (ui/menu-link @@ -296,12 +296,10 @@ (ui/menu-link {:key "open-in-sidebar" :on-click (fn [] - (let [block (db/pull [:block/uuid block-ref-id])] - (state/sidebar-add-block! - (state/get-current-repo) - block-ref-id - :block-ref - {:block block})) )} + (state/sidebar-add-block! + (state/get-current-repo) + block-ref-id + :block-ref))} "Open in sidebar") (ui/menu-link {:key "copy" diff --git a/src/main/frontend/components/encryption.cljs b/src/main/frontend/components/encryption.cljs new file mode 100644 index 0000000000..3a59279d46 --- /dev/null +++ b/src/main/frontend/components/encryption.cljs @@ -0,0 +1,176 @@ +(ns frontend.components.encryption + (:require [clojure.string :as string] + [frontend.context.i18n :refer [t]] + [frontend.encrypt :as e] + [frontend.handler.metadata :as metadata-handler] + [frontend.handler.notification :as notification] + [frontend.state :as state] + [frontend.ui :as ui] + [frontend.util :as util] + [promesa.core :as p] + [rum.core :as rum])) + +(rum/defcs encryption-dialog-inner < + (rum/local false ::reveal-secret-phrase?) + [state repo-url close-fn] + (let [reveal-secret-phrase? (get state ::reveal-secret-phrase?) + public-key (e/get-public-key repo-url) + private-key (e/get-secret-key repo-url)] + [:div + [:div.sm:flex.sm:items-start + [:div.mt-3.text-center.sm:mt-0.sm:text-left + [:h3#modal-headline.text-lg.leading-6.font-medium + "This graph is encrypted with " [:a {:href "https://age-encryption.org/" :target "_blank" :rel "noopener"} "age-encryption.org/v1"]]]] + + [:div.mt-1 + [:div.max-w-2xl.rounded-md.shadow-sm.sm:max-w-xl + [:div.cursor-pointer.block.w-full.rounded-sm.p-2 + {:on-click (fn [] + (when (not @reveal-secret-phrase?) + (reset! reveal-secret-phrase? true)))} + [:div.font-medium "Public Key:"] + [:div.font-mono.select-all.break-all public-key] + (if @reveal-secret-phrase? + [:div + [:div.mt-1.font-medium "Private Key:"] + [:div.font-mono.select-all.break-all private-key]] + [:div.underline "click to view the private key"])]]] + + [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse + [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto + [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5 + {:type "button" + :on-click close-fn} + (t :close)]]]])) + +(defn encryption-dialog + [repo-url] + (fn [close-fn] + (encryption-dialog-inner repo-url close-fn))) + +(rum/defcs input-password-inner < + (rum/local "" ::password) + (rum/local "" ::password-confirm) + [state repo-url close-fn] + (let [password (get state ::password) + password-confirm (get state ::password-confirm)] + [:div + [:div.sm:flex.sm:items-start + [:div.mt-3.text-center.sm:mt-0.sm:text-left + [:h3#modal-headline.text-lg.leading-6.font-medium.font-bold + "Enter a password"]]] + + (ui/admonition + :warning + [:div.opacity-70 + "Choose a strong and hard to guess password.\nIf you lose your password, all the data can't be decrypted!! Please make sure you remember the password you have set, or you can keep a secure backup of the password."]) + [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2 + {:type "password" + :placeholder "Password" + :auto-focus true + :on-change (fn [e] + (reset! password (util/evalue e)))}] + [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2 + {:type "password" + :placeholder "Re-enter the password" + :on-change (fn [e] + (reset! password-confirm (util/evalue e)))}] + + [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse + [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto + [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5 + {:type "button" + :on-click (fn [] + (let [value @password] + (cond + (string/blank? value) + nil + + (not= @password @password-confirm) + (notification/show! "The passwords are not matched." :error) + + :else + (p/let [keys (e/generate-key-pair-and-save! repo-url) + db-encrypted-secret (e/encrypt-with-passphrase value keys)] + (metadata-handler/set-db-encrypted-secret! db-encrypted-secret) + (close-fn true)))))} + "Submit"]]]])) + +(defn input-password + [repo-url close-fn] + (fn [_close-fn] + (input-password-inner repo-url close-fn))) + +(rum/defcs encryption-setup-dialog-inner + [state repo-url close-fn] + [:div + [:div.sm:flex.sm:items-start + [:div.mt-3.text-center.sm:mt-0.sm:text-left + [:h3#modal-headline.text-lg.leading-6.font-medium + "Do you want to create an encrypted graph?"]]] + + [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse + [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto + [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5 + {:type "button" + :on-click (fn [] + (state/set-modal! (input-password repo-url close-fn)))} + (t :yes)]] + [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto + [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5 + {:type "button" + :on-click (fn [] (close-fn false))} + (t :no)]]]]) + +(defn encryption-setup-dialog + [repo-url close-fn] + (fn [close-modal-fn] + (let [close-fn (fn [encrypted?] + (close-fn encrypted?) + (close-modal-fn))] + (encryption-setup-dialog-inner repo-url close-fn)))) + +(rum/defcs encryption-input-secret-inner < + (rum/local "" ::secret) + (rum/local false ::loading) + [state _repo-url db-encrypted-secret close-fn] + (let [secret (::secret state) + loading (::loading state)] + [:div + [:div.sm:flex.sm:items-start + [:div.mt-3.text-center.sm:mt-0.sm:text-left + [:h3#modal-headline.text-lg.leading-6.font-medium + "Enter your password"]]] + + [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2 + {:type "password" + :auto-focus true + :on-change (fn [e] + (reset! secret (util/evalue e)))}] + + [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse + [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto + [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5 + {:type "button" + :on-click (fn [] + (reset! loading true) + (let [value @secret] + (when-not (string/blank? value) ; TODO: length or other checks + (let [repo (state/get-current-repo)] + (p/do! + (-> (e/decrypt-with-passphrase value db-encrypted-secret) + (p/then (fn [keys] + (e/save-key-pair! repo keys) + (close-fn true) + (state/set-state! :encryption/graph-parsing? false))) + (p/catch #(notification/show! "The password is not matched." :warning true)) + (p/finally #(reset! loading false))))))))} + (if @loading (ui/loading "Decrypting") "Decrypt")]]]])) + +(defn encryption-input-secret-dialog + [repo-url db-encrypted-secret close-fn] + (fn [close-modal-fn] + (let [close-fn (fn [encrypted?] + (close-fn encrypted?) + (close-modal-fn))] + (encryption-input-secret-inner repo-url db-encrypted-secret close-fn)))) diff --git a/src/main/frontend/components/file.cljs b/src/main/frontend/components/file.cljs index ccc3913d70..deff736389 100644 --- a/src/main/frontend/components/file.cljs +++ b/src/main/frontend/components/file.cljs @@ -87,8 +87,7 @@ (state/sidebar-add-block! (state/get-current-repo) (:db/id page) - :page - {:page page})) + :page)) (util/stop e)))} original-name]]) diff --git a/src/main/frontend/components/journal.cljs b/src/main/frontend/components/journal.cljs index 62b476e1d6..e9496d6671 100644 --- a/src/main/frontend/components/journal.cljs +++ b/src/main/frontend/components/journal.cljs @@ -48,9 +48,7 @@ (state/sidebar-add-block! (state/get-current-repo) (:db/id page) - :page - {:page page - :journal? true})) + :page)) (.preventDefault e)))} [:h1.title (util/capitalize-all title)]] diff --git a/src/main/frontend/components/page.cljs b/src/main/frontend/components/page.cljs index ed26d49ad7..9e90c6b3a8 100644 --- a/src/main/frontend/components/page.cljs +++ b/src/main/frontend/components/page.cljs @@ -261,8 +261,7 @@ (state/sidebar-add-block! repo (:db/id page) - :page - {:page page})) + :page)) (when (and (not hls-file?) (not fmt-journal?)) (reset! *edit? true))))} [:h1.title.ls-page-title {:data-ref page-name} @@ -369,7 +368,7 @@ (let [config {:id "block-parent" :block? true}] [:div.mb-4 - (block/block-parents config repo block-id {:level-limit 3})])) + (block/breadcrumb config repo block-id {:level-limit 3})])) ;; blocks (let [page (if block? @@ -944,8 +943,7 @@ (state/sidebar-add-block! repo (:db/id page) - :page - {:page (:block/name page)})))) + :page)))) :href (rfe/href :page {:name (:block/name page)})} (block/page-cp {} page)]] diff --git a/src/main/frontend/components/page_menu.cljs b/src/main/frontend/components/page_menu.cljs index 1499b576ca..f195e5d6cc 100644 --- a/src/main/frontend/components/page_menu.cljs +++ b/src/main/frontend/components/page_menu.cljs @@ -91,8 +91,7 @@ (state/sidebar-add-block! repo (:db/id page) - :page-presentation - {:page page}))}}) + :page-presentation))}}) ;; TODO: In the future, we'd like to extract file-related actions ;; (such as open-in-finder & open-with-default-app) into a sub-menu of @@ -103,7 +102,7 @@ :options {:on-click #(js/window.apis.showItemInFolder file-path)}} {:title (t :page/open-with-default-app) :options {:on-click #(js/window.apis.openPath file-path)}}]) - + (when (util/electron?) {:title (t :page/copy-page-url) :options {:on-click #(util/copy-to-clipboard! diff --git a/src/main/frontend/components/query_table.cljs b/src/main/frontend/components/query_table.cljs index ebb96dec2a..7b0a6399ed 100644 --- a/src/main/frontend/components/query_table.cljs +++ b/src/main/frontend/components/query_table.cljs @@ -159,8 +159,7 @@ (state/sidebar-add-block! (state/get-current-repo) (:db/id item) - :block-ref - {:block item})))} + :block-ref)))} (when value (if (= :element (first value)) (second value) diff --git a/src/main/frontend/components/reference.cljs b/src/main/frontend/components/reference.cljs index 6f6d2127fe..623c1eb32d 100644 --- a/src/main/frontend/components/reference.cljs +++ b/src/main/frontend/components/reference.cljs @@ -67,6 +67,22 @@ [page (map #(block-with-ref-level % 1) blocks)]) page-blocks)) +(rum/defc block-linked-references < rum/reactive db-mixins/query + [block-id] + (let [refed-blocks-ids (model-db/get-referenced-blocks-ids (str block-id))] + (when (seq refed-blocks-ids) + (let [ref-blocks (db/get-block-referenced-blocks block-id) + ref-hiccup (block/->hiccup ref-blocks + {:id (str block-id) + :ref? true + :breadcrumb-show? true + :group-by-page? true + :editor-box editor/box} + {})] + [:div.references-blocks + (content/content block-id + {:hiccup ref-hiccup})])))) + (rum/defcs references* < rum/reactive db-mixins/query (rum/local nil ::n-ref) {:init (fn [state] diff --git a/src/main/frontend/components/repo.cljs b/src/main/frontend/components/repo.cljs index d024ba07b6..b6a2ebb8c8 100644 --- a/src/main/frontend/components/repo.cljs +++ b/src/main/frontend/components/repo.cljs @@ -17,7 +17,9 @@ [frontend.text :as text] [promesa.core :as p] [electron.ipc :as ipc] - [goog.object :as gobj])) + [goog.object :as gobj] + [frontend.components.encryption :as encryption] + [frontend.encrypt :as e])) (rum/defc add-repo [args] @@ -60,6 +62,11 @@ :href url} (db/get-repo-path url)]) [:div.controls + (when (e/encrypted-db? url) + [:a.control {:title "Show encryption information about this graph" + :on-click (fn [] + (state/set-modal! (encryption/encryption-dialog url)))} + "🔐"]) [:a.text-gray-400.ml-4.font-medium.text-sm {:title "No worries, unlink this graph will clear its cache only, it does not remove your files on the disk." :on-click (fn [] @@ -116,24 +123,7 @@ :options (cond-> {:on-click (fn [] - (if @*multiple-windows? - (state/pub-event! - [:modal/show - [:div - [:p (t :re-index-multiple-windows-warning)]]]) - (state/pub-event! - [:modal/show - [:div {:style {:max-width 700}} - [:p (t :re-index-discard-unsaved-changes-warning)] - (ui/button - (t :yes) - :autoFocus "on" - :large? true - :on-click (fn [] - (state/close-modal!) - (repo-handler/re-index! - nfs-handler/rebuild-index! - page-handler/create-today-journal!)))]])))})} + (state/pub-event! [:graph/ask-for-re-index *multiple-windows?]))})} new-window-link (when (util/electron?) {:title (t :open-new-window) :options {:on-click #(state/pub-event! [:graph/open-new-window nil])}})] diff --git a/src/main/frontend/components/right_sidebar.cljs b/src/main/frontend/components/right_sidebar.cljs index 1e94f4c654..d864613ca3 100644 --- a/src/main/frontend/components/right_sidebar.cljs +++ b/src/main/frontend/components/right_sidebar.cljs @@ -48,8 +48,18 @@ (when-let [contents (db/entity [:block/name "contents"])] (page/contents-page contents))]) +(defn- block-with-breadcrumb + [repo block idx sidebar-key ref?] + (let [block-id (:block/uuid block)] + [[:div.mt-1 {:class (if ref? "ml-8" "ml-1")} + (block/breadcrumb {:id "block-parent" + :block? true + :sidebar-key sidebar-key} repo block-id {})] + [:div.ml-2 + (block-cp repo idx block)]])) + (defn build-sidebar-item - [repo idx db-id block-type block-data t] + [repo idx db-id block-type] (case block-type :contents [(t :right-side-bar/contents) @@ -64,33 +74,21 @@ :block-ref #_:clj-kondo/ignore - (when-let [block (db/entity repo [:block/uuid (:block/uuid (:block block-data))])] - [(t :right-side-bar/block-ref) - (let [block (:block block-data) - block-id (:block/uuid block) - format (:block/format block)] - [[:div.ml-2.mt-1 - (block/block-parents {:id "block-parent" - :block? true} repo block-id {})] - [:div.ml-2 - (block-cp repo idx block)]])]) + (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])] + (when-let [block (db/entity repo lookup)] + [(t :right-side-bar/block-ref) + (block-with-breadcrumb repo block idx [repo db-id block-type] true)])) :block #_:clj-kondo/ignore - (when-let [block (db/entity repo [:block/uuid (:block/uuid block-data)])] - (let [block-id (:block/uuid block-data) - format (:block/format block-data)] - [(block/block-parents {:id "block-parent" - :block? true} repo block-id {}) - [:div.ml-2 - (block-cp repo idx block-data)]])) + (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])] + (when-let [block (db/entity repo lookup)] + (block-with-breadcrumb repo block idx [repo db-id block-type] false))) :page - (let [page-name (or (:block/name block-data) - db-id) - page-name (if (integer? db-id) - (:block/name (db/entity db-id)) - page-name)] + (when-let [page-name (if (integer? db-id) + (:block/name (db/entity db-id)) + db-id)] [[:a.page-title {:href (rfe/href :page {:name page-name}) :on-click (fn [e] (when (gobj/get e "shiftKey") @@ -100,7 +98,7 @@ (page-cp repo page-name)]]) :page-presentation - (let [page-name (get-in block-data [:page :block/name])] + (let [page-name (:block/name (db/entity db-id))] [[:a {:href (rfe/href :page {:name page-name})} (db-model/get-page-original-name page-name)] [:div.ml-2.slide.mt-2 @@ -120,15 +118,8 @@ svg/close])) (rum/defc sidebar-item < rum/reactive - [repo idx db-id block-type block-data t] - - (let [item - (if (= :page block-type) - (let [lookup-ref (if (number? db-id) db-id [:block/name (util/page-name-sanity-lc db-id)]) - page (db/query-entity-in-component lookup-ref)] - (when (seq page) - (build-sidebar-item repo idx db-id block-type page t))) - (build-sidebar-item repo idx db-id block-type block-data t))] + [repo idx db-id block-type] + (let [item (build-sidebar-item repo idx db-id block-type)] (when item (let [collapse? (state/sub [:ui/sidebar-collapsed-blocks db-id])] [:div.sidebar-item.content.color-level.px-4.shadow-lg @@ -210,7 +201,7 @@ [:div.cp__right-sidebar-settings.hide-scrollbar {:key "right-sidebar-settings"} [:div.ml-4.text-sm [:a.cp__right-sidebar-settings-btn {:on-click (fn [_e] - (state/sidebar-add-block! repo "contents" :contents nil))} + (state/sidebar-add-block! repo "contents" :contents))} (t :right-side-bar/contents)]] [:div.ml-4.text-sm @@ -218,14 +209,13 @@ (when-let [page (get-current-page)] (state/sidebar-add-block! repo - (str "page-graph-" page) - :page-graph - page)))} + page + :page-graph)))} (t :right-side-bar/page)]] [:div.ml-4.text-sm [:a.cp__right-sidebar-settings-btn {:on-click (fn [_e] - (state/sidebar-add-block! repo "help" :help nil))} + (state/sidebar-add-block! repo "help" :help))} (t :right-side-bar/help)]]] [:div.flex.align-items {:style {:z-index 999 @@ -234,9 +224,9 @@ [:.sidebar-item-list.flex-1.scrollbar-spacing (if @*anim-finished? - (for [[idx [repo db-id block-type block-data]] (medley/indexed blocks)] + (for [[idx [repo db-id block-type]] (medley/indexed blocks)] (rum/with-key - (sidebar-item repo idx db-id block-type block-data t) + (sidebar-item repo idx db-id block-type) (str "sidebar-block-" idx))) [:div.p-4 [:span.font-medium.opacity-50 "Loading ..."]])]]])) diff --git a/src/main/frontend/components/search.cljs b/src/main/frontend/components/search.cljs index 0267b4d743..978b6bdd1e 100644 --- a/src/main/frontend/components/search.cljs +++ b/src/main/frontend/components/search.cljs @@ -75,12 +75,12 @@ [:div (when (not= search-mode :page) [:div {:class "mb-1" :key "parents"} - (block/block-parents {:id "block-search-block-parent" - :block? true - :search? true} - repo - (clojure.core/uuid uuid) - {:indent? false})]) + (block/breadcrumb {:id "block-search-block-parent" + :block? true + :search? true} + repo + (clojure.core/uuid uuid) + {:indent? false})]) [:div {:class "font-medium" :key "content"} (highlight-exact-query content q)]])) @@ -158,8 +158,7 @@ (state/sidebar-add-block! repo (:db/id page) - :page - {:page page}))) + :page))) :block (let [block-uuid (uuid (:block/uuid data)) @@ -167,8 +166,7 @@ (state/sidebar-add-block! repo (:db/id block) - :block - block)) + :block)) :new-page (page-handler/create! search-q) @@ -326,8 +324,7 @@ (state/sidebar-add-block! (state/get-current-repo) (:db/id page) - :page - {:page page})))) + :page)))) nil)) :item-render (fn [{:keys [type data]}] diff --git a/src/main/frontend/components/settings.cljs b/src/main/frontend/components/settings.cljs index e63a117346..31bf78ebdf 100644 --- a/src/main/frontend/components/settings.cljs +++ b/src/main/frontend/components/settings.cljs @@ -390,6 +390,23 @@ ;; (let [value (not enable-block-timestamps?)] ;; (config-handler/set-config! :feature/enable-block-timestamps? value))))) +(defn encryption-row [t enable-encryption?] + (toggle "enable_encryption" + (t :settings-page/enable-encryption) + enable-encryption? + #(let [value (not enable-encryption?)] + (config-handler/set-config! :feature/enable-encryption? value) + (when value + (state/close-modal!) + (js/setTimeout (fn [] (state/pub-event! [:graph/ask-for-re-index (atom false)])) + 100))) + [:p.text-sm.opacity-50 "⚠️ This feature is experimental! " + [:span "You can use "] + [:a {:href "https://github.com/kanru/logseq-encrypt-ui" + :target "_blank"} + "logseq-encrypt-ui"] + [:span " to decrypt your graph."]])) + (rum/defc keyboard-shortcuts-row [t] (row-with-button-action {:left-label (t :settings-page/customize-shortcuts) @@ -544,6 +561,7 @@ preferred-workflow (state/get-preferred-workflow) enable-timetracking? (state/enable-timetracking?) enable-journals? (state/enable-journals? current-repo) + enable-encryption? (state/enable-encryption? current-repo) enable-all-pages-public? (state/all-pages-public?) logical-outdenting? (state/logical-outdenting?) enable-tooltip? (state/enable-tooltip?) @@ -578,6 +596,7 @@ :on-key-press (fn [e] (when (= "Enter" (util/ekey e)) (update-home-page e)))}]]]]) + (encryption-row t enable-encryption?) (enable-all-pages-public-row t enable-all-pages-public?) (zotero-settings-row t) (auto-push-row t current-repo enable-git-auto-push?)])) diff --git a/src/main/frontend/components/sidebar.cljs b/src/main/frontend/components/sidebar.cljs index 0ac16cd6af..485f8cbdbd 100644 --- a/src/main/frontend/components/sidebar.cljs +++ b/src/main/frontend/components/sidebar.cljs @@ -74,8 +74,7 @@ (state/sidebar-add-block! (state/get-current-repo) (:db/id page-entity) - :page - {:page page-entity})) + :page)) (route-handler/redirect-to-page! name))))} [:span.page-icon icon] (pdf-assets/fix-local-asset-filename original-name)])) @@ -360,7 +359,7 @@ [db-id block-type] (if (= page "contents") ["contents" :contents] [page :page])] - (state/sidebar-add-block! current-repo db-id block-type nil))) + (state/sidebar-add-block! current-repo db-id block-type))) (reset! sidebar-inited? true)))) state) :did-mount (fn [state] @@ -403,7 +402,7 @@ ;; FIXME: why will this happen? :else - [:div "bingo"])]))) + [:div])]))) (rum/defc custom-context-menu < rum/reactive [] @@ -438,7 +437,7 @@ [:div.cp__sidebar-help-btn {:title (t :help-shortcut-title) :on-click (fn [] - (state/sidebar-add-block! (state/get-current-repo) "help" :help nil))} + (state/sidebar-add-block! (state/get-current-repo) "help" :help))} "?"])) (defn- hide-context-menu-and-clear-selection diff --git a/src/main/frontend/config.cljs b/src/main/frontend/config.cljs index 39e42dc3b0..4134852f8a 100644 --- a/src/main/frontend/config.cljs +++ b/src/main/frontend/config.cljs @@ -276,6 +276,7 @@ (def config-file "config.edn") (def custom-css-file "custom.css") (def custom-js-file "custom.js") +(def metadata-file "metadata.edn") (def pages-metadata-file "pages-metadata.edn") (def config-default-content (rc/inline "config.edn")) @@ -368,6 +369,13 @@ (when repo (get-file-path repo (str app-name "/" config-file))))) +(defn get-metadata-path + ([] + (get-metadata-path (state/get-current-repo))) + ([repo] + (when repo + (get-file-path repo (str app-name "/" metadata-file))))) + (defn get-pages-metadata-path ([] (get-pages-metadata-path (state/get-current-repo))) diff --git a/src/main/frontend/db_schema.cljs b/src/main/frontend/db_schema.cljs index afd9b06660..1f01050c3b 100644 --- a/src/main/frontend/db_schema.cljs +++ b/src/main/frontend/db_schema.cljs @@ -8,6 +8,8 @@ :ast/version {} :db/type {} :db/ident {:db/unique :db.unique/identity} + :db/encrypted? {} + :db/encryption-keys {} :recent/pages {} diff --git a/src/main/frontend/dicts.cljc b/src/main/frontend/dicts.cljc index 2a570cb87e..98ad39f53f 100644 --- a/src/main/frontend/dicts.cljc +++ b/src/main/frontend/dicts.cljc @@ -62,7 +62,7 @@ :right-side-bar/contents "Contents" :right-side-bar/favorites "Favorites" :right-side-bar/page-graph "Page graph" - :right-side-bar/block-ref "Block reference" + :right-side-bar/block-ref "Block references" :right-side-bar/graph-view "Graph view" :right-side-bar/all-pages "All pages" :right-side-bar/flashcards "Flashcards" @@ -162,6 +162,7 @@ :settings-page/enable-tooltip "Tooltips" :settings-page/enable-journals "Journals" :settings-page/enable-all-pages-public "All pages public when publishing" + :settings-page/enable-encryption "Encryption" :settings-page/customize-shortcuts "Keyboard shortcuts" :settings-page/shortcut-settings "Customize shortcuts" :settings-page/home-default-page "Set the default home page" @@ -585,6 +586,7 @@ :settings-page/customize-shortcuts "Tastaturbefehle" :settings-page/disable-sentry "Nutzungs- und Diagnostik-Daten an Logseq senden" :settings-page/edit-custom-css "custom.css bearbeiten" + :settings-page/enable-encryption "Verschlüsselung" :settings-page/enable-shortcut-tooltip "Tooltips für Verknüpfungen aktivieren" :settings-page/enable-tooltip "Tooltips" :settings-page/shortcut-settings "Verknüpfungen anpassen" @@ -895,6 +897,7 @@ :settings-page/enable-tooltip "开启提示框" :settings-page/enable-journals "开启日记" :settings-page/enable-all-pages-public "发布所有页面" + :settings-page/enable-encryption "激活加密功能" :settings-page/customize-shortcuts "自定义快捷键" :settings-page/shortcut-settings "快捷键设置" :settings-page/home-default-page "设置首页默认页面" @@ -1476,6 +1479,7 @@ :settings-page/enable-timetracking "Habilitar rastreo de tiempo" :settings-page/enable-tooltip "Habilitar descripción emergente" :settings-page/enable-journals "Habilitar diarios" + :settings-page/enable-encryption "Habilitar función de cifrado" :settings-page/home-default-page "Establecer página de inicio" :settings-page/enable-block-time "Habilitar marcas temporales de bloque" :settings-page/clear-cache "Limpiar caché" @@ -1706,6 +1710,7 @@ :settings-page/preferred-workflow "Foretrukket arbeidslflyt" :settings-page/enable-shortcut-tooltip "Skru på tooltip for snarveier" :settings-page/enable-timetracking "Aktiver tidssporing" + :settings-page/enable-encryption "Aktiver kryptering" :settings-page/enable-tooltip "Aktiver verktøytips" :settings-page/enable-journals "Aktiver dagbøker" :settings-page/enable-all-pages-public "Aktiver alle sider som offentlige ved publisering" @@ -1994,6 +1999,7 @@ :settings-page/enable-tooltip "Ativar dicas de ferramentas" :settings-page/enable-journals "Ativar diários" :settings-page/enable-all-pages-public "Ativar todas as páginas públicas ao publicar" + :settings-page/enable-encryption "Ativar funcionalidade de criptografia" :settings-page/customize-shortcuts "Atalhos de teclado" :settings-page/shortcut-settings "Personalizar atalhos" :settings-page/home-default-page "Definir a página inicial padrão" @@ -2315,6 +2321,7 @@ :settings-page/enable-tooltip "Dicas de atalhos" :settings-page/enable-journals "Diários" :settings-page/enable-all-pages-public "Todas as páginas públicas ao publicar" + :settings-page/enable-encryption "Encriptação" :settings-page/customize-shortcuts "Atalhos de teclado" :settings-page/shortcut-settings "Personalizar atalhos" :settings-page/home-default-page "Definir a página inicial predefinida" @@ -2610,6 +2617,7 @@ :settings-page/enable-shortcut-tooltip "Всплывающие подсказки горячих клавиш" :settings-page/enable-journals "Включить Дневники" :settings-page/enable-all-pages-public "Все страницы общедоступны при публикации" + :settings-page/enable-encryption "Функции шифрования" :settings-page/customize-shortcuts "Горячие клавиши" :settings-page/shortcut-settings "Настроить горячие клавиши" :settings-page/home-default-page "Установить домашнюю страницу по умолчанию" @@ -2897,6 +2905,7 @@ :settings-page/enable-tooltip "ツールチップ" :settings-page/enable-journals "日誌" :settings-page/enable-all-pages-public "パブリッシュ時には全てのページを公開する" + :settings-page/enable-encryption "暗号化" :settings-page/customize-shortcuts "キーボードショートカット" :settings-page/shortcut-settings "ショートカットをカスタマイズ" :settings-page/home-default-page "デフォルトのホームページを設定" @@ -3059,7 +3068,7 @@ :file-sync/other-user-graph "現在のローカルグラフは他のユーザーのリモートグラフにバインドされています。同期を開始できません。" :file-sync/graph-deleted "現在のリモートグラフが削除されました"} - + :it {:tutorial/text #?(:cljs (rc/inline "tutorial-en.md") :default "tutorial-en.md") :tutorial/dummy-notes #?(:cljs (rc/inline "dummy-notes-en.md") diff --git a/src/main/frontend/encrypt.cljs b/src/main/frontend/encrypt.cljs new file mode 100644 index 0000000000..b5be163153 --- /dev/null +++ b/src/main/frontend/encrypt.cljs @@ -0,0 +1,104 @@ +(ns frontend.encrypt + (:require [frontend.utf8 :as utf8] + [frontend.db.utils :as db-utils] + [frontend.db :as db] + [promesa.core :as p] + [frontend.state :as state] + [clojure.string :as str] + [cljs.reader :as reader] + [shadow.loader :as loader] + [lambdaisland.glogi :as log])) + +(defonce age-pem-header-line "-----BEGIN AGE ENCRYPTED FILE-----") +(defonce age-version-line "age-encryption.org/v1") + +(defn content-encrypted? + [content] + (when content + (or (str/starts-with? content age-pem-header-line) + (str/starts-with? content age-version-line)))) + +(defn encrypted-db? + [repo-url] + (db-utils/get-key-value repo-url :db/encrypted?)) + +(defn get-key-pair + [repo-url] + (db-utils/get-key-value repo-url :db/encryption-keys)) + +(defn save-key-pair! + [repo-url keys] + (let [keys (if (string? keys) (reader/read-string keys) keys)] + (db/set-key-value repo-url :db/encryption-keys keys) + (db/set-key-value repo-url :db/encrypted? true))) + +(defn generate-key-pair + [] + (p/let [_ (loader/load :age-encryption) + lazy-keygen (resolve 'frontend.extensions.age-encryption/keygen) + js-keys (lazy-keygen)] + (array-seq js-keys))) + +(defn generate-key-pair-and-save! + [repo-url] + (when-not (get-key-pair repo-url) + (p/let [keys (generate-key-pair)] + (save-key-pair! repo-url keys) + (pr-str keys)))) + +(defn get-public-key + [repo-url] + (second (get-key-pair repo-url))) + +(defn get-secret-key + [repo-url] + (first (get-key-pair repo-url))) + +(defn encrypt + ([content] + (encrypt (state/get-current-repo) content)) + ([repo-url content] + (cond + (encrypted-db? repo-url) + (p/let [_ (loader/load :age-encryption) + lazy-encrypt-with-x25519 (resolve 'frontend.extensions.age-encryption/encrypt-with-x25519) + content (utf8/encode content) + public-key (get-public-key repo-url) + encrypted (lazy-encrypt-with-x25519 public-key content true)] + (utf8/decode encrypted)) + :else + (p/resolved content)))) + +(defn decrypt + ([content] + (decrypt (state/get-current-repo) content)) + ([repo-url content] + (cond + (and (encrypted-db? repo-url) + (content-encrypted? content)) + (let [content (utf8/encode content)] + (if-let [secret-key (get-secret-key repo-url)] + (p/let [_ (loader/load :age-encryption) + lazy-decrypt-with-x25519 (resolve 'frontend.extensions.age-encryption/decrypt-with-x25519) + decrypted (lazy-decrypt-with-x25519 secret-key content)] + (utf8/decode decrypted)) + (log/error :encrypt/empty-secret-key (str "Can't find the secret key for repo: " repo-url)))) + :else + (p/resolved content)))) + +(defn encrypt-with-passphrase + [passphrase content] + (p/let [_ (loader/load :age-encryption) + lazy-encrypt-with-user-passphrase (resolve 'frontend.extensions.age-encryption/encrypt-with-user-passphrase) + content (utf8/encode content) + encrypted (@lazy-encrypt-with-user-passphrase passphrase content true)] + (utf8/decode encrypted))) + +;; ;; TODO: What if decryption failed +(defn decrypt-with-passphrase + [passphrase content] + (p/let [_ (loader/load :age-encryption) + lazy-decrypt-with-user-passphrase (resolve 'frontend.extensions.age-encryption/decrypt-with-user-passphrase) + content (utf8/encode content) + decrypted (lazy-decrypt-with-user-passphrase passphrase content)] + (utf8/decode decrypted))) diff --git a/src/main/frontend/extensions/age_encryption.cljs b/src/main/frontend/extensions/age_encryption.cljs new file mode 100644 index 0000000000..0b0be977ea --- /dev/null +++ b/src/main/frontend/extensions/age_encryption.cljs @@ -0,0 +1,23 @@ +(ns frontend.extensions.age-encryption + (:require ["regenerator-runtime/runtime"] ;; required for async npm module + ["@kanru/rage-wasm" :as rage])) + +(defn keygen + [] + (rage/keygen)) + +(defn encrypt-with-x25519 + [public-key content armor] + (rage/encrypt_with_x25519 public-key content armor)) + +(defn decrypt-with-x25519 + [secret-key content] + (rage/decrypt_with_x25519 secret-key content)) + +(defn encrypt-with-user-passphrase + [passphrase content armor] + (rage/encrypt_with_user_passphrase passphrase content armor)) + +(defn decrypt-with-user-passphrase + [passphrase content] + (rage/decrypt_with_user_passphrase passphrase content)) diff --git a/src/main/frontend/extensions/srs.cljs b/src/main/frontend/extensions/srs.cljs index eabae1b8a2..832b4980de 100644 --- a/src/main/frontend/extensions/srs.cljs +++ b/src/main/frontend/extensions/srs.cljs @@ -436,7 +436,7 @@ (util/hiccup->class ".flex.flex-col.resize.overflow-y-auto"))} (let [repo (state/get-current-repo)] [:div {:style {:margin-top 20}} - (component-block/block-parents {} repo root-block-id {})]) + (component-block/breadcrumb {} repo root-block-id {})]) (component-block/blocks-container blocks (merge (show-cycle-config card @phase) diff --git a/src/main/frontend/format/block.cljs b/src/main/frontend/format/block.cljs index 138dbc1d0c..63c1c74490 100644 --- a/src/main/frontend/format/block.cljs +++ b/src/main/frontend/format/block.cljs @@ -258,13 +258,14 @@ [original-page-name page-name journal-day] (convert-page-if-journal original-page-name) namespace? (and (not (boolean (text/get-nested-page-name original-page-name))) (text/namespace-page? original-page-name)) - page-entity (db/entity [:block/name page-name])] + page-entity (db/entity [:block/name page-name]) + original-page-name (or (:block/original-name page-entity) original-page-name)] (merge {:block/name page-name :block/original-name original-page-name} (when with-id? (if page-entity - {} + {:block/uuid (:block/uuid page-entity)} {:block/uuid (db/new-block-id)})) (when namespace? (let [namespace (first (gp-util/split-last "/" original-page-name))] diff --git a/src/main/frontend/fs.cljs b/src/main/frontend/fs.cljs index 391b8ae1ca..fa61e9171c 100644 --- a/src/main/frontend/fs.cljs +++ b/src/main/frontend/fs.cljs @@ -11,7 +11,8 @@ [lambdaisland.glogi :as log] [promesa.core :as p] [frontend.db :as db] - [clojure.string :as string])) + [clojure.string :as string] + [frontend.encrypt :as encrypt])) (defonce nfs-record (nfs/->Nfs)) (defonce bfs-record (bfs/->Bfs)) @@ -74,17 +75,20 @@ (defn write-file! [repo dir path content opts] (when content - (-> - (p/let [_ (protocol/write-file! (get-fs dir) repo dir path content opts)] - (when (= bfs-record (get-fs dir)) - (db/set-file-last-modified-at! repo (config/get-file-path repo path) (js/Date.)))) - (p/catch (fn [error] - (log/error :file/write-failed {:dir dir - :path path - :error error}) - ;; Disable this temporarily - ;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.") - ))))) + (let [fs-record (get-fs dir)] + (p/let [md-or-org? (contains? #{"md" "markdown" "org"} (util/get-file-ext path)) + content (if-not md-or-org? content (encrypt/encrypt content))] + (-> + (p/let [_ (protocol/write-file! (get-fs dir) repo dir path content opts)] + (when (= bfs-record fs-record) + (db/set-file-last-modified-at! repo (config/get-file-path repo path) (js/Date.)))) + (p/catch (fn [error] + (log/error :file/write-failed {:dir dir + :path path + :error error}) + ;; Disable this temporarily + ;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.") + ))))))) (defn read-file ([dir path] diff --git a/src/main/frontend/fs/capacitor_fs.cljs b/src/main/frontend/fs/capacitor_fs.cljs index a5a706e64b..01ad4e3002 100644 --- a/src/main/frontend/fs/capacitor_fs.cljs +++ b/src/main/frontend/fs/capacitor_fs.cljs @@ -9,7 +9,8 @@ [promesa.core :as p] [rum.core :as rum] [frontend.state :as state] - [frontend.db :as db])) + [frontend.db :as db] + [frontend.encrypt :as encrypt])) (when (mobile-util/native-ios?) (defn iOS-ensure-documents! @@ -101,7 +102,10 @@ (defn- contents-matched? [disk-content db-content] (when (and (string? disk-content) (string? db-content)) - (p/resolved (= (string/trim disk-content) (string/trim db-content))))) + (if (encrypt/encrypted-db? (state/get-current-repo)) + (p/let [decrypted-content (encrypt/decrypt disk-content)] + (= (string/trim decrypted-content) (string/trim db-content))) + (p/resolved (= (string/trim disk-content) (string/trim db-content)))))) (defn- write-file-impl! [_this repo _dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat] @@ -136,7 +140,8 @@ (not (contains? #{"excalidraw" "edn" "css"} ext)) (not (string/includes? path "/.recycle/")) (zero? pending-writes)) - (state/pub-event! [:file/not-matched-from-disk path disk-content content]) + (p/let [disk-content (encrypt/decrypt disk-content)] + (state/pub-event! [:file/not-matched-from-disk path disk-content content])) :else (-> @@ -144,7 +149,10 @@ :data content :encoding (.-UTF8 Encoding) :recursive true}))] - (db/set-file-content! repo (js/decodeURI path) content) + (p/let [content (if (encrypt/encrypted-db? (state/get-current-repo)) + (encrypt/decrypt content) + content)] + (db/set-file-content! repo (js/decodeURI path) content)) (when ok-handler (ok-handler repo path result)) result) diff --git a/src/main/frontend/fs/nfs.cljs b/src/main/frontend/fs/nfs.cljs index bfc5d101e1..85e59ee3de 100644 --- a/src/main/frontend/fs/nfs.cljs +++ b/src/main/frontend/fs/nfs.cljs @@ -10,7 +10,8 @@ [frontend.config :as config] [frontend.state :as state] [frontend.handler.notification :as notification] - ["/frontend/utils" :as utils])) + ["/frontend/utils" :as utils] + [frontend.encrypt :as encrypt])) ;; We need to cache the file handles in the memory so that ;; the browser will not keep asking permissions. @@ -57,7 +58,10 @@ (defn- contents-matched? [disk-content db-content] (when (and (string? disk-content) (string? db-content)) - (p/resolved (= (string/trim disk-content) (string/trim db-content))))) + (if (encrypt/encrypted-db? (state/get-current-repo)) + (p/let [decrypted-content (encrypt/decrypt disk-content)] + (= (string/trim decrypted-content) (string/trim db-content))) + (p/resolved (= (string/trim disk-content) (string/trim db-content)))))) (defrecord ^:large-vars/cleanup-todo Nfs [] protocol/Fs @@ -171,12 +175,16 @@ (not (contains? #{"excalidraw" "edn" "css"} ext)) (not (string/includes? path "/.recycle/")) (zero? pending-writes)) - (state/pub-event! [:file/not-matched-from-disk path local-content content]) + (p/let [local-content (encrypt/decrypt local-content)] + (state/pub-event! [:file/not-matched-from-disk path local-content content])) (p/let [_ (verify-permission repo file-handle true) _ (utils/writeFile file-handle content) file (.getFile file-handle)] (when file - (db/set-file-content! repo path content) + (p/let [content (if (encrypt/encrypted-db? (state/get-current-repo)) + (encrypt/decrypt content) + content)] + (db/set-file-content! repo path content)) (nfs-saved-handler repo path file)))))) (p/catch (fn [e] (js/console.error e)))) diff --git a/src/main/frontend/fs/node.cljs b/src/main/frontend/fs/node.cljs index 445fb48e90..8939b70f12 100644 --- a/src/main/frontend/fs/node.cljs +++ b/src/main/frontend/fs/node.cljs @@ -8,7 +8,8 @@ [frontend.util :as util] [goog.object :as gobj] [lambdaisland.glogi :as log] - [promesa.core :as p])) + [promesa.core :as p] + [frontend.encrypt :as encrypt])) (defn concat-path [dir path] @@ -27,7 +28,10 @@ (defn- contents-matched? [disk-content db-content] (when (and (string? disk-content) (string? db-content)) - (p/resolved (= (string/trim disk-content) (string/trim db-content))))) + (if (encrypt/encrypted-db? (state/get-current-repo)) + (p/let [decrypted-content (encrypt/decrypt disk-content)] + (= (string/trim decrypted-content) (string/trim db-content))) + (p/resolved (= (string/trim disk-content) (string/trim db-content)))))) (defn- write-file-impl! [this repo dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat] @@ -58,14 +62,18 @@ (not (contains? #{"excalidraw" "edn" "css"} ext)) (not (string/includes? path "/.recycle/")) (zero? pending-writes)) - (state/pub-event! [:file/not-matched-from-disk path disk-content content]) + (p/let [disk-content (encrypt/decrypt disk-content)] + (state/pub-event! [:file/not-matched-from-disk path disk-content content])) :else (-> (p/let [result (ipc/ipc "writeFile" repo path content) mtime (gobj/get result "mtime")] (db/set-file-last-modified-at! repo path mtime) - (db/set-file-content! repo path content) + (p/let [content (if (encrypt/encrypted-db? (state/get-current-repo)) + (encrypt/decrypt content) + content)] + (db/set-file-content! repo path content)) (when ok-handler (ok-handler repo path result)) result) diff --git a/src/main/frontend/fs/watcher_handler.cljs b/src/main/frontend/fs/watcher_handler.cljs index cd2789fe21..195f0b170c 100644 --- a/src/main/frontend/fs/watcher_handler.cljs +++ b/src/main/frontend/fs/watcher_handler.cljs @@ -13,7 +13,8 @@ [lambdaisland.glogi :as log] [electron.ipc :as ipc] [promesa.core :as p] - [frontend.state :as state])) + [frontend.state :as state] + [frontend.encrypt :as encrypt])) ;; all IPC paths must be normalized! (via gp-util/path-normalize) @@ -47,7 +48,9 @@ pages-metadata-path (config/get-pages-metadata-path) {:keys [mtime]} stat db-content (or (db/get-file repo path) "")] - (when (or content (= type "unlink")) + (when (and (or content (= type "unlink")) + (not (encrypt/content-encrypted? content)) + (not (:encryption/graph-parsing? @state/state))) (cond (and (= "add" type) (not= (string/trim content) (string/trim db-content)) diff --git a/src/main/frontend/handler.cljs b/src/main/frontend/handler.cljs index a4f0558686..d8bcdd0fc8 100644 --- a/src/main/frontend/handler.cljs +++ b/src/main/frontend/handler.cljs @@ -2,6 +2,7 @@ (:require [electron.ipc :as ipc] [electron.listener :as el] [frontend.components.page :as page] + [frontend.components.reference :as reference] [frontend.config :as config] [frontend.context.i18n :as i18n] [frontend.db :as db] @@ -164,6 +165,7 @@ (defn- register-components-fns! [] (state/set-page-blocks-cp! page/page-blocks-cp) + (state/set-component! :block/linked-references reference/block-linked-references) (command-palette/register-global-shortcut-commands)) (defn start! diff --git a/src/main/frontend/handler/common.cljs b/src/main/frontend/handler/common.cljs index 75854a82b7..fd9035d635 100644 --- a/src/main/frontend/handler/common.cljs +++ b/src/main/frontend/handler/common.cljs @@ -80,6 +80,14 @@ (state/set-config! repo-url config) config))) +(defn read-metadata! + [content] + (try + (reader/read-string content) + (catch :default e + (log/error :parse/metadata-failed e) + {}))) + (defn get-page-default-properties [page-name] {:title page-name diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index ce3b003556..6e35eca7bf 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -159,13 +159,12 @@ (defn open-block-in-sidebar! [block-id] (when block-id - (when-let [block (db/pull [:block/uuid block-id])] + (when-let [block (db/entity [:block/uuid block-id])] (let [page? (nil? (:block/page block))] (state/sidebar-add-block! (state/get-current-repo) (:db/id block) - (if page? :page :block) - block))))) + (if page? :page :block)))))) (defn reset-cursor-range! [node] @@ -659,12 +658,12 @@ new-block)))))) (defn insert-first-page-block-if-not-exists! - ([page-name] - (insert-first-page-block-if-not-exists! page-name {})) - ([page-name opts] - (when (and (string? page-name) - (not (string/blank? page-name))) - (state/pub-event! [:page/create page-name opts])))) + ([page-title] + (insert-first-page-block-if-not-exists! page-title {})) + ([page-title opts] + (when (and (string? page-title) + (not (string/blank? page-title))) + (state/pub-event! [:page/create page-title opts])))) (defn properties-block [properties format page] @@ -1150,13 +1149,11 @@ (state/sidebar-add-block! (state/get-current-repo) (:db/id page) - :block - page) + :block) (state/sidebar-add-block! (state/get-current-repo) (:db/id page) - :page - {:page page})))))) + :page)))))) (defn zoom-in! [] (if (state/editing?) diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index f9e6229d3a..38de38cc8e 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -25,6 +25,7 @@ [frontend.handler.repo :as repo-handler] [frontend.handler.file :as file-handler] [frontend.handler.route :as route-handler] + [frontend.handler.web.nfs :as nfs-handler] [frontend.modules.shortcut.core :as st] [frontend.modules.outliner.file :as outliner-file] [frontend.commands :as commands] @@ -38,7 +39,9 @@ [frontend.fs :as fs] [clojure.string :as string] [frontend.util.persist-var :as persist-var] - [frontend.fs.sync :as sync])) + [frontend.fs.sync :as sync] + [frontend.components.encryption :as encryption] + [frontend.encrypt :as encrypt])) ;; TODO: should we move all events here? @@ -233,7 +236,8 @@ {:label "diff__cp"})))) (defmethod handle :modal/display-file-version [[_ path content hash]] - (state/set-modal! #(git-component/file-specific-version path hash content))) + (p/let [content (when content (encrypt/decrypt content))] + (state/set-modal! #(git-component/file-specific-version path hash content)))) (defmethod handle :graph/ready [[_ repo]] (search-handler/rebuild-indices-when-stale! repo) @@ -365,6 +369,38 @@ (defmethod handle :rebuild-slash-commands-list [[_]] (page-handler/rebuild-slash-commands-list!)) +(defmethod handle :graph/ask-for-re-index [[_ *multiple-windows?]] + (if (and (util/atom? *multiple-windows?) @*multiple-windows?) + (handle + [:modal/show + [:div + [:p (t :re-index-multiple-windows-warning)]]]) + (handle + [:modal/show + [:div {:style {:max-width 700}} + [:p (t :re-index-discard-unsaved-changes-warning)] + (ui/button + (t :yes) + :autoFocus "on" + :large? true + :on-click (fn [] + (state/close-modal!) + (repo-handler/re-index! + nfs-handler/rebuild-index! + page-handler/create-today-journal!)))]]))) + +;; encryption +(defmethod handle :modal/encryption-setup-dialog [[_ repo-url close-fn]] + (state/set-modal! + (encryption/encryption-setup-dialog repo-url close-fn))) + +(defmethod handle :modal/encryption-input-secret-dialog [[_ repo-url db-encrypted-secret close-fn]] + (state/set-modal! + (encryption/encryption-input-secret-dialog + repo-url + db-encrypted-secret + close-fn))) + (defn run! [] (let [chan (state/get-events-chan)] diff --git a/src/main/frontend/handler/extract.cljs b/src/main/frontend/handler/extract.cljs index 855724af58..000a8a0eef 100644 --- a/src/main/frontend/handler/extract.cljs +++ b/src/main/frontend/handler/extract.cljs @@ -32,7 +32,7 @@ file-name (when-let [file-name (last (string/split file #"/"))] (let [result (first (gp-util/split-last "." file-name))] (if (config/mldoc-support? (string/lower-case (util/get-file-ext file))) - (string/replace result "." "/") + (util/url-decode (string/replace result "." "/")) result)))] (or property-name (if (= (state/page-name-order) "heading") diff --git a/src/main/frontend/handler/file.cljs b/src/main/frontend/handler/file.cljs index 6a08158cb2..2629b78db7 100644 --- a/src/main/frontend/handler/file.cljs +++ b/src/main/frontend/handler/file.cljs @@ -16,7 +16,7 @@ [frontend.handler.ui :as ui-handler] [frontend.state :as state] [frontend.util :as util] - [logseq.graph-parser.util :as gp-util] + [logseq.graph-parser.util :as gp-util] [lambdaisland.glogi :as log] [promesa.core :as p] [frontend.mobile.util :as mobile] @@ -272,6 +272,17 @@ (when-let [dir (config/get-repo-dir repo)] (fs/watch-dir! dir)))) +(defn create-metadata-file + [repo-url encrypted?] + (let [repo-dir (config/get-repo-dir repo-url) + path (str config/app-name "/" config/metadata-file) + file-path (str "/" path) + default-content (if encrypted? "{:db/encrypted? true}" "{}")] + (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name)) + file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)] + (when-not file-exists? + (reset-file! repo-url path default-content))))) + (defn create-pages-metadata-file [repo-url] (let [repo-dir (config/get-repo-dir repo-url) diff --git a/src/main/frontend/handler/metadata.cljs b/src/main/frontend/handler/metadata.cljs index 7dc3ab8a92..2d59e01e08 100644 --- a/src/main/frontend/handler/metadata.cljs +++ b/src/main/frontend/handler/metadata.cljs @@ -1,12 +1,44 @@ (ns frontend.handler.metadata - (:require [cljs.pprint] + (:require [cljs.reader :as reader] + [cljs.pprint] + [clojure.string :as string] + [datascript.db :as ddb] [frontend.config :as config] [frontend.db :as db] [frontend.fs :as fs] [frontend.handler.common :as common-handler] [frontend.handler.file :as file-handler] + [frontend.state :as state] [promesa.core :as p])) +(def default-metadata-str "{}") + +(defn set-metadata! + [k v] + (when-let [repo (state/get-current-repo)] + (let [encrypted? (= k :db/encrypted-secret) + path (config/get-metadata-path) + file-content (db/get-file path)] + (p/let [_ (file-handler/create-metadata-file repo false)] + (let [metadata-str (or file-content default-metadata-str) + metadata (try + (reader/read-string metadata-str) + (catch js/Error e + (println "Parsing metadata.edn failed: ") + (js/console.dir e) + {})) + new-metadata (cond + (= k :block/properties) + (update metadata :block/properties v) ; v should be a function + :else + (let [ks (if (vector? k) k [k])] + (assoc-in metadata ks v))) + new-metadata (if encrypted? + (assoc new-metadata :db/encrypted? true) + new-metadata) + new-content (pr-str new-metadata)] + (file-handler/set-file-content! repo path new-content)))))) + (defn set-pages-metadata! [repo] (let [path (config/get-pages-metadata-path repo) @@ -23,3 +55,31 @@ path new-content {}))))) + +(defn set-db-encrypted-secret! + [encrypted-secret] + (when-not (string/blank? encrypted-secret) + (set-metadata! :db/encrypted-secret encrypted-secret))) + +(defn- handler-properties! + [all-properties properties-tx] + (reduce + (fn [acc datom] + (let [v (:v datom) + id (or (get v :id) + (get v :title))] + (if id + (let [added? (ddb/datom-added datom) + remove-all-properties? (and (not added?) + ;; only id + (= 1 (count v)))] + (if remove-all-properties? + (dissoc acc id) + (assoc acc id v))) + acc))) + all-properties + properties-tx)) + +(defn update-properties! + [properties-tx] + (set-metadata! :block/properties #(handler-properties! % properties-tx))) diff --git a/src/main/frontend/handler/page.cljs b/src/main/frontend/handler/page.cljs index 2ca9a2ce6d..a1b3483f46 100644 --- a/src/main/frontend/handler/page.cljs +++ b/src/main/frontend/handler/page.cljs @@ -91,12 +91,16 @@ (defn- build-page-tx [format properties page journal?] (when (:block/uuid page) (let [page-entity [:block/uuid (:block/uuid page)] - create-title? (create-title-property? journal? (:block/name page)) + create-title? (create-title-property? journal? + (or + (:block/original-name page) + (:block/name page))) page (if (seq properties) (assoc page :block/properties properties) page)] (cond create-title? - [page - (default-properties-block (build-title page) format page-entity properties)] + (let [properties-block (default-properties-block (build-title page) format page-entity properties)] + [page + properties-block]) (seq properties) [page (editor-handler/properties-block properties format page-entity)] @@ -117,7 +121,7 @@ title (util/remove-boundary-slashes title) page-name (util/page-name-sanity-lc title) repo (state/get-current-repo)] - (when-not (db/page-exists? page-name) + (when (db/page-empty? repo page-name) (let [pages (if split-namespace? (util/split-namespace-pages title) [title]) @@ -130,16 +134,19 @@ ;; for namespace pages, only last page need properties drop-last (mapcat #(build-page-tx format nil % journal?)) - (remove nil?)) + (remove nil?) + (remove (fn [m] + (some? (db/entity [:block/name (:block/name m)]))))) last-txs (build-page-tx format properties (last pages) journal?) txs (concat txs last-txs)] - (db/transact! txs))) + (when (seq txs) + (db/transact! txs))) - (when create-first-block? - (when (or - (db/page-empty? repo (:db/id (db/entity [:block/name page-name]))) - (create-title-property? journal? page-name)) - (editor-handler/api-insert-new-block! "" {:page page-name}))) + (when create-first-block? + (when (or + (db/page-empty? repo (:db/id (db/entity [:block/name page-name]))) + (create-title-property? journal? page-name)) + (editor-handler/api-insert-new-block! "" {:page page-name})))) (when redirect? (route-handler/redirect-to-page! page-name)) @@ -361,7 +368,7 @@ "Only accepts unsanitized page names" [old-name new-name redirect?] (let [old-page-name (util/page-name-sanity-lc old-name) - new-file-name (util/page-name-sanity new-name true) + new-file-name (util/file-name-sanity new-name) new-page-name (util/page-name-sanity-lc new-name) repo (state/get-current-repo) page (db/pull [:block/name old-page-name])] @@ -754,8 +761,7 @@ (state/sidebar-add-block! (state/get-current-repo) (:db/id page) - :page - page))) + :page))) (defn open-file-in-default-app [] (when-let [file-path (and (util/electron?) (get-page-file-path))] diff --git a/src/main/frontend/handler/repo.cljs b/src/main/frontend/handler/repo.cljs index ed60897343..c00fc15edb 100644 --- a/src/main/frontend/handler/repo.cljs +++ b/src/main/frontend/handler/repo.cljs @@ -25,7 +25,8 @@ [logseq.graph-parser.util :as gp-util] [electron.ipc :as ipc] [clojure.set :as set] - [clojure.core.async :as async])) + [clojure.core.async :as async] + [frontend.encrypt :as encrypt])) ;; Project settings should be checked in two situations: ;; 1. User changes the config.edn directly in logseq.com (fn: alter-file) @@ -128,16 +129,19 @@ (ui-handler/re-render-root!))))))) (defn create-default-files! - [repo-url] - (spec/validate :repos/url repo-url) - (let [repo-dir (config/get-repo-dir repo-url)] - (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name)) - _ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name "/" config/recycle-dir)) - _ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-journals-directory))) - _ (create-config-file-if-not-exists repo-url) - _ (create-contents-file repo-url) - _ (create-custom-theme repo-url)] - (state/pub-event! [:page/create-today-journal repo-url])))) + ([repo-url] + (create-default-files! repo-url false)) + ([repo-url encrypted?] + (spec/validate :repos/url repo-url) + (let [repo-dir (config/get-repo-dir repo-url)] + (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name)) + _ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name "/" config/recycle-dir)) + _ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-journals-directory))) + _ (file-handler/create-metadata-file repo-url encrypted?) + _ (create-config-file-if-not-exists repo-url) + _ (create-contents-file repo-url) + _ (create-custom-theme repo-url)] + (state/pub-event! [:page/create-today-journal repo-url]))))) (defn- load-pages-metadata! "force?: if set true, skip the metadata timestamp range check" @@ -197,18 +201,21 @@ (update m :finished inc)))) (defn- after-parse - [repo-url files file-paths re-render? re-render-opts opts graph-added-chan] + [repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan] (load-pages-metadata! repo-url file-paths files true) - (when (:new-graph? opts) - (create-default-files! repo-url)) + (when (or (:new-graph? opts) (not (:refresh? opts))) + (if (and (not db-encrypted?) (state/enable-encryption? repo-url)) + (state/pub-event! [:modal/encryption-setup-dialog repo-url + #(create-default-files! repo-url %)]) + (create-default-files! repo-url db-encrypted?))) (when re-render? (ui-handler/re-render-root! re-render-opts)) (state/pub-event! [:graph/added repo-url opts]) (state/reset-parsing-state!) (async/offer! graph-added-chan true)) -(defn- parse-files-and-create-default-files! - [repo-url files delete-files delete-blocks file-paths re-render? re-render-opts opts] +(defn- parse-files-and-create-default-files-inner! + [repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts] (let [support-files (filter (fn [file] (let [format (format/get-format (:file/path file))] @@ -234,16 +241,27 @@ (do (doseq [file support-files'] (parse-and-load-file! repo-url file new-graph?)) - (after-parse repo-url files file-paths re-render? re-render-opts opts graph-added-chan)) + (after-parse repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan)) (async/go-loop [] (if-let [file (async/ (p/all (map (fn [file] (p/let [content (if nfs? (.text (:file/file file)) - (:file/content file))] + (:file/content file)) + content (encrypt/decrypt content)] (assoc file :file/content content))) markup-files)) (p/then (fn [result] (let [files (map #(dissoc % :file/file) result)] @@ -247,7 +249,8 @@ (when-let [file (get-file-f path new-files)] (p/let [content (if nfs? (.text (:file/file file)) - (:file/content file))] + (:file/content file)) + content (encrypt/decrypt content)] (assoc file :file/content content)))) added-or-modified)) (p/then (fn [result] (let [files (map #(dissoc % :file/file :file/handle) result) diff --git a/src/main/frontend/modules/file/core.cljs b/src/main/frontend/modules/file/core.cljs index 0e11dee997..faf0d04631 100644 --- a/src/main/frontend/modules/file/core.cljs +++ b/src/main/frontend/modules/file/core.cljs @@ -30,8 +30,10 @@ (defn transform-content [{:block/keys [collapsed? format pre-block? unordered content heading-level left page parent properties]} level {:keys [heading-to-list?]}] (let [content (or content "") - first-block? (= left page) - pre-block? (and first-block? pre-block?) + pre-block? (or pre-block? + (and (= page parent left) ; first block + (= :markdown format) + (string/includes? (first (string/split-lines content)) ":: "))) markdown? (= format :markdown) content (cond pre-block? @@ -125,7 +127,7 @@ (if journal-page? (date/journal-title->default title) (-> (or (:block/original-name page) (:block/name page)) - (util/page-name-sanity true))) "." + (util/file-name-sanity))) "." (if (= format "markdown") "md" format)) file-path (config/get-file-path repo path) file {:file/path file-path} diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index 2bab5ed115..b957a98d4a 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -220,6 +220,8 @@ :file-sync/sync-state nil :file-sync/sync-uploading-files nil :file-sync/sync-downloading-files nil + + :encryption/graph-parsing? false }))) ;; block uuid -> {content(String) -> ast} @@ -753,12 +755,12 @@ (swap! state assoc :ui/sidebar-open? false)) (defn sidebar-add-block! - [repo db-id block-type block-data] + [repo db-id block-type] (when (not (util/sm-breakpoint?)) (when db-id (update-state! :sidebar/blocks (fn [blocks] (->> (remove #(= (second %) db-id) blocks) - (cons [repo db-id block-type block-data]) + (cons [repo db-id block-type]) (distinct)))) (open-right-sidebar!) (when-let [elem (gdom/getElementByClass "cp__right-sidebar-scrollable")] @@ -773,6 +775,13 @@ (when (empty? (:sidebar/blocks @state)) (hide-right-sidebar!))) +(defn sidebar-replace-block! + [old-sidebar-key new-sidebar-key] + (update-state! :sidebar/blocks (fn [blocks] + (map #(if (= % old-sidebar-key) + new-sidebar-key + %) blocks)))) + (defn sidebar-block-exists? [idx] (some #(= (second %) idx) (:sidebar/blocks @state))) @@ -1491,6 +1500,15 @@ [] (get-in @state [:view/components :page-blocks])) +;; To avoid circular dependencies +(defn set-component! + [k value] + (set-state! [:view/components k] value)) + +(defn get-component + [k] + (get-in @state [:view/components k])) + (defn exit-editing-and-set-selected-blocks! ([blocks] (exit-editing-and-set-selected-blocks! blocks :down)) @@ -1651,3 +1669,8 @@ (update-state! [:graph/parsing-state (get-current-repo)] (if (fn? m) m (fn [old-value] (merge old-value m))))) + +(defn enable-encryption? + [repo] + (:feature/enable-encryption? + (get (sub-config) repo))) diff --git a/src/main/frontend/util.cljc b/src/main/frontend/util.cljc index 45a37dc94c..74d456e6ff 100644 --- a/src/main/frontend/util.cljc +++ b/src/main/frontend/util.cljc @@ -387,16 +387,6 @@ ([animate?] (scroll-to (app-scroll-container-node) 0 animate?)))) -#?(:cljs - (defn url-encode - [string] - (some-> string str (js/encodeURIComponent) (.replace "+" "%20")))) - -#?(:cljs - (defn url-decode - [string] - (some-> string str (js/decodeURIComponent)))) - #?(:cljs (defn link? [node] @@ -889,6 +879,16 @@ (when (gp-util/uuid-string? block-id) (first (array-seq (js/document.getElementsByClassName block-id)))))))) +#?(:cljs + (defn url-encode + [string] + (some-> string str (js/encodeURIComponent) (.replace "+" "%20")))) + +#?(:cljs + (defn url-decode + [string] + (some-> string str (js/decodeURIComponent)))) + (def windows-reserved-chars #"[:\\*\\?\"<>|]+") #?(:cljs @@ -928,21 +928,30 @@ (removeAccents (.normalize (string/lower-case s) "NFKC")))) (defn page-name-sanity - "Sanitize the page-name for file name (strict), for file writting" + "Sanitize the page-name." ([page-name] (page-name-sanity page-name false)) ([page-name replace-slash?] (let [page (some-> page-name (remove-boundary-slashes) - ;; Windows reserved path characters - (string/replace windows-reserved-chars "_") - ;; for android filesystem compatiblity - (string/replace #"[\\#|%]+" "_") (normalize))] (if replace-slash? - (string/replace page #"/" ".") + (string/replace page #"/" "%2A") page)))) +#?(:cljs + (defn file-name-sanity + "Sanitize page-name for file name (strict), for file writing." + [page-name] + (some-> page-name + page-name-sanity + ;; for android filesystem compatiblity + (string/replace #"[\\#|%]+" url-encode) + ;; Windows reserved path characters + (string/replace windows-reserved-chars url-encode) + (string/replace #"/" url-encode) + (string/replace "*" "%2A")))) + (defn page-name-sanity-lc "Sanitize the query string for a page name (mandate for :block/name)" [s] diff --git a/src/main/frontend/version.cljs b/src/main/frontend/version.cljs index abbe6ac4c8..3980243a21 100644 --- a/src/main/frontend/version.cljs +++ b/src/main/frontend/version.cljs @@ -1,3 +1,3 @@ (ns frontend.version) -(defonce version "0.6.7") +(defonce version "0.6.8") diff --git a/src/test/frontend/handler/repo_test.cljs b/src/test/frontend/handler/repo_test.cljs index ad9a0b7439..1a03d87ae9 100644 --- a/src/test/frontend/handler/repo_test.cljs +++ b/src/test/frontend/handler/repo_test.cljs @@ -159,7 +159,7 @@ (into {}))) "Counts for blocks with common block attributes") - (is (= #{"term" "setting" "book" "templates" "Query" "Query/table" "page"} + (is (= #{"term" "setting" "book" "Templates" "Query" "Query/table" "page"} (->> (d/q '[:find (pull ?n [*]) :where [?b :block/namespace ?n]] db) (map (comp :block/original-name first)) set))