mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-19 00:42:29 +08:00
Merge branch 'develop' of https://github.com/TriliumNext/Notes into develop
This commit is contained in:
commit
f6e869f2ac
@ -1618,7 +1618,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
* @param matchBy - choose by which property we detect if to update an existing attachment.
|
* @param matchBy - choose by which property we detect if to update an existing attachment.
|
||||||
* Supported values are either 'attachmentId' (default) or 'title'
|
* Supported values are either 'attachmentId' (default) or 'title'
|
||||||
*/
|
*/
|
||||||
saveAttachment({ attachmentId, role, mime, title, content, position }: AttachmentRow, matchBy = "attachmentId") {
|
saveAttachment({ attachmentId, role, mime, title, content, position }: AttachmentRow, matchBy: "attachmentId" | "title" | undefined = "attachmentId") {
|
||||||
if (!["attachmentId", "title"].includes(matchBy)) {
|
if (!["attachmentId", "title"].includes(matchBy)) {
|
||||||
throw new Error(`Unsupported value '${matchBy}' for matchBy param, has to be either 'attachmentId' or 'title'.`);
|
throw new Error(`Unsupported value '${matchBy}' for matchBy param, has to be either 'attachmentId' or 'title'.`);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import becca from "../becca.js";
|
|||||||
import AbstractBeccaEntity from "./abstract_becca_entity.js";
|
import AbstractBeccaEntity from "./abstract_becca_entity.js";
|
||||||
import sql from "../../services/sql.js";
|
import sql from "../../services/sql.js";
|
||||||
import BAttachment from "./battachment.js";
|
import BAttachment from "./battachment.js";
|
||||||
import type { AttachmentRow, RevisionRow } from "./rows.js";
|
import type { AttachmentRow, NoteType, RevisionRow } from "./rows.js";
|
||||||
import eraseService from "../../services/erase.js";
|
import eraseService from "../../services/erase.js";
|
||||||
|
|
||||||
interface ContentOpts {
|
interface ContentOpts {
|
||||||
@ -36,7 +36,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
|
|||||||
|
|
||||||
revisionId?: string;
|
revisionId?: string;
|
||||||
noteId!: string;
|
noteId!: string;
|
||||||
type!: string;
|
type!: NoteType;
|
||||||
mime!: string;
|
mime!: string;
|
||||||
title!: string;
|
title!: string;
|
||||||
dateLastEdited?: string;
|
dateLastEdited?: string;
|
||||||
|
@ -22,7 +22,7 @@ export interface AttachmentRow {
|
|||||||
export interface RevisionRow {
|
export interface RevisionRow {
|
||||||
revisionId?: string;
|
revisionId?: string;
|
||||||
noteId: string;
|
noteId: string;
|
||||||
type: string;
|
type: NoteType;
|
||||||
mime: string;
|
mime: string;
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
|
12
src/errors/forbidden_error.ts
Normal file
12
src/errors/forbidden_error.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import HttpError from "./http_error.js";
|
||||||
|
|
||||||
|
class ForbiddenError extends HttpError {
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message, 403);
|
||||||
|
this.name = "ForbiddenError";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ForbiddenError;
|
13
src/errors/http_error.ts
Normal file
13
src/errors/http_error.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class HttpError extends Error {
|
||||||
|
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
constructor(message: string, statusCode: number) {
|
||||||
|
super(message);
|
||||||
|
this.name = "HttpError";
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HttpError;
|
@ -1,9 +1,12 @@
|
|||||||
class NotFoundError {
|
import HttpError from "./http_error.js";
|
||||||
message: string;
|
|
||||||
|
class NotFoundError extends HttpError {
|
||||||
|
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
this.message = message;
|
super(message, 404);
|
||||||
|
this.name = "NotFoundError";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotFoundError;
|
export default NotFoundError;
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
class ValidationError {
|
import HttpError from "./http_error.js";
|
||||||
message: string;
|
|
||||||
|
class ValidationError extends HttpError {
|
||||||
|
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
this.message = message;
|
super(message, 400)
|
||||||
|
this.name = "ValidationError";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ValidationError;
|
export default ValidationError;
|
||||||
|
@ -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",
|
"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_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": "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.",
|
"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.",
|
"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",
|
"page_size": "number of items per page in note listing",
|
||||||
|
@ -33,7 +33,9 @@ function getAllAttachments(req: Request) {
|
|||||||
function saveAttachment(req: Request) {
|
function saveAttachment(req: Request) {
|
||||||
const { noteId } = req.params;
|
const { noteId } = req.params;
|
||||||
const { attachmentId, role, mime, title, content } = req.body;
|
const { attachmentId, role, mime, title, content } = req.body;
|
||||||
const { matchBy } = req.query as any;
|
const matchByQuery = req.query.matchBy
|
||||||
|
const isValidMatchBy = (typeof matchByQuery === "string") && (matchByQuery === "attachmentId" || matchByQuery === "title");
|
||||||
|
const matchBy = isValidMatchBy ? matchByQuery : undefined;
|
||||||
|
|
||||||
const note = becca.getNoteOrThrow(noteId);
|
const note = becca.getNoteOrThrow(noteId);
|
||||||
note.saveAttachment({ attachmentId, role, mime, title, content }, matchBy);
|
note.saveAttachment({ attachmentId, role, mime, title, content }, matchBy);
|
||||||
@ -41,7 +43,14 @@ function saveAttachment(req: Request) {
|
|||||||
|
|
||||||
function uploadAttachment(req: Request) {
|
function uploadAttachment(req: Request) {
|
||||||
const { noteId } = req.params;
|
const { noteId } = req.params;
|
||||||
const { file } = req as any;
|
const { file } = req;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return {
|
||||||
|
uploaded: false,
|
||||||
|
message: `Missing attachment data.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const note = becca.getNoteOrThrow(noteId);
|
const note = becca.getNoteOrThrow(noteId);
|
||||||
let url;
|
let url;
|
||||||
|
@ -30,12 +30,12 @@ function addClipping(req: Request) {
|
|||||||
// if a note under the clipperInbox has the same 'pageUrl' attribute,
|
// if a note under the clipperInbox has the same 'pageUrl' attribute,
|
||||||
// add the content to that note and clone it under today's inbox
|
// add the content to that note and clone it under today's inbox
|
||||||
// otherwise just create a new note under today's inbox
|
// otherwise just create a new note under today's inbox
|
||||||
let { title, content, pageUrl, images } = req.body;
|
const { title, content, images } = req.body;
|
||||||
const clipType = "clippings";
|
const clipType = "clippings";
|
||||||
|
|
||||||
const clipperInbox = getClipperInboxNote();
|
const clipperInbox = getClipperInboxNote();
|
||||||
|
|
||||||
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
|
const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl);
|
||||||
let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType);
|
let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType);
|
||||||
|
|
||||||
if (!clippingNote) {
|
if (!clippingNote) {
|
||||||
@ -100,16 +100,15 @@ function getClipperInboxNote() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createNote(req: Request) {
|
function createNote(req: Request) {
|
||||||
let { title, content, pageUrl, images, clipType, labels } = req.body;
|
const { content, images, labels } = req.body;
|
||||||
|
|
||||||
if (!title || !title.trim()) {
|
const clipType = htmlSanitizer.sanitize(req.body.clipType);
|
||||||
title = `Clipped note from ${pageUrl}`;
|
const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl);
|
||||||
}
|
|
||||||
|
|
||||||
clipType = htmlSanitizer.sanitize(clipType);
|
const trimmedTitle = (typeof req.body.title === "string") ? req.body.title.trim() : "";
|
||||||
|
const title = trimmedTitle || `Clipped note from ${pageUrl}`;
|
||||||
|
|
||||||
const clipperInbox = getClipperInboxNote();
|
const clipperInbox = getClipperInboxNote();
|
||||||
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
|
|
||||||
let note = findClippingNote(clipperInbox, pageUrl, clipType);
|
let note = findClippingNote(clipperInbox, pageUrl, clipType);
|
||||||
|
|
||||||
if (!note) {
|
if (!note) {
|
||||||
@ -123,8 +122,6 @@ function createNote(req: Request) {
|
|||||||
note.setLabel("clipType", clipType);
|
note.setLabel("clipType", clipType);
|
||||||
|
|
||||||
if (pageUrl) {
|
if (pageUrl) {
|
||||||
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
|
|
||||||
|
|
||||||
note.setLabel("pageUrl", pageUrl);
|
note.setLabel("pageUrl", pageUrl);
|
||||||
note.setLabel("iconClass", "bx bx-globe");
|
note.setLabel("iconClass", "bx bx-globe");
|
||||||
}
|
}
|
||||||
@ -139,7 +136,7 @@ function createNote(req: Request) {
|
|||||||
|
|
||||||
const existingContent = note.getContent();
|
const existingContent = note.getContent();
|
||||||
if (typeof existingContent !== "string") {
|
if (typeof existingContent !== "string") {
|
||||||
throw new ValidationError("Invalid note content tpye.");
|
throw new ValidationError("Invalid note content type.");
|
||||||
}
|
}
|
||||||
const rewrittenContent = processContent(images, note, content);
|
const rewrittenContent = processContent(images, note, content);
|
||||||
const newContent = `${existingContent}${existingContent.trim() ? "<br/>" : ""}${rewrittenContent}`;
|
const newContent = `${existingContent}${existingContent.trim() ? "<br/>" : ""}${rewrittenContent}`;
|
||||||
@ -219,9 +216,9 @@ function handshake() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findNotesByUrl(req: Request) {
|
function findNotesByUrl(req: Request) {
|
||||||
let pageUrl = req.params.noteUrl;
|
const pageUrl = req.params.noteUrl;
|
||||||
const clipperInbox = getClipperInboxNote();
|
const clipperInbox = getClipperInboxNote();
|
||||||
let foundPage = findClippingNote(clipperInbox, pageUrl, null);
|
const foundPage = findClippingNote(clipperInbox, pageUrl, null);
|
||||||
return {
|
return {
|
||||||
noteId: foundPage ? foundPage.noteId : null
|
noteId: foundPage ? foundPage.noteId : null
|
||||||
};
|
};
|
||||||
|
@ -9,6 +9,7 @@ import log from "../../services/log.js";
|
|||||||
import NotFoundError from "../../errors/not_found_error.js";
|
import NotFoundError from "../../errors/not_found_error.js";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import ValidationError from "../../errors/validation_error.js";
|
import ValidationError from "../../errors/validation_error.js";
|
||||||
|
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
|
||||||
|
|
||||||
function exportBranch(req: Request, res: Response) {
|
function exportBranch(req: Request, res: Response) {
|
||||||
const { branchId, type, format, version, taskId } = req.params;
|
const { branchId, type, format, version, taskId } = req.params;
|
||||||
@ -37,11 +38,12 @@ function exportBranch(req: Request, res: Response) {
|
|||||||
} else {
|
} else {
|
||||||
throw new NotFoundError(`Unrecognized export format '${format}'`);
|
throw new NotFoundError(`Unrecognized export format '${format}'`);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
const message = `Export failed with following error: '${e.message}'. More details might be in the logs.`;
|
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||||
|
const message = `Export failed with following error: '${errMessage}'. More details might be in the logs.`;
|
||||||
taskContext.reportError(message);
|
taskContext.reportError(message);
|
||||||
|
|
||||||
log.error(message + e.stack);
|
log.error(errMessage + errStack);
|
||||||
|
|
||||||
res.setHeader("Content-Type", "text/plain").status(500).send(message);
|
res.setHeader("Content-Type", "text/plain").status(500).send(message);
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ function updateImage(req: Request) {
|
|||||||
const { noteId } = req.params;
|
const { noteId } = req.params;
|
||||||
const { file } = req;
|
const { file } = req;
|
||||||
|
|
||||||
const note = becca.getNoteOrThrow(noteId);
|
const _note = becca.getNoteOrThrow(noteId);
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return {
|
return {
|
||||||
|
@ -13,6 +13,7 @@ import TaskContext from "../../services/task_context.js";
|
|||||||
import ValidationError from "../../errors/validation_error.js";
|
import ValidationError from "../../errors/validation_error.js";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import type BNote from "../../becca/entities/bnote.js";
|
import type BNote from "../../becca/entities/bnote.js";
|
||||||
|
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
|
||||||
|
|
||||||
async function importNotesToBranch(req: Request) {
|
async function importNotesToBranch(req: Request) {
|
||||||
const { parentNoteId } = req.params;
|
const { parentNoteId } = req.params;
|
||||||
@ -68,11 +69,12 @@ async function importNotesToBranch(req: Request) {
|
|||||||
} else {
|
} else {
|
||||||
note = await singleImportService.importSingleFile(taskContext, file, parentNote);
|
note = await singleImportService.importSingleFile(taskContext, file, parentNote);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
const message = `Import failed with following error: '${e.message}'. More details might be in the logs.`;
|
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||||
|
const message = `Import failed with following error: '${errMessage}'. More details might be in the logs.`;
|
||||||
taskContext.reportError(message);
|
taskContext.reportError(message);
|
||||||
|
|
||||||
log.error(message + e.stack);
|
log.error(message + errStack);
|
||||||
|
|
||||||
return [500, message];
|
return [500, message];
|
||||||
}
|
}
|
||||||
@ -120,11 +122,13 @@ async function importAttachmentsToNote(req: Request) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await singleImportService.importAttachment(taskContext, file, parentNote);
|
await singleImportService.importAttachment(taskContext, file, parentNote);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
const message = `Import failed with following error: '${e.message}'. More details might be in the logs.`;
|
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||||
|
|
||||||
|
const message = `Import failed with following error: '${errMessage}'. More details might be in the logs.`;
|
||||||
taskContext.reportError(message);
|
taskContext.reportError(message);
|
||||||
|
|
||||||
log.error(message + e.stack);
|
log.error(message + errStack);
|
||||||
|
|
||||||
return [500, message];
|
return [500, message];
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import protectedSessionService from "../../services/protected_session.js";
|
|||||||
import noteService from "../../services/notes.js";
|
import noteService from "../../services/notes.js";
|
||||||
import becca from "../../becca/becca.js";
|
import becca from "../../becca/becca.js";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import type { RevisionRow } from "../../becca/entities/rows.js";
|
|
||||||
|
|
||||||
interface RecentChangeRow {
|
interface RecentChangeRow {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
|
@ -30,7 +30,7 @@ function getRelationMap(req: Request) {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const questionMarks = noteIds.map((noteId) => "?").join(",");
|
const questionMarks = noteIds.map((_noteId) => "?").join(",");
|
||||||
|
|
||||||
const relationMapNote = becca.getNoteOrThrow(relationMapNoteId);
|
const relationMapNote = becca.getNoteOrThrow(relationMapNoteId);
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import beccaService from "../../becca/becca_service.js";
|
import beccaService from "../../becca/becca_service.js";
|
||||||
import revisionService from "../../services/revisions.js";
|
|
||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
import sql from "../../services/sql.js";
|
import sql from "../../services/sql.js";
|
||||||
import cls from "../../services/cls.js";
|
import cls from "../../services/cls.js";
|
||||||
@ -111,7 +110,7 @@ function eraseRevision(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function eraseAllExcessRevisions() {
|
function eraseAllExcessRevisions() {
|
||||||
let allNoteIds = sql.getRows("SELECT noteId FROM notes WHERE SUBSTRING(noteId, 1, 1) != '_'") as { noteId: string }[];
|
const allNoteIds = sql.getRows("SELECT noteId FROM notes WHERE SUBSTRING(noteId, 1, 1) != '_'") as { noteId: string }[];
|
||||||
allNoteIds.forEach((row) => {
|
allNoteIds.forEach((row) => {
|
||||||
becca.getNote(row.noteId)?.eraseExcessRevisionSnapshots();
|
becca.getNote(row.noteId)?.eraseExcessRevisionSnapshots();
|
||||||
});
|
});
|
||||||
@ -145,7 +144,7 @@ function restoreRevision(req: Request) {
|
|||||||
|
|
||||||
note.title = revision.title;
|
note.title = revision.title;
|
||||||
note.mime = revision.mime;
|
note.mime = revision.mime;
|
||||||
note.type = revision.type as any;
|
note.type = revision.type;
|
||||||
note.setContent(revisionContent, { forceSave: true });
|
note.setContent(revisionContent, { forceSave: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import becca from "../../becca/becca.js";
|
|||||||
import syncService from "../../services/sync.js";
|
import syncService from "../../services/sync.js";
|
||||||
import sql from "../../services/sql.js";
|
import sql from "../../services/sql.js";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
|
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
|
||||||
|
|
||||||
interface ScriptBody {
|
interface ScriptBody {
|
||||||
script: string;
|
script: string;
|
||||||
@ -33,8 +34,12 @@ async function exec(req: Request) {
|
|||||||
executionResult: result,
|
executionResult: result,
|
||||||
maxEntityChangeId: syncService.getMaxEntityChangeId()
|
maxEntityChangeId: syncService.getMaxEntityChangeId()
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
return { success: false, error: e.message };
|
const [errMessage] = safeExtractMessageAndStackFromError(e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errMessage
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import becca from "../../becca/becca.js";
|
|||||||
async function getSimilarNotes(req: Request) {
|
async function getSimilarNotes(req: Request) {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
|
||||||
const note = becca.getNoteOrThrow(noteId);
|
const _note = becca.getNoteOrThrow(noteId);
|
||||||
|
|
||||||
return await similarityService.findSimilarNotes(noteId);
|
return await similarityService.findSimilarNotes(noteId);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import sql from "../../services/sql.js";
|
|||||||
import becca from "../../becca/becca.js";
|
import becca from "../../becca/becca.js";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import ValidationError from "../../errors/validation_error.js";
|
import ValidationError from "../../errors/validation_error.js";
|
||||||
|
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
|
||||||
|
|
||||||
function getSchema() {
|
function getSchema() {
|
||||||
const tableNames = sql.getColumn(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
const tableNames = sql.getColumn(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
||||||
@ -56,10 +57,11 @@ function execute(req: Request) {
|
|||||||
success: true,
|
success: true,
|
||||||
results
|
results
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
|
const [errMessage] = safeExtractMessageAndStackFromError(e);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: e.message
|
error: errMessage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import optionService from "../../services/options.js";
|
|||||||
import contentHashService from "../../services/content_hash.js";
|
import contentHashService from "../../services/content_hash.js";
|
||||||
import log from "../../services/log.js";
|
import log from "../../services/log.js";
|
||||||
import syncOptions from "../../services/sync_options.js";
|
import syncOptions from "../../services/sync_options.js";
|
||||||
import utils from "../../services/utils.js";
|
import utils, { safeExtractMessageAndStackFromError } from "../../services/utils.js";
|
||||||
import ws from "../../services/ws.js";
|
import ws from "../../services/ws.js";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import type { EntityChange } from "../../services/entity_changes_interface.js";
|
import type { EntityChange } from "../../services/entity_changes_interface.js";
|
||||||
@ -30,10 +30,11 @@ async function testSync() {
|
|||||||
syncService.sync();
|
syncService.sync();
|
||||||
|
|
||||||
return { success: true, message: t("test_sync.successful") };
|
return { success: true, message: t("test_sync.successful") };
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
|
const [errMessage] = safeExtractMessageAndStackFromError(e);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: e.message
|
error: errMessage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Application, Router } from "express";
|
import type { Application } from "express";
|
||||||
import swaggerUi from "swagger-ui-express";
|
import swaggerUi from "swagger-ui-express";
|
||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
@ -5,7 +5,7 @@ import express from "express";
|
|||||||
import { isDev, isElectron } from "../services/utils.js";
|
import { isDev, isElectron } from "../services/utils.js";
|
||||||
import type serveStatic from "serve-static";
|
import type serveStatic from "serve-static";
|
||||||
|
|
||||||
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<any, Record<string, any>>>) => {
|
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<unknown, Record<string, unknown>>>) => {
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
options = {
|
options = {
|
||||||
maxAge: "1y",
|
maxAge: "1y",
|
||||||
|
@ -5,6 +5,7 @@ import cls from "../services/cls.js";
|
|||||||
import sql from "../services/sql.js";
|
import sql from "../services/sql.js";
|
||||||
import becca from "../becca/becca.js";
|
import becca from "../becca/becca.js";
|
||||||
import type { Request, Response, Router } from "express";
|
import type { Request, Response, Router } from "express";
|
||||||
|
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||||
|
|
||||||
function handleRequest(req: Request, res: Response) {
|
function handleRequest(req: Request, res: Response) {
|
||||||
// express puts content after first slash into 0 index element
|
// express puts content after first slash into 0 index element
|
||||||
@ -25,8 +26,9 @@ function handleRequest(req: Request, res: Response) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
match = path.match(regex);
|
match = path.match(regex);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${e.message}, stack: ${e.stack}`);
|
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||||
|
log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${errMessage}, stack: ${errStack}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,10 +47,10 @@ function handleRequest(req: Request, res: Response) {
|
|||||||
req,
|
req,
|
||||||
res
|
res
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
log.error(`Custom handler '${note.noteId}' failed with: ${e.message}, ${e.stack}`);
|
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||||
|
log.error(`Custom handler '${note.noteId}' failed with: ${errMessage}, ${errStack}`);
|
||||||
res.setHeader("Content-Type", "text/plain").status(500).send(e.message);
|
res.setHeader("Content-Type", "text/plain").status(500).send(errMessage);
|
||||||
}
|
}
|
||||||
} else if (attr.name === "customResourceProvider") {
|
} else if (attr.name === "customResourceProvider") {
|
||||||
fileService.downloadNoteInt(attr.noteId, res);
|
fileService.downloadNoteInt(attr.noteId, res);
|
||||||
@ -68,7 +70,7 @@ function handleRequest(req: Request, res: Response) {
|
|||||||
function register(router: Router) {
|
function register(router: Router) {
|
||||||
// explicitly no CSRF middleware since it's meant to allow integration from external services
|
// explicitly no CSRF middleware since it's meant to allow integration from external services
|
||||||
|
|
||||||
router.all("/custom/:path*", (req: Request, res: Response, next) => {
|
router.all("/custom/:path*", (req: Request, res: Response, _next) => {
|
||||||
cls.namespace.bindEmitter(req);
|
cls.namespace.bindEmitter(req);
|
||||||
cls.namespace.bindEmitter(res);
|
cls.namespace.bindEmitter(res);
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ interface Response {
|
|||||||
setHeader: (name: string, value: string) => Response;
|
setHeader: (name: string, value: string) => Response;
|
||||||
header: (name: string, value: string) => Response;
|
header: (name: string, value: string) => Response;
|
||||||
status: (statusCode: number) => Response;
|
status: (statusCode: number) => Response;
|
||||||
send: (obj: {}) => void;
|
send: (obj: {}) => void; // eslint-disable-line @typescript-eslint/no-empty-object-type
|
||||||
}
|
}
|
||||||
|
|
||||||
function init(app: Application) {
|
function init(app: Application) {
|
||||||
|
@ -1,38 +1,46 @@
|
|||||||
import type { Application, NextFunction, Request, Response } from "express";
|
import type { Application, NextFunction, Request, Response } from "express";
|
||||||
import log from "../services/log.js";
|
import log from "../services/log.js";
|
||||||
|
import NotFoundError from "../errors/not_found_error.js";
|
||||||
|
import ForbiddenError from "../errors/forbidden_error.js";
|
||||||
|
import HttpError from "../errors/http_error.js";
|
||||||
|
|
||||||
function register(app: Application) {
|
function register(app: Application) {
|
||||||
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
|
||||||
if (err.code !== "EBADCSRFTOKEN") {
|
app.use((err: unknown | Error, req: Request, res: Response, next: NextFunction) => {
|
||||||
return next(err);
|
|
||||||
|
const isCsrfTokenError = typeof err === "object"
|
||||||
|
&& err
|
||||||
|
&& "code" in err
|
||||||
|
&& err.code === "EBADCSRFTOKEN";
|
||||||
|
|
||||||
|
if (isCsrfTokenError) {
|
||||||
|
log.error(`Invalid CSRF token: ${req.headers["x-csrf-token"]}, secret: ${req.cookies["_csrf"]}`);
|
||||||
|
return next(new ForbiddenError("Invalid CSRF token"));
|
||||||
}
|
}
|
||||||
|
|
||||||
log.error(`Invalid CSRF token: ${req.headers["x-csrf-token"]}, secret: ${req.cookies["_csrf"]}`);
|
return next(err);
|
||||||
|
|
||||||
err = new Error("Invalid CSRF token");
|
|
||||||
err.status = 403;
|
|
||||||
next(err);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// catch 404 and forward to error handler
|
// catch 404 and forward to error handler
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const err = new Error(`Router not found for request ${req.method} ${req.url}`);
|
const err = new NotFoundError(`Router not found for request ${req.method} ${req.url}`);
|
||||||
(err as any).status = 404;
|
|
||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// error handler
|
// error handler
|
||||||
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
app.use((err: unknown | Error, req: Request, res: Response, _next: NextFunction) => {
|
||||||
if (err.status !== 404) {
|
|
||||||
log.info(err);
|
|
||||||
} else {
|
|
||||||
log.info(`${err.status} ${req.method} ${req.url}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(err.status || 500);
|
const statusCode = (err instanceof HttpError) ? err.statusCode : 500;
|
||||||
res.send({
|
const errMessage = (err instanceof Error && statusCode !== 404)
|
||||||
message: err.message
|
? err
|
||||||
|
: `${statusCode} ${req.method} ${req.url}`;
|
||||||
|
|
||||||
|
log.info(errMessage);
|
||||||
|
|
||||||
|
res.status(statusCode).send({
|
||||||
|
message: err instanceof Error ? err.message : "Unknown Error"
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ function index(req: Request, res: Response) {
|
|||||||
device: view,
|
device: view,
|
||||||
csrfToken: csrfToken,
|
csrfToken: csrfToken,
|
||||||
themeCssUrl: getThemeCssUrl(theme, themeNote),
|
themeCssUrl: getThemeCssUrl(theme, themeNote),
|
||||||
themeUseNextAsBase: themeNote?.getAttributeValue("label", "appThemeBase") === "next",
|
themeUseNextAsBase: themeNote?.getAttributeValue("label", "appThemeBase"),
|
||||||
headingStyle: options.headingStyle,
|
headingStyle: options.headingStyle,
|
||||||
layoutOrientation: options.layoutOrientation,
|
layoutOrientation: options.layoutOrientation,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { isElectron } from "../services/utils.js";
|
import { isElectron, safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import log from "../services/log.js";
|
import log from "../services/log.js";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
@ -471,7 +471,7 @@ function route(method: HttpMethod, path: string, middleware: express.Handler[],
|
|||||||
|
|
||||||
if (result?.then) {
|
if (result?.then) {
|
||||||
// promise
|
// promise
|
||||||
result.then((promiseResult: unknown) => handleResponse(resultHandler, req, res, promiseResult, start)).catch((e: any) => handleException(e, method, path, res));
|
result.then((promiseResult: unknown) => handleResponse(resultHandler, req, res, promiseResult, start)).catch((e: unknown) => handleException(e, method, path, res));
|
||||||
} else {
|
} else {
|
||||||
handleResponse(resultHandler, req, res, result, start);
|
handleResponse(resultHandler, req, res, result, start);
|
||||||
}
|
}
|
||||||
@ -487,22 +487,17 @@ function handleResponse(resultHandler: ApiResultHandler, req: express.Request, r
|
|||||||
log.request(req, res, Date.now() - start, responseLength);
|
log.request(req, res, Date.now() - start, responseLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleException(e: any, method: HttpMethod, path: string, res: express.Response) {
|
function handleException(e: unknown | Error, method: HttpMethod, path: string, res: express.Response) {
|
||||||
log.error(`${method} ${path} threw exception: '${e.message}', stack: ${e.stack}`);
|
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||||
|
|
||||||
|
log.error(`${method} ${path} threw exception: '${errMessage}', stack: ${errStack}`);
|
||||||
|
|
||||||
|
const resStatusCode = (e instanceof ValidationError || e instanceof NotFoundError) ? e.statusCode : 500;
|
||||||
|
|
||||||
|
res.status(resStatusCode).json({
|
||||||
|
message: errMessage
|
||||||
|
});
|
||||||
|
|
||||||
if (e instanceof ValidationError) {
|
|
||||||
res.status(400).json({
|
|
||||||
message: e.message
|
|
||||||
});
|
|
||||||
} else if (e instanceof NotFoundError) {
|
|
||||||
res.status(404).json({
|
|
||||||
message: e.message
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.status(500).json({
|
|
||||||
message: e.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createUploadMiddleware() {
|
function createUploadMiddleware() {
|
||||||
|
@ -500,6 +500,23 @@ describe("#isDev", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("#safeExtractMessageAndStackFromError", () => {
|
||||||
|
it("should correctly extract the message and stack property if it gets passed an instance of an Error", () => {
|
||||||
|
const testMessage = "Test Message";
|
||||||
|
const testError = new Error(testMessage);
|
||||||
|
const actual = utils.safeExtractMessageAndStackFromError(testError);
|
||||||
|
expect(actual[0]).toBe(testMessage);
|
||||||
|
expect(actual[1]).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use the fallback 'Unknown Error' message, if it gets passed anything else than an instance of an Error", () => {
|
||||||
|
const testNonError = "this is not an instance of an Error, but JS technically allows us to throw this anyways";
|
||||||
|
const actual = utils.safeExtractMessageAndStackFromError(testNonError);
|
||||||
|
expect(actual[0]).toBe("Unknown Error");
|
||||||
|
expect(actual[1]).toBeUndefined();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
describe("#formatDownloadTitle", () => {
|
describe("#formatDownloadTitle", () => {
|
||||||
//prettier-ignore
|
//prettier-ignore
|
||||||
const testCases: [fnValue: Parameters<typeof utils.formatDownloadTitle>, expectedValue: ReturnType<typeof utils.formatDownloadTitle>][] = [
|
const testCases: [fnValue: Parameters<typeof utils.formatDownloadTitle>, expectedValue: ReturnType<typeof utils.formatDownloadTitle>][] = [
|
||||||
|
@ -362,6 +362,11 @@ export function processStringOrBuffer(data: string | Buffer | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function safeExtractMessageAndStackFromError(err: unknown) {
|
||||||
|
return (err instanceof Error) ? [err.message, err.stack] as const : ["Unknown Error", undefined] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
compareVersions,
|
compareVersions,
|
||||||
crash,
|
crash,
|
||||||
@ -392,6 +397,7 @@ export default {
|
|||||||
removeDiacritic,
|
removeDiacritic,
|
||||||
removeTextFileExtension,
|
removeTextFileExtension,
|
||||||
replaceAll,
|
replaceAll,
|
||||||
|
safeExtractMessageAndStackFromError,
|
||||||
sanitizeSqlIdentifier,
|
sanitizeSqlIdentifier,
|
||||||
stripTags,
|
stripTags,
|
||||||
timeLimit,
|
timeLimit,
|
||||||
|
@ -53,8 +53,12 @@
|
|||||||
<link href="<%= themeCssUrl %>" rel="stylesheet">
|
<link href="<%= themeCssUrl %>" rel="stylesheet">
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (themeUseNextAsBase) { %>
|
<% if (themeUseNextAsBase === "next") { %>
|
||||||
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet">
|
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet">
|
||||||
|
<% } else if (themeUseNextAsBase === "next-dark") { %>
|
||||||
|
<link href="<%= assetPath %>/stylesheets/theme-next-dark.css" rel="stylesheet">
|
||||||
|
<% } else if (themeUseNextAsBase === "next-light") { %>
|
||||||
|
<link href="<%= assetPath %>/stylesheets/theme-next-light.css" rel="stylesheet">
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
|
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user