From 8487c8cd039eb87b8723a970b7741f9568775971 Mon Sep 17 00:00:00 2001 From: matt wilkie Date: Tue, 25 Feb 2025 14:51:29 -0700 Subject: [PATCH 01/32] WIP: allow no share path url prefix at all (not working yet) --- .../options/other/share_settings.ts | 17 ++++++++++ src/public/translations/en/translation.json | 5 ++- src/routes/api/options.ts | 4 ++- src/routes/routes.ts | 2 ++ src/services/auth.ts | 34 ++++++++++++++++++- src/services/options_init.ts | 4 ++- src/services/options_interface.ts | 2 ++ 7 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index e43adc5e188..ac5baa6afaf 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -14,11 +14,24 @@ const TPL = `

${t("share.redirect_bare_domain_description")}

+ +

${t("share.show_login_link_description")}

+ +
+ +

${t("share.use_clean_urls_description")}

+
`; export default class ShareSettingsOptions extends OptionsWidget { @@ -59,6 +72,7 @@ export default class ShareSettingsOptions extends OptionsWidget { } this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked", options.showLoginInShareTheme === "true"); + this.$widget.find('input[name="useCleanUrls"]').prop("checked", options.useCleanUrls === "true"); } async checkShareRoot() { @@ -93,5 +107,8 @@ export default class ShareSettingsOptions extends OptionsWidget { const showLoginInShareTheme = this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked"); await this.updateOption<"showLoginInShareTheme">("showLoginInShareTheme", showLoginInShareTheme.toString()); + + const useCleanUrls = this.$widget.find('input[name="useCleanUrls"]').prop("checked"); + await this.updateOption<"useCleanUrls">("useCleanUrls", useCleanUrls.toString()); } } diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 4c0220d9ce9..36eb03f19ff 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1670,7 +1670,10 @@ "check_share_root": "Check Share Root Status", "share_root_found": "Share root note '{{noteTitle}}' is ready", "share_root_not_found": "No note with #shareRoot label found", - "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared" + "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared", + "use_clean_urls": "Use clean URLs for shared notes", + "use_clean_urls_description": "When enabled, shared note URLs will be simplified from /share/STi3RCMhUvG6 to /STi3RCMhUvG6", + "share_subtree": "Share subtree" }, "time_selector": { "invalid_input": "The entered time value is not a valid number.", diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index 50ff6b6b690..48097cf8f24 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -77,7 +77,9 @@ const ALLOWED_OPTIONS = new Set([ "backgroundEffects", "allowedHtmlTags", "redirectBareDomain", - "showLoginInShareTheme" + "showLoginInShareTheme", + "shareSubtree", + "useCleanUrls" ]); function getOptions() { diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 7d5fea44b4c..51ac12bf87d 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -100,6 +100,8 @@ const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: e }; function register(app: express.Application) { + app.use(auth.checkCleanUrl); + route(GET, "/", [auth.checkAuth, csrfMiddleware], indexRoute.index); route(GET, "/login", [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage); route(GET, "/set-password", [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPasswordPage); diff --git a/src/services/auth.ts b/src/services/auth.ts index 03f40e6e7b3..05c699f5576 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -22,16 +22,47 @@ function checkAuth(req: Request, res: Response, next: NextFunction) { // Check if any note has the #shareRoot label const shareRootNotes = attributes.getNotesWithLabel("shareRoot"); if (shareRootNotes.length === 0) { + // should this be a translation string? res.status(404).json({ message: "Share root not found. Please set up a note with #shareRoot label first." }); return; } } + // not sure about this. 'share' is dynamic, whatever is turned up by shareRootNote res.redirect(redirectToShare ? "share" : "login"); } else { next(); } } +/** + * Checks if a URL path might be a shared note ID when clean URLs are enabled + */ +function checkCleanUrl(req: Request, res: Response, next: NextFunction) { + // Only process if not logged in and clean URLs are enabled + if (!req.session.loggedIn && !isElectron && !noAuthentication && + options.getOptionBool("redirectBareDomain") && + options.getOptionBool("useCleanUrls")) { + + // Get path without leading slash + const path = req.path.substring(1); + + // Skip processing for known routes and empty paths + if (!path || path === 'login' || path === 'setup' || path.startsWith('share/') || path.startsWith('api/')) { + next(); + return; + } + + // Redirect to the share URL with this ID + // broken, we don't know what `/share/` will be. + // oh! we need to add "what should be share path url?" to settings, + // require path begin with slash + // and allow bare `/` as an answer + res.redirect(`/share/${path}`); + } else { + next(); + } +} + // for electron things which need network stuff // currently, we're doing that for file upload because handling form data seems to be difficult function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) { @@ -134,5 +165,6 @@ export default { checkAppNotInitialized, checkApiAuthOrElectron, checkEtapiToken, - checkCredentials + checkCredentials, + checkCleanUrl }; diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 77d4089ae41..8895ddd779a 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -259,7 +259,9 @@ const defaultOptions: DefaultOption[] = [ // Share settings { name: "redirectBareDomain", value: "false", isSynced: true }, - { name: "showLoginInShareTheme", value: "false", isSynced: true } + { name: "showLoginInShareTheme", value: "false", isSynced: true }, + { name: "useCleanUrls", value: "false", isSynced: true }, + { name: "shareSubtree", value: "false", isSynced: true } ]; /** diff --git a/src/services/options_interface.ts b/src/services/options_interface.ts index d8d8c3fcbb5..1ed5c361dc9 100644 --- a/src/services/options_interface.ts +++ b/src/services/options_interface.ts @@ -100,6 +100,8 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions Date: Wed, 26 Feb 2025 03:10:55 -0700 Subject: [PATCH 02/32] WIP: options page works, but routing broken when logged out --- .../options/other/share_settings.ts | 84 +++++++++++++------ src/public/translations/en/translation.json | 6 +- src/routes/api/options.ts | 3 +- src/routes/login.ts | 3 +- src/services/auth.ts | 64 ++++++++++---- src/services/options_init.ts | 11 ++- src/services/options_interface.ts | 1 + src/share/routes.ts | 74 +++++++++++----- 8 files changed, 181 insertions(+), 65 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index ac5baa6afaf..74da46cf084 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -9,7 +9,7 @@ const TPL = `

${t("share.title")}

${t("share.redirect_bare_domain_description")}

@@ -20,21 +20,33 @@ const TPL = ` +

${t("share.use_clean_urls_description")}

+ +
+ +
+ +
+
+ ${t("share.share_path_description")} +
+
+ +

${t("share.show_login_link_description")}

- -
- -

${t("share.use_clean_urls_description")}

-
`; export default class ShareSettingsOptions extends OptionsWidget { + private $redirectBareDomain!: JQuery; + private $showLoginInShareTheme!: JQuery; + private $useCleanUrls!: JQuery; + private $sharePath!: JQuery; private $shareRootCheck!: JQuery; private $shareRootStatus!: JQuery; @@ -42,37 +54,54 @@ export default class ShareSettingsOptions extends OptionsWidget { this.$widget = $(TPL); this.contentSized(); + this.$redirectBareDomain = this.$widget.find(".redirect-bare-domain"); + this.$showLoginInShareTheme = this.$widget.find(".show-login-in-share-theme"); + this.$useCleanUrls = this.$widget.find(".use-clean-urls"); + this.$sharePath = this.$widget.find(".share-path"); this.$shareRootCheck = this.$widget.find('.share-root-check'); this.$shareRootStatus = this.$widget.find('.share-root-status'); - // Add change handlers for both checkboxes - this.$widget.find('input[type="checkbox"]').on("change", (e: JQuery.ChangeEvent) => { + this.$redirectBareDomain.on('change', () => { + const redirectBareDomain = this.$redirectBareDomain.is(":checked"); this.save(); // Show/hide share root status section based on redirectBareDomain checkbox - const target = e.target as HTMLInputElement; - if (target.name === 'redirectBareDomain') { - this.$shareRootCheck.toggle(target.checked); - if (target.checked) { - this.checkShareRoot(); - } + this.$shareRootCheck.toggle(redirectBareDomain); + if (redirectBareDomain) { + this.checkShareRoot(); } }); + this.$showLoginInShareTheme.on('change', () => { + const showLoginInShareTheme = this.$showLoginInShareTheme.is(":checked"); + this.save(); + }); + + this.$useCleanUrls.on('change', () => { + const useCleanUrls = this.$useCleanUrls.is(":checked"); + this.save(); + }); + + this.$sharePath.on('change', () => { + const sharePath = this.$sharePath.val() as string; + this.save(); + }); + // Add click handler for check share root button this.$widget.find('.check-share-root').on("click", () => this.checkShareRoot()); } async optionsLoaded(options: OptionMap) { const redirectBareDomain = options.redirectBareDomain === "true"; - this.$widget.find('input[name="redirectBareDomain"]').prop("checked", redirectBareDomain); + this.$redirectBareDomain.prop("checked", redirectBareDomain); this.$shareRootCheck.toggle(redirectBareDomain); if (redirectBareDomain) { await this.checkShareRoot(); } - this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked", options.showLoginInShareTheme === "true"); - this.$widget.find('input[name="useCleanUrls"]').prop("checked", options.useCleanUrls === "true"); + this.$showLoginInShareTheme.prop("checked", options.showLoginInShareTheme === "true"); + this.$useCleanUrls.prop("checked", options.useCleanUrls === "true"); + this.$sharePath.val(options.sharePath); } async checkShareRoot() { @@ -102,13 +131,20 @@ export default class ShareSettingsOptions extends OptionsWidget { } async save() { - const redirectBareDomain = this.$widget.find('input[name="redirectBareDomain"]').prop("checked"); + const redirectBareDomain = this.$redirectBareDomain.is(":checked"); await this.updateOption<"redirectBareDomain">("redirectBareDomain", redirectBareDomain.toString()); - const showLoginInShareTheme = this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked"); + const showLoginInShareTheme = this.$showLoginInShareTheme.is(":checked"); await this.updateOption<"showLoginInShareTheme">("showLoginInShareTheme", showLoginInShareTheme.toString()); - const useCleanUrls = this.$widget.find('input[name="useCleanUrls"]').prop("checked"); + const useCleanUrls = this.$useCleanUrls.is(":checked"); await this.updateOption<"useCleanUrls">("useCleanUrls", useCleanUrls.toString()); + + // Ensure sharePath always starts with a slash + let sharePath = this.$sharePath.val() as string; + if (sharePath && !sharePath.startsWith('/')) { + sharePath = '/' + sharePath; + } + await this.updateOption<"sharePath">("sharePath", sharePath); } } diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 36eb03f19ff..a776954ff6e 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1673,7 +1673,11 @@ "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared", "use_clean_urls": "Use clean URLs for shared notes", "use_clean_urls_description": "When enabled, shared note URLs will be simplified from /share/STi3RCMhUvG6 to /STi3RCMhUvG6", - "share_subtree": "Share subtree" + "share_path": "Share path", + "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' and '/' --> '/noteId')", + "share_path_placeholder": "/share or / for root", + "share_subtree": "Share subtree", + "share_subtree_description": "Share the entire subtree, not just the note" }, "time_selector": { "invalid_input": "The entered time value is not a valid number.", diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index 48097cf8f24..b2ccfa104ac 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -79,7 +79,8 @@ const ALLOWED_OPTIONS = new Set([ "redirectBareDomain", "showLoginInShareTheme", "shareSubtree", - "useCleanUrls" + "useCleanUrls", + "sharePath" ]); function getOptions() { diff --git a/src/routes/login.ts b/src/routes/login.ts index 68b98e89396..18ecb9fe931 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -46,7 +46,8 @@ function setPassword(req: Request, res: Response) { if (error) { res.render("set_password", { error, - assetPath: assetPath + assetPath: assetPath, + appPath: appPath }); return; } diff --git a/src/services/auth.ts b/src/services/auth.ts index 05c699f5576..c18a23ff58d 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -26,9 +26,23 @@ function checkAuth(req: Request, res: Response, next: NextFunction) { res.status(404).json({ message: "Share root not found. Please set up a note with #shareRoot label first." }); return; } + + // Get the configured share path + const sharePath = options.getOption("sharePath") || '/share'; + + // Check if we're already at the share path to prevent redirect loops + if (req.path === sharePath || req.path.startsWith(`${sharePath}/`)) { + log.info(`checkAuth: Already at share path, skipping redirect. Path: ${req.path}, SharePath: ${sharePath}`); + next(); + return; + } + + // Redirect to the share path + log.info(`checkAuth: Redirecting to share path. From: ${req.path}, To: ${sharePath}`); + res.redirect(sharePath); + } else { + res.redirect("login"); } - // not sure about this. 'share' is dynamic, whatever is turned up by shareRootNote - res.redirect(redirectToShare ? "share" : "login"); } else { next(); } @@ -43,38 +57,54 @@ function checkCleanUrl(req: Request, res: Response, next: NextFunction) { options.getOptionBool("redirectBareDomain") && options.getOptionBool("useCleanUrls")) { + // Get the configured share path + const sharePath = options.getOption("sharePath") || '/share'; + // Get path without leading slash const path = req.path.substring(1); - // Skip processing for known routes and empty paths - if (!path || path === 'login' || path === 'setup' || path.startsWith('share/') || path.startsWith('api/')) { + // Skip processing for known routes, empty paths, and paths that already start with sharePath + if (!path || + path === 'login' || + path === 'setup' || + path.startsWith('api/') || + req.path === sharePath || + req.path.startsWith(`${sharePath}/`)) { + log.info(`checkCleanUrl: Skipping redirect. Path: ${req.path}, SharePath: ${sharePath}`); + next(); + return; + } + + // If sharePath is just '/', we don't need to redirect + if (sharePath === '/') { + log.info(`checkCleanUrl: SharePath is root, skipping redirect. Path: ${req.path}`); next(); return; } // Redirect to the share URL with this ID - // broken, we don't know what `/share/` will be. - // oh! we need to add "what should be share path url?" to settings, - // require path begin with slash - // and allow bare `/` as an answer - res.redirect(`/share/${path}`); + log.info(`checkCleanUrl: Redirecting to share path. From: ${req.path}, To: ${sharePath}/${path}`); + res.redirect(`${sharePath}/${path}`); } else { next(); } } -// for electron things which need network stuff -// currently, we're doing that for file upload because handling form data seems to be difficult -function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) { - if (!req.session.loggedIn && !isElectron && !noAuthentication) { +/** + * Middleware for API authentication - works for both sync and normal API + */ +function checkApiAuth(req: Request, res: Response, next: NextFunction) { + if (!req.session.loggedIn && !noAuthentication) { reject(req, res, "Logged in session not found"); } else { next(); } } -function checkApiAuth(req: Request, res: Response, next: NextFunction) { - if (!req.session.loggedIn && !noAuthentication) { +// for electron things which need network stuff +// currently, we're doing that for file upload because handling form data seems to be difficult +function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) { + if (!req.session.loggedIn && !isElectron && !noAuthentication) { reject(req, res, "Logged in session not found"); } else { next(); @@ -158,6 +188,7 @@ function checkCredentials(req: Request, res: Response, next: NextFunction) { export default { checkAuth, + checkCleanUrl, checkApiAuth, checkAppInitialized, checkPasswordSet, @@ -165,6 +196,5 @@ export default { checkAppNotInitialized, checkApiAuthOrElectron, checkEtapiToken, - checkCredentials, - checkCleanUrl + checkCredentials }; diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 8895ddd779a..b03f76aca97 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -124,7 +124,7 @@ const defaultOptions: DefaultOption[] = [ { name: "highlightsList", value: '["bold","italic","underline","color","bgColor"]', isSynced: true }, { name: "checkForUpdates", value: "true", isSynced: true }, { name: "disableTray", value: "false", isSynced: false }, - { name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true }, // default 30 days + { name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true }, // default 30 days { name: "eraseUnusedAttachmentsAfterTimeScale", value: "86400", isSynced: true }, // default 86400 seconds = Day { name: "customSearchEngineName", value: "DuckDuckGo", isSynced: true }, { name: "customSearchEngineUrl", value: "https://duckduckgo.com/?q={keyword}", isSynced: true }, @@ -258,6 +258,15 @@ const defaultOptions: DefaultOption[] = [ }, // Share settings + { + name: "sharePath", + // ensure always starts with slash + value: (optionsMap) => { + const sharePath = optionsMap.sharePath || "/share"; + return sharePath.startsWith("/") ? sharePath : "/" + sharePath; + }, + isSynced: true + }, { name: "redirectBareDomain", value: "false", isSynced: true }, { name: "showLoginInShareTheme", value: "false", isSynced: true }, { name: "useCleanUrls", value: "false", isSynced: true }, diff --git a/src/services/options_interface.ts b/src/services/options_interface.ts index 1ed5c361dc9..01faea5ec90 100644 --- a/src/services/options_interface.ts +++ b/src/services/options_interface.ts @@ -102,6 +102,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions { - if (req.path.substr(-1) !== "/") { - res.redirect("../share/"); - return; - } + let sharePath = options.getOption("sharePath") || '/share'; - shacaLoader.ensureLoad(); + // Handle root path specially + if (sharePath === '/') { + router.get('/', (req, res, next) => { + shacaLoader.ensureLoad(); - if (!shaca.shareRootNote) { - res.status(404).json({ message: "Share root note not found" }); - return; - } + if (!shaca.shareRootNote) { + res.status(404).json({ message: "Share root not found" }); + return; + } - renderNote(shaca.shareRootNote, req, res); - }); + renderNote(shaca.shareRootNote, req, res); + }); + } else { + router.get(`${sharePath}/`, (req, res, next) => { + if (req.path !== `${sharePath}/`) { + res.redirect(`${sharePath}/`); + return; + } + + shacaLoader.ensureLoad(); + + if (!shaca.shareRootNote) { + res.status(404).json({ message: "Share root not found" }); + return; + } + + renderNote(shaca.shareRootNote, req, res); + }); + } + + if (sharePath === '/' && options.getOptionBool("useCleanUrls") && options.getOptionBool("redirectBareDomain")) { + router.get("/:shareId", (req, res, next) => { + shacaLoader.ensureLoad(); + + const { shareId } = req.params; + + // Skip processing for known routes + if (shareId === 'login' || shareId === 'setup' || shareId.startsWith('api/')) { + next(); + return; + } + + const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; + + renderNote(note, req, res); + }); + } - router.get("/share/:shareId", (req, res, next) => { + router.get(`${sharePath}/:shareId`, (req, res, next) => { shacaLoader.ensureLoad(); const { shareId } = req.params; @@ -221,7 +255,7 @@ function register(router: Router) { renderNote(note, req, res); }); - router.get("/share/api/notes/:noteId", (req, res, next) => { + router.get(`${sharePath}/api/notes/:noteId`, (req, res, next) => { shacaLoader.ensureLoad(); let note: SNote | boolean; @@ -234,7 +268,7 @@ function register(router: Router) { res.json(note.getPojo()); }); - router.get("/share/api/notes/:noteId/download", (req, res, next) => { + router.get(`${sharePath}/api/notes/:noteId/download`, (req, res, next) => { shacaLoader.ensureLoad(); let note: SNote | boolean; @@ -256,7 +290,7 @@ function register(router: Router) { }); // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename - router.get("/share/api/images/:noteId/:filename", (req, res, next) => { + router.get(`${sharePath}/api/images/:noteId/:filename`, (req, res, next) => { shacaLoader.ensureLoad(); let image: SNote | boolean; @@ -282,7 +316,7 @@ function register(router: Router) { }); // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename - router.get("/share/api/attachments/:attachmentId/image/:filename", (req, res, next) => { + router.get(`${sharePath}/api/attachments/:attachmentId/image/:filename`, (req, res, next) => { shacaLoader.ensureLoad(); let attachment: SAttachment | boolean; @@ -300,7 +334,7 @@ function register(router: Router) { } }); - router.get("/share/api/attachments/:attachmentId/download", (req, res, next) => { + router.get(`${sharePath}/api/attachments/:attachmentId/download`, (req, res, next) => { shacaLoader.ensureLoad(); let attachment: SAttachment | boolean; @@ -322,7 +356,7 @@ function register(router: Router) { }); // used for PDF viewing - router.get("/share/api/notes/:noteId/view", (req, res, next) => { + router.get(`${sharePath}/api/notes/:noteId/view`, (req, res, next) => { shacaLoader.ensureLoad(); let note: SNote | boolean; @@ -340,7 +374,7 @@ function register(router: Router) { }); // Used for searching, require noteId so we know the subTreeRoot - router.get("/share/api/notes", (req, res, next) => { + router.get(`${sharePath}/api/notes`, (req, res, next) => { shacaLoader.ensureLoad(); const ancestorNoteId = req.query.ancestorNoteId ?? "_share"; From bba2f6db64ddd1f5dda3437d03ca70a9ebc6546e Mon Sep 17 00:00:00 2001 From: matt wilkie Date: Sat, 8 Mar 2025 09:35:52 -0700 Subject: [PATCH 03/32] wip: another attempt (and use translation syntax this time) --- .../app/widgets/type_widgets/options/other/share_settings.ts | 2 +- src/services/auth.ts | 2 +- src/share/routes.ts | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index 74da46cf084..0a59a317c25 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -28,7 +28,7 @@ const TPL = `
- +
${t("share.share_path_description")} diff --git a/src/services/auth.ts b/src/services/auth.ts index c18a23ff58d..a2ab40b8799 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -39,7 +39,7 @@ function checkAuth(req: Request, res: Response, next: NextFunction) { // Redirect to the share path log.info(`checkAuth: Redirecting to share path. From: ${req.path}, To: ${sharePath}`); - res.redirect(sharePath); + res.redirect(`${sharePath}/`); } else { res.redirect("login"); } diff --git a/src/share/routes.ts b/src/share/routes.ts index 71c6b797058..1181692312c 100644 --- a/src/share/routes.ts +++ b/src/share/routes.ts @@ -210,6 +210,11 @@ function register(router: Router) { renderNote(shaca.shareRootNote, req, res); }); } else { + router.get(`${sharePath}`, (req, res, next) => { + // Redirect to the path with trailing slash for consistency + res.redirect(`${sharePath}/`); + }); + router.get(`${sharePath}/`, (req, res, next) => { if (req.path !== `${sharePath}/`) { res.redirect(`${sharePath}/`); From 81d2fbc057d30a10208ce09f2223340c1de32424 Mon Sep 17 00:00:00 2001 From: Jin <22962980+JYC333@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:54:54 +0200 Subject: [PATCH 04/32] =?UTF-8?q?fix:=20=F0=9F=90=9B=20add=20back=20missin?= =?UTF-8?q?g=20translation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/translations/en/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 50ace1d9ba5..b6328a00b31 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -744,7 +744,8 @@ "basic_properties": { "note_type": "Note type", "editable": "Editable", - "basic_properties": "Basic Properties" + "basic_properties": "Basic Properties", + "language": "Language" }, "book_properties": { "view_type": "View type", From 0be508ed7030599828319db125504af48f8d8c16 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 18 Apr 2025 00:10:29 +0200 Subject: [PATCH 05/32] fix(share/routes): fix crash on clean DB startup when sharePath option is not set --- src/share/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/share/routes.ts b/src/share/routes.ts index a0a20f9aec3..4e1d1920156 100644 --- a/src/share/routes.ts +++ b/src/share/routes.ts @@ -203,7 +203,7 @@ function register(router: Router) { } } - const sharePath = options.getOption("sharePath") || '/share'; + const sharePath = options.getOptionOrNull("sharePath") || '/share'; // Handle root path specially if (sharePath === '/') { From d72a0d3c690e9ea888737e41600a2887f3bcbb7e Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 18 Apr 2025 00:15:25 +0200 Subject: [PATCH 06/32] fix(services/auth): fix crash on clean DB startup when options are not set --- src/services/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/auth.ts b/src/services/auth.ts index 26b01579881..53f71b8ea0c 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -80,8 +80,8 @@ function checkAuth(req: Request, res: Response, next: NextFunction) { function checkCleanUrl(req: Request, res: Response, next: NextFunction) { // Only process if not logged in and clean URLs are enabled if (!req.session.loggedIn && !isElectron && !noAuthentication && - options.getOptionBool("redirectBareDomain") && - options.getOptionBool("useCleanUrls")) { + options.getOptionOrNull("redirectBareDomain") === "true" && + options.getOptionOrNull("useCleanUrls") === "true") { // Get the configured share path const sharePath = options.getOption("sharePath") || '/share'; From 9a11fc13d7634be8e2d377fa0bc3a57a8ad9ec96 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 18 Apr 2025 01:07:54 +0200 Subject: [PATCH 07/32] fix(share_settings): fix missing class in redirect-bare-domain input --- .../app/widgets/type_widgets/options/other/share_settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index c13dee9115c..3a93e6b7c37 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -9,7 +9,7 @@ const TPL = /*html*/`

${t("share.title")}

${t("share.redirect_bare_domain_description")}

From df45fa2e1e36ed0bf52397176e40acf133c85393 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 18 Apr 2025 01:43:39 +0200 Subject: [PATCH 08/32] fix(share/routes): fix redirect loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commented out for now, to make sure we get back to it – not sure if it was suppossed to have any special other reason to exist --- src/routes/routes.ts | 3 ++- src/share/routes.ts | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 2bfcad4c74a..77c62ad8c5c 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -105,7 +105,8 @@ const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: e }; function register(app: express.Application) { - app.use(auth.checkCleanUrl); + // @pano9000: comment out for now to fix other functionality first + //app.use(auth.checkCleanUrl); route(GET, "/", [auth.checkAuth, csrfMiddleware], indexRoute.index); route(GET, "/login", [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage); diff --git a/src/share/routes.ts b/src/share/routes.ts index 4e1d1920156..506bdd435ec 100644 --- a/src/share/routes.ts +++ b/src/share/routes.ts @@ -218,10 +218,11 @@ function register(router: Router) { renderNote(shaca.shareRootNote, req, res); }); } else { - router.get(`${sharePath}`, (req, res, next) => { - // Redirect to the path with trailing slash for consistency - res.redirect(`${sharePath}/`); - }); + // @pano9000: comment out for now -> this is what causes the redirect loop + // router.get(`${sharePath}`, (req, res, next) => { + // // Redirect to the path with trailing slash for consistency + // res.redirect(`${sharePath}/`); + // }); router.get(`${sharePath}/`, (req, res, next) => { if (req.path !== `${sharePath}/`) { From 56fc2d9b30bf481cc7be7d1040a1a9eaa7bf4f61 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 18 Apr 2025 02:00:23 +0200 Subject: [PATCH 09/32] fix(share_settings): fix not being able to set share path caused by having other several save operation inside the save() method (which doesn't even make sense to begin with, as far as I can tell) moving it to inside the "change" event handler allows us to set and store a custom share path again --- .../type_widgets/options/other/share_settings.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index 3a93e6b7c37..370ef2c4054 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -82,9 +82,13 @@ export default class ShareSettingsOptions extends OptionsWidget { this.save(); }); - this.$sharePath.on('change', () => { - const sharePath = this.$sharePath.val() as string; - this.save(); + this.$sharePath.on('change', async () => { + // Ensure sharePath always starts with a slash + let sharePath = this.$sharePath.val() as string; + if (sharePath && !sharePath.startsWith('/')) { + sharePath = '/' + sharePath; + } + await this.updateOption<"sharePath">("sharePath", sharePath); }); // Add click handler for check share root button From ab901a5d32e52086bd0c8e3a7ef35ac12efb1b56 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 18 Apr 2025 02:05:40 +0200 Subject: [PATCH 10/32] refactor(share_settings): get rid of save() method there's no need to execute PUT requests for *all* Share Settings, when any option changes moved the code to inside the "change" event handlers --- .../options/other/share_settings.ts | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index 370ef2c4054..fddfccc03d3 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -61,9 +61,10 @@ export default class ShareSettingsOptions extends OptionsWidget { this.$shareRootCheck = this.$widget.find(".share-root-check"); this.$shareRootStatus = this.$widget.find(".share-root-status"); - this.$redirectBareDomain.on('change', () => { + this.$redirectBareDomain.on('change', async () => { + const redirectBareDomain = this.$redirectBareDomain.is(":checked"); - this.save(); + await this.updateOption<"redirectBareDomain">("redirectBareDomain", redirectBareDomain.toString()); // Show/hide share root status section based on redirectBareDomain checkbox this.$shareRootCheck.toggle(redirectBareDomain); @@ -72,14 +73,14 @@ export default class ShareSettingsOptions extends OptionsWidget { } }); - this.$showLoginInShareTheme.on('change', () => { + this.$showLoginInShareTheme.on('change', async () => { const showLoginInShareTheme = this.$showLoginInShareTheme.is(":checked"); - this.save(); + await this.updateOption<"showLoginInShareTheme">("showLoginInShareTheme", showLoginInShareTheme.toString()); }); - this.$useCleanUrls.on('change', () => { + this.$useCleanUrls.on('change', async () => { const useCleanUrls = this.$useCleanUrls.is(":checked"); - this.save(); + await this.updateOption<"useCleanUrls">("useCleanUrls", useCleanUrls.toString()); }); this.$sharePath.on('change', async () => { @@ -131,22 +132,4 @@ export default class ShareSettingsOptions extends OptionsWidget { $button.prop("disabled", false); } } - - async save() { - const redirectBareDomain = this.$redirectBareDomain.is(":checked"); - await this.updateOption<"redirectBareDomain">("redirectBareDomain", redirectBareDomain.toString()); - - const showLoginInShareTheme = this.$showLoginInShareTheme.is(":checked"); - await this.updateOption<"showLoginInShareTheme">("showLoginInShareTheme", showLoginInShareTheme.toString()); - - const useCleanUrls = this.$useCleanUrls.is(":checked"); - await this.updateOption<"useCleanUrls">("useCleanUrls", useCleanUrls.toString()); - - // Ensure sharePath always starts with a slash - let sharePath = this.$sharePath.val() as string; - if (sharePath && !sharePath.startsWith('/')) { - sharePath = '/' + sharePath; - } - await this.updateOption<"sharePath">("sharePath", sharePath); - } } From a8901e6dc886c5bbe1b5a38cf3c0596bf5552585 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 18 Apr 2025 11:53:56 +0200 Subject: [PATCH 11/32] chore: revert back unnecessary changes from unclean merge https://github.com/TriliumNext/Notes/pull/1288/commits/c9d151289cbfddd42f1fbc5fef7d65bc969011c8 --- src/public/translations/en/translation.json | 7 +++---- src/routes/login.ts | 4 ++-- src/share/routes.ts | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 43d52d3ecbd..7e6f075ee73 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -272,7 +272,7 @@ "help_title": "Help on Note Revisions", "close": "Close", "revision_last_edited": "This revision was last edited on {{date}}", - "confirm_delete_all": "Do you want to delete all revisions of this note? This action will erase the revision title and content, but still preserve the revision metadata.", + "confirm_delete_all": "Do you want to delete all revisions of this note?", "no_revisions": "No revisions for this note yet...", "restore_button": "Restore", "confirm_restore": "Do you want to restore this revision? This will overwrite the current title and content of the note with this revision.", @@ -376,7 +376,7 @@ "auto_read_only_disabled": "text/code notes can be set automatically into read mode when they are too large. You can disable this behavior on per-note basis by adding this label to the note", "app_css": "marks CSS notes which are loaded into the Trilium application and can thus be used to modify Trilium's looks.", "app_theme": "marks CSS notes which are full Trilium themes and are thus available in Trilium options.", - "app_theme_base": "set to \"next\" in order to use the TriliumNext theme as a base for a custom theme instead of the legacy one.", + "app_theme_base": "set to \"next\", \"next-light\", or \"next-dark\" to use the corresponding TriliumNext theme (auto, light or dark) as the base for a custom theme, instead of the legacy one.", "css_class": "value of this label is then added as CSS class to the node representing given note in the tree. This can be useful for advanced theming. Can be used in template notes.", "icon_class": "value of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.", "page_size": "number of items per page in note listing", @@ -1465,9 +1465,8 @@ "etapi": { "title": "ETAPI", "description": "ETAPI is a REST API used to access Trilium instance programmatically, without UI.", - "see_more": "See more details on", + "see_more": "See more details in the {{- link_to_wiki}} and the {{- link_to_openapi_spec}} or the {{- link_to_swagger_ui }}.", "wiki": "wiki", - "and": "and", "openapi_spec": "ETAPI OpenAPI spec", "swagger_ui": "ETAPI Swagger UI", "create_token": "Create new ETAPI token", diff --git a/src/routes/login.ts b/src/routes/login.ts index 6a841d9f112..1b2d42b2505 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -51,8 +51,8 @@ function setPassword(req: Request, res: Response) { if (error) { res.render("set_password", { error, - assetPath: assetPath, - appPath: appPath + assetPath, + appPath }); return; } diff --git a/src/share/routes.ts b/src/share/routes.ts index 506bdd435ec..79da9ed5163 100644 --- a/src/share/routes.ts +++ b/src/share/routes.ts @@ -121,8 +121,8 @@ function renderImageAttachment(image: SNote, res: Response, attachmentName: stri && possibleSvgContent !== null && "svg" in possibleSvgContent && typeof possibleSvgContent.svg === "string") - ? possibleSvgContent.svg - : null; + ? possibleSvgContent.svg + : null; if (contentSvg) { svgString = contentSvg; From dabdfaddec8f041d7f070db0d83f5b920d17e35a Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 20 Apr 2025 10:40:20 +0200 Subject: [PATCH 12/32] fix(share/routes): remove unnecessary redirects that cause loops --- src/share/routes.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/share/routes.ts b/src/share/routes.ts index 79da9ed5163..4362f4f6e42 100644 --- a/src/share/routes.ts +++ b/src/share/routes.ts @@ -218,17 +218,8 @@ function register(router: Router) { renderNote(shaca.shareRootNote, req, res); }); } else { - // @pano9000: comment out for now -> this is what causes the redirect loop - // router.get(`${sharePath}`, (req, res, next) => { - // // Redirect to the path with trailing slash for consistency - // res.redirect(`${sharePath}/`); - // }); router.get(`${sharePath}/`, (req, res, next) => { - if (req.path !== `${sharePath}/`) { - res.redirect(`${sharePath}/`); - return; - } shacaLoader.ensureLoad(); @@ -239,6 +230,7 @@ function register(router: Router) { renderNote(shaca.shareRootNote, req, res); }); + } if (sharePath === '/' && options.getOptionBool("useCleanUrls") && options.getOptionBool("redirectBareDomain")) { From 9f0a0238cc136e045a9204a0d21eb6ce8fcd91b8 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 20 Apr 2025 19:19:30 +0200 Subject: [PATCH 13/32] fix(share_settings): stop runnning checkShareRoot on init and on redirectBareDomain change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit → that is what the shareRootCheck button is there for --- .../widgets/type_widgets/options/other/share_settings.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index fddfccc03d3..d0300ed2b35 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -68,9 +68,6 @@ export default class ShareSettingsOptions extends OptionsWidget { // Show/hide share root status section based on redirectBareDomain checkbox this.$shareRootCheck.toggle(redirectBareDomain); - if (redirectBareDomain) { - this.checkShareRoot(); - } }); this.$showLoginInShareTheme.on('change', async () => { @@ -100,9 +97,6 @@ export default class ShareSettingsOptions extends OptionsWidget { const redirectBareDomain = options.redirectBareDomain === "true"; this.$redirectBareDomain.prop("checked", redirectBareDomain); this.$shareRootCheck.toggle(redirectBareDomain); - if (redirectBareDomain) { - await this.checkShareRoot(); - } this.$showLoginInShareTheme.prop("checked", options.showLoginInShareTheme === "true"); this.$useCleanUrls.prop("checked", options.useCleanUrls === "true"); From b0030f89b7383bddf0d91dd0c9a24cd813c68cf2 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 20 Apr 2025 19:21:38 +0200 Subject: [PATCH 14/32] feat(share_settings): group options that belong together logically --- .../options/other/share_settings.ts | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index d0300ed2b35..d07c40e864d 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -8,22 +8,26 @@ const TPL = /*html*/`

${t("share.title")}

- -

${t("share.redirect_bare_domain_description")}

- - - -

${t("share.show_login_link_description")}

+
+ +

${t("share.use_clean_urls_description")}

+
+
`; export default class ShareSettingsOptions extends OptionsWidget { @@ -60,6 +67,7 @@ export default class ShareSettingsOptions extends OptionsWidget { this.$sharePath = this.$widget.find(".share-path"); this.$shareRootCheck = this.$widget.find(".share-root-check"); this.$shareRootStatus = this.$widget.find(".share-root-status"); + this.$shareRootCheck.hide(); this.$redirectBareDomain.on('change', async () => { From 0e31aab1ab425f90dea7703d416187ae158348e7 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 20 Apr 2025 19:24:58 +0200 Subject: [PATCH 15/32] refactor(share_settings): use this.$shareRootCheck instead of creating new local $button variable --- .../app/widgets/type_widgets/options/other/share_settings.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index d07c40e864d..5919264d7fe 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -112,8 +112,7 @@ export default class ShareSettingsOptions extends OptionsWidget { } async checkShareRoot() { - const $button = this.$widget.find(".check-share-root"); - $button.prop("disabled", true); + this.$shareRootCheck.prop("disabled", true); try { const shareRootNotes = await searchService.searchForNotes("#shareRoot"); @@ -131,7 +130,7 @@ export default class ShareSettingsOptions extends OptionsWidget { .text(shareRootNotes.length > 0 ? t("share.share_root_not_shared", { noteTitle: shareRootNotes[0].title }) : t("share.share_root_not_found")); } } finally { - $button.prop("disabled", false); + this.$shareRootCheck.prop("disabled", false); } } } From 6dc687ef43dd4cb134ad4807af2db0214a7d469c Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 21 Apr 2025 07:54:24 +0200 Subject: [PATCH 16/32] feat(share_settings): improve checkShareRoot * add rudimental error handling * add handling of special case, where one has multiple shared notes with a #shareRoot label * refactor styling into auxiliary setCheckShareRootStyle function --- .../options/other/share_settings.ts | 63 ++++++++++++++----- src/public/translations/en/translation.json | 3 + 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index 5919264d7fe..4c7081b7c6a 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -114,23 +114,58 @@ export default class ShareSettingsOptions extends OptionsWidget { async checkShareRoot() { this.$shareRootCheck.prop("disabled", true); + const setCheckShareRootStyle = (removeClassName: string, addClassName: string, text: string) => { + this.$shareRootStatus + .removeClass(removeClassName) + .addClass(addClassName) + .text(text); + + this.$shareRootCheck.prop("disabled", false); + }; + try { const shareRootNotes = await searchService.searchForNotes("#shareRoot"); - const sharedShareRootNote = shareRootNotes.find((note) => note.isShared()); - - if (sharedShareRootNote) { - this.$shareRootStatus - .removeClass("text-danger") - .addClass("text-success") - .text(t("share.share_root_found", { noteTitle: sharedShareRootNote.title })); - } else { - this.$shareRootStatus - .removeClass("text-success") - .addClass("text-danger") - .text(shareRootNotes.length > 0 ? t("share.share_root_not_shared", { noteTitle: shareRootNotes[0].title }) : t("share.share_root_not_found")); + const sharedShareRootNotes = shareRootNotes.filter((note) => note.isShared()); + + // No Note found that has the sharedRoot label AND is currently shared + if (sharedShareRootNotes.length < 1) { + const textMessage = (shareRootNotes.length > 0) + ? t("share.share_root_not_shared", { noteTitle: shareRootNotes[0].title }) + : t("share.share_root_not_found"); + + return setCheckShareRootStyle("text-success", "text-danger", textMessage); } - } finally { - this.$shareRootCheck.prop("disabled", false); + + // more than one currently shared Note found with the sharedRoot label + // → use the first found, but warn user about it + if (sharedShareRootNotes.length > 1) { + + const foundNoteTitles = shareRootNotes.map(note => t("share.share_note_title", { + noteTitle: note.title, + interpolation: { + escapeValue: false + } + })); + const activeNoteTitle = foundNoteTitles[0]; + + return setCheckShareRootStyle("text-danger", "text-success", + t("share.share_root_multiple_found", { + activeNoteTitle, + foundNoteTitles: foundNoteTitles.join(", ") + }) + ); + } + + // exactly one note that has the sharedRoot label AND is currently shared + return setCheckShareRootStyle("text-danger", "text-success", + t("share.share_root_found", { noteTitle: sharedShareRootNotes[0].title }) + ); + + } catch(err) { + console.error(err); + return setCheckShareRootStyle("text-success", "text-danger", + t("share.check_share_root_error",) + ); } } } diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 7e6f075ee73..b1430814346 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1913,9 +1913,12 @@ "show_login_link": "Show Login link in Share theme", "show_login_link_description": "Add a login link to the Share page footer", "check_share_root": "Check Share Root Status", + "check_share_root_error": "An unexpected error happened while checking the Share Root Status, please check the logs for more information.", + "share_note_title": "'{{noteTitle}}'", "share_root_found": "Share root note '{{noteTitle}}' is ready", "share_root_not_found": "No note with #shareRoot label found", "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared", + "share_root_multiple_found": "Found multiple shared notes with a #shareRoot label: {{- foundNoteTitles}}. The note {{- activeNoteTitle}} will be used as shared root note.", "use_clean_urls": "Use clean URLs for shared notes", "use_clean_urls_description": "When enabled, shared note URLs will be simplified from /share/STi3RCMhUvG6 to /STi3RCMhUvG6", "share_path": "Share path", From c90364bd76c1a9eb9df1ead3a55ad0c98caf7e4a Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 21 Apr 2025 09:10:21 +0200 Subject: [PATCH 17/32] feat(share_settings): improve sharePath input handling * add normalization helper to ensure string does not start with multiple trailing slashes or ends with a trailing slash * fall back to default "/share" when empty string is entered --- .../options/other/share_settings.ts | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index 4c7081b7c6a..2d55d8e9372 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -89,18 +89,36 @@ export default class ShareSettingsOptions extends OptionsWidget { }); this.$sharePath.on('change', async () => { - // Ensure sharePath always starts with a slash - let sharePath = this.$sharePath.val() as string; - if (sharePath && !sharePath.startsWith('/')) { - sharePath = '/' + sharePath; - } - await this.updateOption<"sharePath">("sharePath", sharePath); + const DEFAULT_SHAREPATH = "/share"; + const sharePathInput = this.$sharePath.val()?.trim() || ""; + + const normalizedSharePath = this.normalizeSharePathInput(sharePathInput); + const optionValue = (!sharePathInput || !normalizedSharePath) ? DEFAULT_SHAREPATH : normalizedSharePath; + + await this.updateOption<"sharePath">("sharePath", optionValue); }); // Add click handler for check share root button this.$widget.find(".check-share-root").on("click", () => this.checkShareRoot()); } + // Ensure sharePath always starts with a single slash and does not end with (one or multiple) trailing slashes + normalizeSharePathInput(sharePathInput: string) { + + //TriliumNextTODO: -> this also disallows using single "/" as share path -> but do we need it? + const REGEXP_STARTING_SLASH = /^\/+/g; + const REGEXP_TRAILING_SLASH = /\/+$/g; + + const normalizedSharePath = (!sharePathInput.startsWith("/") + ? `/${sharePathInput}` + : sharePathInput) + .replaceAll(REGEXP_TRAILING_SLASH, "") + .replaceAll(REGEXP_STARTING_SLASH, "/"); + + return normalizedSharePath; + + } + async optionsLoaded(options: OptionMap) { const redirectBareDomain = options.redirectBareDomain === "true"; this.$redirectBareDomain.prop("checked", redirectBareDomain); From d1d4b4711176e669740d388e6ae7304d030bff01 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 21 Apr 2025 09:15:48 +0200 Subject: [PATCH 18/32] test(share_settings): add initial test for normalizeSharePathInput it currently fails trying to import the class though a "manual" importing of the function did pass all checks though -> will need to investigate why importing the class does not work like that --- .../options/other/share_settings.spec.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/public/app/widgets/type_widgets/options/other/share_settings.spec.ts diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.spec.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.spec.ts new file mode 100644 index 00000000000..9be36fd8234 --- /dev/null +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.spec.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from "vitest"; + +describe.skip("ShareSettingsOptions", () => {}) +/* + + Test currently fails during import: + + FAIL app widgets/type_widgets/options/other/share_settings.spec.ts [ src/public/app/widgets/type_widgets/options/other/share_settings.spec.ts ] +TypeError: Class extends value undefined is not a constructor or null + ❯ widgets/right_panel_widget.ts:20:32 + 20| class RightPanelWidget extends NoteContextAwareWidget { + 21| private $bodyWrapper!: JQuery; + 22| $body!: JQuery; + + +import ShareSettingsOptions from "./share_settings.js"; + +type TestCase any> = [ + desc: string, + fnParams: Parameters, + expected: ReturnType +]; + + + +describe("ShareSettingsOptions", () => { + + describe("#normalizeSharePathInput", () => { + + const testCases: TestCase[] = [ + [ + "should handle multiple trailing '/' and remove them completely", + ["/trailingtest////"], + "/trailingtest" + ], + [ + "should handle multiple starting '/' and replace them by a single '/'", + ["////startingtest"], + "/startingtest" + ], + [ + "should handle multiple starting & trailing '/' and replace them by a single '/'", + ["////startingAndTrailingTest///"], + "/startingAndTrailingTest" + ], + [ + "should not remove any '/' other than at the end or start of the input", + ["/test/with/subpath"], + "/test/with/subpath" + ], + [ + "should prepend the string with a '/' if it does not start with one", + ["testpath"], + "/testpath" + ], + ]; + + testCases.forEach((testCase) => { + const [desc, fnParams, expected] = testCase; + return it(desc, () => { + const shareSettings = new ShareSettingsOptions(); + const actual = shareSettings.normalizeSharePathInput(...fnParams); + expect(actual).toStrictEqual(expected); + }); + }); + + + }) + +})*/ \ No newline at end of file From 1b7266f083f03c42bcaf6dfbcf026fb57569f635 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 21 Apr 2025 10:25:09 +0200 Subject: [PATCH 19/32] chore(share_settings): remove unnecessary comment --- .../app/widgets/type_widgets/options/other/share_settings.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index 2d55d8e9372..ca09c9816ad 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -70,7 +70,6 @@ export default class ShareSettingsOptions extends OptionsWidget { this.$shareRootCheck.hide(); this.$redirectBareDomain.on('change', async () => { - const redirectBareDomain = this.$redirectBareDomain.is(":checked"); await this.updateOption<"redirectBareDomain">("redirectBareDomain", redirectBareDomain.toString()); @@ -98,7 +97,6 @@ export default class ShareSettingsOptions extends OptionsWidget { await this.updateOption<"sharePath">("sharePath", optionValue); }); - // Add click handler for check share root button this.$widget.find(".check-share-root").on("click", () => this.checkShareRoot()); } From 00b5aef890ca1bb94a2a9d42111876729355c11d Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 21 Apr 2025 10:45:41 +0200 Subject: [PATCH 20/32] feat(share_settings): add support for adding "/" as sharePath --- .../type_widgets/options/other/share_settings.spec.ts | 5 +++++ .../app/widgets/type_widgets/options/other/share_settings.ts | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.spec.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.spec.ts index 9be36fd8234..8a4302a2872 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.spec.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.spec.ts @@ -53,6 +53,11 @@ describe("ShareSettingsOptions", () => { ["testpath"], "/testpath" ], + [ + "should not change anything, if the string is a single '/'", + ["/"], + "/" + ], ]; testCases.forEach((testCase) => { diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index ca09c9816ad..54d0959f113 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -103,9 +103,8 @@ export default class ShareSettingsOptions extends OptionsWidget { // Ensure sharePath always starts with a single slash and does not end with (one or multiple) trailing slashes normalizeSharePathInput(sharePathInput: string) { - //TriliumNextTODO: -> this also disallows using single "/" as share path -> but do we need it? const REGEXP_STARTING_SLASH = /^\/+/g; - const REGEXP_TRAILING_SLASH = /\/+$/g; + const REGEXP_TRAILING_SLASH = /\b\/+$/g; const normalizedSharePath = (!sharePathInput.startsWith("/") ? `/${sharePathInput}` From 43166dbeb5c697c9bbbb36bccc10e2997a532ce1 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 25 Apr 2025 08:27:01 +0200 Subject: [PATCH 21/32] refactor: remove "cleanUrl" related code for now that should be part of a later PR --- .../options/other/share_settings.ts | 16 ------ src/routes/api/options.ts | 1 - src/routes/routes.ts | 2 - src/services/auth.ts | 42 --------------- src/services/options_init.ts | 1 - src/services/options_interface.ts | 1 - src/share/routes.ts | 51 ++++--------------- 7 files changed, 9 insertions(+), 105 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index 54d0959f113..20ca8dbd234 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -39,20 +39,11 @@ const TPL = /*html*/`
-
- -

${t("share.use_clean_urls_description")}

-
- `; export default class ShareSettingsOptions extends OptionsWidget { private $redirectBareDomain!: JQuery; private $showLoginInShareTheme!: JQuery; - private $useCleanUrls!: JQuery; private $sharePath!: JQuery; private $shareRootCheck!: JQuery; private $shareRootStatus!: JQuery; @@ -63,7 +54,6 @@ export default class ShareSettingsOptions extends OptionsWidget { this.$redirectBareDomain = this.$widget.find(".redirect-bare-domain"); this.$showLoginInShareTheme = this.$widget.find(".show-login-in-share-theme"); - this.$useCleanUrls = this.$widget.find(".use-clean-urls"); this.$sharePath = this.$widget.find(".share-path"); this.$shareRootCheck = this.$widget.find(".share-root-check"); this.$shareRootStatus = this.$widget.find(".share-root-status"); @@ -82,11 +72,6 @@ export default class ShareSettingsOptions extends OptionsWidget { await this.updateOption<"showLoginInShareTheme">("showLoginInShareTheme", showLoginInShareTheme.toString()); }); - this.$useCleanUrls.on('change', async () => { - const useCleanUrls = this.$useCleanUrls.is(":checked"); - await this.updateOption<"useCleanUrls">("useCleanUrls", useCleanUrls.toString()); - }); - this.$sharePath.on('change', async () => { const DEFAULT_SHAREPATH = "/share"; const sharePathInput = this.$sharePath.val()?.trim() || ""; @@ -122,7 +107,6 @@ export default class ShareSettingsOptions extends OptionsWidget { this.$shareRootCheck.toggle(redirectBareDomain); this.$showLoginInShareTheme.prop("checked", options.showLoginInShareTheme === "true"); - this.$useCleanUrls.prop("checked", options.useCleanUrls === "true"); this.$sharePath.val(options.sharePath); } diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index 2a8230597c4..7f9d5fa13d9 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -83,7 +83,6 @@ const ALLOWED_OPTIONS = new Set([ "redirectBareDomain", "showLoginInShareTheme", "shareSubtree", - "useCleanUrls", "sharePath", "splitEditorOrientation", diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 77c62ad8c5c..11c4fea57a1 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -105,8 +105,6 @@ const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: e }; function register(app: express.Application) { - // @pano9000: comment out for now to fix other functionality first - //app.use(auth.checkCleanUrl); route(GET, "/", [auth.checkAuth, csrfMiddleware], indexRoute.index); route(GET, "/login", [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage); diff --git a/src/services/auth.ts b/src/services/auth.ts index 53f71b8ea0c..e283a399497 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -74,47 +74,6 @@ function checkAuth(req: Request, res: Response, next: NextFunction) { } } -/** - * Checks if a URL path might be a shared note ID when clean URLs are enabled - */ -function checkCleanUrl(req: Request, res: Response, next: NextFunction) { - // Only process if not logged in and clean URLs are enabled - if (!req.session.loggedIn && !isElectron && !noAuthentication && - options.getOptionOrNull("redirectBareDomain") === "true" && - options.getOptionOrNull("useCleanUrls") === "true") { - - // Get the configured share path - const sharePath = options.getOption("sharePath") || '/share'; - - // Get path without leading slash - const path = req.path.substring(1); - - // Skip processing for known routes, empty paths, and paths that already start with sharePath - if (!path || - path === 'login' || - path === 'setup' || - path.startsWith('api/') || - req.path === sharePath || - req.path.startsWith(`${sharePath}/`)) { - log.info(`checkCleanUrl: Skipping redirect. Path: ${req.path}, SharePath: ${sharePath}`); - next(); - return; - } - - // If sharePath is just '/', we don't need to redirect - if (sharePath === '/') { - log.info(`checkCleanUrl: SharePath is root, skipping redirect. Path: ${req.path}`); - next(); - return; - } - - // Redirect to the share URL with this ID - log.info(`checkCleanUrl: Redirecting to share path. From: ${req.path}, To: ${sharePath}/${path}`); - res.redirect(`${sharePath}/${path}`); - } else { - next(); - } -} /** * Middleware for API authentication - works for both sync and normal API @@ -216,7 +175,6 @@ function checkCredentials(req: Request, res: Response, next: NextFunction) { export default { checkAuth, - checkCleanUrl, checkApiAuth, checkAppInitialized, checkPasswordSet, diff --git a/src/services/options_init.ts b/src/services/options_init.ts index ed882c51a9e..657dc0864af 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -186,7 +186,6 @@ const defaultOptions: DefaultOption[] = [ }, { name: "redirectBareDomain", value: "false", isSynced: true }, { name: "showLoginInShareTheme", value: "false", isSynced: true }, - { name: "useCleanUrls", value: "false", isSynced: true }, { name: "shareSubtree", value: "false", isSynced: true }, // AI Options diff --git a/src/services/options_interface.ts b/src/services/options_interface.ts index 0e7e781c482..211d45fb0ce 100644 --- a/src/services/options_interface.ts +++ b/src/services/options_interface.ts @@ -122,7 +122,6 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions { - shacaLoader.ensureLoad(); + router.get(`${sharePath}/`, (req, res, next) => { - if (!shaca.shareRootNote) { - res.status(404).json({ message: "Share root not found" }); - return; - } - - renderNote(shaca.shareRootNote, req, res); - }); - } else { - - router.get(`${sharePath}/`, (req, res, next) => { - - shacaLoader.ensureLoad(); - - if (!shaca.shareRootNote) { - res.status(404).json({ message: "Share root not found" }); - return; - } - - renderNote(shaca.shareRootNote, req, res); - }); - - } - - if (sharePath === '/' && options.getOptionBool("useCleanUrls") && options.getOptionBool("redirectBareDomain")) { - router.get("/:shareId", (req, res, next) => { - shacaLoader.ensureLoad(); - - const { shareId } = req.params; + shacaLoader.ensureLoad(); - // Skip processing for known routes - if (shareId === 'login' || shareId === 'setup' || shareId.startsWith('api/')) { - next(); - return; - } + if (!shaca.shareRootNote) { + res.status(404).json({ message: "Share root not found" }); + return; + } - const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; + renderNote(shaca.shareRootNote, req, res); + }); - renderNote(note, req, res); - }); - } router.get(`${sharePath}/:shareId`, (req, res, next) => { shacaLoader.ensureLoad(); From f4b5ed73ad4c3d3e92ca8375c8ded64facc3ae34 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 25 Apr 2025 08:31:27 +0200 Subject: [PATCH 22/32] refactor(options_init): remove sharePath normalization normalization is happening in the share settings options widget in the meantime, making it more obvious to the user --- src/services/options_init.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 657dc0864af..3a66e3fc236 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -175,15 +175,7 @@ const defaultOptions: DefaultOption[] = [ }, // Share settings - { - name: "sharePath", - // ensure always starts with slash - value: (optionsMap) => { - const sharePath = optionsMap.sharePath || "/share"; - return sharePath.startsWith("/") ? sharePath : "/" + sharePath; - }, - isSynced: true - }, + { name: "sharePath", value: "/share", isSynced: true }, { name: "redirectBareDomain", value: "false", isSynced: true }, { name: "showLoginInShareTheme", value: "false", isSynced: true }, { name: "shareSubtree", value: "false", isSynced: true }, From 0ae9a29e0dac9b0286e389d1a801725ba8869b95 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 25 Apr 2025 08:38:48 +0200 Subject: [PATCH 23/32] refactor: remove "cleanUrl" related code for now that should be part of a later PR --- src/public/translations/en/translation.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index b1430814346..9c40ff62b41 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1919,8 +1919,6 @@ "share_root_not_found": "No note with #shareRoot label found", "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared", "share_root_multiple_found": "Found multiple shared notes with a #shareRoot label: {{- foundNoteTitles}}. The note {{- activeNoteTitle}} will be used as shared root note.", - "use_clean_urls": "Use clean URLs for shared notes", - "use_clean_urls_description": "When enabled, shared note URLs will be simplified from /share/STi3RCMhUvG6 to /STi3RCMhUvG6", "share_path": "Share path", "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' and '/' --> '/noteId')", "share_path_placeholder": "/share or / for root", From 3b1d7d045ea11dbebf3ce732a667f907a298b082 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 25 Apr 2025 08:40:43 +0200 Subject: [PATCH 24/32] feat: improve example and wording for share_path_description needs to currently mention, that a server restart is required --- src/public/translations/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 9c40ff62b41..78ec379a908 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1920,7 +1920,7 @@ "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared", "share_root_multiple_found": "Found multiple shared notes with a #shareRoot label: {{- foundNoteTitles}}. The note {{- activeNoteTitle}} will be used as shared root note.", "share_path": "Share path", - "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' and '/' --> '/noteId')", + "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' or '/custom-path' --> '/custom-path/noteId'). In order for the change to take effect, you need to restart the server.", "share_path_placeholder": "/share or / for root", "share_subtree": "Share subtree", "share_subtree_description": "Share the entire subtree, not just the note" From 128d8907c33a5954fcaf5130919143f8fadea045 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 25 Apr 2025 08:48:28 +0200 Subject: [PATCH 25/32] chore(share_settings): add a TODO hint for currently active bug --- .../widgets/type_widgets/options/other/share_settings.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index 20ca8dbd234..0adbeda3efa 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -76,6 +76,13 @@ export default class ShareSettingsOptions extends OptionsWidget { const DEFAULT_SHAREPATH = "/share"; const sharePathInput = this.$sharePath.val()?.trim() || ""; + // TODO: inform user if they try to add more than a single path prefix (i.e. /sharePath/test) + // → this currently is not properly working, as for some reason the assets URL is not correctly rewritten + // and it only includes the first path in the URL, e.g. + // http://localhost:8080/sharePath/assets/v0.93.0/node_modules/normalize.css/normalize.css + // instead of + // http://localhost:8080/sharePath/test/assets/v0.93.0/node_modules/normalize.css/normalize.css + // alternatively/better approach: fix this behaviour :-) const normalizedSharePath = this.normalizeSharePathInput(sharePathInput); const optionValue = (!sharePathInput || !normalizedSharePath) ? DEFAULT_SHAREPATH : normalizedSharePath; From 30a191cedf1ad97de0d0b56c75e33b46c3039a62 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 25 Apr 2025 08:53:13 +0200 Subject: [PATCH 26/32] fix(share_settings): disallow "/" as share root for now as it is not working this will be handled by cleanUrl PR later on --- .../app/widgets/type_widgets/options/other/share_settings.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/public/app/widgets/type_widgets/options/other/share_settings.ts b/src/public/app/widgets/type_widgets/options/other/share_settings.ts index 0adbeda3efa..a5f74bc2b8c 100644 --- a/src/public/app/widgets/type_widgets/options/other/share_settings.ts +++ b/src/public/app/widgets/type_widgets/options/other/share_settings.ts @@ -84,7 +84,9 @@ export default class ShareSettingsOptions extends OptionsWidget { // http://localhost:8080/sharePath/test/assets/v0.93.0/node_modules/normalize.css/normalize.css // alternatively/better approach: fix this behaviour :-) const normalizedSharePath = this.normalizeSharePathInput(sharePathInput); - const optionValue = (!sharePathInput || !normalizedSharePath) ? DEFAULT_SHAREPATH : normalizedSharePath; + const optionValue = (!sharePathInput || !normalizedSharePath || normalizedSharePath === "/") + ? DEFAULT_SHAREPATH + : normalizedSharePath; await this.updateOption<"sharePath">("sharePath", optionValue); }); From 34e7901de97ff07e4f638d9b91091d24dd1ae685 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 25 Apr 2025 09:12:29 +0200 Subject: [PATCH 27/32] refactor: remove "cleanUrl" related string for now that should be part of a later PR --- src/public/translations/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 78ec379a908..a2a86ba9d7c 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1921,7 +1921,7 @@ "share_root_multiple_found": "Found multiple shared notes with a #shareRoot label: {{- foundNoteTitles}}. The note {{- activeNoteTitle}} will be used as shared root note.", "share_path": "Share path", "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' or '/custom-path' --> '/custom-path/noteId'). In order for the change to take effect, you need to restart the server.", - "share_path_placeholder": "/share or / for root", + "share_path_placeholder": "/share or /custom-path", "share_subtree": "Share subtree", "share_subtree_description": "Share the entire subtree, not just the note" }, From b25fd6ca3a11bbfc4191071aee9116e12d447aa1 Mon Sep 17 00:00:00 2001 From: Jin <22962980+JYC333@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:30:20 +0200 Subject: [PATCH 28/32] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20update=20custom=20?= =?UTF-8?q?share=20path=20without=20restart=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/translations/en/translation.json | 2 +- apps/client/src/widgets/shared_info.ts | 7 +- .../options/other/share_settings.spec.ts | 20 +- apps/server/src/share/routes.ts | 336 +++++++++--------- 4 files changed, 174 insertions(+), 191 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index a2a86ba9d7c..52c70032885 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1920,7 +1920,7 @@ "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared", "share_root_multiple_found": "Found multiple shared notes with a #shareRoot label: {{- foundNoteTitles}}. The note {{- activeNoteTitle}} will be used as shared root note.", "share_path": "Share path", - "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' or '/custom-path' --> '/custom-path/noteId'). In order for the change to take effect, you need to restart the server.", + "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' or '/custom-path' --> '/custom-path/noteId').", "share_path_placeholder": "/share or /custom-path", "share_subtree": "Share subtree", "share_subtree_description": "Share the entire subtree, not just the note" diff --git a/apps/client/src/widgets/shared_info.ts b/apps/client/src/widgets/shared_info.ts index b6170a3c738..4d0ef64fac6 100644 --- a/apps/client/src/widgets/shared_info.ts +++ b/apps/client/src/widgets/shared_info.ts @@ -37,22 +37,23 @@ export default class SharedInfoWidget extends NoteContextAwareWidget { async refreshWithNote(note: FNote) { const syncServerHost = options.get("syncServerHost"); + const sharePath = options.get("sharePath"); let link; const shareId = this.getShareId(note); if (syncServerHost) { - link = `${syncServerHost}/share/${shareId}`; + link = `${syncServerHost}${sharePath}/${shareId}`; this.$sharedText.text(t("shared_info.shared_publicly")); } else { let host = location.host; if (host.endsWith("/")) { // seems like IE has trailing slash // https://github.com/zadam/trilium/issues/3782 - host = host.substr(0, host.length - 1); + host = host.slice(0, -1); } - link = `${location.protocol}//${host}${location.pathname}share/${shareId}`; + link = `${location.protocol}//${host}${location.pathname}${sharePath.slice(1)}/${shareId}`; this.$sharedText.text(t("shared_info.shared_locally")); } diff --git a/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts b/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts index 8a4302a2872..d7459af281e 100644 --- a/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts +++ b/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts @@ -1,16 +1,16 @@ import { describe, it, expect } from "vitest"; -describe.skip("ShareSettingsOptions", () => {}) -/* +// describe.skip("ShareSettingsOptions", () => { }) - Test currently fails during import: - FAIL app widgets/type_widgets/options/other/share_settings.spec.ts [ src/public/app/widgets/type_widgets/options/other/share_settings.spec.ts ] -TypeError: Class extends value undefined is not a constructor or null - ❯ widgets/right_panel_widget.ts:20:32 - 20| class RightPanelWidget extends NoteContextAwareWidget { - 21| private $bodyWrapper!: JQuery; - 22| $body!: JQuery; +// Test currently fails during import: + +// FAIL app widgets / type_widgets / options / other / share_settings.spec.ts[src / public / app / widgets / type_widgets / options / other / share_settings.spec.ts] +// TypeError: Class extends value undefined is not a constructor or null +// ❯ widgets / right_panel_widget.ts: 20: 32 +// 20 | class RightPanelWidget extends NoteContextAwareWidget { +// 21| private $bodyWrapper!: JQuery; +// 22 | $body!: JQuery; import ShareSettingsOptions from "./share_settings.js"; @@ -72,4 +72,4 @@ describe("ShareSettingsOptions", () => { }) -})*/ \ No newline at end of file +}) \ No newline at end of file diff --git a/apps/server/src/share/routes.ts b/apps/server/src/share/routes.ts index a93c0a33ae7..920f2f8257f 100644 --- a/apps/server/src/share/routes.ts +++ b/apps/server/src/share/routes.ts @@ -1,6 +1,6 @@ import safeCompare from "safe-compare"; -import type { Request, Response, Router } from "express"; +import type { Request, Response, Router, NextFunction } from "express"; import shaca from "./shaca/shaca.js"; import shacaLoader from "./shaca/shaca_loader.js"; @@ -202,183 +202,165 @@ function register(router: Router) { } } - const sharePath = options.getOptionOrNull("sharePath") || "/share"; - - router.get(`${sharePath}/`, (req, res, next) => { - - shacaLoader.ensureLoad(); - - if (!shaca.shareRootNote) { - res.status(404).json({ message: "Share root not found" }); - return; - } - - renderNote(shaca.shareRootNote, req, res); - }); - - - router.get(`${sharePath}/:shareId`, (req, res, next) => { - shacaLoader.ensureLoad(); - - const { shareId } = req.params; - - const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; - - renderNote(note, req, res); - }); - - router.get(`${sharePath}/api/notes/:noteId`, (req, res, next) => { - shacaLoader.ensureLoad(); - let note: SNote | boolean; - - if (!(note = checkNoteAccess(req.params.noteId, req, res))) { - return; - } - - addNoIndexHeader(note, res); - - res.json(note.getPojo()); - }); - - router.get(`${sharePath}/api/notes/:noteId/download`, (req, res, next) => { - shacaLoader.ensureLoad(); - - let note: SNote | boolean; - - if (!(note = checkNoteAccess(req.params.noteId, req, res))) { - return; - } - - addNoIndexHeader(note, res); - - const filename = utils.formatDownloadTitle(note.title, note.type, note.mime); - - res.setHeader("Content-Disposition", utils.getContentDisposition(filename)); - - res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - res.setHeader("Content-Type", note.mime); - - res.send(note.getContent()); - }); - - // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename - router.get(`${sharePath}/api/images/:noteId/:filename`, (req, res, next) => { - shacaLoader.ensureLoad(); - - let image: SNote | boolean; - - if (!(image = checkNoteAccess(req.params.noteId, req, res))) { - return; - } - - if (image.type === "image") { - // normal image - res.set("Content-Type", image.mime); - addNoIndexHeader(image, res); - res.send(image.getContent()); - } else if (image.type === "canvas") { - renderImageAttachment(image, res, "canvas-export.svg"); - } else if (image.type === "mermaid") { - renderImageAttachment(image, res, "mermaid-export.svg"); - } else if (image.type === "mindMap") { - renderImageAttachment(image, res, "mindmap-export.svg"); - } else { - res.status(400).json({ message: "Requested note is not a shareable image" }); - } - }); - - // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename - router.get(`${sharePath}/api/attachments/:attachmentId/image/:filename`, (req, res, next) => { - shacaLoader.ensureLoad(); - - let attachment: SAttachment | boolean; - - if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { - return; - } - - if (attachment.role === "image") { - res.set("Content-Type", attachment.mime); - addNoIndexHeader(attachment.note, res); - res.send(attachment.getContent()); - } else { - res.status(400).json({ message: "Requested attachment is not a shareable image" }); - } - }); - - router.get(`${sharePath}/api/attachments/:attachmentId/download`, (req, res, next) => { - shacaLoader.ensureLoad(); - - let attachment: SAttachment | boolean; - - if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { - return; - } - - addNoIndexHeader(attachment.note, res); - - const filename = utils.formatDownloadTitle(attachment.title, null, attachment.mime); - - res.setHeader("Content-Disposition", utils.getContentDisposition(filename)); - - res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - res.setHeader("Content-Type", attachment.mime); - - res.send(attachment.getContent()); - }); - - // used for PDF viewing - router.get(`${sharePath}/api/notes/:noteId/view`, (req, res, next) => { - shacaLoader.ensureLoad(); - - let note: SNote | boolean; - - if (!(note = checkNoteAccess(req.params.noteId, req, res))) { - return; - } - - addNoIndexHeader(note, res); - - res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - res.setHeader("Content-Type", note.mime); - - res.send(note.getContent()); - }); - - // Used for searching, require noteId so we know the subTreeRoot - router.get(`${sharePath}/api/notes`, (req, res, next) => { - shacaLoader.ensureLoad(); - - const ancestorNoteId = req.query.ancestorNoteId ?? "_share"; - - if (typeof ancestorNoteId !== "string") { - res.status(400).json({ message: "'ancestorNoteId' parameter is mandatory." }); - return; - } - - // This will automatically return if no ancestorNoteId is provided and there is no shareIndex - if (!checkNoteAccess(ancestorNoteId, req, res)) { - return; - } - - const { search } = req.query; - - if (typeof search !== "string" || !search?.trim()) { - res.status(400).json({ message: "'search' parameter is mandatory." }); - return; + // Dynamic dispatch middleware + router.use((req: Request, res: Response, next: NextFunction) => { + const sharePath = options.getOptionOrNull("sharePath") || "/share"; + // Only handle requests starting with sharePath + if (req.path === sharePath || req.path.startsWith(sharePath + "/")) { + // Remove sharePath prefix to get the remaining path + const subPath = req.path.slice(sharePath.length); + // Handle root path + if (subPath === "" || subPath === "/") { + shacaLoader.ensureLoad(); + if (!shaca.shareRootNote) { + res.status(404).json({ message: "Share root not found" }); + return; + } + renderNote(shaca.shareRootNote, req, res); + return; + } + // Handle /:shareId + const shareIdMatch = subPath.match(/^\/([^/]+)$/); + if (shareIdMatch) { + shacaLoader.ensureLoad(); + const shareId = shareIdMatch[1]; + const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; + renderNote(note, req, res); + return; + } + // Handle /api/notes/:noteId + const apiNoteMatch = subPath.match(/^\/api\/notes\/([^/]+)$/); + if (apiNoteMatch) { + shacaLoader.ensureLoad(); + const noteId = apiNoteMatch[1]; + let note: SNote | boolean; + if (!(note = checkNoteAccess(noteId, req, res))) return; + addNoIndexHeader(note, res); + res.json(note.getPojo()); + return; + } + // Handle /api/notes/:noteId/download + const apiNoteDownloadMatch = subPath.match(/^\/api\/notes\/([^/]+)\/download$/); + if (apiNoteDownloadMatch) { + shacaLoader.ensureLoad(); + const noteId = apiNoteDownloadMatch[1]; + let note: SNote | boolean; + if (!(note = checkNoteAccess(noteId, req, res))) return; + addNoIndexHeader(note, res); + const filename = utils.formatDownloadTitle(note.title, note.type, note.mime); + res.setHeader("Content-Disposition", utils.getContentDisposition(filename)); + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + res.setHeader("Content-Type", note.mime); + res.send(note.getContent()); + return; + } + // Handle /api/images/:noteId/:filename + const apiImageMatch = subPath.match(/^\/api\/images\/([^/]+)\/([^/]+)$/); + if (apiImageMatch) { + shacaLoader.ensureLoad(); + const noteId = apiImageMatch[1]; + let image: SNote | boolean; + if (!(image = checkNoteAccess(noteId, req, res))) { + return; + } + if (image.type === "image") { + // normal image + res.set("Content-Type", image.mime); + addNoIndexHeader(image, res); + res.send(image.getContent()); + } else if (image.type === "canvas") { + renderImageAttachment(image, res, "canvas-export.svg"); + } else if (image.type === "mermaid") { + renderImageAttachment(image, res, "mermaid-export.svg"); + } else if (image.type === "mindMap") { + renderImageAttachment(image, res, "mindmap-export.svg"); + } else { + res.status(400).json({ message: "Requested note is not a shareable image" }); + } + return; + } + // Handle /api/attachments/:attachmentId/image/:filename + const apiAttachmentImageMatch = subPath.match(/^\/api\/attachments\/([^/]+)\/image\/([^/]+)$/); + if (apiAttachmentImageMatch) { + shacaLoader.ensureLoad(); + const attachmentId = apiAttachmentImageMatch[1]; + let attachment: SAttachment | boolean; + if (!(attachment = checkAttachmentAccess(attachmentId, req, res))) { + return; + } + if (attachment.role === "image") { + res.set("Content-Type", attachment.mime); + addNoIndexHeader(attachment.note, res); + res.send(attachment.getContent()); + } else { + res.status(400).json({ message: "Requested attachment is not a shareable image" }); + } + return; + } + // Handle /api/attachments/:attachmentId/download + const apiAttachmentDownloadMatch = subPath.match(/^\/api\/attachments\/([^/]+)\/download$/); + if (apiAttachmentDownloadMatch) { + shacaLoader.ensureLoad(); + const attachmentId = apiAttachmentDownloadMatch[1]; + let attachment: SAttachment | boolean; + if (!(attachment = checkAttachmentAccess(attachmentId, req, res))) { + return; + } + addNoIndexHeader(attachment.note, res); + const filename = utils.formatDownloadTitle(attachment.title, null, attachment.mime); + res.setHeader("Content-Disposition", utils.getContentDisposition(filename)); + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + res.setHeader("Content-Type", attachment.mime); + res.send(attachment.getContent()); + return; + } + // Handle /api/notes/:noteId/view + const apiNoteViewMatch = subPath.match(/^\/api\/notes\/([^/]+)\/view$/); + if (apiNoteViewMatch) { + shacaLoader.ensureLoad(); + const noteId = apiNoteViewMatch[1]; + let note: SNote | boolean; + if (!(note = checkNoteAccess(noteId, req, res))) { + return; + } + addNoIndexHeader(note, res); + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + res.setHeader("Content-Type", note.mime); + res.send(note.getContent()); + return; + } + // Handle /api/notes 搜索 + const apiNotesSearchMatch = subPath.match(/^\/api\/notes$/); + if (apiNotesSearchMatch) { + shacaLoader.ensureLoad(); + const ancestorNoteId = req.query.ancestorNoteId ?? "_share"; + if (typeof ancestorNoteId !== "string") { + res.status(400).json({ message: "'ancestorNoteId' parameter is mandatory." }); + return; + } + // This will automatically return if no ancestorNoteId is provided and there is no shareIndex + if (!checkNoteAccess(ancestorNoteId, req, res)) { + return; + } + const { search } = req.query; + if (typeof search !== "string" || !search?.trim()) { + res.status(400).json({ message: "'search' parameter is mandatory." }); + return; + } + const searchContext = new SearchContext({ ancestorNoteId: ancestorNoteId }); + const searchResults = searchService.findResultsWithQuery(search, searchContext); + const filteredResults = searchResults.map((sr) => { + const fullNote = shaca.notes[sr.noteId]; + const startIndex = sr.notePathArray.indexOf(ancestorNoteId); + const localPathArray = sr.notePathArray.slice(startIndex + 1).filter((id) => shaca.notes[id]); + const pathTitle = localPathArray.map((id) => shaca.notes[id].title).join(" / "); + return { id: fullNote.shareId, title: fullNote.title, score: sr.score, path: pathTitle }; + }); + res.json({ results: filteredResults }); + return; + } } - - const searchContext = new SearchContext({ ancestorNoteId: ancestorNoteId }); - const searchResults = searchService.findResultsWithQuery(search, searchContext); - const filteredResults = searchResults.map((sr) => { - const fullNote = shaca.notes[sr.noteId]; - const startIndex = sr.notePathArray.indexOf(ancestorNoteId); - const localPathArray = sr.notePathArray.slice(startIndex + 1).filter((id) => shaca.notes[id]); - const pathTitle = localPathArray.map((id) => shaca.notes[id].title).join(" / "); - return { id: fullNote.shareId, title: fullNote.title, score: sr.score, path: pathTitle }; - }); - - res.json({ results: filteredResults }); + next(); }); } From 519b9648c65120732a691ddc727402bcfa0d3856 Mon Sep 17 00:00:00 2001 From: Jin <22962980+JYC333@users.noreply.github.com> Date: Thu, 19 Jun 2025 01:07:46 +0200 Subject: [PATCH 29/32] =?UTF-8?q?test:=20=F0=9F=92=8D=20Fix=20test=20for?= =?UTF-8?q?=20path=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../options/other/share_path_utils.ts | 13 ++++++++ .../options/other/share_settings.spec.ts | 25 +++------------ .../options/other/share_settings.ts | 31 +++++-------------- 3 files changed, 24 insertions(+), 45 deletions(-) create mode 100644 apps/client/src/widgets/type_widgets/options/other/share_path_utils.ts diff --git a/apps/client/src/widgets/type_widgets/options/other/share_path_utils.ts b/apps/client/src/widgets/type_widgets/options/other/share_path_utils.ts new file mode 100644 index 00000000000..2ff042b1b28 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/options/other/share_path_utils.ts @@ -0,0 +1,13 @@ +// Ensure sharePath always starts with a single slash and does not end with (one or multiple) trailing slashes +export function normalizeSharePathInput(sharePathInput: string) { + const REGEXP_STARTING_SLASH = /^\/+/g; + const REGEXP_TRAILING_SLASH = /\b\/+$/g; + + const normalizedSharePath = (!sharePathInput.startsWith("/") + ? `/${sharePathInput}` + : sharePathInput) + .replaceAll(REGEXP_TRAILING_SLASH, "") + .replaceAll(REGEXP_STARTING_SLASH, "/"); + + return normalizedSharePath; +} diff --git a/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts b/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts index d7459af281e..aa9c4efc5ba 100644 --- a/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts +++ b/apps/client/src/widgets/type_widgets/options/other/share_settings.spec.ts @@ -1,19 +1,5 @@ import { describe, it, expect } from "vitest"; - -// describe.skip("ShareSettingsOptions", () => { }) - - -// Test currently fails during import: - -// FAIL app widgets / type_widgets / options / other / share_settings.spec.ts[src / public / app / widgets / type_widgets / options / other / share_settings.spec.ts] -// TypeError: Class extends value undefined is not a constructor or null -// ❯ widgets / right_panel_widget.ts: 20: 32 -// 20 | class RightPanelWidget extends NoteContextAwareWidget { -// 21| private $bodyWrapper!: JQuery; -// 22 | $body!: JQuery; - - -import ShareSettingsOptions from "./share_settings.js"; +import { normalizeSharePathInput } from "./share_path_utils.js"; type TestCase any> = [ desc: string, @@ -21,13 +7,11 @@ type TestCase any> = [ expected: ReturnType ]; - - describe("ShareSettingsOptions", () => { describe("#normalizeSharePathInput", () => { - const testCases: TestCase[] = [ + const testCases: TestCase[] = [ [ "should handle multiple trailing '/' and remove them completely", ["/trailingtest////"], @@ -62,9 +46,8 @@ describe("ShareSettingsOptions", () => { testCases.forEach((testCase) => { const [desc, fnParams, expected] = testCase; - return it(desc, () => { - const shareSettings = new ShareSettingsOptions(); - const actual = shareSettings.normalizeSharePathInput(...fnParams); + it(desc, () => { + const actual = normalizeSharePathInput(...fnParams); expect(actual).toStrictEqual(expected); }); }); diff --git a/apps/client/src/widgets/type_widgets/options/other/share_settings.ts b/apps/client/src/widgets/type_widgets/options/other/share_settings.ts index 473257d43c2..caa4dc2a7ab 100644 --- a/apps/client/src/widgets/type_widgets/options/other/share_settings.ts +++ b/apps/client/src/widgets/type_widgets/options/other/share_settings.ts @@ -1,8 +1,8 @@ import OptionsWidget from "../options_widget.js"; -import options from "../../../../services/options.js"; import { t } from "../../../../services/i18n.js"; -import type { OptionMap, OptionNames } from "@triliumnext/commons"; +import type { OptionMap } from "@triliumnext/commons"; import searchService from "../../../../services/search.js"; +import { normalizeSharePathInput } from "./share_path_utils.js"; const TPL = /*html*/`
@@ -83,7 +83,7 @@ export default class ShareSettingsOptions extends OptionsWidget { // instead of // http://localhost:8080/sharePath/test/assets/v0.93.0/node_modules/normalize.css/normalize.css // alternatively/better approach: fix this behaviour :-) - const normalizedSharePath = this.normalizeSharePathInput(sharePathInput); + const normalizedSharePath = normalizeSharePathInput(sharePathInput); const optionValue = (!sharePathInput || !normalizedSharePath || normalizedSharePath === "/") ? DEFAULT_SHAREPATH : normalizedSharePath; @@ -94,22 +94,6 @@ export default class ShareSettingsOptions extends OptionsWidget { this.$widget.find(".check-share-root").on("click", () => this.checkShareRoot()); } - // Ensure sharePath always starts with a single slash and does not end with (one or multiple) trailing slashes - normalizeSharePathInput(sharePathInput: string) { - - const REGEXP_STARTING_SLASH = /^\/+/g; - const REGEXP_TRAILING_SLASH = /\b\/+$/g; - - const normalizedSharePath = (!sharePathInput.startsWith("/") - ? `/${sharePathInput}` - : sharePathInput) - .replaceAll(REGEXP_TRAILING_SLASH, "") - .replaceAll(REGEXP_STARTING_SLASH, "/"); - - return normalizedSharePath; - - } - async optionsLoaded(options: OptionMap) { const redirectBareDomain = options.redirectBareDomain === "true"; this.$redirectBareDomain.prop("checked", redirectBareDomain); @@ -124,9 +108,9 @@ export default class ShareSettingsOptions extends OptionsWidget { const setCheckShareRootStyle = (removeClassName: string, addClassName: string, text: string) => { this.$shareRootStatus - .removeClass(removeClassName) - .addClass(addClassName) - .text(text); + .removeClass(removeClassName) + .addClass(addClassName) + .text(text); this.$shareRootCheck.prop("disabled", false); }; @@ -168,8 +152,7 @@ export default class ShareSettingsOptions extends OptionsWidget { return setCheckShareRootStyle("text-danger", "text-success", t("share.share_root_found", { noteTitle: sharedShareRootNotes[0].title }) ); - - } catch(err) { + } catch (err) { console.error(err); return setCheckShareRootStyle("text-success", "text-danger", t("share.check_share_root_error",) From 923eabd75087ac73c23d7028440ad3de487ef72e Mon Sep 17 00:00:00 2001 From: Jin <22962980+JYC333@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:08:22 +0200 Subject: [PATCH 30/32] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Allow=20multi-sege?= =?UTF-8?q?ment=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../options/other/share_settings.ts | 7 ---- apps/server/src/share/content_renderer.ts | 14 ++++---- apps/server/src/share/routes.spec.ts | 36 +++++++++++++++++++ apps/server/src/share/routes.ts | 18 ++++++---- packages/share-theme/src/templates/404.ejs | 2 +- packages/share-theme/src/templates/page.ejs | 4 +-- 6 files changed, 58 insertions(+), 23 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/options/other/share_settings.ts b/apps/client/src/widgets/type_widgets/options/other/share_settings.ts index caa4dc2a7ab..b1c65f1a8fe 100644 --- a/apps/client/src/widgets/type_widgets/options/other/share_settings.ts +++ b/apps/client/src/widgets/type_widgets/options/other/share_settings.ts @@ -76,13 +76,6 @@ export default class ShareSettingsOptions extends OptionsWidget { const DEFAULT_SHAREPATH = "/share"; const sharePathInput = this.$sharePath.val()?.trim() || ""; - // TODO: inform user if they try to add more than a single path prefix (i.e. /sharePath/test) - // → this currently is not properly working, as for some reason the assets URL is not correctly rewritten - // and it only includes the first path in the URL, e.g. - // http://localhost:8080/sharePath/assets/v0.93.0/node_modules/normalize.css/normalize.css - // instead of - // http://localhost:8080/sharePath/test/assets/v0.93.0/node_modules/normalize.css/normalize.css - // alternatively/better approach: fix this behaviour :-) const normalizedSharePath = normalizeSharePathInput(sharePathInput); const optionValue = (!sharePathInput || !normalizedSharePath || normalizedSharePath === "/") ? DEFAULT_SHAREPATH diff --git a/apps/server/src/share/content_renderer.ts b/apps/server/src/share/content_renderer.ts index c1c16e48d0c..8f33c124d7a 100644 --- a/apps/server/src/share/content_renderer.ts +++ b/apps/server/src/share/content_renderer.ts @@ -16,7 +16,7 @@ export interface Result { isEmpty?: boolean; } -function getContent(note: SNote) { +function getContent(note: SNote, relativePath = '../') { if (note.isProtected) { return { header: "", @@ -32,7 +32,7 @@ function getContent(note: SNote) { }; if (note.type === "text") { - renderText(result, note); + renderText(result, note, relativePath); } else if (note.type === "code") { renderCode(result); } else if (note.type === "mermaid") { @@ -65,7 +65,7 @@ function renderIndex(result: Result) { result.content += ""; } -function renderText(result: Result, note: SNote) { +function renderText(result: Result, note: SNote, relativePath: string) { const document = new JSDOM(result.content || "").window.document; result.isEmpty = document.body.textContent?.trim().length === 0 && document.querySelectorAll("img").length === 0; @@ -88,10 +88,10 @@ function renderText(result: Result, note: SNote) { if (result.content.includes(``)) { result.header += ` - - - - + + + + <% if (!isDev && !note.isLabelTruthy("shareOmitDefaultCss")) { %> @@ -56,7 +56,7 @@ <% const customLogoId = subRoot.note.getRelation("shareLogo")?.value; -const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `../${assetUrlFragment}/images/icon-color.svg`; +const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `${relativePath}${assetUrlFragment}/images/icon-color.svg`; const logoWidth = subRoot.note.getLabelValue("shareLogoWidth") ?? 53; const logoHeight = subRoot.note.getLabelValue("shareLogoHeight") ?? 40; const mobileLogoHeight = logoHeight && logoWidth ? 32 / (logoWidth / logoHeight) : ""; From 6ab87507265cfbe6f1e7551cf7f893424c6dff0a Mon Sep 17 00:00:00 2001 From: Jin <22962980+JYC333@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:15:23 +0200 Subject: [PATCH 31/32] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20update=20translati?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client/src/translations/cn/translation.json | 10 +++++++++- apps/client/src/translations/en/translation.json | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json index ccc04e21b77..338d905affd 100644 --- a/apps/client/src/translations/cn/translation.json +++ b/apps/client/src/translations/cn/translation.json @@ -1709,9 +1709,17 @@ "show_login_link": "在共享主题中显示登录链接", "show_login_link_description": "在共享页面底部添加登录链接", "check_share_root": "检查共享根状态", + "check_share_root_error": "检查共享根状态时发生意外错误,请检查日志以获取更多信息。", + "share_note_title": "'{{noteTitle}}'", "share_root_found": "共享根笔记 '{{noteTitle}}' 已准备好", "share_root_not_found": "未找到带有 #shareRoot 标签的笔记", - "share_root_not_shared": "笔记 '{{noteTitle}}' 具有 #shareRoot 标签,但未共享" + "share_root_not_shared": "笔记 '{{noteTitle}}' 具有 #shareRoot 标签,但未共享", + "share_root_multiple_found": "找到多个具有 #shareRoot 标签的共享笔记:{{- foundNoteTitles}}。将使用笔记 {{- activeNoteTitle}} 作为共享根笔记。", + "share_path": "共享路径", + "share_path_description": "共享笔记的 URL 前缀(例如 '/share' --> '/share/noteId' 或 '/custom-path' --> '/custom-path/noteId')。支持多级嵌套(例如 '/custom-path/sub-path' --> '/custom-path/sub-path/noteId')。刷新页面以应用更改。", + "share_path_placeholder": "/share 或 /custom-path", + "share_subtree": "共享子树", + "share_subtree_description": "共享整个子树,而不是仅共享笔记" }, "time_selector": { "invalid_input": "输入的时间值不是有效数字。", diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index eb9a207efde..18e3f5dd65c 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1882,7 +1882,7 @@ "share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared", "share_root_multiple_found": "Found multiple shared notes with a #shareRoot label: {{- foundNoteTitles}}. The note {{- activeNoteTitle}} will be used as shared root note.", "share_path": "Share path", - "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' or '/custom-path' --> '/custom-path/noteId').", + "share_path_description": "The url prefix for shared notes (e.g. '/share' --> '/share/noteId' or '/custom-path' --> '/custom-path/noteId'). Multiple levels of nesting are supported (e.g. '/custom-path/sub-path' --> '/custom-path/sub-path/noteId'). Refresh the page to apply the changes.", "share_path_placeholder": "/share or /custom-path", "share_subtree": "Share subtree", "share_subtree_description": "Share the entire subtree, not just the note" From ea5564c6e6e34592aa7a4a233019c9c4202d9cde Mon Sep 17 00:00:00 2001 From: Jin <22962980+JYC333@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:48:05 +0200 Subject: [PATCH 32/32] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20update=20doc?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../User Guide/Advanced Usage/Sharing.html | 34 ++++++++++++++----- .../Import & Export/Markdown.html | 2 +- .../Markdown/Supported syntax.html | 2 +- .../Text/Block quotes & admonitions.html | 2 +- docs/Release Notes/Release Notes/v0.94.1.md | 2 +- docs/User Guide/!!!meta.json | 30 ++++++++-------- .../User Guide/Advanced Usage/Sharing.md | 12 ++++++- 7 files changed, 55 insertions(+), 29 deletions(-) diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html index 3b34f214763..807c8f369d1 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html @@ -6,8 +6,7 @@ - -

Features, interaction and limitations

+

Features, interaction and limitations

  • Searching by note title.
  • Automatic dark/light mode based on the user's browser settings.
  • @@ -70,21 +69,25 @@

    By note type

    Saved Search Not supported. +   Relation Map Not supported. +   Note Map Not supported. +   Render Note Not supported. +   Book @@ -132,6 +135,7 @@

    By note type

    Web View Not supported. +   Mind Map @@ -147,6 +151,7 @@

    By note type

    Geo Map Not supported. +   File @@ -184,11 +189,9 @@

    How to Share a Note

    Share Note

    -
  • -

    Access the Shared Note: The link provided will open the - note in your browser. If your server is not configured with a public IP, - the URL will refer to localhost (127.0.0.1).

    -
  • +
  • Access the Shared Note: The link provided will open the + note in your browser. If your server is not configured with a public IP, + the URL will refer to localhost (127.0.0.1).
  • Sharing a Note Subtree

    When you share a note, you actually share the entire subtree of notes @@ -262,6 +265,20 @@

    Sharing a Note as the Root

    making it easier to use Trilium as a fully-fledged website. Consider combining this with the #shareIndex label, which will display a list of all shared notes.

    +

    Redirect Bare Domain to Share Page

    +

    This option can be enabled under Option → Other → Share Settings. + When activated, anonymous users accessing the bare domain will be redirected + to the Share page, preventing them from seeing the login option and thereby + improving security. +
    To ensure accessibility for legitimate users, you can also enable a login + link on the Share page, allowing yourself to access the login screen if + you're redirected there.

    +

    Setting a Custom Share Path

    +

    This option can be enabled under Option → Other → Share Settings. + It allows you to customize the share URL prefix before the noteId. + Nested paths are supported. +
    If you're using a proxy service, make sure to update its configuration + accordingly to reflect the new path structure.

    Attribute reference

    @@ -340,8 +357,7 @@

    Attribute reference

    - -

    Credits

    +

    Credits

    Since v0.95.0, a new theme was introduced (and enabled by default) which greatly improves the visual aspect of the Share feature, as well as its functionality (such as mobile support, dark/light mode, collapsible tree, diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown.html index 7f46f8adfe3..d71887009e3 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown.html @@ -42,5 +42,5 @@

    Exporting protected notes

    This will export the notes in an unencrypted form, so if you reimport into Trilium, make sure to re-protect these notes.

    Supported syntax

    -

    See the dedicated page: Supported syntax +

    See the dedicated page: Supported syntax

    \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown/Supported syntax.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown/Supported syntax.html index d07d442649f..23cce355aae 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown/Supported syntax.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown/Supported syntax.html @@ -41,7 +41,7 @@

    Wikilinks

    Trilium-compatible syntax, but it will not export Trilium Notes into Markdown files with this syntax.