2022-05-09 16:38:23 +02:00
|
|
|
const express = require('express');
|
|
|
|
const path = require('path');
|
2022-07-31 21:45:32 +02:00
|
|
|
const safeCompare = require('safe-compare');
|
2022-05-09 16:38:23 +02:00
|
|
|
|
2021-10-17 14:44:59 +02:00
|
|
|
const shaca = require("./shaca/shaca");
|
|
|
|
const shacaLoader = require("./shaca/shaca_loader");
|
2021-10-19 22:48:38 +02:00
|
|
|
const shareRoot = require("./share_root");
|
2022-01-10 17:09:20 +01:00
|
|
|
const contentRenderer = require("./content_renderer");
|
2022-10-26 23:50:54 +02:00
|
|
|
const assetPath = require("../services/asset_path");
|
2022-12-25 11:58:24 +01:00
|
|
|
const appPath = require("../services/app_path");
|
2021-10-19 22:48:38 +02:00
|
|
|
|
2021-12-23 20:54:48 +01:00
|
|
|
function getSharedSubTreeRoot(note) {
|
2021-10-19 22:48:38 +02:00
|
|
|
if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
2021-12-23 20:54:48 +01:00
|
|
|
// share root itself is not shared
|
2021-10-19 22:48:38 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2021-12-23 20:54:48 +01:00
|
|
|
// every path leads to share root, but which one to choose?
|
2023-06-30 11:18:34 +02:00
|
|
|
// for the sake of simplicity, URLs are not note paths
|
2021-10-19 22:48:38 +02:00
|
|
|
const parentNote = note.getParentNotes()[0];
|
|
|
|
|
|
|
|
if (parentNote.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
|
|
|
return note;
|
|
|
|
}
|
|
|
|
|
2021-12-23 20:54:48 +01:00
|
|
|
return getSharedSubTreeRoot(parentNote);
|
2021-10-19 22:48:38 +02:00
|
|
|
}
|
2021-10-17 14:44:59 +02:00
|
|
|
|
2022-03-22 23:17:47 +01:00
|
|
|
function addNoIndexHeader(note, res) {
|
2023-06-29 00:14:12 +02:00
|
|
|
if (note.isLabelTruthy('shareDisallowRobotIndexing')) {
|
2022-03-22 23:17:47 +01:00
|
|
|
res.setHeader('X-Robots-Tag', 'noindex');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-01 19:56:09 +02:00
|
|
|
function requestCredentials(res) {
|
2022-07-31 21:45:32 +02:00
|
|
|
res.setHeader('WWW-Authenticate', 'Basic realm="User Visible Realm", charset="UTF-8"')
|
|
|
|
.sendStatus(401);
|
|
|
|
}
|
|
|
|
|
2023-06-05 23:05:05 +02:00
|
|
|
/** @returns {SAttachment|boolean} */
|
|
|
|
function checkAttachmentAccess(attachmentId, req, res) {
|
|
|
|
const attachment = shaca.getAttachment(attachmentId);
|
|
|
|
|
|
|
|
if (!attachment) {
|
|
|
|
res.status(404)
|
|
|
|
.json({ message: `Attachment '${attachmentId}' not found.` });
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const note = checkNoteAccess(attachment.parentId, req, res);
|
|
|
|
|
2023-06-30 11:18:34 +02:00
|
|
|
// truthy note means the user has access, and we can return the attachment
|
2023-06-05 23:05:05 +02:00
|
|
|
return note ? attachment : false;
|
|
|
|
}
|
|
|
|
|
2023-04-11 22:55:50 +02:00
|
|
|
/** @returns {SNote|boolean} */
|
2022-07-31 21:45:32 +02:00
|
|
|
function checkNoteAccess(noteId, req, res) {
|
|
|
|
const note = shaca.getNote(noteId);
|
|
|
|
|
|
|
|
if (!note) {
|
2022-12-19 21:39:12 +01:00
|
|
|
res.status(404)
|
2023-06-05 23:05:05 +02:00
|
|
|
.json({ message: `Note '${noteId}' not found.` });
|
2022-12-19 21:39:12 +01:00
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-12-21 16:11:00 +01:00
|
|
|
if (noteId === '_share' && !shaca.shareIndexEnabled) {
|
2022-12-19 21:39:12 +01:00
|
|
|
res.status(403)
|
|
|
|
.json({ message: `Accessing share index is forbidden.` });
|
2022-07-31 21:45:32 +02:00
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const credentials = note.getCredentials();
|
|
|
|
|
|
|
|
if (credentials.length === 0) {
|
|
|
|
return note;
|
|
|
|
}
|
|
|
|
|
|
|
|
const header = req.header("Authorization");
|
|
|
|
|
|
|
|
if (!header?.startsWith("Basic ")) {
|
2022-08-01 19:56:09 +02:00
|
|
|
requestCredentials(res);
|
2022-07-31 21:45:32 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const base64Str = header.substring("Basic ".length);
|
|
|
|
const buffer = Buffer.from(base64Str, 'base64');
|
|
|
|
const authString = buffer.toString('utf-8');
|
|
|
|
|
|
|
|
for (const credentialLabel of credentials) {
|
|
|
|
if (safeCompare(authString, credentialLabel.value)) {
|
|
|
|
return note; // success;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-10-17 14:44:59 +02:00
|
|
|
function register(router) {
|
2022-07-31 21:45:32 +02:00
|
|
|
function renderNote(note, req, res) {
|
2022-03-22 23:17:47 +01:00
|
|
|
if (!note) {
|
2021-12-22 09:10:38 +01:00
|
|
|
res.status(404).render("share/404");
|
2022-03-22 23:17:47 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
if (!checkNoteAccess(note.noteId, req, res)) {
|
2022-08-01 19:56:09 +02:00
|
|
|
requestCredentials(res);
|
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-22 23:17:47 +01:00
|
|
|
addNoIndexHeader(note, res);
|
|
|
|
|
2023-06-29 00:14:12 +02:00
|
|
|
if (note.isLabelTruthy('shareRaw')) {
|
2022-07-01 00:01:29 +02:00
|
|
|
res.setHeader('Content-Type', note.mime)
|
|
|
|
.send(note.getContent());
|
2022-03-22 23:17:47 +01:00
|
|
|
|
|
|
|
return;
|
2021-10-17 14:44:59 +02:00
|
|
|
}
|
2022-03-22 23:17:47 +01:00
|
|
|
|
|
|
|
const {header, content, isEmpty} = contentRenderer.getContent(note);
|
|
|
|
|
|
|
|
const subRoot = getSharedSubTreeRoot(note);
|
|
|
|
|
|
|
|
res.render("share/page", {
|
|
|
|
note,
|
|
|
|
header,
|
|
|
|
content,
|
|
|
|
isEmpty,
|
2022-10-26 23:50:54 +02:00
|
|
|
subRoot,
|
2022-12-25 11:58:24 +01:00
|
|
|
assetPath,
|
|
|
|
appPath
|
2022-03-22 23:17:47 +01:00
|
|
|
});
|
2022-01-17 23:13:56 +01:00
|
|
|
}
|
|
|
|
|
2022-05-11 09:06:30 +02:00
|
|
|
router.use('/share/canvas_share.js', express.static(path.join(__dirname, 'canvas_share.js')));
|
2022-05-09 16:38:23 +02:00
|
|
|
|
2022-10-30 09:05:12 +01:00
|
|
|
router.get('/share/', (req, res, next) => {
|
|
|
|
if (req.path.substr(-1) !== '/') {
|
|
|
|
res.redirect('../share/');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-01-17 23:13:56 +01:00
|
|
|
shacaLoader.ensureLoad();
|
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
renderNote(shaca.shareRootNote, req, res);
|
2022-01-17 23:13:56 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
router.get('/share/:shareId', (req, res, next) => {
|
|
|
|
shacaLoader.ensureLoad();
|
|
|
|
|
2022-05-01 23:16:47 +02:00
|
|
|
const {shareId} = req.params;
|
|
|
|
|
2022-01-17 23:13:56 +01:00
|
|
|
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
|
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
renderNote(note, req, res);
|
2021-10-17 14:44:59 +02:00
|
|
|
});
|
2021-10-19 22:48:38 +02:00
|
|
|
|
2022-01-01 13:23:09 +01:00
|
|
|
router.get('/share/api/notes/:noteId', (req, res, next) => {
|
2022-05-01 23:16:47 +02:00
|
|
|
shacaLoader.ensureLoad();
|
2022-07-31 21:45:32 +02:00
|
|
|
let note;
|
2022-05-01 23:16:47 +02:00
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
|
|
|
|
return;
|
2021-10-19 22:48:38 +02:00
|
|
|
}
|
|
|
|
|
2022-03-22 23:17:47 +01:00
|
|
|
addNoIndexHeader(note, res);
|
|
|
|
|
2023-06-05 23:05:05 +02:00
|
|
|
res.json(note.getPojo());
|
2021-10-19 22:48:38 +02:00
|
|
|
});
|
2021-12-06 22:53:17 +01:00
|
|
|
|
2021-12-22 09:36:38 +01:00
|
|
|
router.get('/share/api/notes/:noteId/download', (req, res, next) => {
|
2022-05-01 23:16:47 +02:00
|
|
|
shacaLoader.ensureLoad();
|
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
let note;
|
2021-12-06 22:53:17 +01:00
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
|
|
|
|
return;
|
2021-12-06 22:53:17 +01:00
|
|
|
}
|
|
|
|
|
2022-03-22 23:17:47 +01:00
|
|
|
addNoIndexHeader(note, res);
|
|
|
|
|
2021-12-06 22:53:17 +01:00
|
|
|
const utils = require("../services/utils");
|
|
|
|
|
|
|
|
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());
|
|
|
|
});
|
2021-12-27 20:48:14 +01:00
|
|
|
|
2023-05-05 23:17:23 +02:00
|
|
|
// :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename
|
2022-01-01 13:23:09 +01:00
|
|
|
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
|
2022-05-01 23:16:47 +02:00
|
|
|
shacaLoader.ensureLoad();
|
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
let image;
|
2022-01-01 13:23:09 +01:00
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
if (!(image = checkNoteAccess(req.params.noteId, req, res))) {
|
|
|
|
return;
|
2022-01-01 13:23:09 +01:00
|
|
|
}
|
2022-07-31 21:45:32 +02:00
|
|
|
|
|
|
|
if (!["image", "canvas"].includes(image.type)) {
|
2022-12-19 21:39:12 +01:00
|
|
|
return res.status(400)
|
|
|
|
.json({ message: "Requested note is not a shareable image" });
|
2022-05-10 13:43:05 +02:00
|
|
|
} else if (image.type === "canvas") {
|
2022-04-11 21:38:05 +02:00
|
|
|
/**
|
2022-07-01 00:01:29 +02:00
|
|
|
* special "image" type. the canvas is actually type application/json
|
2022-05-03 22:06:24 +02:00
|
|
|
* to avoid bitrot and enable usage as referenced image the svg is included.
|
2022-04-11 21:38:05 +02:00
|
|
|
*/
|
|
|
|
const content = image.getContent();
|
|
|
|
try {
|
2022-05-03 22:06:24 +02:00
|
|
|
const data = JSON.parse(content);
|
|
|
|
|
|
|
|
const svg = data.svg || '<svg />';
|
2022-05-03 21:56:52 +02:00
|
|
|
addNoIndexHeader(image, res);
|
missing path2d support for freedawings, remove node-side rendering, allow async getContent()
* ## Excalidraw and SVG
* 2022-04-16 - @thfrei
*
* Known issues:
* - excalidraw-to-svg (node.js) does not render any hand drawn (freedraw) paths. There is an issue with
* Path2D object not present in node-canvas library used by jsdom. (See Trilium PR for samples and other issues
* in respective library. Link will be added later). Related links:
* - https://github.com/Automattic/node-canvas/pull/2013
* - https://github.com/google/canvas-5-polyfill
* - https://github.com/Automattic/node-canvas/issues/1116
* - https://www.npmjs.com/package/path2d-polyfill
* - excalidraw-to-svg (node.js) takes quite some time to load an image (1-2s)
* - excalidraw-utils (browser) does render freedraw, however NOT freedraw with background
*
* Due to this issues, we opt to use **only excalidraw in the frontend**. Upon saving, we will also get the SVG
* output from the live excalidraw instance. We will save this **SVG side by side the native excalidraw format
* in the trilium note**.
*
* Pro: we will combat bit-rot. Showing the SVG will be very fast, since it is already rendered.
* Con: The note will get bigger (maybe +30%?), we will generate more bandwith.
* (However, using trilium desktop instance, does not care too much about bandwidth. Size increase is probably
* acceptable, as a trade off.)
2022-04-19 00:21:20 +02:00
|
|
|
res.set('Content-Type', "image/svg+xml");
|
|
|
|
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
|
|
res.send(svg);
|
2022-12-19 21:39:12 +01:00
|
|
|
} catch (err) {
|
|
|
|
res.status(500)
|
|
|
|
.json({ message: "There was an error parsing excalidraw to svg." });
|
2022-04-11 21:38:05 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// normal image
|
|
|
|
res.set('Content-Type', image.mime);
|
2022-05-03 21:56:52 +02:00
|
|
|
addNoIndexHeader(image, res);
|
2022-04-11 21:38:05 +02:00
|
|
|
res.send(image.getContent());
|
2022-01-01 13:23:09 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-06-05 23:05:05 +02:00
|
|
|
// :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) => {
|
|
|
|
shacaLoader.ensureLoad();
|
|
|
|
|
|
|
|
let attachment;
|
|
|
|
|
|
|
|
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 {
|
|
|
|
return res.status(400)
|
|
|
|
.json({ message: "Requested attachment is not a shareable image" });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-06-06 00:16:32 +02:00
|
|
|
router.get('/share/api/attachments/:attachmentId/download', (req, res, next) => {
|
|
|
|
shacaLoader.ensureLoad();
|
|
|
|
|
|
|
|
let attachment;
|
|
|
|
|
|
|
|
if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
addNoIndexHeader(attachment.note, res);
|
|
|
|
|
|
|
|
const utils = require("../services/utils");
|
|
|
|
|
|
|
|
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());
|
|
|
|
});
|
|
|
|
|
2022-01-01 13:23:09 +01:00
|
|
|
// used for PDF viewing
|
2021-12-24 21:36:31 +00:00
|
|
|
router.get('/share/api/notes/:noteId/view', (req, res, next) => {
|
2022-05-01 23:16:47 +02:00
|
|
|
shacaLoader.ensureLoad();
|
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
let note;
|
2021-12-24 21:36:31 +00:00
|
|
|
|
2022-07-31 21:45:32 +02:00
|
|
|
if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
|
|
|
|
return;
|
2021-12-24 21:36:31 +00:00
|
|
|
}
|
|
|
|
|
2022-03-22 23:17:47 +01:00
|
|
|
addNoIndexHeader(note, res);
|
|
|
|
|
2021-12-24 21:36:31 +00:00
|
|
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
|
|
res.setHeader('Content-Type', note.mime);
|
|
|
|
|
|
|
|
res.send(note.getContent());
|
|
|
|
});
|
2021-10-17 14:44:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
register
|
|
|
|
}
|