From c08393f04b1552a34413d9795b2f4ddac3f2f301 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 10 Apr 2024 19:04:38 +0300 Subject: [PATCH] server-ts: Port share/routes --- package-lock.json | 26 ++++++ package.json | 2 + src/becca/entities/bnote.ts | 7 +- src/routes/routes.js | 2 +- src/share/{routes.js => routes.ts} | 117 ++++++++++++++---------- src/share/shaca/entities/sattachment.ts | 4 +- src/share/shaca/entities/sbranch.ts | 2 +- src/share/shaca/entities/snote.ts | 25 ++++- src/share/shaca/shaca-interface.ts | 6 +- 9 files changed, 131 insertions(+), 60 deletions(-) rename src/share/{routes.js => routes.ts} (74%) diff --git a/package-lock.json b/package-lock.json index e80b41f85..a0fdabf82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,6 +92,7 @@ "@types/better-sqlite3": "^7.6.9", "@types/cls-hooked": "^4.3.8", "@types/csurf": "^1.11.5", + "@types/ejs": "^3.1.5", "@types/escape-html": "^1.0.4", "@types/express": "^4.17.21", "@types/express-session": "^1.18.0", @@ -101,6 +102,7 @@ "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", "@types/node": "^20.11.19", + "@types/safe-compare": "^1.1.2", "@types/sanitize-html": "^2.11.0", "@types/sax": "^1.2.7", "@types/stream-throttle": "^0.1.4", @@ -1271,6 +1273,12 @@ "@types/ms": "*" } }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, "node_modules/@types/escape-html": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", @@ -1537,6 +1545,12 @@ "@types/node": "*" } }, + "node_modules/@types/safe-compare": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/safe-compare/-/safe-compare-1.1.2.tgz", + "integrity": "sha512-kK/IM1+pvwCMom+Kezt/UlP8LMEwm8rP6UgGbRc6zUnhU/csoBQ5rWgmD2CJuHxiMiX+H1VqPGpo0kDluJGXYA==", + "dev": true + }, "node_modules/@types/sanitize-html": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", @@ -14276,6 +14290,12 @@ "@types/ms": "*" } }, + "@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, "@types/escape-html": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", @@ -14535,6 +14555,12 @@ "@types/node": "*" } }, + "@types/safe-compare": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/safe-compare/-/safe-compare-1.1.2.tgz", + "integrity": "sha512-kK/IM1+pvwCMom+Kezt/UlP8LMEwm8rP6UgGbRc6zUnhU/csoBQ5rWgmD2CJuHxiMiX+H1VqPGpo0kDluJGXYA==", + "dev": true + }, "@types/sanitize-html": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", diff --git a/package.json b/package.json index 89ccc993c..8cc9cf947 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "@types/better-sqlite3": "^7.6.9", "@types/cls-hooked": "^4.3.8", "@types/csurf": "^1.11.5", + "@types/ejs": "^3.1.5", "@types/escape-html": "^1.0.4", "@types/express": "^4.17.21", "@types/express-session": "^1.18.0", @@ -122,6 +123,7 @@ "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", "@types/node": "^20.11.19", + "@types/safe-compare": "^1.1.2", "@types/sanitize-html": "^2.11.0", "@types/sax": "^1.2.7", "@types/stream-throttle": "^0.1.4", diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index c29945a93..1b681d546 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -209,7 +209,7 @@ class BNote extends AbstractBeccaEntity { .map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId)) as BBranch[]; } - /* + /** * Note content has quite special handling - it's not a separate entity, but a lazily loaded * part of Note entity with its own sync. Reasons behind this hybrid design has been: * @@ -222,7 +222,8 @@ class BNote extends AbstractBeccaEntity { } /** - * @throws Error in case of invalid JSON */ + * @throws Error in case of invalid JSON + */ getJsonContent(): any | null { const content = this.getContent(); @@ -233,7 +234,7 @@ class BNote extends AbstractBeccaEntity { return JSON.parse(content); } - /** @returns {*|null} valid object or null if the content cannot be parsed as JSON */ + /** @returns valid object or null if the content cannot be parsed as JSON */ getJsonContentSafely() { try { return this.getJsonContent(); diff --git a/src/routes/routes.js b/src/routes/routes.js index 00c803cb0..7aa6a64ef 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -59,7 +59,7 @@ const fontsRoute = require('./api/fonts'); const etapiTokensApiRoutes = require('./api/etapi_tokens'); const relationMapApiRoute = require('./api/relation-map'); const otherRoute = require('./api/other'); -const shareRoutes = require('../share/routes.js'); +const shareRoutes = require('../share/routes'); const etapiAuthRoutes = require('../etapi/auth'); const etapiAppInfoRoutes = require('../etapi/app_info'); diff --git a/src/share/routes.js b/src/share/routes.ts similarity index 74% rename from src/share/routes.js rename to src/share/routes.ts index 719fa9368..ccac51295 100644 --- a/src/share/routes.js +++ b/src/share/routes.ts @@ -1,23 +1,24 @@ -const express = require('express'); -const path = require('path'); -const safeCompare = require('safe-compare'); -const ejs = require("ejs"); +import express = require('express'); +import path = require('path'); +import safeCompare = require('safe-compare'); +import ejs = require("ejs"); -const shaca = require('./shaca/shaca'); -const shacaLoader = require('./shaca/shaca_loader'); -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'); +import shaca = require('./shaca/shaca'); +import shacaLoader = require('./shaca/shaca_loader'); +import shareRoot = require('./share_root'); +import contentRenderer = require('./content_renderer'); +import assetPath = require('../services/asset_path'); +import appPath = require('../services/app_path'); +import searchService = require('../services/search/services/search'); +import SearchContext = require('../services/search/search_context'); +import log = require('../services/log'); +import SNote = require('./shaca/entities/snote'); +import SBranch = require('./shaca/entities/sbranch'); +import SAttachment = require('./shaca/entities/sattachment'); +import BNote = require('../becca/entities/bnote'); +import BRevision = require('../becca/entities/brevision'); -/** - * @param {SNote} note - * @return {{note: SNote, branch: SBranch}|{}} - */ -function getSharedSubTreeRoot(note) { +function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } { if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) { // share root itself is not shared return {}; @@ -37,19 +38,18 @@ function getSharedSubTreeRoot(note) { return getSharedSubTreeRoot(parentBranch.getParentNote()); } -function addNoIndexHeader(note, res) { +function addNoIndexHeader(note: SNote, res: express.Response) { if (note.isLabelTruthy('shareDisallowRobotIndexing')) { res.setHeader('X-Robots-Tag', 'noindex'); } } -function requestCredentials(res) { +function requestCredentials(res: express.Response) { res.setHeader('WWW-Authenticate', 'Basic realm="User Visible Realm", charset="UTF-8"') .sendStatus(401); } -/** @returns {SAttachment|boolean} */ -function checkAttachmentAccess(attachmentId, req, res) { +function checkAttachmentAccess(attachmentId: string, req: express.Request, res: express.Response) { const attachment = shaca.getAttachment(attachmentId); if (!attachment) { @@ -65,8 +65,7 @@ function checkAttachmentAccess(attachmentId, req, res) { return note ? attachment : false; } -/** @returns {SNote|boolean} */ -function checkNoteAccess(noteId, req, res) { +function checkNoteAccess(noteId: string, req: express.Request, res: express.Response) { const note = shaca.getNote(noteId); if (!note) { @@ -109,12 +108,16 @@ function checkNoteAccess(noteId, req, res) { return false; } -function renderImageAttachment(image, res, attachmentName) { +function renderImageAttachment(image: SNote, res: express.Response, attachmentName: string) { let svgString = '' const attachment = image.getAttachmentByTitle(attachmentName); - - if (attachment) { - svgString = attachment.getContent(); + if (!attachment) { + res.status(404).render("share/404"); + return; + } + const content = attachment.getContent(); + if (typeof content === "string") { + svgString = content; } else { // backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key const contentSvg = image.getJsonContentSafely()?.svg; @@ -130,8 +133,8 @@ function renderImageAttachment(image, res, attachmentName) { res.send(svg); } -function register(router) { - function renderNote(note, req, res) { +function register(router: express.Router) { + function renderNote(note: SNote, req: express.Request, res: express.Response) { if (!note) { res.status(404).render("share/404"); return; @@ -160,27 +163,34 @@ function register(router) { // 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); + const templateId = note.getRelation('shareTemplate')?.value; + const templateNote = templateId && shaca.getNote(templateId); // Make sure the note type is correct - if (templateNote.type === 'code' && templateNote.mime === 'application/x-ejs') { + if (templateNote && 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 includer = (path: string) => { 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() }; + if (!childNote) throw new Error("Unable to find child note."); + if (childNote.type !== 'code' || childNote.mime !== 'application/x-ejs') throw new Error("Incorrect child note type."); + + const template = childNote.getContent(); + if (typeof template !== "string") throw new Error("Invalid template content type."); + + return { template }; }; // 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 + const content = templateNote.getContent(); + if (typeof content === "string") { + const ejsResult = ejs.render(content, opts, { includer }); + res.send(ejsResult); + useDefaultView = false; // Rendering went okay, don't use default view + } } - catch (e) { + catch (e: any) { log.error(`Rendering user provided share template (${templateId}) threw exception ${e.message} with stacktrace: ${e.stack}`); } } @@ -199,6 +209,11 @@ function register(router) { shacaLoader.ensureLoad(); + if (!shaca.shareRootNote) { + return res.status(404) + .json({ message: "Share root note not found" }); + } + renderNote(shaca.shareRootNote, req, res); }); @@ -214,7 +229,7 @@ function register(router) { router.get('/share/api/notes/:noteId', (req, res, next) => { shacaLoader.ensureLoad(); - let note; + let note: SNote | boolean; if (!(note = checkNoteAccess(req.params.noteId, req, res))) { return; @@ -228,7 +243,7 @@ function register(router) { router.get('/share/api/notes/:noteId/download', (req, res, next) => { shacaLoader.ensureLoad(); - let note; + let note: SNote | boolean; if (!(note = checkNoteAccess(req.params.noteId, req, res))) { return; @@ -252,7 +267,7 @@ function register(router) { router.get('/share/api/images/:noteId/:filename', (req, res, next) => { shacaLoader.ensureLoad(); - let image; + let image: SNote | boolean; if (!(image = checkNoteAccess(req.params.noteId, req, res))) { return; @@ -277,7 +292,7 @@ function register(router) { router.get('/share/api/attachments/:attachmentId/image/:filename', (req, res, next) => { shacaLoader.ensureLoad(); - let attachment; + let attachment: SAttachment | boolean; if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { return; @@ -296,7 +311,7 @@ function register(router) { router.get('/share/api/attachments/:attachmentId/download', (req, res, next) => { shacaLoader.ensureLoad(); - let attachment; + let attachment: SAttachment | boolean; if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { return; @@ -320,7 +335,7 @@ function register(router) { router.get('/share/api/notes/:noteId/view', (req, res, next) => { shacaLoader.ensureLoad(); - let note; + let note: SNote | boolean; if (!(note = checkNoteAccess(req.params.noteId, req, res))) { return; @@ -341,6 +356,10 @@ function register(router) { const ancestorNoteId = req.query.ancestorNoteId ?? "_share"; let note; + if (typeof ancestorNoteId !== "string") { + return res.status(400).json({ message: "'ancestorNoteId' parameter is mandatory." }); + } + // This will automatically return if no ancestorNoteId is provided and there is no shareIndex if (!(note = checkNoteAccess(ancestorNoteId, req, res))) { return; @@ -348,7 +367,7 @@ function register(router) { const { search } = req.query; - if (!search?.trim()) { + if (typeof search !== "string" || !search?.trim()) { return res.status(400).json({ message: "'search' parameter is mandatory." }); } @@ -366,6 +385,6 @@ function register(router) { }); } -module.exports = { +export = { register } diff --git a/src/share/shaca/entities/sattachment.ts b/src/share/shaca/entities/sattachment.ts index c8d58cd17..1e9565ce3 100644 --- a/src/share/shaca/entities/sattachment.ts +++ b/src/share/shaca/entities/sattachment.ts @@ -8,10 +8,10 @@ import { Blob } from '../../../services/blob-interface'; class SAttachment extends AbstractShacaEntity { private attachmentId: string; - private ownerId: string; + ownerId: string; title: string; role: string; - private mime: string; + mime: string; private blobId: string; /** used for caching of images */ private utcDateModified: string; diff --git a/src/share/shaca/entities/sbranch.ts b/src/share/shaca/entities/sbranch.ts index a4cf57c51..0ff356922 100644 --- a/src/share/shaca/entities/sbranch.ts +++ b/src/share/shaca/entities/sbranch.ts @@ -7,7 +7,7 @@ class SBranch extends AbstractShacaEntity { private branchId: string; private noteId: string; - private parentNoteId: string; + parentNoteId: string; private prefix: string; private isExpanded: boolean; isHidden: boolean; diff --git a/src/share/shaca/entities/snote.ts b/src/share/shaca/entities/snote.ts index 5df224af5..89b6562b4 100644 --- a/src/share/shaca/entities/snote.ts +++ b/src/share/shaca/entities/snote.ts @@ -17,7 +17,7 @@ const isCredentials = (attr: SAttribute) => attr.type === 'label' && attr.name = class SNote extends AbstractShacaEntity { noteId: string; - private title: string; + title: string; type: string; mime: string; private blobId: string; @@ -223,6 +223,29 @@ class SNote extends AbstractShacaEntity { } } + /** + * @throws Error in case of invalid JSON + */ + getJsonContent(): any | null { + const content = this.getContent(); + + if (typeof content !== "string" || !content || !content.trim()) { + return null; + } + + return JSON.parse(content); + } + + /** @returns valid object or null if the content cannot be parsed as JSON */ + getJsonContentSafely() { + try { + return this.getJsonContent(); + } + catch (e) { + return null; + } + } + hasAttribute(type: string, name: string) { return !!this.getAttributes().find(attr => attr.type === type && attr.name === name); } diff --git a/src/share/shaca/shaca-interface.ts b/src/share/shaca/shaca-interface.ts index 495741fcc..9cff96eeb 100644 --- a/src/share/shaca/shaca-interface.ts +++ b/src/share/shaca/shaca-interface.ts @@ -10,10 +10,10 @@ export default class Shaca { childParentToBranch!: Record; private attributes!: Record; attachments!: Record; - private aliasToNote!: Record; - private shareRootNote!: SNote | null; + aliasToNote!: Record; + shareRootNote!: SNote | null; /** true if the index of all shared subtrees is enabled */ - private shareIndexEnabled!: boolean; + shareIndexEnabled!: boolean; loaded!: boolean; constructor() {