diff --git a/bin/tpl/anonymize-database.sql b/bin/tpl/anonymize-database.sql
index ff935d6e1d..86615370f8 100644
--- a/bin/tpl/anonymize-database.sql
+++ b/bin/tpl/anonymize-database.sql
@@ -13,13 +13,13 @@ UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'
'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox',
'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId',
'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top',
- 'fullContentWidth', 'shareHiddenFromTree', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
+ 'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate',
'template', 'toc', 'color', 'keepCurrentHoisting', 'executeButton', 'executeDescription', 'newNotesOnTop',
'clipperInbox', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation',
'runOnNoteTitleChange', 'runOnNoteChange', 'runOnNoteContentChange', 'runOnNoteDeletion', 'runOnBranchCreation',
'runOnBranchDeletion', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template',
- 'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon');
+ 'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareTemplate', 'shareFavicon');
UPDATE attributes SET name = 'name' WHERE type = 'relation'
AND name NOT IN
('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss',
@@ -30,13 +30,13 @@ UPDATE attributes SET name = 'name' WHERE type = 'relation'
'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox',
'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId',
'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top',
- 'fullContentWidth', 'shareHiddenFromTree', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
+ 'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate',
'template', 'toc', 'color', 'keepCurrentHoisting', 'executeButton', 'executeDescription', 'newNotesOnTop',
'clipperInbox', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation',
'runOnNoteTitleChange', 'runOnNoteChange', 'runOnNoteContentChange', 'runOnNoteDeletion', 'runOnBranchCreation',
'runOnBranchDeletion', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template',
- 'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon');
+ 'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareTemplate', 'shareFavicon');
UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL AND prefix != 'recovered';
UPDATE options SET value = 'anonymized' WHERE name IN
('documentId', 'documentSecret', 'encryptedDataKey',
diff --git a/src/public/app/widgets/attribute_widgets/attribute_detail.js b/src/public/app/widgets/attribute_widgets/attribute_detail.js
index 715d3f093f..db209e168a 100644
--- a/src/public/app/widgets/attribute_widgets/attribute_detail.js
+++ b/src/public/app/widgets/attribute_widgets/attribute_detail.js
@@ -225,6 +225,7 @@ const ATTR_HELP = {
"sqlConsoleHome": "default location of SQL console notes",
"bookmarkFolder": "note with this label will appear in bookmarks as folder (allowing access to its children)",
"shareHiddenFromTree": "this note is hidden from left navigation tree, but still accessible with its URL",
+ "shareExternalLink": "note will act as a link to an external website in the share tree",
"shareAlias": "define an alias using which the note will be available under https://your_trilium_host/share/[your_alias]",
"shareOmitDefaultCss": "default share page CSS will be omitted. Use when you make extensive styling changes.",
"shareRoot": "marks note which is served on /share root.",
@@ -271,6 +272,7 @@ const ATTR_HELP = {
"widget": "target of this relation will be executed and rendered as a widget in the sidebar",
"shareCss": "CSS note which will be injected into the share page. CSS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree' and 'shareOmitDefaultCss' as well.",
"shareJs": "JavaScript note which will be injected into the share page. JS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
+ "shareTemplate": "Embedded JavaScript note that will be used as the template for displaying the shared note. Falls back to the default template. Consider using 'shareHiddenFromTree'.",
"shareFavicon": "Favicon note to be set in the shared page. Typically you want to set it to share root and make it inheritable. Favicon note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
}
};
diff --git a/src/services/builtin_attributes.js b/src/services/builtin_attributes.js
index 5083f3dc76..0c2fa5f0ce 100644
--- a/src/services/builtin_attributes.js
+++ b/src/services/builtin_attributes.js
@@ -48,6 +48,7 @@ module.exports = [
{ type: 'label', name: 'bottom' },
{ type: 'label', name: 'fullContentWidth' },
{ type: 'label', name: 'shareHiddenFromTree' },
+ { type: 'label', name: 'shareExternalLink' },
{ type: 'label', name: 'shareAlias' },
{ type: 'label', name: 'shareOmitDefaultCss' },
{ type: 'label', name: 'shareRoot' },
@@ -89,5 +90,6 @@ module.exports = [
{ type: 'relation', name: 'renderNote', isDangerous: true },
{ type: 'relation', name: 'shareCss' },
{ type: 'relation', name: 'shareJs' },
+ { type: 'relation', name: 'shareTemplate' },
{ type: 'relation', name: 'shareFavicon' },
];
diff --git a/src/share/content_renderer.js b/src/share/content_renderer.js
index cc456bc2ef..1870f039b4 100644
--- a/src/share/content_renderer.js
+++ b/src/share/content_renderer.js
@@ -44,7 +44,10 @@ function renderIndex(result) {
const rootNote = shaca.getNote(shareRoot.SHARE_ROOT_NOTE_ID);
for (const childNote of rootNote.getChildNotes()) {
- result.content += `
${childNote.escapedTitle}`;
+ const isExternalLink = childNote.hasLabel("shareExternalLink");
+ const href = isExternalLink ? childNote.getLabelValue("shareExternalLink") : `./${childNote.shareId}`;
+ const target = isExternalLink ? `target="_blank" rel="noopener noreferrer"` : "";
+ result.content += `${childNote.escapedTitle}`;
}
result.content += '';
@@ -84,7 +87,13 @@ function renderText(result, note) {
const noteId = notePathSegments[notePathSegments.length - 1];
const linkedNote = shaca.getNote(noteId);
if (linkedNote) {
- linkEl.setAttribute("href", linkedNote.shareId);
+ const isExternalLink = linkedNote.hasLabel("shareExternalLink");
+ const href = isExternalLink ? linkedNote.getLabelValue("shareExternalLink") : `./${linkedNote.shareId}`;
+ linkEl.setAttribute("href", href);
+ if (isExternalLink) {
+ linkEl.setAttribute("target", "_blank");
+ linkEl.setAttribute("rel", "noopener noreferrer");
+ }
linkEl.classList.add(`type-${linkedNote.type}`);
} else {
linkEl.removeAttribute("href");
diff --git a/src/share/routes.js b/src/share/routes.js
index 462ad1aeb1..a22691ce27 100644
--- a/src/share/routes.js
+++ b/src/share/routes.js
@@ -1,6 +1,7 @@
const express = require('express');
const path = require('path');
const safeCompare = require('safe-compare');
+const ejs = require("ejs");
const shaca = require("./shaca/shaca");
const shacaLoader = require("./shaca/shaca_loader");
@@ -8,6 +9,9 @@ const shareRoot = require("./share_root");
const contentRenderer = require("./content_renderer");
const assetPath = require("../services/asset_path");
const appPath = require("../services/app_path");
+const searchService = require("../services/search/services/search");
+const SearchContext = require("../services/search/search_context");
+const log = require("../services/log");
/**
* @param {SNote} note
@@ -128,18 +132,42 @@ function register(router) {
}
const {header, content, isEmpty} = contentRenderer.getContent(note);
-
const subRoot = getSharedSubTreeRoot(note);
+ const opts = {note, header, content, isEmpty, subRoot, assetPath, appPath};
+ let useDefaultView = true;
+
+ // Check if the user has their own template
+ if (note.hasRelation('shareTemplate')) {
+ // Get the template note and content
+ const templateId = note.getRelation('shareTemplate').value;
+ const templateNote = shaca.getNote(templateId);
+
+ // Make sure the note type is correct
+ if (templateNote.type === 'code' && templateNote.mime === 'application/x-ejs') {
+
+ // EJS caches the result of this so we don't need to pre-cache
+ const includer = (path) => {
+ const childNote = templateNote.children.find(n => path === n.title);
+ if (!childNote) return null;
+ if (childNote.type !== 'code' || childNote.mime !== 'application/x-ejs') return null;
+ return { template: childNote.getContent() };
+ };
+
+ // Try to render user's template, w/ fallback to default view
+ try {
+ const ejsResult = ejs.render(templateNote.getContent(), opts, {includer});
+ res.send(ejsResult);
+ useDefaultView = false; // Rendering went okay, don't use default view
+ }
+ catch (e) {
+ log.error(`Rendering user provided share template (${templateId}) threw exception ${e.message} with stacktrace: ${e.stack}`);
+ }
+ }
+ }
- res.render("share/page", {
- note,
- header,
- content,
- isEmpty,
- subRoot,
- assetPath,
- appPath
- });
+ if (useDefaultView) {
+ res.render('share/page', opts);
+ }
}
router.get('/share/', (req, res, next) => {
@@ -303,6 +331,37 @@ function register(router) {
res.send(note.getContent());
});
+
+ // Used for searching, require noteId so we know the subTreeRoot
+ router.get('/share/api/notes', (req, res, next) => {
+ shacaLoader.ensureLoad();
+
+ const ancestorNoteId = req.query.ancestorNoteId ?? "_share";
+ let note;
+
+ // This will automatically return if no ancestorNoteId is provided and there is no shareIndex
+ if (!(note = checkNoteAccess(ancestorNoteId, req, res))) {
+ return;
+ }
+
+ const {search} = req.query;
+
+ if (!search?.trim()) {
+ return res.status(400).json({ message: "'search' parameter is mandatory." });
+ }
+
+ 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 });
+ });
}
module.exports = {
diff --git a/src/views/share/page.ejs b/src/views/share/page.ejs
index 52e891dbcf..8c6c3033d5 100644
--- a/src/views/share/page.ejs
+++ b/src/views/share/page.ejs
@@ -32,7 +32,7 @@
<%- header %>
<%= note.title %>
-
+
<% if (note.parents[0].noteId !== '_share' && note.parents.length !== 0) { %>
@@ -62,9 +62,14 @@
<% } %>
- <% for (const childNote of note.getVisibleChildNotes()) { %>
+ <%
+ for (const childNote of note.getVisibleChildNotes()) {
+ const isExternalLink = childNote.hasLabel('shareExternalLink');
+ const linkHref = isExternalLink ? childNote.getLabelValue('shareExternalLink') : `./${childNote.shareId}`;
+ const target = isExternalLink ? `target="_blank" rel="noopener noreferrer"` : '';
+ %>
-
-
class="type-<%= childNote.type %>"><%= childNote.title %>
<% } %>
diff --git a/src/views/share/tree_item.ejs b/src/views/share/tree_item.ejs
index 12eec3fbb9..9953170363 100644
--- a/src/views/share/tree_item.ejs
+++ b/src/views/share/tree_item.ejs
@@ -1,10 +1,15 @@
+<%
+const isExternalLink = note.hasLabel('shareExternalLink');
+const linkHref = isExternalLink ? note.getLabelValue('shareExternalLink') : `./${note.shareId}`;
+const target = isExternalLink ? ` target="_blank" rel="noopener noreferrer"` : '';
+%>
<% const titleWithPrefix = (branch.prefix ? `${branch.prefix} - ` : '') + note.title; %>
<% if (activeNote.noteId === note.noteId) { %>
<%= titleWithPrefix %>
<% } else { %>
- <%= titleWithPrefix %>
+ ><%= titleWithPrefix %>
<% } %>