From 8487c8cd039eb87b8723a970b7741f9568775971 Mon Sep 17 00:00:00 2001 From: matt wilkie Date: Tue, 25 Feb 2025 14:51:29 -0700 Subject: [PATCH 01/29] 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 e43adc5e18..ac5baa6afa 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 4c0220d9ce..36eb03f19f 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 50ff6b6b69..48097cf8f2 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 7d5fea44b4..51ac12bf87 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 03f40e6e7b..05c699f557 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 77d4089ae4..8895ddd779 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 d8d8c3fcbb..1ed5c361dc 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/29] 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 ac5baa6afa..74da46cf08 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 36eb03f19f..a776954ff6 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 48097cf8f2..b2ccfa104a 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 68b98e8939..18ecb9fe93 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 05c699f557..c18a23ff58 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 8895ddd779..b03f76aca9 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 1ed5c361dc..01faea5ec9 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/29] 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 74da46cf08..0a59a317c2 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 c18a23ff58..a2ab40b879 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 71c6b79705..1181692312 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/29] =?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 50ace1d9ba..b6328a00b3 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/29] 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 a0a20f9aec..4e1d192015 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/29] 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 26b0157988..53f71b8ea0 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/29] 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 c13dee9115..3a93e6b7c3 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/29] 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 2bfcad4c74..77c62ad8c5 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 4e1d192015..506bdd435e 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/29] 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 3a93e6b7c3..370ef2c405 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/29] 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 370ef2c405..fddfccc03d 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/29] 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 43d52d3ecb..7e6f075ee7 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 6a841d9f11..1b2d42b250 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 506bdd435e..79da9ed516 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/29] 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 79da9ed516..4362f4f6e4 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/29] 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 fddfccc03d..d0300ed2b3 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/29] 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 d0300ed2b3..d07c40e864 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/29] 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 d07c40e864..5919264d7f 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/29] 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 5919264d7f..4c7081b7c6 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 7e6f075ee7..b143081434 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/29] 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 4c7081b7c6..2d55d8e937 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/29] 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 0000000000..9be36fd823 --- /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/29] 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 2d55d8e937..ca09c9816a 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/29] 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 9be36fd823..8a4302a287 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 ca09c9816a..54d0959f11 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/29] 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 54d0959f11..20ca8dbd23 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 2a8230597c..7f9d5fa13d 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 77c62ad8c5..11c4fea57a 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 53f71b8ea0..e283a39949 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 ed882c51a9..657dc0864a 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 0e7e781c48..211d45fb0c 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/29] 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 657dc0864a..3a66e3fc23 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/29] 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 b143081434..9c40ff62b4 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/29] 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 9c40ff62b4..78ec379a90 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/29] 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 20ca8dbd23..0adbeda3ef 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/29] 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 0adbeda3ef..a5f74bc2b8 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/29] 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 78ec379a90..a2a86ba9d7 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 771e5272195c195fd4969ebbb50c73ded591ec22 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/29] =?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 aa04a8a392..641b9f36b4 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1881,7 +1881,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 b6170a3c73..4d0ef64fac 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 8a4302a287..d7459af281 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 3f59f9551c..475e4aad0e 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"; @@ -218,183 +218,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 cf661c1ffd0e8aef916e8faa82115342a45907e8 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/29] =?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 0000000000..2ff042b1b2 --- /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 d7459af281..aa9c4efc5b 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 473257d43c..caa4dc2a7a 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",)