chore(prettier): fix all files

This commit is contained in:
Elian Doran
2025-01-09 18:07:02 +02:00
parent 19ee861699
commit 4cbb529fd4
571 changed files with 23226 additions and 23940 deletions

View File

@@ -9,9 +9,9 @@ import path from "path";
function getFullAnonymizationScript() {
// we want to delete all non-builtin attributes because they can contain sensitive names and values
// on the other hand builtin/system attrs should not contain any sensitive info
const builtinAttrNames = BUILTIN_ATTRIBUTES
.filter(attr => !["shareCredentials", "shareAlias"].includes(attr.name))
.map(attr => `'${attr.name}'`).join(', ');
const builtinAttrNames = BUILTIN_ATTRIBUTES.filter((attr) => !["shareCredentials", "shareAlias"].includes(attr.name))
.map((attr) => `'${attr.name}'`)
.join(", ");
const anonymizeScript = `
UPDATE etapi_tokens SET tokenHash = 'API token hash value';
@@ -49,7 +49,7 @@ function getLightAnonymizationScript() {
}
async function createAnonymizedCopy(type: "full" | "light") {
if (!['full', 'light'].includes(type)) {
if (!["full", "light"].includes(type)) {
throw new Error(`Unrecognized anonymization type '${type}'`);
}
@@ -63,9 +63,7 @@ async function createAnonymizedCopy(type: "full" | "light") {
const db = new Database(anonymizedFile);
const anonymizationScript = type === 'light'
? getLightAnonymizationScript()
: getFullAnonymizationScript();
const anonymizationScript = type === "light" ? getLightAnonymizationScript() : getFullAnonymizationScript();
db.exec(anonymizationScript);
@@ -82,9 +80,10 @@ function getExistingAnonymizedDatabases() {
return [];
}
return fs.readdirSync(dataDir.ANONYMIZED_DB_DIR)
.filter(fileName => fileName.includes("anonymized"))
.map(fileName => ({
return fs
.readdirSync(dataDir.ANONYMIZED_DB_DIR)
.filter((fileName) => fileName.includes("anonymized"))
.map((fileName) => ({
fileName: fileName,
filePath: path.resolve(dataDir.ANONYMIZED_DB_DIR, fileName)
}));
@@ -94,4 +93,4 @@ export default {
getFullAnonymizationScript,
createAnonymizedCopy,
getExistingAnonymizedDatabases
}
};

View File

@@ -13,5 +13,5 @@ export interface SetupStatusResponse {
*/
export interface SetupSyncSeedResponse {
syncVersion: number;
options: OptionRow[]
options: OptionRow[];
}

View File

@@ -23,9 +23,7 @@ Terminal=false
* We overwrite this file during every run as it might have been updated.
*/
function installLocalAppIcon() {
if (!isElectron()
|| ["win32", "darwin"].includes(os.platform())
|| (config.General && config.General.noDesktopIcon)) {
if (!isElectron() || ["win32", "darwin"].includes(os.platform()) || (config.General && config.General.noDesktopIcon)) {
return;
}
@@ -35,7 +33,7 @@ function installLocalAppIcon() {
return;
}
const desktopDir = path.resolve(os.homedir(), '.local/share/applications');
const desktopDir = path.resolve(os.homedir(), ".local/share/applications");
fs.stat(desktopDir, function (err, stats) {
if (err) {
@@ -56,9 +54,7 @@ function installLocalAppIcon() {
}
function getDesktopFileContent() {
return template
.replace("#APP_ROOT_DIR#", escapePath(resourceDir.ELECTRON_APP_ROOT_DIR))
.replace("#EXE_PATH#", escapePath(getExePath()));
return template.replace("#APP_ROOT_DIR#", escapePath(resourceDir.ELECTRON_APP_ROOT_DIR)).replace("#EXE_PATH#", escapePath(getExePath()));
}
function escapePath(path: string) {
@@ -66,7 +62,7 @@ function escapePath(path: string) {
}
function getExePath() {
return path.resolve(resourceDir.ELECTRON_APP_ROOT_DIR, 'trilium');
return path.resolve(resourceDir.ELECTRON_APP_ROOT_DIR, "trilium");
}
export default {

View File

@@ -1,6 +1,4 @@
import assetPath from "./asset_path.js";
import env from "./env.js";
export default env.isDev()
? assetPath + "/app"
: assetPath + "/app-dist";
export default env.isDev() ? assetPath + "/app" : assetPath + "/app-dist";

View File

@@ -3,26 +3,24 @@
import { AttributeRow } from "../becca/entities/rows.js";
function formatAttrForSearch(attr: AttributeRow, searchWithValue: boolean) {
let searchStr = '';
let searchStr = "";
if (attr.type === 'label') {
searchStr += '#';
}
else if (attr.type === 'relation') {
searchStr += '~';
}
else {
if (attr.type === "label") {
searchStr += "#";
} else if (attr.type === "relation") {
searchStr += "~";
} else {
throw new Error(`Unrecognized attribute type ${JSON.stringify(attr)}`);
}
searchStr += attr.name;
if (searchWithValue && attr.value) {
if (attr.type === 'relation') {
if (attr.type === "relation") {
searchStr += ".noteId";
}
searchStr += '=';
searchStr += "=";
searchStr += formatValue(attr.value);
}
@@ -32,17 +30,13 @@ function formatAttrForSearch(attr: AttributeRow, searchWithValue: boolean) {
function formatValue(val: string) {
if (!/[^\w]/.test(val)) {
return val;
}
else if (!val.includes('"')) {
} else if (!val.includes('"')) {
return `"${val}"`;
}
else if (!val.includes("'")) {
} else if (!val.includes("'")) {
return `'${val}'`;
}
else if (!val.includes("`")) {
} else if (!val.includes("`")) {
return `\`${val}\``;
}
else {
} else {
return `"${val.replace(/"/g, '\\"')}"`;
}
}

View File

@@ -7,12 +7,12 @@ import BAttribute from "../becca/entities/battribute.js";
import attributeFormatter from "./attribute_formatter.js";
import BUILTIN_ATTRIBUTES from "./builtin_attributes.js";
import BNote from "../becca/entities/bnote.js";
import { AttributeRow } from '../becca/entities/rows.js';
import { AttributeRow } from "../becca/entities/rows.js";
const ATTRIBUTE_TYPES = new Set(['label', 'relation']);
const ATTRIBUTE_TYPES = new Set(["label", "relation"]);
function getNotesWithLabel(name: string, value?: string): BNote[] {
const query = attributeFormatter.formatAttrForSearch({type: 'label', name, value}, value !== undefined);
const query = attributeFormatter.formatAttrForSearch({ type: "label", name, value }, value !== undefined);
return searchService.searchNotes(query, {
includeArchivedNotes: true,
ignoreHoistedNote: true
@@ -22,7 +22,7 @@ function getNotesWithLabel(name: string, value?: string): BNote[] {
// TODO: should be in search service
function getNoteWithLabel(name: string, value?: string): BNote | null {
// optimized version (~20 times faster) without using normal search, useful for e.g., finding date notes
const attrs = becca.findAttributes('label', name);
const attrs = becca.findAttributes("label", name);
if (value === undefined) {
return attrs[0]?.getNote();
@@ -42,7 +42,7 @@ function getNoteWithLabel(name: string, value?: string): BNote | null {
function createLabel(noteId: string, name: string, value: string = "") {
return createAttribute({
noteId: noteId,
type: 'label',
type: "label",
name: name,
value: value
});
@@ -51,7 +51,7 @@ function createLabel(noteId: string, name: string, value: string = "") {
function createRelation(noteId: string, name: string, targetNoteId: string) {
return createAttribute({
noteId: noteId,
type: 'relation',
type: "relation",
name: name,
value: targetNoteId
});
@@ -69,7 +69,9 @@ function getAttributeNames(type: string, nameLike: string) {
FROM attributes
WHERE isDeleted = 0
AND type = ?
AND name LIKE ?`, [type, `%${nameLike}%`]);
AND name LIKE ?`,
[type, `%${nameLike}%`]
);
for (const attr of BUILTIN_ATTRIBUTES) {
if (attr.type === type && attr.name.toLowerCase().includes(nameLike) && !names.includes(attr.name)) {
@@ -77,12 +79,7 @@ function getAttributeNames(type: string, nameLike: string) {
}
}
names = names.filter(name => ![
'internalLink',
'imageLink',
'includeNoteLink',
'relationMapLink'
].includes(name));
names = names.filter((name) => !["internalLink", "imageLink", "includeNoteLink", "relationMapLink"].includes(name));
names.sort((a, b) => {
const aPrefix = a.toLowerCase().startsWith(nameLike);
@@ -103,11 +100,7 @@ function isAttributeType(type: string): boolean {
}
function isAttributeDangerous(type: string, name: string): boolean {
return BUILTIN_ATTRIBUTES.some(attr =>
attr.type === type &&
attr.name.toLowerCase() === name.trim().toLowerCase() &&
attr.isDangerous
);
return BUILTIN_ATTRIBUTES.some((attr) => attr.type === type && attr.name.toLowerCase() === name.trim().toLowerCase() && attr.isDangerous);
}
export default {

View File

@@ -7,18 +7,16 @@ import { isElectron } from "./utils.js";
import passwordEncryptionService from "./encryption/password_encryption.js";
import config from "./config.js";
import passwordService from "./encryption/password.js";
import type { NextFunction, Request, Response } from 'express';
import type { NextFunction, Request, Response } from "express";
const noAuthentication = config.General && config.General.noAuthentication === true;
function checkAuth(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
res.redirect("setup");
}
else if (!req.session.loggedIn && !isElectron() && !noAuthentication) {
} else if (!req.session.loggedIn && !isElectron() && !noAuthentication) {
res.redirect("login");
}
else {
} else {
next();
}
}
@@ -28,8 +26,7 @@ function checkAuth(req: Request, res: Response, next: NextFunction) {
function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) {
if (!req.session.loggedIn && !isElectron() && !noAuthentication) {
reject(req, res, "Logged in session not found");
}
else {
} else {
next();
}
}
@@ -37,8 +34,7 @@ function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction)
function checkApiAuth(req: Request, res: Response, next: NextFunction) {
if (!req.session.loggedIn && !noAuthentication) {
reject(req, res, "Logged in session not found");
}
else {
} else {
next();
}
}
@@ -46,8 +42,7 @@ function checkApiAuth(req: Request, res: Response, next: NextFunction) {
function checkAppInitialized(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
res.redirect("setup");
}
else {
} else {
next();
}
}
@@ -71,8 +66,7 @@ function checkPasswordNotSet(req: Request, res: Response, next: NextFunction) {
function checkAppNotInitialized(req: Request, res: Response, next: NextFunction) {
if (sqlInit.isDbInitialized()) {
reject(req, res, "App already initialized.");
}
else {
} else {
next();
}
}
@@ -80,8 +74,7 @@ function checkAppNotInitialized(req: Request, res: Response, next: NextFunction)
function checkEtapiToken(req: Request, res: Response, next: NextFunction) {
if (etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
next();
}
else {
} else {
reject(req, res, "Token not found");
}
}
@@ -89,45 +82,34 @@ function checkEtapiToken(req: Request, res: Response, next: NextFunction) {
function reject(req: Request, res: Response, message: string) {
log.info(`${req.method} ${req.path} rejected with 401 ${message}`);
res.setHeader("Content-Type", "text/plain")
.status(401)
.send(message);
res.setHeader("Content-Type", "text/plain").status(401).send(message);
}
function checkCredentials(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
res.setHeader("Content-Type", "text/plain")
.status(400)
.send('Database is not initialized yet.');
res.setHeader("Content-Type", "text/plain").status(400).send("Database is not initialized yet.");
return;
}
if (!passwordService.isPasswordSet()) {
res.setHeader("Content-Type", "text/plain")
.status(400)
.send('Password has not been set yet. Please set a password and repeat the action');
res.setHeader("Content-Type", "text/plain").status(400).send("Password has not been set yet. Please set a password and repeat the action");
return;
}
const header = req.headers['trilium-cred'] || '';
const header = req.headers["trilium-cred"] || "";
if (typeof header !== "string") {
res.setHeader("Content-Type", "text/plain")
.status(400)
.send('Invalid data type for trilium-cred.');
res.setHeader("Content-Type", "text/plain").status(400).send("Invalid data type for trilium-cred.");
return;
}
const auth = Buffer.from(header, 'base64').toString();
const colonIndex = auth.indexOf(':');
const auth = Buffer.from(header, "base64").toString();
const colonIndex = auth.indexOf(":");
const password = colonIndex === -1 ? "" : auth.substr(colonIndex + 1);
// username is ignored
if (!passwordEncryptionService.verifyPassword(password)) {
res.setHeader("Content-Type", "text/plain")
.status(401)
.send('Incorrect password');
}
else {
res.setHeader("Content-Type", "text/plain").status(401).send("Incorrect password");
} else {
next();
}
}

View File

@@ -9,7 +9,7 @@ import config from "./config.js";
import axios from "axios";
import dayjs from "dayjs";
import xml2js from "xml2js";
import * as cheerio from 'cheerio';
import * as cheerio from "cheerio";
import cloningService from "./cloning.js";
import appInfo from "./app_info.js";
import searchService from "./search/services/search.js";
@@ -31,11 +31,10 @@ import BAttachment from "../becca/entities/battachment.js";
import BRevision from "../becca/entities/brevision.js";
import BEtapiToken from "../becca/entities/betapi_token.js";
import BOption from "../becca/entities/boption.js";
import { AttributeRow } from '../becca/entities/rows.js';
import Becca from '../becca/becca-interface.js';
import { NoteParams } from './note-interface.js';
import { ApiParams } from './backend_script_api_interface.js';
import { AttributeRow } from "../becca/entities/rows.js";
import Becca from "../becca/becca-interface.js";
import { NoteParams } from "./note-interface.js";
import { ApiParams } from "./backend_script_api_interface.js";
/**
* A whole number
@@ -61,51 +60,51 @@ interface NoteAndBranch {
interface Api {
/**
* Note where the script started executing (entrypoint).
* As an analogy, in C this would be the file which contains the main() function of the current process.
*/
* Note where the script started executing (entrypoint).
* As an analogy, in C this would be the file which contains the main() function of the current process.
*/
startNote?: BNote | null;
/**
* Note where the script is currently executing. This comes into play when your script is spread in multiple code
* notes, the script starts in "startNote", but then through function calls may jump into another note (currentNote).
* A similar concept in C would be __FILE__
* Don't mix this up with the concept of active note.
*/
* Note where the script is currently executing. This comes into play when your script is spread in multiple code
* notes, the script starts in "startNote", but then through function calls may jump into another note (currentNote).
* A similar concept in C would be __FILE__
* Don't mix this up with the concept of active note.
*/
currentNote: BNote;
/**
* Entity whose event triggered this execution
*/
* Entity whose event triggered this execution
*/
originEntity?: AbstractBeccaEntity<any> | null;
/**
* Axios library for HTTP requests. See {@link https://axios-http.com} for documentation
* @deprecated use native (browser compatible) fetch() instead
*/
* Axios library for HTTP requests. See {@link https://axios-http.com} for documentation
* @deprecated use native (browser compatible) fetch() instead
*/
axios: typeof axios;
/**
* day.js library for date manipulation. See {@link https://day.js.org} for documentation
*/
* day.js library for date manipulation. See {@link https://day.js.org} for documentation
*/
dayjs: typeof dayjs;
/**
* xml2js library for XML parsing. See {@link https://github.com/Leonidas-from-XIV/node-xml2js} for documentation
*/
* xml2js library for XML parsing. See {@link https://github.com/Leonidas-from-XIV/node-xml2js} for documentation
*/
xml2js: typeof xml2js;
/**
* cheerio library for HTML parsing and manipulation. See {@link https://cheerio.js.org} for documentation
*/
* cheerio library for HTML parsing and manipulation. See {@link https://cheerio.js.org} for documentation
*/
cheerio: typeof cheerio;
/**
* Instance name identifies particular Trilium instance. It can be useful for scripts
* if some action needs to happen on only one specific instance.
*/
* Instance name identifies particular Trilium instance. It can be useful for scripts
* if some action needs to happen on only one specific instance.
*/
getInstanceName(): string | null;
getNote(noteId: string): BNote | null;
@@ -120,208 +119,224 @@ interface Api {
getAttribute(attributeId: string): BAttribute | null;
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "#dateModified =* MONTH AND #log". See {@link https://triliumnext.github.io/Docs/Wiki/search.html} for full documentation for all options
*/
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "#dateModified =* MONTH AND #log". See {@link https://triliumnext.github.io/Docs/Wiki/search.html} for full documentation for all options
*/
searchForNotes(query: string, searchParams: SearchParams): BNote[];
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "#dateModified =* MONTH AND #log". See {@link https://triliumnext.github.io/Docs/Wiki/search.html} for full documentation for all options
*/
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "#dateModified =* MONTH AND #log". See {@link https://triliumnext.github.io/Docs/Wiki/search.html} for full documentation for all options
*/
searchForNote(query: string, searchParams: SearchParams): BNote | null;
/**
* Retrieves notes with given label name & value
*
* @param name - attribute name
* @param value - attribute value
*/
* Retrieves notes with given label name & value
*
* @param name - attribute name
* @param value - attribute value
*/
getNotesWithLabel(name: string, value?: string): BNote[];
/**
* Retrieves first note with given label name & value
*
* @param name - attribute name
* @param value - attribute value
*/
* Retrieves first note with given label name & value
*
* @param name - attribute name
* @param value - attribute value
*/
getNoteWithLabel(name: string, value?: string): BNote | null;
/**
* If there's no branch between note and parent note, create one. Otherwise, do nothing. Returns the new or existing branch.
*
* @param prefix - if branch is created between note and parent note, set this prefix
*/
ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefix: string): {
branch: BBranch | null
* If there's no branch between note and parent note, create one. Otherwise, do nothing. Returns the new or existing branch.
*
* @param prefix - if branch is created between note and parent note, set this prefix
*/
ensureNoteIsPresentInParent(
noteId: string,
parentNoteId: string,
prefix: string
): {
branch: BBranch | null;
};
/**
* If there's a branch between note and parent note, remove it. Otherwise, do nothing.
*/
* If there's a branch between note and parent note, remove it. Otherwise, do nothing.
*/
ensureNoteIsAbsentFromParent(noteId: string, parentNoteId: string): void;
/**
* Based on the value, either create or remove branch between note and parent note.
*
* @param present - true if we want the branch to exist, false if we want it gone
* @param prefix - if branch is created between note and parent note, set this prefix
*/
* Based on the value, either create or remove branch between note and parent note.
*
* @param present - true if we want the branch to exist, false if we want it gone
* @param prefix - if branch is created between note and parent note, set this prefix
*/
toggleNoteInParent(present: true, noteId: string, parentNoteId: string, prefix: string): void;
/**
* Create text note. See also createNewNote() for more options.
*/
* Create text note. See also createNewNote() for more options.
*/
createTextNote(parentNoteId: string, title: string, content: string): NoteAndBranch;
/**
* Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and
* JSON MIME type. See also createNewNote() for more options.
*/
* Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and
* JSON MIME type. See also createNewNote() for more options.
*/
createDataNote(parentNoteId: string, title: string, content: {}): NoteAndBranch;
/**
* @returns object contains newly created entities note and branch
*/
* @returns object contains newly created entities note and branch
*/
createNewNote(params: NoteParams): NoteAndBranch;
/**
* @deprecated please use createTextNote() with similar API for simpler use cases or createNewNote() for more complex needs
* @param parentNoteId - create new note under this parent
* @returns object contains newly created entities note and branch
*/
createNote(parentNoteId: string, title: string, content: string, extraOptions: Omit<NoteParams, "title" | "content" | "type" | "parentNoteId"> & {
/** should the note be JSON */
json?: boolean;
attributes?: AttributeRow[]
}): NoteAndBranch;
* @deprecated please use createTextNote() with similar API for simpler use cases or createNewNote() for more complex needs
* @param parentNoteId - create new note under this parent
* @returns object contains newly created entities note and branch
*/
createNote(
parentNoteId: string,
title: string,
content: string,
extraOptions: Omit<NoteParams, "title" | "content" | "type" | "parentNoteId"> & {
/** should the note be JSON */
json?: boolean;
attributes?: AttributeRow[];
}
): NoteAndBranch;
logMessages: Record<string, string[]>;
logSpacedUpdates: Record<string, SpacedUpdate>;
/**
* Log given message to trilium logs and log pane in UI
*/
* Log given message to trilium logs and log pane in UI
*/
log(message: string): void;
/**
* Returns root note of the calendar.
*/
* Returns root note of the calendar.
*/
getRootCalendarNote(): BNote | null;
/**
* Returns day note for given date. If such note doesn't exist, it is created.
*
* @method
* @param date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
* Returns day note for given date. If such note doesn't exist, it is created.
*
* @method
* @param date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getDayNote(date: string, rootNote?: BNote): BNote | null;
/**
* Returns today's day note. If such note doesn't exist, it is created.
*
* @param rootNote specify calendar root note, normally leave empty to use the default calendar
*/
* Returns today's day note. If such note doesn't exist, it is created.
*
* @param rootNote specify calendar root note, normally leave empty to use the default calendar
*/
getTodayNote(rootNote?: BNote): BNote | null;
/**
* Returns note for the first date of the week of the given date.
*
* @param date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getWeekNote(date: string, options: {
// TODO: Deduplicate type with date_notes.ts once ES modules are added.
/** either "monday" (default) or "sunday" */
startOfTheWeek: "monday" | "sunday";
}, rootNote: BNote): BNote | null;
* Returns note for the first date of the week of the given date.
*
* @param date in YYYY-MM-DD format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getWeekNote(
date: string,
options: {
// TODO: Deduplicate type with date_notes.ts once ES modules are added.
/** either "monday" (default) or "sunday" */
startOfTheWeek: "monday" | "sunday";
},
rootNote: BNote
): BNote | null;
/**
* Returns month note for given date. If such a note doesn't exist, it is created.
*
* @param date in YYYY-MM format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
* Returns month note for given date. If such a note doesn't exist, it is created.
*
* @param date in YYYY-MM format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getMonthNote(date: string, rootNote: BNote): BNote | null;
/**
* Returns year note for given year. If such a note doesn't exist, it is created.
*
* @param year in YYYY format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
* Returns year note for given year. If such a note doesn't exist, it is created.
*
* @param year in YYYY format
* @param rootNote - specify calendar root note, normally leave empty to use the default calendar
*/
getYearNote(year: string, rootNote?: BNote): BNote | null;
/**
* Sort child notes of a given note.
*/
sortNotes(parentNoteId: string, sortConfig: {
/** 'title', 'dateCreated', 'dateModified' or a label name
* See {@link https://triliumnext.github.io/Docs/Wiki/sorting.html} for details. */
sortBy?: string;
reverse?: boolean;
foldersFirst?: boolean;
}): void;
* Sort child notes of a given note.
*/
sortNotes(
parentNoteId: string,
sortConfig: {
/** 'title', 'dateCreated', 'dateModified' or a label name
* See {@link https://triliumnext.github.io/Docs/Wiki/sorting.html} for details. */
sortBy?: string;
reverse?: boolean;
foldersFirst?: boolean;
}
): void;
/**
* This method finds note by its noteId and prefix and either sets it to the given parentNoteId
* or removes the branch (if parentNoteId is not given).
*
* This method looks similar to toggleNoteInParent() but differs because we're looking up branch by prefix.
*
* @deprecated this method is pretty confusing and serves specialized purpose only
*/
* This method finds note by its noteId and prefix and either sets it to the given parentNoteId
* or removes the branch (if parentNoteId is not given).
*
* This method looks similar to toggleNoteInParent() but differs because we're looking up branch by prefix.
*
* @deprecated this method is pretty confusing and serves specialized purpose only
*/
setNoteToParent(noteId: string, prefix: string, parentNoteId: string | null): void;
/**
* This functions wraps code which is supposed to be running in transaction. If transaction already
* exists, then we'll use that transaction.
*
* @param func
* @returns result of func callback
*/
* This functions wraps code which is supposed to be running in transaction. If transaction already
* exists, then we'll use that transaction.
*
* @param func
* @returns result of func callback
*/
transactional(func: () => void): any;
/**
* Return randomly generated string of given length. This random string generation is NOT cryptographically secure.
*
* @param length of the string
* @returns random string
*/
* Return randomly generated string of given length. This random string generation is NOT cryptographically secure.
*
* @param length of the string
* @returns random string
*/
randomString(length: number): string;
/**
* @param to escape
* @returns escaped string
*/
* @param to escape
* @returns escaped string
*/
escapeHtml(string: string): string;
/**
* @param string to unescape
* @returns unescaped string
*/
* @param string to unescape
* @returns unescaped string
*/
unescapeHtml(string: string): string;
/**
* sql
* @type {module:sql}
*/
* sql
* @type {module:sql}
*/
sql: any;
getAppInfo(): typeof appInfo;
/**
* Creates a new launcher to the launchbar. If the launcher (id) already exists, it will be updated.
*/
* Creates a new launcher to the launchbar. If the launcher (id) already exists, it will be updated.
*/
createOrUpdateLauncher(opts: {
/** id of the launcher, only alphanumeric at least 6 characters long */
id: string;
/** one of
* - "note" - activating the launcher will navigate to the target note (specified in targetNoteId param)
* - "script" - activating the launcher will execute the script (specified in scriptNoteId param)
* - "customWidget" - the launcher will be rendered with a custom widget (specified in widgetNoteId param)
*/
* - "note" - activating the launcher will navigate to the target note (specified in targetNoteId param)
* - "script" - activating the launcher will execute the script (specified in scriptNoteId param)
* - "customWidget" - the launcher will be rendered with a custom widget (specified in widgetNoteId param)
*/
type: "note" | "script" | "customWidget";
title: string;
/** if true, will be created in the "Visible launchers", otherwise in "Available launchers" */
@@ -339,44 +354,44 @@ interface Api {
}): { note: BNote };
/**
* @param format - either 'html' or 'markdown'
*/
* @param format - either 'html' or 'markdown'
*/
exportSubtreeToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string): Promise<void>;
/**
* Executes given anonymous function on the frontend(s).
* Internally, this serializes the anonymous function into string and sends it to frontend(s) via WebSocket.
* Note that there can be multiple connected frontend instances (e.g. in different tabs). In such case, all
* instances execute the given function.
*
* @param script - script to be executed on the frontend
* @param params - list of parameters to the anonymous function to be sent to frontend
* @returns no return value is provided.
*/
* Executes given anonymous function on the frontend(s).
* Internally, this serializes the anonymous function into string and sends it to frontend(s) via WebSocket.
* Note that there can be multiple connected frontend instances (e.g. in different tabs). In such case, all
* instances execute the given function.
*
* @param script - script to be executed on the frontend
* @param params - list of parameters to the anonymous function to be sent to frontend
* @returns no return value is provided.
*/
runOnFrontend(script: () => void | string, params: []): void;
/**
* Sync process can make data intermittently inconsistent. Scripts which require strong data consistency
* can use this function to wait for a possible sync process to finish and prevent new sync process from starting
* while it is running.
*
* Because this is an async process, the inner callback doesn't have automatic transaction handling, so in case
* you need to make some DB changes, you need to surround your call with api.transactional(...)
*
* @param callback - function to be executed while sync process is not running
* @returns resolves once the callback is finished (callback is awaited)
*/
* Sync process can make data intermittently inconsistent. Scripts which require strong data consistency
* can use this function to wait for a possible sync process to finish and prevent new sync process from starting
* while it is running.
*
* Because this is an async process, the inner callback doesn't have automatic transaction handling, so in case
* you need to make some DB changes, you need to surround your call with api.transactional(...)
*
* @param callback - function to be executed while sync process is not running
* @returns resolves once the callback is finished (callback is awaited)
*/
runOutsideOfSync(callback: () => void): Promise<void>;
/**
* @param backupName - If the backupName is e.g. "now", then the backup will be written to "backup-now.db" file
* @returns resolves once the backup is finished
*/
* @param backupName - If the backupName is e.g. "now", then the backup will be written to "backup-now.db" file
* @returns resolves once the backup is finished
*/
backupNow(backupName: string): Promise<string>;
/**
* This object contains "at your risk" and "no BC guarantees" objects for advanced use cases.
*/
* This object contains "at your risk" and "no BC guarantees" objects for advanced use cases.
*/
__private: {
/** provides access to the backend in-memory object graph, see {@link Becca} */
becca: Becca;
@@ -405,17 +420,17 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
this.dayjs = dayjs;
this.xml2js = xml2js;
this.cheerio = cheerio;
this.getInstanceName = () => config.General ? config.General.instanceName : null;
this.getNote = noteId => becca.getNote(noteId);
this.getBranch = branchId => becca.getBranch(branchId);
this.getAttribute = attributeId => becca.getAttribute(attributeId);
this.getAttachment = attachmentId => becca.getAttachment(attachmentId);
this.getRevision = revisionId => becca.getRevision(revisionId);
this.getEtapiToken = etapiTokenId => becca.getEtapiToken(etapiTokenId);
this.getInstanceName = () => (config.General ? config.General.instanceName : null);
this.getNote = (noteId) => becca.getNote(noteId);
this.getBranch = (branchId) => becca.getBranch(branchId);
this.getAttribute = (attributeId) => becca.getAttribute(attributeId);
this.getAttachment = (attachmentId) => becca.getAttachment(attachmentId);
this.getRevision = (revisionId) => becca.getRevision(revisionId);
this.getEtapiToken = (etapiTokenId) => becca.getEtapiToken(etapiTokenId);
this.getEtapiTokens = () => becca.getEtapiTokens();
this.getOption = optionName => becca.getOption(optionName);
this.getOption = (optionName) => becca.getOption(optionName);
this.getOptions = () => optionsService.getOptions();
this.getAttribute = attributeId => becca.getAttribute(attributeId);
this.getAttribute = (attributeId) => becca.getAttribute(attributeId);
this.searchForNotes = (query, searchParams = {}) => {
if (searchParams.includeArchivedNotes === undefined) {
@@ -426,13 +441,11 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
searchParams.ignoreHoistedNote = true;
}
const noteIds = searchService.findResultsWithQuery(query, new SearchContext(searchParams))
.map(sr => sr.noteId);
const noteIds = searchService.findResultsWithQuery(query, new SearchContext(searchParams)).map((sr) => sr.noteId);
return becca.getNotes(noteIds);
};
this.searchForNote = (query, searchParams = {}) => {
const notes = this.searchForNotes(query, searchParams);
@@ -444,20 +457,22 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
this.ensureNoteIsPresentInParent = cloningService.ensureNoteIsPresentInParent;
this.ensureNoteIsAbsentFromParent = cloningService.ensureNoteIsAbsentFromParent;
this.toggleNoteInParent = cloningService.toggleNoteInParent;
this.createTextNote = (parentNoteId, title, content = '') => noteService.createNewNote({
parentNoteId,
title,
content,
type: 'text'
});
this.createTextNote = (parentNoteId, title, content = "") =>
noteService.createNewNote({
parentNoteId,
title,
content,
type: "text"
});
this.createDataNote = (parentNoteId, title, content = {}) => noteService.createNewNote({
parentNoteId,
title,
content: JSON.stringify(content, null, '\t'),
type: 'code',
mime: 'application/json'
});
this.createDataNote = (parentNoteId, title, content = {}) =>
noteService.createNewNote({
parentNoteId,
title,
content: JSON.stringify(content, null, "\t"),
type: "code",
mime: "application/json"
});
this.createNewNote = noteService.createNewNote;
@@ -476,15 +491,14 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
};
// code note type can be inherited, otherwise "text" is the default
extraOptions.type = parentNote.type === 'code' ? 'code' : 'text';
extraOptions.mime = parentNote.type === 'code' ? parentNote.mime : 'text/html';
extraOptions.type = parentNote.type === "code" ? "code" : "text";
extraOptions.mime = parentNote.type === "code" ? parentNote.mime : "text/html";
if (_extraOptions.json) {
extraOptions.content = JSON.stringify(content || {}, null, '\t');
extraOptions.type = 'code';
extraOptions.mime = 'application/json';
}
else {
extraOptions.content = JSON.stringify(content || {}, null, "\t");
extraOptions.type = "code";
extraOptions.mime = "application/json";
} else {
extraOptions.content = content;
}
@@ -508,7 +522,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
this.logMessages = {};
this.logSpacedUpdates = {};
this.log = message => {
this.log = (message) => {
log.info(message);
if (!this.startNote) {
@@ -518,16 +532,18 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
const { noteId } = this.startNote;
this.logMessages[noteId] = this.logMessages[noteId] || [];
this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
const messages = this.logMessages[noteId];
this.logMessages[noteId] = [];
this.logSpacedUpdates[noteId] =
this.logSpacedUpdates[noteId] ||
new SpacedUpdate(() => {
const messages = this.logMessages[noteId];
this.logMessages[noteId] = [];
ws.sendMessageToAllClients({
type: 'api-log-messages',
noteId,
messages
});
}, 100);
ws.sendMessageToAllClients({
type: "api-log-messages",
noteId,
messages
});
}, 100);
this.logMessages[noteId].push(message);
this.logSpacedUpdates[noteId].scheduleUpdate();
@@ -540,12 +556,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
this.getMonthNote = dateNoteService.getMonthNote;
this.getYearNote = dateNoteService.getYearNote;
this.sortNotes = (parentNoteId, sortConfig = {}) => treeService.sortNotes(
parentNoteId,
sortConfig.sortBy || "title",
!!sortConfig.reverse,
!!sortConfig.foldersFirst
);
this.sortNotes = (parentNoteId, sortConfig = {}) => treeService.sortNotes(parentNoteId, sortConfig.sortBy || "title", !!sortConfig.reverse, !!sortConfig.foldersFirst);
this.setNoteToParent = treeService.setNoteToParent;
this.transactional = sql.transactional;
@@ -555,26 +566,41 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
this.sql = sql;
this.getAppInfo = () => appInfo;
this.createOrUpdateLauncher = (opts) => {
if (!opts.id) {
throw new Error("ID is a mandatory parameter for api.createOrUpdateLauncher(opts)");
}
if (!opts.id.match(/[a-z0-9]{6,1000}/i)) {
throw new Error(`ID must be an alphanumeric string at least 6 characters long.`);
}
if (!opts.type) {
throw new Error("Launcher Type is a mandatory parameter for api.createOrUpdateLauncher(opts)");
}
if (!["note", "script", "customWidget"].includes(opts.type)) {
throw new Error(`Given launcher type '${opts.type}'`);
}
if (!opts.title?.trim()) {
throw new Error("Title is a mandatory parameter for api.createOrUpdateLauncher(opts)");
}
if (opts.type === "note" && !opts.targetNoteId) {
throw new Error("targetNoteId is mandatory for launchers of type 'note'");
}
if (opts.type === "script" && !opts.scriptNoteId) {
throw new Error("scriptNoteId is mandatory for launchers of type 'script'");
}
if (opts.type === "customWidget" && !opts.widgetNoteId) {
throw new Error("widgetNoteId is mandatory for launchers of type 'customWidget'");
}
this.createOrUpdateLauncher = opts => {
if (!opts.id) { throw new Error("ID is a mandatory parameter for api.createOrUpdateLauncher(opts)"); }
if (!opts.id.match(/[a-z0-9]{6,1000}/i)) { throw new Error(`ID must be an alphanumeric string at least 6 characters long.`); }
if (!opts.type) { throw new Error("Launcher Type is a mandatory parameter for api.createOrUpdateLauncher(opts)"); }
if (!["note", "script", "customWidget"].includes(opts.type)) { throw new Error(`Given launcher type '${opts.type}'`); }
if (!opts.title?.trim()) { throw new Error("Title is a mandatory parameter for api.createOrUpdateLauncher(opts)"); }
if (opts.type === 'note' && !opts.targetNoteId) { throw new Error("targetNoteId is mandatory for launchers of type 'note'"); }
if (opts.type === 'script' && !opts.scriptNoteId) { throw new Error("scriptNoteId is mandatory for launchers of type 'script'"); }
if (opts.type === 'customWidget' && !opts.widgetNoteId) { throw new Error("widgetNoteId is mandatory for launchers of type 'customWidget'"); }
const parentNoteId = opts.isVisible ? '_lbVisibleLaunchers' : '_lbAvailableLaunchers';
const noteId = 'al_' + opts.id;
const parentNoteId = opts.isVisible ? "_lbVisibleLaunchers" : "_lbAvailableLaunchers";
const noteId = "al_" + opts.id;
const launcherNote =
becca.getNote(noteId) ||
specialNotesService.createLauncher({
noteId: noteId,
parentNoteId: parentNoteId,
launcherType: opts.type,
launcherType: opts.type
}).note;
if (launcherNote.title !== opts.title) {
@@ -590,26 +616,26 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
}
}
if (opts.type === 'note') {
launcherNote.setRelation('target', opts.targetNoteId);
} else if (opts.type === 'script') {
launcherNote.setRelation('script', opts.scriptNoteId);
} else if (opts.type === 'customWidget') {
launcherNote.setRelation('widget', opts.widgetNoteId);
if (opts.type === "note") {
launcherNote.setRelation("target", opts.targetNoteId);
} else if (opts.type === "script") {
launcherNote.setRelation("script", opts.scriptNoteId);
} else if (opts.type === "customWidget") {
launcherNote.setRelation("widget", opts.widgetNoteId);
} else {
throw new Error(`Unrecognized launcher type '${opts.type}'`);
}
if (opts.keyboardShortcut) {
launcherNote.setLabel('keyboardShortcut', opts.keyboardShortcut);
launcherNote.setLabel("keyboardShortcut", opts.keyboardShortcut);
} else {
launcherNote.removeLabel('keyboardShortcut');
launcherNote.removeLabel("keyboardShortcut");
}
if (opts.icon) {
launcherNote.setLabel('iconClass', `bx ${opts.icon}`);
launcherNote.setLabel("iconClass", `bx ${opts.icon}`);
} else {
launcherNote.removeLabel('iconClass');
launcherNote.removeLabel("iconClass");
}
return { note: launcherNote };
@@ -626,7 +652,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
}
ws.sendMessageToAllClients({
type: 'execute-script',
type: "execute-script",
script: script,
params: prepareParams(params),
startNoteId: this.startNote?.noteId,
@@ -640,11 +666,10 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
return params;
}
return params.map(p => {
return params.map((p) => {
if (typeof p === "function") {
return `!@#Function: ${p.toString()}`;
}
else {
} else {
return p;
}
});
@@ -656,9 +681,9 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
this.__private = {
becca
}
};
}
export default BackendScriptApi as any as {
new (currentNote: BNote, apiParams: ApiParams): Api
new (currentNote: BNote, apiParams: ApiParams): Api;
};

View File

@@ -5,7 +5,7 @@ import BNote from "../becca/entities/bnote.js";
export interface ApiParams {
startNote?: BNote | null;
originEntity?: AbstractBeccaEntity<any> | null;
pathParams?: string[],
req?: Request,
res?: Response
pathParams?: string[];
req?: Request;
res?: Response;
}

View File

@@ -11,30 +11,31 @@ import sql from "./sql.js";
import path from "path";
import type { OptionNames } from "./options_interface.js";
type BackupType = ("daily" | "weekly" | "monthly");
type BackupType = "daily" | "weekly" | "monthly";
function getExistingBackups() {
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
return [];
}
return fs.readdirSync(dataDir.BACKUP_DIR)
.filter(fileName => fileName.includes("backup"))
.map(fileName => {
return fs
.readdirSync(dataDir.BACKUP_DIR)
.filter((fileName) => fileName.includes("backup"))
.map((fileName) => {
const filePath = path.resolve(dataDir.BACKUP_DIR, fileName);
const stat = fs.statSync(filePath)
const stat = fs.statSync(filePath);
return {fileName, filePath, mtime: stat.mtime};
return { fileName, filePath, mtime: stat.mtime };
});
}
function regularBackup() {
cls.init(() => {
periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
periodBackup("lastDailyBackupDate", "daily", 24 * 3600);
periodBackup('lastWeeklyBackupDate', 'weekly', 7 * 24 * 3600);
periodBackup("lastWeeklyBackupDate", "weekly", 7 * 24 * 3600);
periodBackup('lastMonthlyBackupDate', 'monthly', 30 * 24 * 3600);
periodBackup("lastMonthlyBackupDate", "monthly", 30 * 24 * 3600);
});
}

View File

@@ -50,7 +50,7 @@ function processContent(content: Buffer | string | null, isProtected: boolean, i
}
}
function calculateContentHash({blobId, content}: Blob) {
function calculateContentHash({ blobId, content }: Blob) {
return hash(`${blobId}|${content.toString()}`);
}

View File

@@ -4,7 +4,7 @@ import BBranch from "../becca/entities/bbranch.js";
function moveBranchToNote(branchToMove: BBranch, targetParentNoteId: string) {
if (branchToMove.parentNoteId === targetParentNoteId) {
return {success: true}; // no-op
return { success: true }; // no-op
}
const validationResult = treeService.validateParentChild(targetParentNoteId, branchToMove.noteId, branchToMove.branchId);
@@ -13,7 +13,7 @@ function moveBranchToNote(branchToMove: BBranch, targetParentNoteId: string) {
return [200, validationResult];
}
const maxNotePos = sql.getValue<number | null>('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [targetParentNoteId]);
const maxNotePos = sql.getValue<number | null>("SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [targetParentNoteId]);
const newNotePos = !maxNotePos ? 0 : maxNotePos + 10;
const newBranch = branchToMove.createClone(targetParentNoteId, newNotePos);

View File

@@ -1,99 +1,99 @@
export default [
// label names
{ type: 'label', name: 'inbox' },
{ type: 'label', name: 'disableVersioning' },
{ type: 'label', name: 'calendarRoot' },
{ type: 'label', name: 'archived' },
{ type: 'label', name: 'excludeFromExport' },
{ type: 'label', name: 'disableInclusion' },
{ type: 'label', name: 'appCss' },
{ type: 'label', name: 'appTheme' },
{ type: 'label', name: 'appThemeBase' },
{ type: 'label', name: 'hidePromotedAttributes' },
{ type: 'label', name: 'readOnly' },
{ type: 'label', name: 'autoReadOnlyDisabled' },
{ type: 'label', name: 'cssClass' },
{ type: 'label', name: 'iconClass' },
{ type: 'label', name: 'keyboardShortcut' },
{ type: 'label', name: 'run', isDangerous: true },
{ type: 'label', name: 'runOnInstance', isDangerous: false },
{ type: 'label', name: 'runAtHour', isDangerous: false },
{ type: 'label', name: 'customRequestHandler', isDangerous: true },
{ type: 'label', name: 'customResourceProvider', isDangerous: true },
{ type: 'label', name: 'widget', isDangerous: true },
{ type: 'label', name: 'noteInfoWidgetDisabled' },
{ type: 'label', name: 'linkMapWidgetDisabled' },
{ type: 'label', name: 'revisionsWidgetDisabled' },
{ type: 'label', name: 'whatLinksHereWidgetDisabled' },
{ type: 'label', name: 'similarNotesWidgetDisabled' },
{ type: 'label', name: 'workspace' },
{ type: 'label', name: 'workspaceIconClass' },
{ type: 'label', name: 'workspaceTabBackgroundColor' },
{ type: 'label', name: 'workspaceCalendarRoot' },
{ type: 'label', name: 'workspaceTemplate' },
{ type: 'label', name: 'searchHome' },
{ type: 'label', name: 'workspaceInbox' },
{ type: 'label', name: 'workspaceSearchHome' },
{ type: 'label', name: 'sqlConsoleHome' },
{ type: 'label', name: 'datePattern' },
{ type: 'label', name: 'pageSize' },
{ type: 'label', name: 'viewType' },
{ type: 'label', name: 'mapRootNoteId' },
{ type: 'label', name: 'bookmarkFolder' },
{ type: 'label', name: 'sorted' },
{ type: 'label', name: 'sortDirection' },
{ type: 'label', name: 'sortFoldersFirst' },
{ type: 'label', name: 'sortNatural' },
{ type: 'label', name: 'sortLocale' },
{ type: 'label', name: 'top' },
{ 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' },
{ type: 'label', name: 'shareDescription' },
{ type: 'label', name: 'shareRaw' },
{ type: 'label', name: 'shareDisallowRobotIndexing' },
{ type: 'label', name: 'shareCredentials' },
{ type: 'label', name: 'shareIndex' },
{ type: 'label', name: 'displayRelations' },
{ type: 'label', name: 'hideRelations' },
{ type: 'label', name: 'titleTemplate', isDangerous: true },
{ type: 'label', name: 'template' },
{ type: 'label', name: 'toc' },
{ type: 'label', name: 'color' },
{ type: 'label', name: 'keepCurrentHoisting'},
{ type: 'label', name: 'executeButton'},
{ type: 'label', name: 'executeDescription'},
{ type: 'label', name: 'newNotesOnTop'},
{ type: 'label', name: 'clipperInbox'},
{ type: 'label', name: 'webViewSrc', isDangerous: true },
{ type: 'label', name: 'hideHighlightWidget' },
{ type: "label", name: "inbox" },
{ type: "label", name: "disableVersioning" },
{ type: "label", name: "calendarRoot" },
{ type: "label", name: "archived" },
{ type: "label", name: "excludeFromExport" },
{ type: "label", name: "disableInclusion" },
{ type: "label", name: "appCss" },
{ type: "label", name: "appTheme" },
{ type: "label", name: "appThemeBase" },
{ type: "label", name: "hidePromotedAttributes" },
{ type: "label", name: "readOnly" },
{ type: "label", name: "autoReadOnlyDisabled" },
{ type: "label", name: "cssClass" },
{ type: "label", name: "iconClass" },
{ type: "label", name: "keyboardShortcut" },
{ type: "label", name: "run", isDangerous: true },
{ type: "label", name: "runOnInstance", isDangerous: false },
{ type: "label", name: "runAtHour", isDangerous: false },
{ type: "label", name: "customRequestHandler", isDangerous: true },
{ type: "label", name: "customResourceProvider", isDangerous: true },
{ type: "label", name: "widget", isDangerous: true },
{ type: "label", name: "noteInfoWidgetDisabled" },
{ type: "label", name: "linkMapWidgetDisabled" },
{ type: "label", name: "revisionsWidgetDisabled" },
{ type: "label", name: "whatLinksHereWidgetDisabled" },
{ type: "label", name: "similarNotesWidgetDisabled" },
{ type: "label", name: "workspace" },
{ type: "label", name: "workspaceIconClass" },
{ type: "label", name: "workspaceTabBackgroundColor" },
{ type: "label", name: "workspaceCalendarRoot" },
{ type: "label", name: "workspaceTemplate" },
{ type: "label", name: "searchHome" },
{ type: "label", name: "workspaceInbox" },
{ type: "label", name: "workspaceSearchHome" },
{ type: "label", name: "sqlConsoleHome" },
{ type: "label", name: "datePattern" },
{ type: "label", name: "pageSize" },
{ type: "label", name: "viewType" },
{ type: "label", name: "mapRootNoteId" },
{ type: "label", name: "bookmarkFolder" },
{ type: "label", name: "sorted" },
{ type: "label", name: "sortDirection" },
{ type: "label", name: "sortFoldersFirst" },
{ type: "label", name: "sortNatural" },
{ type: "label", name: "sortLocale" },
{ type: "label", name: "top" },
{ 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" },
{ type: "label", name: "shareDescription" },
{ type: "label", name: "shareRaw" },
{ type: "label", name: "shareDisallowRobotIndexing" },
{ type: "label", name: "shareCredentials" },
{ type: "label", name: "shareIndex" },
{ type: "label", name: "displayRelations" },
{ type: "label", name: "hideRelations" },
{ type: "label", name: "titleTemplate", isDangerous: true },
{ type: "label", name: "template" },
{ type: "label", name: "toc" },
{ type: "label", name: "color" },
{ type: "label", name: "keepCurrentHoisting" },
{ type: "label", name: "executeButton" },
{ type: "label", name: "executeDescription" },
{ type: "label", name: "newNotesOnTop" },
{ type: "label", name: "clipperInbox" },
{ type: "label", name: "webViewSrc", isDangerous: true },
{ type: "label", name: "hideHighlightWidget" },
// relation names
{ type: 'relation', name: 'internalLink' },
{ type: 'relation', name: 'imageLink' },
{ type: 'relation', name: 'relationMapLink' },
{ type: 'relation', name: 'includeMapLink' },
{ type: 'relation', name: 'runOnNoteCreation', isDangerous: true },
{ type: 'relation', name: 'runOnNoteTitleChange', isDangerous: true },
{ type: 'relation', name: 'runOnNoteChange', isDangerous: true },
{ type: 'relation', name: 'runOnNoteContentChange', isDangerous: true },
{ type: 'relation', name: 'runOnNoteDeletion', isDangerous: true },
{ type: 'relation', name: 'runOnBranchCreation', isDangerous: true },
{ type: 'relation', name: 'runOnBranchChange', isDangerous: true },
{ type: 'relation', name: 'runOnBranchDeletion', isDangerous: true },
{ type: 'relation', name: 'runOnChildNoteCreation', isDangerous: true },
{ type: 'relation', name: 'runOnAttributeCreation', isDangerous: true },
{ type: 'relation', name: 'runOnAttributeChange', isDangerous: true },
{ type: 'relation', name: 'template' },
{ type: 'relation', name: 'inherit' },
{ type: 'relation', name: 'widget', isDangerous: true },
{ type: 'relation', name: 'renderNote', isDangerous: true },
{ type: 'relation', name: 'shareCss' },
{ type: 'relation', name: 'shareJs' },
{ type: 'relation', name: 'shareTemplate' },
{ type: 'relation', name: 'shareFavicon' },
{ type: "relation", name: "internalLink" },
{ type: "relation", name: "imageLink" },
{ type: "relation", name: "relationMapLink" },
{ type: "relation", name: "includeMapLink" },
{ type: "relation", name: "runOnNoteCreation", isDangerous: true },
{ type: "relation", name: "runOnNoteTitleChange", isDangerous: true },
{ type: "relation", name: "runOnNoteChange", isDangerous: true },
{ type: "relation", name: "runOnNoteContentChange", isDangerous: true },
{ type: "relation", name: "runOnNoteDeletion", isDangerous: true },
{ type: "relation", name: "runOnBranchCreation", isDangerous: true },
{ type: "relation", name: "runOnBranchChange", isDangerous: true },
{ type: "relation", name: "runOnBranchDeletion", isDangerous: true },
{ type: "relation", name: "runOnChildNoteCreation", isDangerous: true },
{ type: "relation", name: "runOnAttributeCreation", isDangerous: true },
{ type: "relation", name: "runOnAttributeChange", isDangerous: true },
{ type: "relation", name: "template" },
{ type: "relation", name: "inherit" },
{ type: "relation", name: "widget", isDangerous: true },
{ type: "relation", name: "renderNote", isDangerous: true },
{ type: "relation", name: "shareCss" },
{ type: "relation", name: "shareJs" },
{ type: "relation", name: "shareTemplate" },
{ type: "relation", name: "shareFavicon" }
];

View File

@@ -12,7 +12,6 @@ interface Action {
oldLabelName: string;
newLabelName: string;
relationName: string;
oldRelationName: string;
newRelationName: string;
@@ -37,8 +36,9 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
note.deleteNote(deleteId);
},
deleteRevisions: (action, note) => {
const revisionIds = note.getRevisions()
.map(rev => rev.revisionId)
const revisionIds = note
.getRevisions()
.map((rev) => rev.revisionId)
.filter((rev) => !!rev) as string[];
eraseService.eraseRevisions(revisionIds);
},
@@ -66,7 +66,7 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
renameLabel: (action, note) => {
for (const label of note.getOwnedLabels(action.oldLabelName)) {
// attribute name is immutable, renaming means delete old + create new
const newLabel = label.createClone('label', action.newLabelName, label.value);
const newLabel = label.createClone("label", action.newLabelName, label.value);
newLabel.save();
label.markAsDeleted();
@@ -75,7 +75,7 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
renameRelation: (action, note) => {
for (const relation of note.getOwnedRelations(action.oldRelationName)) {
// attribute name is immutable, renaming means delete old + create new
const newRelation = relation.createClone('relation', action.newRelationName, relation.value);
const newRelation = relation.createClone("relation", action.newRelationName, relation.value);
newRelation.save();
relation.markAsDeleted();
@@ -106,8 +106,7 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
if (note.getParentBranches().length > 1) {
res = cloningService.cloneNoteToParentNote(note.noteId, action.targetParentNoteId);
}
else {
} else {
res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId);
}
@@ -117,7 +116,7 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
},
executeScript: (action, note) => {
if (!action.script || !action.script.trim()) {
log.info("Ignoring executeScript since the script is empty.")
log.info("Ignoring executeScript since the script is empty.");
return;
}
@@ -129,8 +128,9 @@ const ACTION_HANDLERS: Record<string, ActionHandler> = {
};
function getActions(note: BNote) {
return note.getLabels('action')
.map(actionLabel => {
return note
.getLabels("action")
.map((actionLabel) => {
let action;
try {
@@ -147,7 +147,7 @@ function getActions(note: BNote) {
return action;
})
.filter(a => !!a);
.filter((a) => !!a);
}
function executeActions(note: BNote, searchResultNoteIds: string[] | Set<string>) {

View File

@@ -1,11 +1,11 @@
"use strict";
import sql from './sql.js';
import eventChangesService from './entity_changes.js';
import treeService from './tree.js';
import BBranch from '../becca/entities/bbranch.js';
import becca from '../becca/becca.js';
import log from './log.js';
import sql from "./sql.js";
import eventChangesService from "./entity_changes.js";
import treeService from "./tree.js";
import BBranch from "../becca/entities/bbranch.js";
import becca from "../becca/becca.js";
import log from "./log.js";
export interface CloneResponse {
success: boolean;
@@ -16,15 +16,15 @@ export interface CloneResponse {
function cloneNoteToParentNote(noteId: string, parentNoteId: string, prefix: string | null = null): CloneResponse {
if (!(noteId in becca.notes) || !(parentNoteId in becca.notes)) {
return { success: false, message: 'Note cannot be cloned because either the cloned note or the intended parent is deleted.' };
return { success: false, message: "Note cannot be cloned because either the cloned note or the intended parent is deleted." };
}
const parentNote = becca.getNote(parentNoteId);
if (!parentNote) {
return { success: false, message: 'Note cannot be cloned because the parent note could not be found.' };
return { success: false, message: "Note cannot be cloned because the parent note could not be found." };
}
if (parentNote.type === 'search') {
if (parentNote.type === "search") {
return {
success: false,
message: "Can't clone into a search note"
@@ -80,7 +80,7 @@ function ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefi
if (!parentNote) {
return { branch: null, success: false, message: "Can't find parent note." };
}
if (parentNote.type === 'search') {
if (parentNote.type === "search") {
return { branch: null, success: false, message: "Can't clone into a search note" };
}
@@ -125,14 +125,13 @@ function ensureNoteIsAbsentFromParent(noteId: string, parentNoteId: string) {
function toggleNoteInParent(present: boolean, noteId: string, parentNoteId: string, prefix?: string) {
if (present) {
return ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
}
else {
} else {
return ensureNoteIsAbsentFromParent(noteId, parentNoteId);
}
}
function cloneNoteAfter(noteId: string, afterBranchId: string) {
if (['_hidden', 'root'].includes(noteId)) {
if (["_hidden", "root"].includes(noteId)) {
return { success: false, message: `Cloning the note '${noteId}' is forbidden.` };
}
@@ -142,8 +141,8 @@ function cloneNoteAfter(noteId: string, afterBranchId: string) {
return { success: false, message: `Branch '${afterBranchId}' does not exist.` };
}
if (afterBranch.noteId === '_hidden') {
return { success: false, message: 'Cannot clone after the hidden branch.' };
if (afterBranch.noteId === "_hidden") {
return { success: false, message: "Cannot clone after the hidden branch." };
}
const afterNote = becca.getBranch(afterBranchId);
@@ -156,7 +155,7 @@ function cloneNoteAfter(noteId: string, afterBranchId: string) {
const parentNote = becca.getNote(afterNote.parentNoteId);
if (!parentNote || parentNote.type === 'search') {
if (!parentNote || parentNote.type === "search") {
return {
success: false,
message: "Can't clone into a search note"
@@ -171,8 +170,7 @@ function cloneNoteAfter(noteId: string, afterBranchId: string) {
// we don't change utcDateModified, so other changes are prioritized in case of conflict
// also we would have to sync all those modified branches otherwise hash checks would fail
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
[afterNote.parentNoteId, afterNote.notePosition]);
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [afterNote.parentNoteId, afterNote.notePosition]);
eventChangesService.putNoteReorderingEntityChange(afterNote.parentNoteId);

View File

@@ -1,5 +1,5 @@
import clsHooked from "cls-hooked";
import { EntityChange } from './entity_changes_interface.js';
import { EntityChange } from "./entity_changes_interface.js";
const namespace = clsHooked.createNamespace("trilium");
type Callback = (...args: any[]) => any;
@@ -12,11 +12,10 @@ function wrap(callback: Callback) {
return () => {
try {
init(callback);
}
catch (e: any) {
} catch (e: any) {
console.log(`Error occurred: ${e.message}: ${e.stack}`);
}
}
};
}
function get(key: string) {
@@ -28,64 +27,64 @@ function set(key: string, value: any) {
}
function getHoistedNoteId() {
return namespace.get('hoistedNoteId') || 'root';
return namespace.get("hoistedNoteId") || "root";
}
function getComponentId() {
return namespace.get('componentId');
return namespace.get("componentId");
}
function getLocalNowDateTime() {
return namespace.get('localNowDateTime');
return namespace.get("localNowDateTime");
}
function disableEntityEvents() {
namespace.set('disableEntityEvents', true);
namespace.set("disableEntityEvents", true);
}
function enableEntityEvents() {
namespace.set('disableEntityEvents', false);
namespace.set("disableEntityEvents", false);
}
function isEntityEventsDisabled() {
return !!namespace.get('disableEntityEvents');
return !!namespace.get("disableEntityEvents");
}
function setMigrationRunning(running: boolean) {
namespace.set('migrationRunning', !!running);
namespace.set("migrationRunning", !!running);
}
function isMigrationRunning() {
return !!namespace.get('migrationRunning');
return !!namespace.get("migrationRunning");
}
function disableSlowQueryLogging(disable: boolean) {
namespace.set('disableSlowQueryLogging', disable);
namespace.set("disableSlowQueryLogging", disable);
}
function isSlowQueryLoggingDisabled() {
return !!namespace.get('disableSlowQueryLogging');
return !!namespace.get("disableSlowQueryLogging");
}
function getAndClearEntityChangeIds() {
const entityChangeIds = namespace.get('entityChangeIds') || [];
const entityChangeIds = namespace.get("entityChangeIds") || [];
namespace.set('entityChangeIds', []);
namespace.set("entityChangeIds", []);
return entityChangeIds;
}
function putEntityChange(entityChange: EntityChange) {
if (namespace.get('ignoreEntityChangeIds')) {
if (namespace.get("ignoreEntityChangeIds")) {
return;
}
const entityChangeIds = namespace.get('entityChangeIds') || [];
const entityChangeIds = namespace.get("entityChangeIds") || [];
// store only ID since the record can be modified (e.g., in erase)
entityChangeIds.push(entityChange.id);
namespace.set('entityChangeIds', entityChangeIds);
namespace.set("entityChangeIds", entityChangeIds);
}
function reset() {
@@ -93,7 +92,7 @@ function reset() {
}
function ignoreEntityChangeIds() {
namespace.set('ignoreEntityChangeIds', true);
namespace.set("ignoreEntityChangeIds", true);
}
export default {

View File

@@ -5,7 +5,7 @@
*/
import fs from "fs";
import themeNames from "./code_block_theme_names.json" with { type: "json" }
import themeNames from "./code_block_theme_names.json" with { type: "json" };
import { t } from "i18next";
import { join } from "path";
import { isElectron, getResourceDir } from "./utils.js";
@@ -42,7 +42,7 @@ export function listSyntaxHighlightingThemes() {
}
],
...groupThemesByLightOrDark(systemThemes)
}
};
}
function getStylesDirectory() {
@@ -62,7 +62,8 @@ function getStylesDirectory() {
* @returns the list of themes.
*/
function readThemesFromFileSystem(path: string): ColorTheme[] {
return fs.readdirSync(path)
return fs
.readdirSync(path)
.filter((el) => el.endsWith(".min.css"))
.map((name) => {
const nameWithoutExtension = name.replace(".min.css", "");

View File

@@ -1,75 +1,75 @@
{
"1c light": "1C (Light)",
"a11y dark": "a11y (Dark)",
"a11y light": "a11y (Light)",
"agate": "Agate (Dark)",
"an old hope": "An Old Hope (Dark)",
"androidstudio": "Android Studio (Dark)",
"arduino light": "Arduino (Light)",
"arta": "Arta (Dark)",
"ascetic": "Ascetic (Light)",
"atom one dark reasonable": "Atom One with ReasonML support (Dark)",
"atom one dark": "Atom One (Dark)",
"atom one light": "Atom One (Light)",
"brown paper": "Brown Paper (Light)",
"codepen embed": "CodePen Embed (Dark)",
"color brewer": "Color Brewer (Light)",
"dark": "Dark",
"default": "Original highlight.js Theme (Light)",
"devibeans": "devibeans (Dark)",
"docco": "Docco (Light)",
"far": "FAR (Dark)",
"felipec": "FelipeC (Dark)",
"foundation": "Foundation 4 Docs (Light)",
"github dark dimmed": "GitHub Dimmed (Dark)",
"github dark": "GitHub (Dark)",
"github": "GitHub (Light)",
"gml": "GML (Dark)",
"googlecode": "Google Code (Light)",
"gradient dark": "Gradient (Dark)",
"gradient light": "Gradient (Light)",
"grayscale": "Grayscale (Light)",
"hybrid": "hybrid (Dark)",
"idea": "Idea (Light)",
"intellij light": "IntelliJ (Light)",
"ir black": "IR Black (Dark)",
"isbl editor dark": "ISBL Editor (Dark)",
"isbl editor light": "ISBL Editor (Light)",
"kimbie dark": "Kimbie (Dark)",
"kimbie light": "Kimbie (Light)",
"lightfair": "Lightfair (Light)",
"lioshi": "Lioshi (Dark)",
"magula": "Magula (Light)",
"mono blue": "Mono Blue (Light)",
"monokai sublime": "Monokai Sublime (Dark)",
"monokai": "Monokai (Dark)",
"night owl": "Night Owl (Dark)",
"nnfx dark": "NNFX (Dark)",
"nnfx light": "NNFX (Light)",
"nord": "Nord (Dark)",
"obsidian": "Obsidian (Dark)",
"panda syntax dark": "Panda (Dark)",
"panda syntax light": "Panda (Light)",
"paraiso dark": "Paraiso (Dark)",
"paraiso light": "Paraiso (Light)",
"pojoaque": "Pojoaque (Dark)",
"purebasic": "PureBasic (Light)",
"qtcreator dark": "Qt Creator (Dark)",
"qtcreator light": "Qt Creator (Light)",
"rainbow": "Rainbow (Dark)",
"routeros": "RouterOS Script (Light)",
"school book": "School Book (Light)",
"shades of purple": "Shades of Purple (Dark)",
"srcery": "Srcery (Dark)",
"stackoverflow dark": "Stack Overflow (Dark)",
"stackoverflow light": "Stack Overflow (Light)",
"sunburst": "Sunburst (Dark)",
"tokyo night dark": "Tokyo Night (Dark)",
"tokyo night light": "Tokyo Night (Light)",
"tomorrow night blue": "Tomorrow Night Blue (Dark)",
"tomorrow night bright": "Tomorrow Night Bright (Dark)",
"vs": "Visual Studio (Light)",
"vs2015": "Visual Studio 2015 (Dark)",
"xcode": "Xcode (Light)",
"xt256": "xt256 (Dark)"
"1c light": "1C (Light)",
"a11y dark": "a11y (Dark)",
"a11y light": "a11y (Light)",
"agate": "Agate (Dark)",
"an old hope": "An Old Hope (Dark)",
"androidstudio": "Android Studio (Dark)",
"arduino light": "Arduino (Light)",
"arta": "Arta (Dark)",
"ascetic": "Ascetic (Light)",
"atom one dark reasonable": "Atom One with ReasonML support (Dark)",
"atom one dark": "Atom One (Dark)",
"atom one light": "Atom One (Light)",
"brown paper": "Brown Paper (Light)",
"codepen embed": "CodePen Embed (Dark)",
"color brewer": "Color Brewer (Light)",
"dark": "Dark",
"default": "Original highlight.js Theme (Light)",
"devibeans": "devibeans (Dark)",
"docco": "Docco (Light)",
"far": "FAR (Dark)",
"felipec": "FelipeC (Dark)",
"foundation": "Foundation 4 Docs (Light)",
"github dark dimmed": "GitHub Dimmed (Dark)",
"github dark": "GitHub (Dark)",
"github": "GitHub (Light)",
"gml": "GML (Dark)",
"googlecode": "Google Code (Light)",
"gradient dark": "Gradient (Dark)",
"gradient light": "Gradient (Light)",
"grayscale": "Grayscale (Light)",
"hybrid": "hybrid (Dark)",
"idea": "Idea (Light)",
"intellij light": "IntelliJ (Light)",
"ir black": "IR Black (Dark)",
"isbl editor dark": "ISBL Editor (Dark)",
"isbl editor light": "ISBL Editor (Light)",
"kimbie dark": "Kimbie (Dark)",
"kimbie light": "Kimbie (Light)",
"lightfair": "Lightfair (Light)",
"lioshi": "Lioshi (Dark)",
"magula": "Magula (Light)",
"mono blue": "Mono Blue (Light)",
"monokai sublime": "Monokai Sublime (Dark)",
"monokai": "Monokai (Dark)",
"night owl": "Night Owl (Dark)",
"nnfx dark": "NNFX (Dark)",
"nnfx light": "NNFX (Light)",
"nord": "Nord (Dark)",
"obsidian": "Obsidian (Dark)",
"panda syntax dark": "Panda (Dark)",
"panda syntax light": "Panda (Light)",
"paraiso dark": "Paraiso (Dark)",
"paraiso light": "Paraiso (Light)",
"pojoaque": "Pojoaque (Dark)",
"purebasic": "PureBasic (Light)",
"qtcreator dark": "Qt Creator (Dark)",
"qtcreator light": "Qt Creator (Light)",
"rainbow": "Rainbow (Dark)",
"routeros": "RouterOS Script (Light)",
"school book": "School Book (Light)",
"shades of purple": "Shades of Purple (Dark)",
"srcery": "Srcery (Dark)",
"stackoverflow dark": "Stack Overflow (Dark)",
"stackoverflow light": "Stack Overflow (Light)",
"sunburst": "Sunburst (Dark)",
"tokyo night dark": "Tokyo Night (Dark)",
"tokyo night light": "Tokyo Night (Light)",
"tomorrow night blue": "Tomorrow Night Blue (Dark)",
"tomorrow night bright": "Tomorrow Night Bright (Dark)",
"vs": "Visual Studio (Light)",
"vs2015": "Visual Studio 2015 (Dark)",
"xcode": "Xcode (Light)",
"xt256": "xt256 (Dark)"
}

View File

@@ -9,11 +9,11 @@ import resourceDir from "./resource_dir.js";
const configSampleFilePath = path.resolve(resourceDir.RESOURCE_DIR, "config-sample.ini");
if (!fs.existsSync(dataDir.CONFIG_INI_PATH)) {
const configSample = fs.readFileSync(configSampleFilePath).toString('utf8');
const configSample = fs.readFileSync(configSampleFilePath).toString("utf8");
fs.writeFileSync(dataDir.CONFIG_INI_PATH, configSample);
}
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, 'utf-8'));
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
export default config;

View File

@@ -14,21 +14,20 @@ import { hash as getHash, hashedBlobId, randomString } from "../services/utils.j
import eraseService from "../services/erase.js";
import sanitizeAttributeName from "./sanitize_attribute_name.js";
import noteTypesService from "../services/note_types.js";
import { BranchRow } from '../becca/entities/rows.js';
import { EntityChange } from './entity_changes_interface.js';
import { BranchRow } from "../becca/entities/rows.js";
import { EntityChange } from "./entity_changes_interface.js";
import becca_loader from "../becca/becca_loader.js";
const noteTypes = noteTypesService.getNoteTypeNames();
class ConsistencyChecks {
private autoFix: boolean;
private unrecoveredConsistencyErrors: boolean;
private fixedIssues: boolean;
private reloadNeeded: boolean;
/**
* @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast)
*/
* @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast)
*/
constructor(autoFix: boolean) {
this.autoFix = autoFix;
this.unrecoveredConsistencyErrors = false;
@@ -71,7 +70,7 @@ class ConsistencyChecks {
/** @returns true if cycle was found and we should try again */
const checkTreeCycle = (noteId: string, path: string[]) => {
if (noteId === 'root') {
if (noteId === "root") {
return false;
}
@@ -80,13 +79,12 @@ class ConsistencyChecks {
if (this.autoFix) {
const branch = becca.getBranchFromChildAndParent(noteId, parentNoteId);
if (branch) {
branch.markAsDeleted('cycle-autofix');
branch.markAsDeleted("cycle-autofix");
logFix(`Branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' has been deleted since it was causing a tree cycle.`);
}
return true;
}
else {
} else {
logError(`Tree cycle detected at parent-child relationship: '${parentNoteId}' - '${noteId}', whole path: '${path}'`);
this.unrecoveredConsistencyErrors = true;
@@ -135,13 +133,14 @@ class ConsistencyChecks {
}
findBrokenReferenceIssues() {
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT branchId, branches.noteId
FROM branches
LEFT JOIN notes USING (noteId)
WHERE branches.isDeleted = 0
AND notes.noteId IS NULL`,
({branchId, noteId}) => {
({ branchId, noteId }) => {
if (this.autoFix) {
const branch = becca.getBranch(branchId);
if (!branch) {
@@ -155,16 +154,18 @@ class ConsistencyChecks {
} else {
logError(`Branch '${branchId}' references missing note '${noteId}'`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT branchId, branches.parentNoteId AS parentNoteId
FROM branches
LEFT JOIN notes ON notes.noteId = branches.parentNoteId
WHERE branches.isDeleted = 0
AND branches.noteId != 'root'
AND notes.noteId IS NULL`,
({branchId, parentNoteId}) => {
({ branchId, parentNoteId }) => {
if (this.autoFix) {
// Delete the old branch and recreate it with root as parent.
const oldBranch = becca.getBranch(branchId);
@@ -184,9 +185,9 @@ class ConsistencyChecks {
if (note.getParentBranches().length === 0) {
const newBranch = new BBranch({
parentNoteId: 'root',
parentNoteId: "root",
noteId: noteId,
prefix: 'recovered'
prefix: "recovered"
}).save();
message += `${newBranch.branchId} was created in the root instead.`;
@@ -200,15 +201,17 @@ class ConsistencyChecks {
} else {
logError(`Branch '${branchId}' references missing parent note '${parentNoteId}'`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT attributeId, attributes.noteId
FROM attributes
LEFT JOIN notes USING (noteId)
WHERE attributes.isDeleted = 0
AND notes.noteId IS NULL`,
({attributeId, noteId}) => {
({ attributeId, noteId }) => {
if (this.autoFix) {
const attribute = becca.getAttribute(attributeId);
if (!attribute) {
@@ -222,16 +225,18 @@ class ConsistencyChecks {
} else {
logError(`Attribute '${attributeId}' references missing source note '${noteId}'`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT attributeId, attributes.value AS noteId
FROM attributes
LEFT JOIN notes ON notes.noteId = attributes.value
WHERE attributes.isDeleted = 0
AND attributes.type = 'relation'
AND notes.noteId IS NULL`,
({attributeId, noteId}) => {
({ attributeId, noteId }) => {
if (this.autoFix) {
const attribute = becca.getAttribute(attributeId);
if (!attribute) {
@@ -241,13 +246,15 @@ class ConsistencyChecks {
this.reloadNeeded = true;
logFix(`Relation '${attributeId}' has been deleted since it references missing note '${noteId}'`)
logFix(`Relation '${attributeId}' has been deleted since it references missing note '${noteId}'`);
} else {
logError(`Relation '${attributeId}' references missing note '${noteId}'`)
logError(`Relation '${attributeId}' references missing note '${noteId}'`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT attachmentId, attachments.ownerId AS noteId
FROM attachments
WHERE attachments.ownerId NOT IN (
@@ -256,7 +263,7 @@ class ConsistencyChecks {
SELECT revisionId FROM revisions
)
AND attachments.isDeleted = 0`,
({attachmentId, ownerId}) => {
({ attachmentId, ownerId }) => {
if (this.autoFix) {
const attachment = becca.getAttachment(attachmentId);
if (!attachment) {
@@ -270,7 +277,8 @@ class ConsistencyChecks {
} else {
logError(`Attachment '${attachmentId}' references missing note/revision '${ownerId}'`);
}
});
}
);
}
findExistencyIssues() {
@@ -280,14 +288,15 @@ class ConsistencyChecks {
// the order here is important - first we might need to delete inconsistent branches, and after that
// another check might create missing branch
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT branchId,
noteId
FROM branches
JOIN notes USING (noteId)
WHERE notes.isDeleted = 1
AND branches.isDeleted = 0`,
({branchId, noteId}) => {
({ branchId, noteId }) => {
if (this.autoFix) {
const branch = becca.getBranch(branchId);
if (!branch) return;
@@ -297,57 +306,65 @@ class ConsistencyChecks {
logFix(`Branch '${branchId}' has been deleted since the associated note '${noteId}' is deleted.`);
} else {
logError(`Branch '${branchId}' is not deleted even though the associated note '${noteId}' is deleted.`)
logError(`Branch '${branchId}' is not deleted even though the associated note '${noteId}' is deleted.`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT branchId,
parentNoteId
FROM branches
JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId
WHERE parentNote.isDeleted = 1
AND branches.isDeleted = 0
`, ({branchId, parentNoteId}) => {
if (this.autoFix) {
const branch = becca.getBranch(branchId);
if (!branch) {
return;
`,
({ branchId, parentNoteId }) => {
if (this.autoFix) {
const branch = becca.getBranch(branchId);
if (!branch) {
return;
}
branch.markAsDeleted();
this.reloadNeeded = true;
logFix(`Branch '${branchId}' has been deleted since the associated parent note '${parentNoteId}' is deleted.`);
} else {
logError(`Branch '${branchId}' is not deleted even though the associated parent note '${parentNoteId}' is deleted.`);
}
branch.markAsDeleted();
this.reloadNeeded = true;
logFix(`Branch '${branchId}' has been deleted since the associated parent note '${parentNoteId}' is deleted.`);
} else {
logError(`Branch '${branchId}' is not deleted even though the associated parent note '${parentNoteId}' is deleted.`)
}
});
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT DISTINCT notes.noteId
FROM notes
LEFT JOIN branches ON notes.noteId = branches.noteId AND branches.isDeleted = 0
WHERE notes.isDeleted = 0
AND branches.branchId IS NULL
`, ({noteId}) => {
if (this.autoFix) {
const branch = new BBranch({
parentNoteId: 'root',
noteId: noteId,
prefix: 'recovered'
}).save();
`,
({ noteId }) => {
if (this.autoFix) {
const branch = new BBranch({
parentNoteId: "root",
noteId: noteId,
prefix: "recovered"
}).save();
this.reloadNeeded = true;
this.reloadNeeded = true;
logFix(`Created missing branch '${branch.branchId}' for note '${noteId}'`);
} else {
logError(`No undeleted branch found for note '${noteId}'`);
logFix(`Created missing branch '${branch.branchId}' for note '${noteId}'`);
} else {
logError(`No undeleted branch found for note '${noteId}'`);
}
}
});
);
// there should be a unique relationship between note and its parent
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT noteId,
parentNoteId
FROM branches
@@ -355,17 +372,19 @@ class ConsistencyChecks {
GROUP BY branches.parentNoteId,
branches.noteId
HAVING COUNT(1) > 1`,
({noteId, parentNoteId}) => {
({ noteId, parentNoteId }) => {
if (this.autoFix) {
const branchIds = sql.getColumn<string>(
`SELECT branchId
`SELECT branchId
FROM branches
WHERE noteId = ?
and parentNoteId = ?
and isDeleted = 0
ORDER BY utcDateModified`, [noteId, parentNoteId]);
ORDER BY utcDateModified`,
[noteId, parentNoteId]
);
const branches = branchIds.map(branchId => becca.getBranch(branchId));
const branches = branchIds.map((branchId) => becca.getBranch(branchId));
// it's not necessarily "original" branch, it's just the only one which will survive
const origBranch = branches[0];
@@ -389,16 +408,18 @@ class ConsistencyChecks {
} else {
logError(`Duplicate branches for note '${noteId}' and parent '${parentNoteId}'`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT attachmentId,
attachments.ownerId AS noteId
FROM attachments
JOIN notes ON notes.noteId = attachments.ownerId
WHERE notes.isDeleted = 1
AND attachments.isDeleted = 0`,
({attachmentId, noteId}) => {
({ attachmentId, noteId }) => {
if (this.autoFix) {
const attachment = becca.getAttachment(attachmentId);
if (!attachment) return;
@@ -408,41 +429,45 @@ class ConsistencyChecks {
logFix(`Attachment '${attachmentId}' has been deleted since the associated note '${noteId}' is deleted.`);
} else {
logError(`Attachment '${attachmentId}' is not deleted even though the associated note '${noteId}' is deleted.`)
logError(`Attachment '${attachmentId}' is not deleted even though the associated note '${noteId}' is deleted.`);
}
});
}
);
}
findLogicIssues() {
const noteTypesStr = noteTypes.map(nt => `'${nt}'`).join(", ");
const noteTypesStr = noteTypes.map((nt) => `'${nt}'`).join(", ");
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT noteId, type
FROM notes
WHERE isDeleted = 0
AND type NOT IN (${noteTypesStr})`,
({noteId, type}) => {
({ noteId, type }) => {
if (this.autoFix) {
const note = becca.getNote(noteId);
if (!note) return;
note.type = 'file'; // file is a safe option to recover notes if the type is not known
note.type = "file"; // file is a safe option to recover notes if the type is not known
note.save();
this.reloadNeeded = true;
logFix(`Note '${noteId}' type has been change to file since it had invalid type '${type}'`)
logFix(`Note '${noteId}' type has been change to file since it had invalid type '${type}'`);
} else {
logError(`Note '${noteId}' has invalid type '${type}'`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT notes.noteId, notes.isProtected, notes.type, notes.mime
FROM notes
LEFT JOIN blobs USING (blobId)
WHERE blobs.blobId IS NULL
AND notes.isDeleted = 0`,
({noteId, isProtected, type, mime}) => {
({ noteId, isProtected, type, mime }) => {
if (this.autoFix) {
// it might be possible that the blob is not available only because of the interrupted
// sync, and it will come later. It's therefore important to guarantee that this artificial
@@ -469,7 +494,7 @@ class ConsistencyChecks {
const hash = getHash(randomString(10));
entityChangesService.putEntityChange({
entityName: 'blobs',
entityName: "blobs",
entityId: blobId,
hash: hash,
isErased: false,
@@ -486,19 +511,21 @@ class ConsistencyChecks {
} else {
logError(`Note '${noteId}' content row does not exist`);
}
});
}
);
if (sqlInit.getDbSize() < 500000) {
// querying for "content IS NULL" is expensive since content is not indexed. See e.g. https://github.com/zadam/trilium/issues/2887
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT notes.noteId, notes.type, notes.mime
FROM notes
JOIN blobs USING (blobId)
WHERE isDeleted = 0
AND isProtected = 0
AND content IS NULL`,
({noteId, type, mime}) => {
({ noteId, type, mime }) => {
if (this.autoFix) {
const note = becca.getNote(noteId);
const blankContent = getBlankContent(false, type, mime);
@@ -514,15 +541,17 @@ class ConsistencyChecks {
} else {
logError(`Note '${noteId}' content is null even though it is not deleted`);
}
});
}
);
}
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT revisions.revisionId, blobs.blobId
FROM revisions
LEFT JOIN blobs USING (blobId)
WHERE blobs.blobId IS NULL`,
({revisionId, blobId}) => {
({ revisionId, blobId }) => {
if (this.autoFix) {
eraseService.eraseRevisions([revisionId]);
@@ -532,14 +561,16 @@ class ConsistencyChecks {
} else {
logError(`Note revision '${revisionId}' blob '${blobId}' does not exist`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT attachments.attachmentId, blobs.blobId
FROM attachments
LEFT JOIN blobs USING (blobId)
WHERE blobs.blobId IS NULL`,
({attachmentId, blobId}) => {
({ attachmentId, blobId }) => {
if (this.autoFix) {
eraseService.eraseAttachments([attachmentId]);
@@ -549,24 +580,29 @@ class ConsistencyChecks {
} else {
logError(`Attachment '${attachmentId}' blob '${blobId}' does not exist`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT parentNoteId
FROM branches
JOIN notes ON notes.noteId = branches.parentNoteId
WHERE notes.isDeleted = 0
AND notes.type == 'search'
AND branches.isDeleted = 0`,
({parentNoteId}) => {
({ parentNoteId }) => {
if (this.autoFix) {
const branchIds = sql.getColumn<string>(`
const branchIds = sql.getColumn<string>(
`
SELECT branchId
FROM branches
WHERE isDeleted = 0
AND parentNoteId = ?`, [parentNoteId]);
AND parentNoteId = ?`,
[parentNoteId]
);
const branches = branchIds.map(branchId => becca.getBranch(branchId));
const branches = branchIds.map((branchId) => becca.getBranch(branchId));
for (const branch of branches) {
if (!branch) continue;
@@ -576,27 +612,29 @@ class ConsistencyChecks {
// create a replacement branch in root parent
new BBranch({
parentNoteId: 'root',
parentNoteId: "root",
noteId: branch.noteId,
prefix: 'recovered'
prefix: "recovered"
}).save();
logFix(`Note '${branch.noteId}' has been moved to root since it was a child of a search note '${parentNoteId}'`)
logFix(`Note '${branch.noteId}' has been moved to root since it was a child of a search note '${parentNoteId}'`);
}
this.reloadNeeded = true;
} else {
logError(`Search note '${parentNoteId}' has children`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT attributeId
FROM attributes
WHERE isDeleted = 0
AND type = 'relation'
AND value = ''`,
({attributeId}) => {
({ attributeId }) => {
if (this.autoFix) {
const relation = becca.getAttribute(attributeId);
if (!relation) return;
@@ -608,20 +646,22 @@ class ConsistencyChecks {
} else {
logError(`Relation '${attributeId}' has empty target.`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT attributeId,
type
FROM attributes
WHERE isDeleted = 0
AND type != 'label'
AND type != 'relation'`,
({attributeId, type}) => {
({ attributeId, type }) => {
if (this.autoFix) {
const attribute = becca.getAttribute(attributeId);
if (!attribute) return;
attribute.type = 'label';
attribute.type = "label";
attribute.save();
this.reloadNeeded = true;
@@ -630,16 +670,18 @@ class ConsistencyChecks {
} else {
logError(`Attribute '${attributeId}' has invalid type '${type}'`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT attributeId,
attributes.noteId
FROM attributes
JOIN notes ON attributes.noteId = notes.noteId
WHERE attributes.isDeleted = 0
AND notes.isDeleted = 1`,
({attributeId, noteId}) => {
({ attributeId, noteId }) => {
if (this.autoFix) {
const attribute = becca.getAttribute(attributeId);
if (!attribute) return;
@@ -651,9 +693,11 @@ class ConsistencyChecks {
} else {
logError(`Attribute '${attributeId}' is not deleted even though owning note '${noteId}' is deleted.`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT attributeId,
attributes.value AS targetNoteId
FROM attributes
@@ -661,7 +705,7 @@ class ConsistencyChecks {
WHERE attributes.type = 'relation'
AND attributes.isDeleted = 0
AND notes.isDeleted = 1`,
({attributeId, targetNoteId}) => {
({ attributeId, targetNoteId }) => {
if (this.autoFix) {
const attribute = becca.getAttribute(attributeId);
if (!attribute) return;
@@ -673,16 +717,18 @@ class ConsistencyChecks {
} else {
logError(`Attribute '${attributeId}' is not deleted even though target note '${targetNoteId}' is deleted.`);
}
});
}
);
}
runEntityChangeChecks(entityName: string, key: string) {
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT ${key} as entityId
FROM ${entityName}
LEFT JOIN entity_changes ec ON ec.entityName = '${entityName}' AND ec.entityId = ${entityName}.${key}
WHERE ec.id IS NULL`,
({entityId}) => {
({ entityId }) => {
const entityRow = sql.getRow<EntityChange>(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
if (this.autoFix) {
@@ -692,16 +738,18 @@ class ConsistencyChecks {
hash: randomString(10), // doesn't matter, will force sync, but that's OK
isErased: false,
utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated,
isSynced: entityName !== 'options' || entityRow.isSynced
isSynced: entityName !== "options" || entityRow.isSynced
});
logFix(`Created missing entity change for entityName '${entityName}', entityId '${entityId}'`);
} else {
logError(`Missing entity change for entityName '${entityName}', entityId '${entityId}'`);
}
});
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT id, entityId
FROM entity_changes
LEFT JOIN ${entityName} ON entityId = ${entityName}.${key}
@@ -709,24 +757,26 @@ class ConsistencyChecks {
entity_changes.isErased = 0
AND entity_changes.entityName = '${entityName}'
AND ${entityName}.${key} IS NULL`,
({id, entityId}) => {
if (this.autoFix) {
sql.execute("DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
({ id, entityId }) => {
if (this.autoFix) {
sql.execute("DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
logFix(`Deleted extra entity change id '${id}', entityName '${entityName}', entityId '${entityId}'`);
} else {
logError(`Unrecognized entity change id '${id}', entityName '${entityName}', entityId '${entityId}'`);
}
});
logFix(`Deleted extra entity change id '${id}', entityName '${entityName}', entityId '${entityId}'`);
} else {
logError(`Unrecognized entity change id '${id}', entityName '${entityName}', entityId '${entityId}'`);
}
}
);
this.findAndFixIssues(`
this.findAndFixIssues(
`
SELECT id, entityId
FROM entity_changes
JOIN ${entityName} ON entityId = ${entityName}.${key}
WHERE
entity_changes.isErased = 1
AND entity_changes.entityName = '${entityName}'`,
({id, entityId}) => {
({ id, entityId }) => {
if (this.autoFix) {
sql.execute(`DELETE FROM ${entityName} WHERE ${key} = ?`, [entityId]);
@@ -736,7 +786,8 @@ class ConsistencyChecks {
} else {
logError(`Entity change id '${id}' has entityName '${entityName}', entityId '${entityId}' as erased, but it's not.`);
}
});
}
);
}
findEntityChangeIssues() {
@@ -763,14 +814,13 @@ class ConsistencyChecks {
// - renaming the attribute would break the invariant that single attribute never changes the name
// - deleting the old attribute and creating new will create duplicates across synchronized cluster (specifically in the initial migration)
// But in general, we assume there won't be many such problems
sql.execute('UPDATE attributes SET name = ? WHERE name = ?', [fixedName, origName]);
sql.execute("UPDATE attributes SET name = ? WHERE name = ?", [fixedName, origName]);
this.fixedIssues = true;
this.reloadNeeded = true;
logFix(`Renamed incorrectly named attributes '${origName}' to '${fixedName}'`);
}
else {
} else {
this.unrecoveredConsistencyErrors = true;
logFix(`There are incorrectly named attributes '${origName}'`);
@@ -790,8 +840,7 @@ class ConsistencyChecks {
this.fixedIssues = true;
logFix(`Fixed incorrect lastSyncedPush - was ${lastSyncedPush}, needs to be at maximum ${maxEntityChangeId}`);
}
else {
} else {
this.unrecoveredConsistencyErrors = true;
logFix(`Incorrect lastSyncedPush - is ${lastSyncedPush}, needs to be at maximum ${maxEntityChangeId}`);
@@ -839,9 +888,9 @@ class ConsistencyChecks {
return `${tableName}: ${count}`;
}
const tables = [ "notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs" ];
const tables = ["notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs"];
log.info(`Table counts: ${tables.map(tableName => getTableRowCount(tableName)).join(", ")}`);
log.info(`Table counts: ${tables.map((tableName) => getTableRowCount(tableName)).join(", ")}`);
}
async runChecks() {
@@ -860,12 +909,9 @@ class ConsistencyChecks {
if (this.unrecoveredConsistencyErrors) {
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms)`);
ws.sendMessageToAllClients({type: 'consistency-checks-failed'});
ws.sendMessageToAllClients({ type: "consistency-checks-failed" });
} else {
log.info(`All consistency checks passed ` +
(this.fixedIssues ? "after some fixes" : "with no errors detected") +
` (took ${elapsedTimeMs}ms)`
);
log.info(`All consistency checks passed ` + (this.fixedIssues ? "after some fixes" : "with no errors detected") + ` (took ${elapsedTimeMs}ms)`);
}
}
}
@@ -875,11 +921,11 @@ function getBlankContent(isProtected: boolean, type: string, mime: string) {
return null; // this is wrong for protected non-erased notes, but we cannot create a valid value without a password
}
if (mime === 'application/json') {
return '{}';
if (mime === "application/json") {
return "{}";
}
return ''; // empty string might be a wrong choice for some note types, but it's the best guess
return ""; // empty string might be a wrong choice for some note types, but it's the best guess
}
function logFix(message: string) {
@@ -891,7 +937,7 @@ function logError(message: string) {
}
function runPeriodicChecks() {
const autoFix = optionsService.getOptionBool('autoFixConsistencyIssues');
const autoFix = optionsService.getOptionBool("autoFixConsistencyIssues");
const consistencyChecks = new ConsistencyChecks(autoFix);
consistencyChecks.runChecks();

View File

@@ -14,9 +14,9 @@ function getEntityHashes() {
const startTime = new Date();
// we know this is slow and the total content hash calculation time is logged
type HashRow = [ string, string, string, boolean ];
const hashRows = sql.disableSlowQueryLogging(
() => sql.getRawRows<HashRow>(`
type HashRow = [string, string, string, boolean];
const hashRows = sql.disableSlowQueryLogging(() =>
sql.getRawRows<HashRow>(`
SELECT entityName,
entityId,
hash,
@@ -28,12 +28,12 @@ function getEntityHashes() {
// sorting is faster in memory
// sorting by entityId is enough, hashes will be segmented by entityName later on anyway
hashRows.sort((a, b) => a[1] < b[1] ? -1 : 1);
hashRows.sort((a, b) => (a[1] < b[1] ? -1 : 1));
const hashMap: Record<string, SectorHash> = {};
for (const [entityName, entityId, hash, isErased] of hashRows) {
const entityHashMap = hashMap[entityName] = hashMap[entityName] || {};
const entityHashMap = (hashMap[entityName] = hashMap[entityName] || {});
const sector = entityId[0];

View File

@@ -15,13 +15,11 @@ import path from "path";
function getAppDataDir() {
let appDataDir = os.homedir(); // fallback if OS is not recognized
if (os.platform() === 'win32' && process.env.APPDATA) {
if (os.platform() === "win32" && process.env.APPDATA) {
appDataDir = process.env.APPDATA;
}
else if (os.platform() === 'linux') {
} else if (os.platform() === "linux") {
appDataDir = `${os.homedir()}/.local/share`;
}
else if (os.platform() === 'darwin') {
} else if (os.platform() === "darwin") {
appDataDir = `${os.homedir()}/Library/Application Support`;
}
@@ -33,7 +31,7 @@ function getAppDataDir() {
return appDataDir;
}
const DIR_NAME = 'trilium-data';
const DIR_NAME = "trilium-data";
function getTriliumDataDir() {
if (process.env.TRILIUM_DATA_DIR) {

View File

@@ -11,17 +11,26 @@ import hoistedNoteService from "./hoisted_note.js";
import BNote from "../becca/entities/bnote.js";
import { t } from "i18next";
const CALENDAR_ROOT_LABEL = 'calendarRoot';
const YEAR_LABEL = 'yearNote';
const MONTH_LABEL = 'monthNote';
const DATE_LABEL = 'dateNote';
const CALENDAR_ROOT_LABEL = "calendarRoot";
const YEAR_LABEL = "yearNote";
const MONTH_LABEL = "monthNote";
const DATE_LABEL = "dateNote";
const WEEKDAY_TRANSLATION_IDS = [
"weekdays.sunday", "weekdays.monday", "weekdays.tuesday", "weekdays.wednesday", "weekdays.thursday", "weekdays.friday", "weekdays.saturday", "weekdays.sunday"
];
const WEEKDAY_TRANSLATION_IDS = ["weekdays.sunday", "weekdays.monday", "weekdays.tuesday", "weekdays.wednesday", "weekdays.thursday", "weekdays.friday", "weekdays.saturday", "weekdays.sunday"];
const MONTH_TRANSLATION_IDS = [
"months.january", "months.february", "months.march", "months.april", "months.may", "months.june", "months.july", "months.august", "months.september", "months.october", "months.november", "months.december"
"months.january",
"months.february",
"months.march",
"months.april",
"months.may",
"months.june",
"months.july",
"months.august",
"months.september",
"months.october",
"months.november",
"months.december"
];
type StartOfWeek = "monday" | "sunday";
@@ -30,9 +39,9 @@ function createNote(parentNote: BNote, noteTitle: string) {
return noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: noteTitle,
content: '',
content: "",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: 'text'
type: "text"
}).note;
}
@@ -42,7 +51,7 @@ function getRootCalendarNote(): BNote {
const workspaceNote = hoistedNoteService.getWorkspaceNote();
if (!workspaceNote || !workspaceNote.isRoot()) {
rootNote = searchService.findFirstNoteWithQuery('#workspaceCalendarRoot', new SearchContext({ignoreHoistedNote: false}));
rootNote = searchService.findFirstNoteWithQuery("#workspaceCalendarRoot", new SearchContext({ ignoreHoistedNote: false }));
}
if (!rootNote) {
@@ -52,16 +61,16 @@ function getRootCalendarNote(): BNote {
if (!rootNote) {
sql.transactional(() => {
rootNote = noteService.createNewNote({
parentNoteId: 'root',
title: 'Calendar',
target: 'into',
parentNoteId: "root",
title: "Calendar",
target: "into",
isProtected: false,
type: 'text',
content: ''
type: "text",
content: ""
}).note;
attributeService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL);
attributeService.createLabel(rootNote.noteId, 'sorted');
attributeService.createLabel(rootNote.noteId, "sorted");
});
}
@@ -73,8 +82,7 @@ function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
const yearStr = dateStr.trim().substr(0, 4);
let yearNote = searchService.findFirstNoteWithQuery(`#${YEAR_LABEL}="${yearStr}"`,
new SearchContext({ancestorNoteId: rootNote.noteId}));
let yearNote = searchService.findFirstNoteWithQuery(`#${YEAR_LABEL}="${yearStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
if (yearNote) {
return yearNote;
@@ -84,12 +92,12 @@ function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
yearNote = createNote(rootNote, yearStr);
attributeService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
attributeService.createLabel(yearNote.noteId, 'sorted');
attributeService.createLabel(yearNote.noteId, "sorted");
const yearTemplateAttr = rootNote.getOwnedAttribute('relation', 'yearTemplate');
const yearTemplateAttr = rootNote.getOwnedAttribute("relation", "yearTemplate");
if (yearTemplateAttr) {
attributeService.createRelation(yearNote.noteId, 'template', yearTemplateAttr.value);
attributeService.createRelation(yearNote.noteId, "template", yearTemplateAttr.value);
}
});
@@ -101,8 +109,8 @@ function getMonthNoteTitle(rootNote: BNote, monthNumber: string, dateObj: Date)
const monthName = t(MONTH_TRANSLATION_IDS[dateObj.getMonth()]);
return pattern
.replace(/{shortMonth3}/g, monthName.slice(0,3))
.replace(/{shortMonth4}/g, monthName.slice(0,4))
.replace(/{shortMonth3}/g, monthName.slice(0, 3))
.replace(/{shortMonth4}/g, monthName.slice(0, 4))
.replace(/{monthNumberPadded}/g, monthNumber)
.replace(/{month}/g, monthName);
}
@@ -113,8 +121,7 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
const monthStr = dateStr.substr(0, 7);
const monthNumber = dateStr.substr(5, 2);
let monthNote = searchService.findFirstNoteWithQuery(`#${MONTH_LABEL}="${monthStr}"`,
new SearchContext({ancestorNoteId: rootNote.noteId}));
let monthNote = searchService.findFirstNoteWithQuery(`#${MONTH_LABEL}="${monthStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
if (monthNote) {
return monthNote;
@@ -130,12 +137,12 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
monthNote = createNote(yearNote, noteTitle);
attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
attributeService.createLabel(monthNote.noteId, 'sorted');
attributeService.createLabel(monthNote.noteId, "sorted");
const monthTemplateAttr = rootNote.getOwnedAttribute('relation', 'monthTemplate');
const monthTemplateAttr = rootNote.getOwnedAttribute("relation", "monthTemplate");
if (monthTemplateAttr) {
attributeService.createRelation(monthNote.noteId, 'template', monthTemplateAttr.value);
attributeService.createRelation(monthNote.noteId, "template", monthTemplateAttr.value);
}
});
@@ -168,8 +175,7 @@ function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
dateStr = dateStr.trim().substr(0, 10);
let dateNote = searchService.findFirstNoteWithQuery(`#${DATE_LABEL}="${dateStr}"`,
new SearchContext({ancestorNoteId: rootNote.noteId}));
let dateNote = searchService.findFirstNoteWithQuery(`#${DATE_LABEL}="${dateStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
if (dateNote) {
return dateNote;
@@ -187,10 +193,10 @@ function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substr(0, 10));
const dateTemplateAttr = rootNote.getOwnedAttribute('relation', 'dateTemplate');
const dateTemplateAttr = rootNote.getOwnedAttribute("relation", "dateTemplate");
if (dateTemplateAttr) {
attributeService.createRelation(dateNote.noteId, 'template', dateTemplateAttr.value);
attributeService.createRelation(dateNote.noteId, "template", dateTemplateAttr.value);
}
});
@@ -205,13 +211,11 @@ function getStartOfTheWeek(date: Date, startOfTheWeek: StartOfWeek) {
const day = date.getDay();
let diff;
if (startOfTheWeek === 'monday') {
if (startOfTheWeek === "monday") {
diff = date.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
}
else if (startOfTheWeek === 'sunday') {
} else if (startOfTheWeek === "sunday") {
diff = date.getDate() - day;
}
else {
} else {
throw new Error(`Unrecognized start of the week ${startOfTheWeek}`);
}
@@ -219,7 +223,7 @@ function getStartOfTheWeek(date: Date, startOfTheWeek: StartOfWeek) {
}
interface WeekNoteOpts {
startOfTheWeek?: StartOfWeek
startOfTheWeek?: StartOfWeek;
}
function getWeekNote(dateStr: string, options: WeekNoteOpts = {}, rootNote: BNote | null = null) {

View File

@@ -1,8 +1,8 @@
import dayjs from "dayjs";
import cls from "./cls.js";
const LOCAL_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSSZZ';
const UTC_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ssZ';
const LOCAL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss.SSSZZ";
const UTC_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ssZ";
function utcNowDateTime() {
return utcDateTimeStr(new Date());
@@ -12,8 +12,7 @@ function utcNowDateTime() {
// so we'd prefer client timezone to be used to record local dates. For this reason, requests from clients contain
// "trilium-local-now-datetime" header which is then stored in CLS
function localNowDateTime() {
return cls.getLocalNowDateTime()
|| dayjs().format(LOCAL_DATETIME_FORMAT)
return cls.getLocalNowDateTime() || dayjs().format(LOCAL_DATETIME_FORMAT);
}
function localNowDate() {
@@ -21,8 +20,7 @@ function localNowDate() {
if (clsDateTime) {
return clsDateTime.substr(0, 10);
}
else {
} else {
const date = new Date();
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
@@ -34,11 +32,11 @@ function pad(num: number) {
}
function utcDateStr(date: Date) {
return date.toISOString().split('T')[0];
return date.toISOString().split("T")[0];
}
function utcDateTimeStr(date: Date) {
return date.toISOString().replace('T', ' ');
return date.toISOString().replace("T", " ");
}
/**
@@ -48,8 +46,7 @@ function utcDateTimeStr(date: Date) {
function parseDateTime(str: string) {
try {
return new Date(Date.parse(str));
}
catch (e: any) {
} catch (e: any) {
throw new Error(`Can't parse date from '${str}': ${e.stack}`);
}
}
@@ -62,7 +59,7 @@ function parseLocalDate(str: string) {
}
function getDateTimeForFile() {
return new Date().toISOString().substr(0, 19).replace(/:/g, '');
return new Date().toISOString().substr(0, 19).replace(/:/g, "");
}
function validateLocalDateTime(str: string | null | undefined) {
@@ -74,7 +71,6 @@ function validateLocalDateTime(str: string | null | undefined) {
return `Invalid local date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110+0200'`;
}
if (!dayjs(str, LOCAL_DATETIME_FORMAT)) {
return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`;
}
@@ -89,7 +85,6 @@ function validateUtcDateTime(str: string | undefined) {
return `Invalid UTC date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110Z'`;
}
if (!dayjs(str, UTC_DATETIME_FORMAT)) {
return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`;
}

View File

@@ -14,14 +14,13 @@ function arraysIdentical(a: any[] | Buffer, b: any[] | Buffer) {
function shaArray(content: crypto.BinaryLike) {
// we use this as a simple checksum and don't rely on its security, so SHA-1 is good enough
return crypto.createHash('sha1').update(content).digest();
return crypto.createHash("sha1").update(content).digest();
}
function pad(data: Buffer): Buffer {
if (data.length > 16) {
data = data.slice(0, 16);
}
else if (data.length < 16) {
} else if (data.length < 16) {
const zeros = Array(16 - data.length).fill(0);
data = Buffer.concat([data, Buffer.from(zeros)]);
@@ -38,7 +37,7 @@ function encrypt(key: Buffer, plainText: Buffer | string) {
const plainTextBuffer = Buffer.from(plainText);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-128-cbc', pad(key), pad(iv));
const cipher = crypto.createCipheriv("aes-128-cbc", pad(key), pad(iv));
const digest = shaArray(plainTextBuffer).slice(0, 4);
@@ -48,7 +47,7 @@ function encrypt(key: Buffer, plainText: Buffer | string) {
const encryptedDataWithIv = Buffer.concat([iv, encryptedData]);
return encryptedDataWithIv.toString('base64');
return encryptedDataWithIv.toString("base64");
}
function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | null {
@@ -61,7 +60,7 @@ function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | nul
}
try {
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), 'base64');
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
@@ -70,7 +69,7 @@ function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | nul
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
const decipher = crypto.createDecipheriv('aes-128-cbc', pad(key), pad(iv));
const decipher = crypto.createDecipheriv("aes-128-cbc", pad(key), pad(iv));
const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]);
@@ -84,15 +83,13 @@ function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | nul
}
return payload;
}
catch (e: any) {
} catch (e: any) {
// recovery from https://github.com/zadam/trilium/issues/510
if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) {
log.info("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead");
return Buffer.from(cipherText);
}
else {
} else {
throw e;
}
}
@@ -109,7 +106,7 @@ function decryptString(dataKey: Buffer, cipherText: string) {
throw new Error("Could not decrypt string.");
}
return buffer.toString('utf-8');
return buffer.toString("utf-8");
}
export default {

View File

@@ -4,20 +4,19 @@ import optionService from "../options.js";
import crypto from "crypto";
function getVerificationHash(password: crypto.BinaryLike) {
const salt = optionService.getOption('passwordVerificationSalt');
const salt = optionService.getOption("passwordVerificationSalt");
return getScryptHash(password, salt);
}
function getPasswordDerivedKey(password: crypto.BinaryLike) {
const salt = optionService.getOption('passwordDerivedKeySalt');
const salt = optionService.getOption("passwordDerivedKeySalt");
return getScryptHash(password, salt);
}
function getScryptHash(password: crypto.BinaryLike, salt: crypto.BinaryLike) {
const hashed = crypto.scryptSync(password, salt, 32,
{N: 16384, r:8, p:1});
const hashed = crypto.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
return hashed;
}

View File

@@ -25,8 +25,8 @@ function changePassword(currentPassword: string, newPassword: string) {
sql.transactional(() => {
const decryptedDataKey = passwordEncryptionService.getDataKey(currentPassword);
optionService.setOption('passwordVerificationSalt', randomSecureToken(32));
optionService.setOption('passwordDerivedKeySalt', randomSecureToken(32));
optionService.setOption("passwordVerificationSalt", randomSecureToken(32));
optionService.setOption("passwordDerivedKeySalt", randomSecureToken(32));
const newPasswordVerificationKey = toBase64(myScryptService.getVerificationHash(newPassword));
@@ -35,7 +35,7 @@ function changePassword(currentPassword: string, newPassword: string) {
passwordEncryptionService.setDataKey(newPassword, decryptedDataKey);
}
optionService.setOption('passwordVerificationHash', newPasswordVerificationKey);
optionService.setOption("passwordVerificationHash", newPasswordVerificationKey);
});
return {
@@ -48,14 +48,14 @@ function setPassword(password: string) {
throw new Error("Password is set already. Either change it or perform 'reset password' first.");
}
optionService.createOption('passwordVerificationSalt', randomSecureToken(32), true);
optionService.createOption('passwordDerivedKeySalt', randomSecureToken(32), true);
optionService.createOption("passwordVerificationSalt", randomSecureToken(32), true);
optionService.createOption("passwordDerivedKeySalt", randomSecureToken(32), true);
const passwordVerificationKey = toBase64(myScryptService.getVerificationHash(password));
optionService.createOption('passwordVerificationHash', passwordVerificationKey, true);
optionService.createOption("passwordVerificationHash", passwordVerificationKey, true);
// passwordEncryptionService expects these options to already exist
optionService.createOption('encryptedDataKey', '', true);
optionService.createOption("encryptedDataKey", "", true);
passwordEncryptionService.setDataKey(password, randomSecureToken(16));
@@ -67,10 +67,10 @@ function setPassword(password: string) {
function resetPassword() {
// user forgot the password,
sql.transactional(() => {
optionService.setOption('passwordVerificationSalt', '');
optionService.setOption('passwordDerivedKeySalt', '');
optionService.setOption('encryptedDataKey', '');
optionService.setOption('passwordVerificationHash', '');
optionService.setOption("passwordVerificationSalt", "");
optionService.setOption("passwordDerivedKeySalt", "");
optionService.setOption("encryptedDataKey", "");
optionService.setOption("passwordVerificationHash", "");
});
return {

View File

@@ -6,7 +6,7 @@ import dataEncryptionService from "./data_encryption.js";
function verifyPassword(password: string) {
const givenPasswordHash = toBase64(myScryptService.getVerificationHash(password));
const dbPasswordHash = optionService.getOptionOrNull('passwordVerificationHash');
const dbPasswordHash = optionService.getOptionOrNull("passwordVerificationHash");
if (!dbPasswordHash) {
return false;
@@ -20,13 +20,13 @@ function setDataKey(password: string, plainTextDataKey: string | Buffer) {
const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, plainTextDataKey);
optionService.setOption('encryptedDataKey', newEncryptedDataKey);
optionService.setOption("encryptedDataKey", newEncryptedDataKey);
}
function getDataKey(password: string) {
const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password);
const encryptedDataKey = optionService.getOption('encryptedDataKey');
const encryptedDataKey = optionService.getOption("encryptedDataKey");
const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey);

View File

@@ -6,26 +6,26 @@ import { randomString } from "./utils.js";
import instanceId from "./instance_id.js";
import becca from "../becca/becca.js";
import blobService from "../services/blob.js";
import { EntityChange } from './entity_changes_interface.js';
import { EntityChange } from "./entity_changes_interface.js";
import type { Blob } from "./blob-interface.js";
import eventService from "./events.js";
let maxEntityChangeId = 0;
function putEntityChangeWithInstanceId(origEntityChange: EntityChange, instanceId: string) {
const ec = {...origEntityChange, instanceId};
const ec = { ...origEntityChange, instanceId };
putEntityChange(ec);
}
function putEntityChangeWithForcedChange(origEntityChange: EntityChange) {
const ec = {...origEntityChange, changeId: null};
const ec = { ...origEntityChange, changeId: null };
putEntityChange(ec);
}
function putEntityChange(origEntityChange: EntityChange) {
const ec = {...origEntityChange};
const ec = { ...origEntityChange };
delete ec.id;
@@ -50,7 +50,7 @@ function putNoteReorderingEntityChange(parentNoteId: string, componentId?: strin
putEntityChange({
entityName: "note_reordering",
entityId: parentNoteId,
hash: 'N/A',
hash: "N/A",
isErased: false,
utcDateChanged: dateUtils.utcNowDateTime(),
isSynced: true,
@@ -59,7 +59,7 @@ function putNoteReorderingEntityChange(parentNoteId: string, componentId?: strin
});
eventService.emit(eventService.ENTITY_CHANGED, {
entityName: 'note_reordering',
entityName: "note_reordering",
entity: sql.getMap(`SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ?`, [parentNoteId])
});
}
@@ -78,10 +78,10 @@ function addEntityChangesForSector(entityName: string, sector: string) {
let entitiesInserted = entityChanges.length;
sql.transactional(() => {
if (entityName === 'blobs') {
entitiesInserted += addEntityChangesForDependingEntity(sector, 'notes', 'noteId');
entitiesInserted += addEntityChangesForDependingEntity(sector, 'attachments', 'attachmentId');
entitiesInserted += addEntityChangesForDependingEntity(sector, 'revisions', 'revisionId');
if (entityName === "blobs") {
entitiesInserted += addEntityChangesForDependingEntity(sector, "notes", "noteId");
entitiesInserted += addEntityChangesForDependingEntity(sector, "attachments", "attachmentId");
entitiesInserted += addEntityChangesForDependingEntity(sector, "revisions", "revisionId");
}
for (const ec of entityChanges) {
@@ -94,12 +94,15 @@ function addEntityChangesForSector(entityName: string, sector: string) {
function addEntityChangesForDependingEntity(sector: string, tableName: string, primaryKeyColumn: string) {
// problem in blobs might be caused by problem in entity referencing the blob
const dependingEntityChanges = sql.getRows<EntityChange>(`
const dependingEntityChanges = sql.getRows<EntityChange>(
`
SELECT dep_change.*
FROM entity_changes orig_sector
JOIN ${tableName} ON ${tableName}.blobId = orig_sector.entityId
JOIN entity_changes dep_change ON dep_change.entityName = '${tableName}' AND dep_change.entityId = ${tableName}.${primaryKeyColumn}
WHERE orig_sector.entityName = 'blobs' AND SUBSTR(orig_sector.entityId, 1, 1) = ?`, [sector]);
WHERE orig_sector.entityName = 'blobs' AND SUBSTR(orig_sector.entityId, 1, 1) = ?`,
[sector]
);
for (const ec of dependingEntityChanges) {
putEntityChangeWithForcedChange(ec);
@@ -118,7 +121,7 @@ function cleanupEntityChangesForMissingEntities(entityName: string, entityPrimar
AND entityId NOT IN (SELECT ${entityPrimaryKey} FROM ${entityName})`);
}
function fillEntityChanges(entityName: string, entityPrimaryKey: string, condition = '') {
function fillEntityChanges(entityName: string, entityPrimaryKey: string, condition = "") {
cleanupEntityChangesForMissingEntities(entityName, entityPrimaryKey);
sql.transactional(() => {
@@ -142,7 +145,7 @@ function fillEntityChanges(entityName: string, entityPrimaryKey: string, conditi
isErased: false
};
if (entityName === 'blobs') {
if (entityName === "blobs") {
const blob = sql.getRow<Blob>("SELECT blobId, content, utcDateModified FROM blobs WHERE blobId = ?", [entityId]);
ec.hash = blobService.calculateContentHash(blob);
ec.utcDateChanged = blob.utcDateModified;
@@ -153,7 +156,7 @@ function fillEntityChanges(entityName: string, entityPrimaryKey: string, conditi
if (entity) {
ec.hash = entity.generateHash();
ec.utcDateChanged = entity.getUtcDateChanged() || dateUtils.utcNowDateTime();
ec.isSynced = entityName !== 'options' || !!entity.isSynced;
ec.isSynced = entityName !== "options" || !!entity.isSynced;
} else {
// entity might be null (not present in becca) when it's deleted
// this will produce different hash value than when entity is being deleted since then
@@ -184,7 +187,7 @@ function fillAllEntityChanges() {
fillEntityChanges("blobs", "blobId");
fillEntityChanges("attributes", "attributeId");
fillEntityChanges("etapi_tokens", "etapiTokenId");
fillEntityChanges("options", "name", 'WHERE isSynced = 1');
fillEntityChanges("options", "name", "WHERE isSynced = 1");
});
}

View File

@@ -1,5 +1,5 @@
function isDev() {
return !!(process.env.TRILIUM_ENV && process.env.TRILIUM_ENV === 'dev');
return !!(process.env.TRILIUM_ENV && process.env.TRILIUM_ENV === "dev");
}
export default {

View File

@@ -16,18 +16,15 @@ function eraseNotes(noteIdsToErase: string[]) {
setEntityChangesAsErased(sql.getManyRows(`SELECT * FROM entity_changes WHERE entityName = 'notes' AND entityId IN (???)`, noteIdsToErase));
// we also need to erase all "dependent" entities of the erased notes
const branchIdsToErase = sql.getManyRows<{ branchId: string }>(`SELECT branchId FROM branches WHERE noteId IN (???)`, noteIdsToErase)
.map(row => row.branchId);
const branchIdsToErase = sql.getManyRows<{ branchId: string }>(`SELECT branchId FROM branches WHERE noteId IN (???)`, noteIdsToErase).map((row) => row.branchId);
eraseBranches(branchIdsToErase);
const attributeIdsToErase = sql.getManyRows<{ attributeId: string }>(`SELECT attributeId FROM attributes WHERE noteId IN (???)`, noteIdsToErase)
.map(row => row.attributeId);
const attributeIdsToErase = sql.getManyRows<{ attributeId: string }>(`SELECT attributeId FROM attributes WHERE noteId IN (???)`, noteIdsToErase).map((row) => row.attributeId);
eraseAttributes(attributeIdsToErase);
const revisionIdsToErase = sql.getManyRows<{ revisionId: string }>(`SELECT revisionId FROM revisions WHERE noteId IN (???)`, noteIdsToErase)
.map(row => row.revisionId);
const revisionIdsToErase = sql.getManyRows<{ revisionId: string }>(`SELECT revisionId FROM revisions WHERE noteId IN (???)`, noteIdsToErase).map((row) => row.revisionId);
eraseRevisions(revisionIdsToErase);
@@ -120,7 +117,7 @@ function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds: number | null = n
// this is important also so that the erased entity changes are sent to the connected clients
sql.transactional(() => {
if (eraseEntitiesAfterTimeInSeconds === null) {
eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt('eraseEntitiesAfterTimeInSeconds');
eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt("eraseEntitiesAfterTimeInSeconds");
}
const cutoffDate = new Date(Date.now() - eraseEntitiesAfterTimeInSeconds * 1000);
@@ -167,11 +164,11 @@ function eraseUnusedAttachmentsNow() {
function eraseScheduledAttachments(eraseUnusedAttachmentsAfterSeconds: number | null = null) {
if (eraseUnusedAttachmentsAfterSeconds === null) {
eraseUnusedAttachmentsAfterSeconds = optionService.getOptionInt('eraseUnusedAttachmentsAfterSeconds');
eraseUnusedAttachmentsAfterSeconds = optionService.getOptionInt("eraseUnusedAttachmentsAfterSeconds");
}
const cutOffDate = dateUtils.utcDateTimeStr(new Date(Date.now() - (eraseUnusedAttachmentsAfterSeconds * 1000)));
const attachmentIdsToErase = sql.getColumn<string>('SELECT attachmentId FROM attachments WHERE utcDateScheduledForErasureSince < ?', [cutOffDate]);
const cutOffDate = dateUtils.utcDateTimeStr(new Date(Date.now() - eraseUnusedAttachmentsAfterSeconds * 1000));
const attachmentIdsToErase = sql.getColumn<string>("SELECT attachmentId FROM attachments WHERE utcDateScheduledForErasureSince < ?", [cutOffDate]);
eraseAttachments(attachmentIdsToErase);
}
@@ -179,11 +176,23 @@ function eraseScheduledAttachments(eraseUnusedAttachmentsAfterSeconds: number |
export function startScheduledCleanup() {
sqlInit.dbReady.then(() => {
// first cleanup kickoff 5 minutes after startup
setTimeout(cls.wrap(() => eraseDeletedEntities()), 5 * 60 * 1000);
setTimeout(cls.wrap(() => eraseScheduledAttachments()), 6 * 60 * 1000);
setTimeout(
cls.wrap(() => eraseDeletedEntities()),
5 * 60 * 1000
);
setTimeout(
cls.wrap(() => eraseScheduledAttachments()),
6 * 60 * 1000
);
setInterval(cls.wrap(() => eraseDeletedEntities()), 4 * 3600 * 1000);
setInterval(cls.wrap(() => eraseScheduledAttachments()), 3600 * 1000);
setInterval(
cls.wrap(() => eraseDeletedEntities()),
4 * 3600 * 1000
);
setInterval(
cls.wrap(() => eraseScheduledAttachments()),
3600 * 1000
);
});
}
@@ -193,5 +202,5 @@ export default {
eraseNotesWithDeleteId,
eraseUnusedBlobs,
eraseAttachments,
eraseRevisions,
eraseRevisions
};

View File

@@ -8,7 +8,7 @@ function getTokens() {
}
function getTokenHash(token: crypto.BinaryLike) {
return crypto.createHash('sha256').update(token).digest('base64');
return crypto.createHash("sha256").update(token).digest("base64");
}
function createToken(tokenName: string) {
@@ -52,14 +52,12 @@ function parseAuthToken(auth: string | undefined) {
if (chunks.length === 1) {
return { token: auth }; // legacy format without etapiTokenId
}
else if (chunks.length === 2) {
} else if (chunks.length === 2) {
return {
etapiTokenId: chunks[0],
token: chunks[1]
}
}
else {
};
} else {
return null; // wrong format
}
}
@@ -81,8 +79,7 @@ function isValidAuthHeader(auth: string | undefined) {
}
return etapiToken.tokenHash === authTokenHash;
}
else {
} else {
for (const etapiToken of becca.getEtapiTokens()) {
if (etapiToken.tokenHash === authTokenHash) {
return true;

View File

@@ -21,7 +21,7 @@ const eventListeners: Record<string, EventListener[]> = {};
*/
function subscribe(eventTypes: EventType, listener: EventListener) {
if (!Array.isArray(eventTypes)) {
eventTypes = [ eventTypes ];
eventTypes = [eventTypes];
}
for (const eventType of eventTypes) {
@@ -32,7 +32,7 @@ function subscribe(eventTypes: EventType, listener: EventListener) {
function subscribeBeccaLoader(eventTypes: EventType, listener: EventListener) {
if (!Array.isArray(eventTypes)) {
eventTypes = [ eventTypes ];
eventTypes = [eventTypes];
}
for (const eventType of eventTypes) {
@@ -50,8 +50,7 @@ function emit(eventType: string, data?: any) {
for (const listener of listeners) {
try {
listener(data);
}
catch (e: any) {
} catch (e: any) {
log.error(`Listener threw error: ${e.message}, stack: ${e.stack}`);
// we won't stop execution because of listener
}

View File

@@ -6,36 +6,27 @@ import turndownPluginGfm from "joplin-turndown-plugin-gfm";
let instance: TurndownService | null = null;
const fencedCodeBlockFilter: TurndownService.Rule = {
filter: function (node, options) {
return (
options.codeBlockStyle === 'fenced' &&
node.nodeName === 'PRE' &&
node.firstChild !== null &&
node.firstChild.nodeName === 'CODE'
)
},
filter: function (node, options) {
return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild !== null && node.firstChild.nodeName === "CODE";
},
replacement: function (content, node, options) {
if (!node.firstChild || !("getAttribute" in node.firstChild) || typeof node.firstChild.getAttribute !== "function") {
return content;
replacement: function (content, node, options) {
if (!node.firstChild || !("getAttribute" in node.firstChild) || typeof node.firstChild.getAttribute !== "function") {
return content;
}
const className = node.firstChild.getAttribute("class") || "";
const language = rewriteLanguageTag((className.match(/language-(\S+)/) || [null, ""])[1]);
return "\n\n" + options.fence + language + "\n" + node.firstChild.textContent + "\n" + options.fence + "\n\n";
}
const className = node.firstChild.getAttribute('class') || ''
const language = rewriteLanguageTag((className.match(/language-(\S+)/) || [null, ''])[1]);
return (
'\n\n' + options.fence + language + '\n' +
node.firstChild.textContent +
'\n' + options.fence + '\n\n'
)
}
};
function toMarkdown(content: string) {
if (instance === null) {
instance = new TurndownService({ codeBlockStyle: 'fenced' });
instance = new TurndownService({ codeBlockStyle: "fenced" });
// Filter is heavily based on: https://github.com/mixmark-io/turndown/issues/274#issuecomment-458730974
instance.addRule('fencedCodeBlock', fencedCodeBlockFilter);
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
instance.use(turndownPluginGfm.gfm);
}
@@ -44,16 +35,14 @@ function toMarkdown(content: string) {
function rewriteLanguageTag(source: string) {
if (!source) {
return source;
return source;
}
if (source === "text-x-trilium-auto") {
return "";
}
return source
.split("-")
.at(-1);
return source.split("-").at(-1);
}
export default {

View File

@@ -4,10 +4,10 @@ import { getContentDisposition, stripTags } from "../utils.js";
import becca from "../../becca/becca.js";
import TaskContext from "../task_context.js";
import BBranch from "../../becca/entities/bbranch.js";
import { Response } from 'express';
import { Response } from "express";
function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string, res: Response) {
if (!['1.0', '2.0'].includes(version)) {
if (!["1.0", "2.0"].includes(version)) {
throw new Error(`Unrecognized OPML version ${version}`);
}
@@ -17,30 +17,32 @@ function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string
function exportNoteInner(branchId: string) {
const branch = becca.getBranch(branchId);
if (!branch) { throw new Error("Unable to find branch."); }
if (!branch) {
throw new Error("Unable to find branch.");
}
const note = branch.getNote();
if (!note) { throw new Error("Unable to find note."); }
if (!note) {
throw new Error("Unable to find note.");
}
if (note.hasOwnedLabel('excludeFromExport')) {
if (note.hasOwnedLabel("excludeFromExport")) {
return;
}
const title = `${branch.prefix ? (`${branch.prefix} - `) : ''}${note.title}`;
const title = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.title}`;
if (opmlVersion === 1) {
const preparedTitle = escapeXmlAttribute(title);
const preparedContent = note.hasStringContent() ? prepareText(note.getContent() as string) : '';
const preparedContent = note.hasStringContent() ? prepareText(note.getContent() as string) : "";
res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`);
}
else if (opmlVersion === 2) {
} else if (opmlVersion === 2) {
const preparedTitle = escapeXmlAttribute(title);
const preparedContent = note.hasStringContent() ? escapeXmlAttribute(note.getContent() as string) : '';
const preparedContent = note.hasStringContent() ? escapeXmlAttribute(note.getContent() as string) : "";
res.write(`<outline text="${preparedTitle}" _note="${preparedContent}">\n`);
}
else {
} else {
throw new Error(`Unrecognized OPML version ${opmlVersion}`);
}
@@ -52,14 +54,13 @@ function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string
}
}
res.write('</outline>');
res.write("</outline>");
}
const filename = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.title}.opml`;
const filename = `${branch.prefix ? (`${branch.prefix} - `) : ''}${note.title}.opml`;
res.setHeader('Content-Disposition', getContentDisposition(filename));
res.setHeader('Content-Type', 'text/x-opml');
res.setHeader("Content-Disposition", getContentDisposition(filename));
res.setHeader("Content-Type", "text/x-opml");
res.write(`<?xml version="1.0" encoding="UTF-8"?>
<opml version="${version}">
@@ -80,22 +81,17 @@ function exportToOpml(taskContext: TaskContext, branch: BBranch, version: string
}
function prepareText(text: string) {
const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, '\n')
.replace(/&nbsp;/g, ' '); // nbsp isn't in XML standard (only HTML)
const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, "\n").replace(/&nbsp;/g, " "); // nbsp isn't in XML standard (only HTML)
const stripped = stripTags(newLines);
const escaped = escapeXmlAttribute(stripped);
return escaped.replace(/\n/g, '&#10;');
return escaped.replace(/\n/g, "&#10;");
}
function escapeXmlAttribute(text: string) {
return text.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
}
export default {

View File

@@ -7,16 +7,16 @@ import mdService from "./md.js";
import becca from "../../becca/becca.js";
import TaskContext from "../task_context.js";
import BBranch from "../../becca/entities/bbranch.js";
import { Response } from 'express';
import { Response } from "express";
function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response) {
const note = branch.getNote();
if (note.type === 'image' || note.type === 'file') {
if (note.type === "image" || note.type === "file") {
return [400, `Note type '${note.type}' cannot be exported as single file.`];
}
if (format !== 'html' && format !== 'markdown') {
if (format !== "html" && format !== "markdown") {
return [400, `Unrecognized format '${format}'`];
}
@@ -27,42 +27,37 @@ function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "ht
throw new Error("Unsupported content type for export.");
}
if (note.type === 'text') {
if (format === 'html') {
if (note.type === "text") {
if (format === "html") {
content = inlineAttachments(content);
if (!content.toLowerCase().includes("<html")) {
content = `<html><head><meta charset="utf-8"></head><body>${content}</body></html>`;
}
payload = content.length < 100_000
? html.prettyPrint(content, {indent_size: 2})
: content;
payload = content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content;
extension = 'html';
mime = 'text/html';
}
else if (format === 'markdown') {
extension = "html";
mime = "text/html";
} else if (format === "markdown") {
payload = mdService.toMarkdown(content);
extension = 'md';
mime = 'text/x-markdown'
extension = "md";
mime = "text/x-markdown";
}
}
else if (note.type === 'code') {
} else if (note.type === "code") {
payload = content;
extension = mimeTypes.extension(note.mime) || 'code';
extension = mimeTypes.extension(note.mime) || "code";
mime = note.mime;
}
else if (note.type === 'relationMap' || note.type === 'canvas' || note.type === 'search') {
} else if (note.type === "relationMap" || note.type === "canvas" || note.type === "search") {
payload = content;
extension = 'json';
mime = 'application/json';
extension = "json";
mime = "application/json";
}
const fileName = `${note.title}.${extension}`;
res.setHeader('Content-Disposition', getContentDisposition(fileName));
res.setHeader('Content-Type', `${mime}; charset=UTF-8`);
res.setHeader("Content-Disposition", getContentDisposition(fileName));
res.setHeader("Content-Type", `${mime}; charset=UTF-8`);
res.send(payload);
@@ -73,7 +68,7 @@ function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "ht
function inlineAttachments(content: string) {
content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9_]+)\/?[^"]+"/g, (match, noteId) => {
const note = becca.getNote(noteId);
if (!note || !note.mime.startsWith('image/')) {
if (!note || !note.mime.startsWith("image/")) {
return match;
}
@@ -82,7 +77,7 @@ function inlineAttachments(content: string) {
return match;
}
const base64Content = imageContent.toString('base64');
const base64Content = imageContent.toString("base64");
const srcValue = `data:${note.mime};base64,${base64Content}`;
return `src="${srcValue}"`;
@@ -90,7 +85,7 @@ function inlineAttachments(content: string) {
content = content.replace(/src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image\/?[^"]+"/g, (match, attachmentId) => {
const attachment = becca.getAttachment(attachmentId);
if (!attachment || !attachment.mime.startsWith('image/')) {
if (!attachment || !attachment.mime.startsWith("image/")) {
return match;
}
@@ -99,7 +94,7 @@ function inlineAttachments(content: string) {
return match;
}
const base64Content = attachmentContent.toString('base64');
const base64Content = attachmentContent.toString("base64");
const srcValue = `data:${attachment.mime};base64,${base64Content}`;
return `src="${srcValue}"`;
@@ -116,7 +111,7 @@ function inlineAttachments(content: string) {
return match;
}
const base64Content = attachmentContent.toString('base64');
const base64Content = attachmentContent.toString("base64");
const hrefValue = `data:${attachment.mime};base64,${base64Content}`;
return `href="${hrefValue}" download="${escapeHtml(attachment.title)}"`;

View File

@@ -19,15 +19,15 @@ import NoteMeta from "../meta/note_meta.js";
import AttachmentMeta from "../meta/attachment_meta.js";
import AttributeMeta from "../meta/attribute_meta.js";
import BBranch from "../../becca/entities/bbranch.js";
import { Response } from 'express';
import { Response } from "express";
import { RESOURCE_DIR } from "../resource_dir.js";
async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true) {
if (!['html', 'markdown'].includes(format)) {
if (!["html", "markdown"].includes(format)) {
throw new ValidationError(`Only 'html' and 'markdown' allowed as export format, '${format}' given`);
}
const archive = archiver('zip', {
const archive = archiver("zip", {
zlib: { level: 9 } // Sets the compression level.
});
@@ -44,12 +44,10 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
index = existingFileNames[lcFileName]++;
newName = `${index}_${lcFileName}`;
}
while (newName in existingFileNames);
} while (newName in existingFileNames);
return `${index}_${fileName}`;
}
else {
} else {
existingFileNames[lcFileName] = 1;
return fileName;
@@ -63,7 +61,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
if (fileName.length > 30) {
// We use regex to match the extension to preserve multiple dots in extensions (e.g. .tar.gz).
let match = fileName.match(/(\.[a-zA-Z0-9_.!#-]+)$/);
let ext = match ? match[0] : '';
let ext = match ? match[0] : "";
// Crop the extension if extension length exceeds 30
const croppedExt = ext.slice(-30);
// Crop the file name section and append the cropped extension
@@ -75,26 +73,22 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
// the following two are handled specifically since we always want to have these extensions no matter the automatic detection
// and/or existing detected extensions in the note name
if (type === 'text' && format === 'markdown') {
newExtension = 'md';
}
else if (type === 'text' && format === 'html') {
newExtension = 'html';
}
else if (mime === 'application/x-javascript' || mime === 'text/javascript') {
newExtension = 'js';
}
else if (type === 'canvas' || mime === 'application/json') {
newExtension = 'json';
}
else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it
if (type === "text" && format === "markdown") {
newExtension = "md";
} else if (type === "text" && format === "html") {
newExtension = "html";
} else if (mime === "application/x-javascript" || mime === "text/javascript") {
newExtension = "js";
} else if (type === "canvas" || mime === "application/json") {
newExtension = "json";
} else if (existingExtension.length > 0) {
// if the page already has an extension, then we'll just keep it
newExtension = null;
}
else {
} else {
if (mime?.toLowerCase()?.trim() === "image/jpg") {
newExtension = 'jpg';
newExtension = "jpg";
} else if (mime?.toLowerCase()?.trim() === "text/mermaid") {
newExtension = 'txt';
newExtension = "txt";
} else {
newExtension = mimeTypes.extension(mime) || "dat";
}
@@ -111,23 +105,26 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
function createNoteMeta(branch: BBranch, parentMeta: Partial<NoteMeta>, existingFileNames: Record<string, number>): NoteMeta | null {
const note = branch.getNote();
if (note.hasOwnedLabel('excludeFromExport')) {
if (note.hasOwnedLabel("excludeFromExport")) {
return null;
}
const title = note.getTitleOrProtected();
const completeTitle = branch.prefix ? (`${branch.prefix} - ${title}`) : title;
const completeTitle = branch.prefix ? `${branch.prefix} - ${title}` : title;
let baseFileName = sanitize(completeTitle);
if (baseFileName.length > 200) { // the actual limit is 256 bytes(!) but let's be conservative
if (baseFileName.length > 200) {
// the actual limit is 256 bytes(!) but let's be conservative
baseFileName = baseFileName.substr(0, 200);
}
if (!parentMeta.notePath) { throw new Error("Missing parent note path."); }
if (!parentMeta.notePath) {
throw new Error("Missing parent note path.");
}
const notePath = parentMeta.notePath.concat([note.noteId]);
if (note.noteId in noteIdToMeta) {
const fileName = getUniqueFilename(existingFileNames, `${baseFileName}.clone.${format === 'html' ? 'html' : 'md'}`);
const fileName = getUniqueFilename(existingFileNames, `${baseFileName}.clone.${format === "html" ? "html" : "md"}`);
const meta: NoteMeta = {
isClone: true,
@@ -136,7 +133,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
title: note.getTitleOrProtected(),
prefix: branch.prefix,
dataFileName: fileName,
type: 'text', // export will have text description
type: "text", // export will have text description
format: format
};
return meta;
@@ -152,7 +149,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
meta.isExpanded = branch.isExpanded;
meta.type = note.type;
meta.mime = note.mime;
meta.attributes = note.getOwnedAttributes().map(attribute => {
meta.attributes = note.getOwnedAttributes().map((attribute) => {
const attrMeta: AttributeMeta = {
type: attribute.type,
name: attribute.name,
@@ -166,7 +163,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
taskContext.increaseProgressCount();
if (note.type === 'text') {
if (note.type === "text") {
meta.format = format;
}
@@ -174,8 +171,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
// sort children for having a stable / reproducible export format
note.sortChildren();
const childBranches = note.getChildBranches()
.filter(branch => branch?.noteId !== '_hidden');
const childBranches = note.getChildBranches().filter((branch) => branch?.noteId !== "_hidden");
const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable();
@@ -185,23 +181,17 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
}
const attachments = note.getAttachments();
meta.attachments = attachments
.map(attachment => {
const attMeta: AttachmentMeta = {
attachmentId: attachment.attachmentId,
title: attachment.title,
role: attachment.role,
mime: attachment.mime,
position: attachment.position,
dataFileName: getDataFileName(
null,
attachment.mime,
baseFileName + "_" + attachment.title,
existingFileNames
)
};
return attMeta;
});
meta.attachments = attachments.map((attachment) => {
const attMeta: AttachmentMeta = {
attachmentId: attachment.attachmentId,
title: attachment.title,
role: attachment.role,
mime: attachment.mime,
position: attachment.position,
dataFileName: getDataFileName(null, attachment.mime, baseFileName + "_" + attachment.title, existingFileNames)
};
return attMeta;
});
if (childBranches.length > 0) {
meta.dirFileName = getUniqueFilename(existingFileNames, baseFileName);
@@ -211,7 +201,9 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
const childExistingNames = {};
for (const childBranch of childBranches) {
if (!childBranch) { continue; }
if (!childBranch) {
continue;
}
const note = createNoteMeta(childBranch, meta as NoteMeta, childExistingNames);
@@ -288,7 +280,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
function findAttachment(targetAttachmentId: string) {
let url;
const attachmentMeta = (noteMeta.attachments || []).find(attMeta => attMeta.attachmentId === targetAttachmentId);
const attachmentMeta = (noteMeta.attachments || []).find((attMeta) => attMeta.attachmentId === targetAttachmentId);
if (attachmentMeta) {
// easy job here, because attachment will be in the same directory as the note's data file.
url = attachmentMeta.dataFileName;
@@ -300,15 +292,17 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
}
function prepareContent(title: string, content: string | Buffer, noteMeta: NoteMeta): string | Buffer {
if (['html', 'markdown'].includes(noteMeta?.format || "")) {
if (["html", "markdown"].includes(noteMeta?.format || "")) {
content = content.toString();
content = rewriteLinks(content, noteMeta);
}
if (noteMeta.format === 'html' && typeof content === "string") {
if (noteMeta.format === "html" && typeof content === "string") {
if (!content.substr(0, 100).toLowerCase().includes("<html")) {
if (!noteMeta?.notePath?.length) { throw new Error("Missing note path."); }
if (!noteMeta?.notePath?.length) {
throw new Error("Missing note path.");
}
const cssUrl = `${"../".repeat(noteMeta.notePath.length - 1)}style.css`;
const htmlTitle = escapeHtml(title);
@@ -332,10 +326,8 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
</html>`;
}
return content.length < 100_000
? html.prettyPrint(content, {indent_size: 2})
: content;
} else if (noteMeta.format === 'markdown' && typeof content === "string") {
return content.length < 100_000 ? html.prettyPrint(content, { indent_size: 2 }) : content;
} else if (noteMeta.format === "markdown" && typeof content === "string") {
let markdownContent = mdService.toMarkdown(content);
if (markdownContent.trim().length > 0 && !markdownContent.startsWith("# ")) {
@@ -369,8 +361,12 @@ ${markdownContent}`;
}
const note = becca.getNote(noteMeta.noteId);
if (!note) { throw new Error("Unable to find note."); }
if (!note.utcDateModified) { throw new Error("Unable to find modification date."); }
if (!note) {
throw new Error("Unable to find note.");
}
if (!note.utcDateModified) {
throw new Error("Unable to find modification date.");
}
if (noteMeta.dataFileName) {
const content = prepareContent(noteMeta.title, note.getContent(), noteMeta);
@@ -384,7 +380,9 @@ ${markdownContent}`;
taskContext.increaseProgressCount();
for (const attachmentMeta of noteMeta.attachments || []) {
if (!attachmentMeta.attachmentId) { continue; }
if (!attachmentMeta.attachmentId) {
continue;
}
const attachment = note.getAttachmentById(attachmentMeta.attachmentId);
const content = attachment.getContent();
@@ -399,7 +397,7 @@ ${markdownContent}`;
const directoryPath = filePathPrefix + noteMeta.dirFileName;
// create directory
archive.append('', { name: `${directoryPath}/`, date: dateUtils.parseDateTime(note.utcDateModified) });
archive.append("", { name: `${directoryPath}/`, date: dateUtils.parseDateTime(note.utcDateModified) });
for (const childMeta of noteMeta.children || []) {
saveNote(childMeta, `${directoryPath}/`);
@@ -409,27 +407,26 @@ ${markdownContent}`;
function saveNavigation(rootMeta: NoteMeta, navigationMeta: NoteMeta) {
function saveNavigationInner(meta: NoteMeta) {
let html = '<li>';
let html = "<li>";
const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ''}${meta.title}`);
const escapedTitle = escapeHtml(`${meta.prefix ? `${meta.prefix} - ` : ""}${meta.title}`);
if (meta.dataFileName && meta.noteId) {
const targetUrl = getNoteTargetUrl(meta.noteId, rootMeta);
html += `<a href="${targetUrl}" target="detail">${escapedTitle}</a>`;
}
else {
} else {
html += escapedTitle;
}
if (meta.children && meta.children.length > 0) {
html += '<ul>';
html += "<ul>";
for (const child of meta.children) {
html += saveNavigationInner(child);
}
html += '</ul>'
html += "</ul>";
}
return `${html}</li>`;
@@ -444,9 +441,7 @@ ${markdownContent}`;
<ul>${saveNavigationInner(rootMeta)}</ul>
</body>
</html>`;
const prettyHtml = fullHtml.length < 100_000
? html.prettyPrint(fullHtml, {indent_size: 2})
: fullHtml;
const prettyHtml = fullHtml.length < 100_000 ? html.prettyPrint(fullHtml, { indent_size: 2 }) : fullHtml;
archive.append(prettyHtml, { name: navigationMeta.dataFileName });
}
@@ -462,8 +457,7 @@ ${markdownContent}`;
if (curMeta.children && curMeta.children.length > 0) {
curMeta = curMeta.children[0];
}
else {
} else {
break;
}
}
@@ -489,20 +483,20 @@ ${markdownContent}`;
archive.append(cssContent, { name: cssMeta.dataFileName });
}
const existingFileNames: Record<string, number> = format === 'html' ? {'navigation': 0, 'index': 1} : {};
const existingFileNames: Record<string, number> = format === "html" ? { navigation: 0, index: 1 } : {};
const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames);
const metaFile = {
formatVersion: 2,
appVersion: packageInfo.version,
files: [ rootMeta ]
files: [rootMeta]
};
let navigationMeta: NoteMeta | null = null;
let indexMeta: NoteMeta | null = null;
let cssMeta: NoteMeta | null = null;
if (format === 'html') {
if (format === "html") {
navigationMeta = {
noImport: true,
dataFileName: "navigation.html"
@@ -527,12 +521,12 @@ ${markdownContent}`;
for (const noteMeta of Object.values(noteIdToMeta)) {
// filter out relations which are not inside this export
noteMeta.attributes = (noteMeta.attributes || []).filter(attr => {
if (attr.type !== 'relation') {
noteMeta.attributes = (noteMeta.attributes || []).filter((attr) => {
if (attr.type !== "relation") {
return true;
} else if (attr.value in noteIdToMeta) {
return true;
} else if (attr.value === 'root' || attr.value?.startsWith("_")) {
} else if (attr.value === "root" || attr.value?.startsWith("_")) {
// relations to "named" noteIds can be preserved
return true;
} else {
@@ -541,20 +535,21 @@ ${markdownContent}`;
});
}
if (!rootMeta) { // corner case of disabled export for exported note
if (!rootMeta) {
// corner case of disabled export for exported note
if ("sendStatus" in res) {
res.sendStatus(400);
}
return;
}
const metaFileJson = JSON.stringify(metaFile, null, '\t');
const metaFileJson = JSON.stringify(metaFile, null, "\t");
archive.append(metaFileJson, { name: "!!!meta.json" });
saveNote(rootMeta, '');
saveNote(rootMeta, "");
if (format === 'html') {
if (format === "html") {
if (!navigationMeta || !indexMeta || !cssMeta) {
throw new Error("Missing meta.");
}
@@ -568,8 +563,8 @@ ${markdownContent}`;
const zipFileName = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.getTitleOrProtected()}.zip`;
if (setHeaders && "setHeader" in res) {
res.setHeader('Content-Disposition', getContentDisposition(zipFileName));
res.setHeader('Content-Type', 'application/zip');
res.setHeader("Content-Disposition", getContentDisposition(zipFileName));
res.setHeader("Content-Type", "application/zip");
}
archive.pipe(res);
@@ -580,7 +575,7 @@ ${markdownContent}`;
async function exportToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string) {
const fileOutputStream = fs.createWriteStream(zipFilePath);
const taskContext = new TaskContext('no-progress-reporting');
const taskContext = new TaskContext("no-progress-reporting");
const note = becca.getNote(noteId);

View File

@@ -8,7 +8,7 @@ import hiddenSubtreeService from "./hidden_subtree.js";
import oneTimeTimer from "./one_time_timer.js";
import BNote from "../becca/entities/bnote.js";
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import { DefinitionObject } from './promoted_attribute_definition_interface.js';
import { DefinitionObject } from "./promoted_attribute_definition_interface.js";
type Handler = (definition: DefinitionObject, note: BNote, targetNote: BNote) => void;
@@ -19,9 +19,10 @@ function runAttachedRelations(note: BNote, relationName: string, originEntity: A
// the same script note can get here with multiple ways, but execute only once
const notesToRun = new Set(
note.getRelations(relationName)
.map(relation => relation.getTargetNote())
.filter(note => !!note) as BNote[]
note
.getRelations(relationName)
.map((relation) => relation.getTargetNote())
.filter((note) => !!note) as BNote[]
);
for (const noteToRun of notesToRun) {
@@ -29,8 +30,8 @@ function runAttachedRelations(note: BNote, relationName: string, originEntity: A
}
}
eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
runAttachedRelations(note, 'runOnNoteTitleChange', note);
eventService.subscribe(eventService.NOTE_TITLE_CHANGED, (note) => {
runAttachedRelations(note, "runOnNoteTitleChange", note);
if (!note.isRoot()) {
const noteFromCache = becca.notes[note.noteId];
@@ -48,23 +49,22 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
});
eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED], ({ entityName, entity }) => {
if (entityName === 'attributes') {
runAttachedRelations(entity.getNote(), 'runOnAttributeChange', entity);
if (entityName === "attributes") {
runAttachedRelations(entity.getNote(), "runOnAttributeChange", entity);
if (entity.type === 'label' && ['sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale'].includes(entity.name)) {
if (entity.type === "label" && ["sorted", "sortDirection", "sortFoldersFirst", "sortNatural", "sortLocale"].includes(entity.name)) {
handleSortedAttribute(entity);
} else if (entity.type === 'label') {
} else if (entity.type === "label") {
handleMaybeSortingLabel(entity);
}
}
else if (entityName === 'notes') {
} else if (entityName === "notes") {
// ENTITY_DELETED won't trigger anything since all branches/attributes are already deleted at this point
runAttachedRelations(entity, 'runOnNoteChange', entity);
runAttachedRelations(entity, "runOnNoteChange", entity);
}
});
eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => {
if (entityName === 'branches') {
if (entityName === "branches") {
const parentNote = becca.getNote(entity.parentNoteId);
if (parentNote?.hasLabel("sorted")) {
@@ -74,20 +74,20 @@ eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) =>
const childNote = becca.getNote(entity.noteId);
if (childNote) {
runAttachedRelations(childNote, 'runOnBranchChange', entity);
runAttachedRelations(childNote, "runOnBranchChange", entity);
}
}
});
eventService.subscribe(eventService.NOTE_CONTENT_CHANGE, ({ entity }) => {
runAttachedRelations(entity, 'runOnNoteContentChange', entity);
runAttachedRelations(entity, "runOnNoteContentChange", entity);
});
eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => {
if (entityName === 'attributes') {
runAttachedRelations(entity.getNote(), 'runOnAttributeCreation', entity);
if (entityName === "attributes") {
runAttachedRelations(entity.getNote(), "runOnAttributeCreation", entity);
if (entity.type === 'relation' && entity.name === 'template') {
if (entity.type === "relation" && entity.name === "template") {
const note = becca.getNote(entity.noteId);
if (!note) {
return;
@@ -101,12 +101,13 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
const content = note.getContent();
if (["text", "code"].includes(note.type)
&& typeof content === "string"
if (
["text", "code"].includes(note.type) &&
typeof content === "string" &&
// if the note has already content we're not going to overwrite it with template's one
&& (!content || content.trim().length === 0)
&& templateNote.hasStringContent()) {
(!content || content.trim().length === 0) &&
templateNote.hasStringContent()
) {
const templateNoteContent = templateNote.getContent();
if (templateNoteContent) {
@@ -123,32 +124,28 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
if (note.getChildNotes().length === 0 && !note.isDescendantOfNote(templateNote.noteId)) {
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
}
}
else if (entity.type === 'label' && ['sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale'].includes(entity.name)) {
} else if (entity.type === "label" && ["sorted", "sortDirection", "sortFoldersFirst", "sortNatural", "sortLocale"].includes(entity.name)) {
handleSortedAttribute(entity);
}
else if (entity.type === 'label') {
} else if (entity.type === "label") {
handleMaybeSortingLabel(entity);
}
}
else if (entityName === 'branches') {
runAttachedRelations(entity.getNote(), 'runOnBranchCreation', entity);
} else if (entityName === "branches") {
runAttachedRelations(entity.getNote(), "runOnBranchCreation", entity);
if (entity.parentNote?.hasLabel("sorted")) {
treeService.sortNotesIfNeeded(entity.parentNoteId);
}
}
else if (entityName === 'notes') {
runAttachedRelations(entity, 'runOnNoteCreation', entity);
} else if (entityName === "notes") {
runAttachedRelations(entity, "runOnNoteCreation", entity);
}
});
eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote }) => {
runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote);
runAttachedRelations(parentNote, "runOnChildNoteCreation", childNote);
});
function processInverseRelations(entityName: string, entity: BAttribute, handler: Handler) {
if (entityName === 'attributes' && entity.type === 'relation') {
if (entityName === "attributes" && entity.type === "relation") {
const note = entity.getNote();
const relDefinitions = note.getLabels(`relation:${entity.name}`);
@@ -194,9 +191,11 @@ function handleMaybeSortingLabel(entity: BAttribute) {
continue;
}
if (sorted.includes(entity.name) // hacky check if this label is used in the sort
|| entity.name === "top"
|| entity.name === "bottom") {
if (
sorted.includes(entity.name) || // hacky check if this label is used in the sort
entity.name === "top" ||
entity.name === "bottom"
) {
treeService.sortNotesIfNeeded(parentNote.noteId);
}
}
@@ -207,13 +206,12 @@ eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) =>
processInverseRelations(entityName, entity, (definition, note, targetNote) => {
// we need to make sure that also target's inverse attribute exists and if not, then create it
// inverse attribute has to target our note as well
const hasInverseAttribute = (targetNote.getRelations(definition.inverseRelation))
.some(attr => attr.value === note.noteId);
const hasInverseAttribute = targetNote.getRelations(definition.inverseRelation).some((attr) => attr.value === note.noteId);
if (!hasInverseAttribute) {
new BAttribute({
noteId: targetNote.noteId,
type: 'relation',
type: "relation",
name: definition.inverseRelation || "",
value: note.noteId,
isInheritable: entity.isInheritable
@@ -237,15 +235,14 @@ eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) =>
}
});
if (entityName === 'branches') {
runAttachedRelations(entity.getNote(), 'runOnBranchDeletion', entity);
if (entityName === "branches") {
runAttachedRelations(entity.getNote(), "runOnBranchDeletion", entity);
}
if (entityName === 'notes' && entity.noteId.startsWith("_")) {
if (entityName === "notes" && entity.noteId.startsWith("_")) {
// "named" note has been deleted, we will probably need to rebuild the hidden subtree
// scheduling so that bulk deletes won't trigger so many checks
oneTimeTimer.scheduleExecution('hidden-subtree-check', 1000,
() => hiddenSubtreeService.checkHiddenSubtree());
oneTimeTimer.scheduleExecution("hidden-subtree-check", 1000, () => hiddenSubtreeService.checkHiddenSubtree());
}
});

View File

@@ -20,7 +20,7 @@ interface Attribute {
type: AttributeType;
name: string;
isInheritable?: boolean;
value?: string
value?: string;
}
interface Item {
@@ -58,253 +58,292 @@ let hiddenSubtreeDefinition: Item;
function buildHiddenSubtreeDefinition(): Item {
return {
id: '_hidden',
id: "_hidden",
title: t("hidden-subtree.root-title"),
type: 'doc',
icon: 'bx bx-hide',
type: "doc",
icon: "bx bx-hide",
// we want to keep the hidden subtree always last, otherwise there will be problems with e.g., keyboard navigation
// over tree when it's in the middle
notePosition: 999_999_999,
attributes: [
{ type: 'label', name: 'excludeFromNoteMap', isInheritable: true },
{ type: 'label', name: 'docName', value: 'hidden' }
{ type: "label", name: "excludeFromNoteMap", isInheritable: true },
{ type: "label", name: "docName", value: "hidden" }
],
children: [
{
id: '_search',
id: "_search",
title: t("hidden-subtree.search-history-title"),
type: 'doc'
type: "doc"
},
{
id: '_globalNoteMap',
id: "_globalNoteMap",
title: t("hidden-subtree.note-map-title"),
type: 'noteMap',
type: "noteMap",
attributes: [
{ type: 'label', name: 'mapRootNoteId', value: 'hoisted' },
{ type: 'label', name: 'keepCurrentHoisting' }
{ type: "label", name: "mapRootNoteId", value: "hoisted" },
{ type: "label", name: "keepCurrentHoisting" }
]
},
{
id: '_sqlConsole',
id: "_sqlConsole",
title: t("hidden-subtree.sql-console-history-title"),
type: 'doc',
icon: 'bx-data'
type: "doc",
icon: "bx-data"
},
{
id: '_share',
id: "_share",
title: t("hidden-subtree.shared-notes-title"),
type: 'doc',
attributes: [ { type: 'label', name: 'docName', value: 'share' } ]
type: "doc",
attributes: [{ type: "label", name: "docName", value: "share" }]
},
{
id: '_bulkAction',
id: "_bulkAction",
title: t("hidden-subtree.bulk-action-title"),
type: 'doc',
type: "doc"
},
{
id: '_backendLog',
id: "_backendLog",
title: t("hidden-subtree.backend-log-title"),
type: 'contentWidget',
icon: 'bx-terminal',
type: "contentWidget",
icon: "bx-terminal",
attributes: [
{ type: 'label', name: 'keepCurrentHoisting' },
{ type: 'label', name: 'fullContentWidth' }
{ type: "label", name: "keepCurrentHoisting" },
{ type: "label", name: "fullContentWidth" }
]
},
{
// place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
id: '_userHidden',
id: "_userHidden",
title: t("hidden-subtree.user-hidden-title"),
type: 'doc',
attributes: [ { type: 'label', name: 'docName', value: 'user_hidden' } ]
type: "doc",
attributes: [{ type: "label", name: "docName", value: "user_hidden" }]
},
{
id: LBTPL_ROOT,
title: t("hidden-subtree.launch-bar-templates-title"),
type: 'doc',
type: "doc",
children: [
{
id: LBTPL_BASE,
title: t("hidden-subtree.base-abstract-launcher-title"),
type: 'doc'
type: "doc"
},
{
id: LBTPL_COMMAND,
title: t("hidden-subtree.command-launcher-title"),
type: 'doc',
type: "doc",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'command' },
{ type: 'label', name: 'docName', value: 'launchbar_command_launcher' }
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "command" },
{ type: "label", name: "docName", value: "launchbar_command_launcher" }
]
},
{
id: LBTPL_NOTE_LAUNCHER,
title: t("hidden-subtree.note-launcher-title"),
type: 'doc',
type: "doc",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'note' },
{ type: 'label', name: 'relation:target', value: 'promoted' },
{ type: 'label', name: 'relation:hoistedNote', value: 'promoted' },
{ type: 'label', name: 'label:keyboardShortcut', value: 'promoted,text' },
{ type: 'label', name: 'docName', value: 'launchbar_note_launcher' }
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "note" },
{ type: "label", name: "relation:target", value: "promoted" },
{ type: "label", name: "relation:hoistedNote", value: "promoted" },
{ type: "label", name: "label:keyboardShortcut", value: "promoted,text" },
{ type: "label", name: "docName", value: "launchbar_note_launcher" }
]
},
{
id: LBTPL_SCRIPT,
title: t("hidden-subtree.script-launcher-title"),
type: 'doc',
type: "doc",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'script' },
{ type: 'label', name: 'relation:script', value: 'promoted' },
{ type: 'label', name: 'label:keyboardShortcut', value: 'promoted,text' },
{ type: 'label', name: 'docName', value: 'launchbar_script_launcher' }
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "script" },
{ type: "label", name: "relation:script", value: "promoted" },
{ type: "label", name: "label:keyboardShortcut", value: "promoted,text" },
{ type: "label", name: "docName", value: "launchbar_script_launcher" }
]
},
{
id: LBTPL_BUILTIN_WIDGET,
title: t("hidden-subtree.built-in-widget-title"),
type: 'doc',
type: "doc",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'builtinWidget' }
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "builtinWidget" }
]
},
{
id: LBTPL_SPACER,
title: t("hidden-subtree.spacer-title"),
type: 'doc',
icon: 'bx-move-vertical',
type: "doc",
icon: "bx-move-vertical",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET },
{ type: 'label', name: 'builtinWidget', value: 'spacer' },
{ type: 'label', name: 'label:baseSize', value: 'promoted,number' },
{ type: 'label', name: 'label:growthFactor', value: 'promoted,number' },
{ type: 'label', name: 'docName', value: 'launchbar_spacer' }
{ type: "relation", name: "template", value: LBTPL_BUILTIN_WIDGET },
{ type: "label", name: "builtinWidget", value: "spacer" },
{ type: "label", name: "label:baseSize", value: "promoted,number" },
{ type: "label", name: "label:growthFactor", value: "promoted,number" },
{ type: "label", name: "docName", value: "launchbar_spacer" }
]
},
{
id: LBTPL_CUSTOM_WIDGET,
title: t("hidden-subtree.custom-widget-title"),
type: 'doc',
type: "doc",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'customWidget' },
{ type: 'label', name: 'relation:widget', value: 'promoted' },
{ type: 'label', name: 'docName', value: 'launchbar_widget_launcher' }
]
},
]
},
{
id: '_lbRoot',
title: t("hidden-subtree.launch-bar-title"),
type: 'doc',
icon: 'bx-sidebar',
isExpanded: true,
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
children: [
{
id: '_lbAvailableLaunchers',
title: t("hidden-subtree.available-launchers-title"),
type: 'doc',
icon: 'bx-hide',
isExpanded: true,
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
children: [
{ id: '_lbBackInHistory', title: t("hidden-subtree.go-to-previous-note-title"), type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-chevron-left',
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
{ id: '_lbForwardInHistory', title: t("hidden-subtree.go-to-next-note-title"), type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-chevron-right',
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
{ id: '_lbBackendLog', title: t("hidden-subtree.backend-log-title"), type: 'launcher', targetNoteId: '_backendLog', icon: 'bx bx-terminal' },
]
},
{
id: '_lbVisibleLaunchers',
title: t("hidden-subtree.visible-launchers-title"),
type: 'doc',
icon: 'bx-show',
isExpanded: true,
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
children: [
{ id: '_lbNewNote', title: t("hidden-subtree.new-note-title"), type: 'launcher', command: 'createNoteIntoInbox', icon: 'bx bx-file-blank' },
{ id: '_lbSearch', title: t("hidden-subtree.search-notes-title"), type: 'launcher', command: 'searchNotes', icon: 'bx bx-search', attributes: [
{ type: 'label', name: 'desktopOnly' }
] },
{ id: '_lbJumpTo', title: t("hidden-subtree.jump-to-note-title"), type: 'launcher', command: 'jumpToNote', icon: 'bx bx-send', attributes: [
{ type: 'label', name: 'desktopOnly' }
] },
{ id: '_lbNoteMap', title: t("hidden-subtree.note-map-title"), type: 'launcher', targetNoteId: '_globalNoteMap', icon: 'bx bxs-network-chart' },
{ id: '_lbCalendar', title: t("hidden-subtree.calendar-title"), type: 'launcher', builtinWidget: 'calendar', icon: 'bx bx-calendar' },
{ id: '_lbRecentChanges', title: t("hidden-subtree.recent-changes-title"), type: 'launcher', command: 'showRecentChanges', icon: 'bx bx-history', attributes: [
{ type: 'label', name: 'desktopOnly' }
] },
{ id: '_lbSpacer1', title: t("hidden-subtree.spacer-title"), type: 'launcher', builtinWidget: 'spacer', baseSize: "50", growthFactor: "0" },
{ id: '_lbBookmarks', title: t("hidden-subtree.bookmarks-title"), type: 'launcher', builtinWidget: 'bookmarks', icon: 'bx bx-bookmark' },
{ id: '_lbToday', title: t("hidden-subtree.open-today-journal-note-title"), type: 'launcher', builtinWidget: 'todayInJournal', icon: 'bx bx-calendar-star' },
{ id: '_lbSpacer2', title: t("hidden-subtree.spacer-title"), type: 'launcher', builtinWidget: 'spacer', baseSize: "0", growthFactor: "1" },
{ id: '_lbQuickSearch', title: t("hidden-subtree.quick-search-title"), type: "launcher", builtinWidget: "quickSearch", icon: "bx bx-rectangle" },
{ id: '_lbProtectedSession', title: t("hidden-subtree.protected-session-title"), type: 'launcher', builtinWidget: 'protectedSession', icon: 'bx bx bx-shield-quarter' },
{ id: '_lbSyncStatus', title: t("hidden-subtree.sync-status-title"), type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' },
{ id: '_lbSettings', title: t("hidden-subtree.settings-title"), type: 'launcher', command: 'showOptions', icon: 'bx bx-cog' }
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "customWidget" },
{ type: "label", name: "relation:widget", value: "promoted" },
{ type: "label", name: "docName", value: "launchbar_widget_launcher" }
]
}
]
},
{
id: '_lbMobileRoot',
id: "_lbRoot",
title: t("hidden-subtree.launch-bar-title"),
type: "doc",
icon: "bx-sidebar",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{
id: "_lbAvailableLaunchers",
title: t("hidden-subtree.available-launchers-title"),
type: "doc",
icon: "bx-hide",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{
id: "_lbBackInHistory",
title: t("hidden-subtree.go-to-previous-note-title"),
type: "launcher",
builtinWidget: "backInHistoryButton",
icon: "bx bxs-chevron-left",
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
},
{
id: "_lbForwardInHistory",
title: t("hidden-subtree.go-to-next-note-title"),
type: "launcher",
builtinWidget: "forwardInHistoryButton",
icon: "bx bxs-chevron-right",
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
},
{ id: "_lbBackendLog", title: t("hidden-subtree.backend-log-title"), type: "launcher", targetNoteId: "_backendLog", icon: "bx bx-terminal" }
]
},
{
id: "_lbVisibleLaunchers",
title: t("hidden-subtree.visible-launchers-title"),
type: "doc",
icon: "bx-show",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{ id: "_lbNewNote", title: t("hidden-subtree.new-note-title"), type: "launcher", command: "createNoteIntoInbox", icon: "bx bx-file-blank" },
{
id: "_lbSearch",
title: t("hidden-subtree.search-notes-title"),
type: "launcher",
command: "searchNotes",
icon: "bx bx-search",
attributes: [{ type: "label", name: "desktopOnly" }]
},
{
id: "_lbJumpTo",
title: t("hidden-subtree.jump-to-note-title"),
type: "launcher",
command: "jumpToNote",
icon: "bx bx-send",
attributes: [{ type: "label", name: "desktopOnly" }]
},
{ id: "_lbNoteMap", title: t("hidden-subtree.note-map-title"), type: "launcher", targetNoteId: "_globalNoteMap", icon: "bx bxs-network-chart" },
{ id: "_lbCalendar", title: t("hidden-subtree.calendar-title"), type: "launcher", builtinWidget: "calendar", icon: "bx bx-calendar" },
{
id: "_lbRecentChanges",
title: t("hidden-subtree.recent-changes-title"),
type: "launcher",
command: "showRecentChanges",
icon: "bx bx-history",
attributes: [{ type: "label", name: "desktopOnly" }]
},
{ id: "_lbSpacer1", title: t("hidden-subtree.spacer-title"), type: "launcher", builtinWidget: "spacer", baseSize: "50", growthFactor: "0" },
{ id: "_lbBookmarks", title: t("hidden-subtree.bookmarks-title"), type: "launcher", builtinWidget: "bookmarks", icon: "bx bx-bookmark" },
{ id: "_lbToday", title: t("hidden-subtree.open-today-journal-note-title"), type: "launcher", builtinWidget: "todayInJournal", icon: "bx bx-calendar-star" },
{ id: "_lbSpacer2", title: t("hidden-subtree.spacer-title"), type: "launcher", builtinWidget: "spacer", baseSize: "0", growthFactor: "1" },
{ id: "_lbQuickSearch", title: t("hidden-subtree.quick-search-title"), type: "launcher", builtinWidget: "quickSearch", icon: "bx bx-rectangle" },
{ id: "_lbProtectedSession", title: t("hidden-subtree.protected-session-title"), type: "launcher", builtinWidget: "protectedSession", icon: "bx bx bx-shield-quarter" },
{ id: "_lbSyncStatus", title: t("hidden-subtree.sync-status-title"), type: "launcher", builtinWidget: "syncStatus", icon: "bx bx-wifi" },
{ id: "_lbSettings", title: t("hidden-subtree.settings-title"), type: "launcher", command: "showOptions", icon: "bx bx-cog" }
]
}
]
},
{
id: "_lbMobileRoot",
title: "Mobile Launch Bar",
type: "doc",
icon: "bx-mobile",
isExpanded: true,
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{
id: "_lbMobileAvailableLaunchers",
title: t("hidden-subtree.available-launchers-title"),
type: 'doc',
icon: 'bx-hide',
type: "doc",
icon: "bx-hide",
isExpanded: true,
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: []
},
{
id: "_lbMobileVisibleLaunchers",
title: t("hidden-subtree.visible-launchers-title"),
type: 'doc',
icon: 'bx-show',
type: "doc",
icon: "bx-show",
isExpanded: true,
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{ id: '_lbMobileBackInHistory', title: t("hidden-subtree.go-to-previous-note-title"), type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-chevron-left',
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
{ id: '_lbMobileForwardInHistory', title: t("hidden-subtree.go-to-next-note-title"), type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-chevron-right',
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
{ id: '_lbMobileJumpTo', title: t("hidden-subtree.jump-to-note-title"), type: 'launcher', command: 'jumpToNote', icon: 'bx bx-plus-circle' }
{
id: "_lbMobileBackInHistory",
title: t("hidden-subtree.go-to-previous-note-title"),
type: "launcher",
builtinWidget: "backInHistoryButton",
icon: "bx bxs-chevron-left",
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
},
{
id: "_lbMobileForwardInHistory",
title: t("hidden-subtree.go-to-next-note-title"),
type: "launcher",
builtinWidget: "forwardInHistoryButton",
icon: "bx bxs-chevron-right",
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
},
{ id: "_lbMobileJumpTo", title: t("hidden-subtree.jump-to-note-title"), type: "launcher", command: "jumpToNote", icon: "bx bx-plus-circle" }
]
}
]
},
{
id: '_options',
id: "_options",
title: t("hidden-subtree.options-title"),
type: 'book',
icon: 'bx-cog',
type: "book",
icon: "bx-cog",
children: [
{ id: '_optionsAppearance', title: t("hidden-subtree.appearance-title"), type: 'contentWidget', icon: 'bx-layout' },
{ id: '_optionsShortcuts', title: t("hidden-subtree.shortcuts-title"), type: 'contentWidget', icon: 'bxs-keyboard' },
{ id: '_optionsTextNotes', title: t("hidden-subtree.text-notes"), type: 'contentWidget', icon: 'bx-text' },
{ id: '_optionsCodeNotes', title: t("hidden-subtree.code-notes-title"), type: 'contentWidget', icon: 'bx-code' },
{ id: '_optionsImages', title: t("hidden-subtree.images-title"), type: 'contentWidget', icon: 'bx-image' },
{ id: '_optionsSpellcheck', title: t("hidden-subtree.spellcheck-title"), type: 'contentWidget', icon: 'bx-check-double' },
{ id: '_optionsPassword', title: t("hidden-subtree.password-title"), type: 'contentWidget', icon: 'bx-lock' },
{ id: '_optionsEtapi', title: t("hidden-subtree.etapi-title"), type: 'contentWidget', icon: 'bx-extension' },
{ id: '_optionsBackup', title: t("hidden-subtree.backup-title"), type: 'contentWidget', icon: 'bx-data' },
{ id: '_optionsSync', title: t("hidden-subtree.sync-title"), type: 'contentWidget', icon: 'bx-wifi' },
{ id: '_optionsOther', title: t("hidden-subtree.other"), type: 'contentWidget', icon: 'bx-dots-horizontal' },
{ id: '_optionsAdvanced', title: t("hidden-subtree.advanced-title"), type: 'contentWidget' }
{ id: "_optionsAppearance", title: t("hidden-subtree.appearance-title"), type: "contentWidget", icon: "bx-layout" },
{ id: "_optionsShortcuts", title: t("hidden-subtree.shortcuts-title"), type: "contentWidget", icon: "bxs-keyboard" },
{ id: "_optionsTextNotes", title: t("hidden-subtree.text-notes"), type: "contentWidget", icon: "bx-text" },
{ id: "_optionsCodeNotes", title: t("hidden-subtree.code-notes-title"), type: "contentWidget", icon: "bx-code" },
{ id: "_optionsImages", title: t("hidden-subtree.images-title"), type: "contentWidget", icon: "bx-image" },
{ id: "_optionsSpellcheck", title: t("hidden-subtree.spellcheck-title"), type: "contentWidget", icon: "bx-check-double" },
{ id: "_optionsPassword", title: t("hidden-subtree.password-title"), type: "contentWidget", icon: "bx-lock" },
{ id: "_optionsEtapi", title: t("hidden-subtree.etapi-title"), type: "contentWidget", icon: "bx-extension" },
{ id: "_optionsBackup", title: t("hidden-subtree.backup-title"), type: "contentWidget", icon: "bx-data" },
{ id: "_optionsSync", title: t("hidden-subtree.sync-title"), type: "contentWidget", icon: "bx-wifi" },
{ id: "_optionsOther", title: t("hidden-subtree.other"), type: "contentWidget", icon: "bx-dots-horizontal" },
{ id: "_optionsAdvanced", title: t("hidden-subtree.advanced-title"), type: "contentWidget" }
]
}
]
@@ -326,7 +365,7 @@ function checkHiddenSubtree(force = false, extraOpts: CheckHiddenExtraOpts = {})
hiddenSubtreeDefinition = buildHiddenSubtreeDefinition();
}
checkHiddenSubtreeRecursively('root', hiddenSubtreeDefinition, extraOpts);
checkHiddenSubtreeRecursively("root", hiddenSubtreeDefinition, extraOpts);
}
function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOpts: CheckHiddenExtraOpts = {}) {
@@ -334,7 +373,7 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOp
throw new Error(`Item does not contain mandatory properties: ${JSON.stringify(item)}`);
}
if (item.id.charAt(0) !== '_') {
if (item.id.charAt(0) !== "_") {
throw new Error(`ID has to start with underscore, given '${item.id}'`);
}
@@ -342,41 +381,41 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOp
let branch;
if (!note) {
({note, branch} = noteService.createNewNote({
({ note, branch } = noteService.createNewNote({
noteId: item.id,
title: item.title,
type: item.type,
parentNoteId: parentNoteId,
content: '',
content: "",
ignoreForbiddenParents: true
}));
} else {
branch = note.getParentBranches().find(branch => branch.parentNoteId === parentNoteId);
branch = note.getParentBranches().find((branch) => branch.parentNoteId === parentNoteId);
}
const attrs = [...(item.attributes || [])];
if (item.icon) {
attrs.push({ type: 'label', name: 'iconClass', value: `bx ${item.icon}` });
attrs.push({ type: "label", name: "iconClass", value: `bx ${item.icon}` });
}
if (item.type === 'launcher') {
if (item.type === "launcher") {
if (item.command) {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_COMMAND });
attrs.push({ type: 'label', name: 'command', value: item.command });
attrs.push({ type: "relation", name: "template", value: LBTPL_COMMAND });
attrs.push({ type: "label", name: "command", value: item.command });
} else if (item.builtinWidget) {
if (item.builtinWidget === 'spacer') {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_SPACER });
attrs.push({ type: 'label', name: 'baseSize', value: item.baseSize });
attrs.push({ type: 'label', name: 'growthFactor', value: item.growthFactor });
if (item.builtinWidget === "spacer") {
attrs.push({ type: "relation", name: "template", value: LBTPL_SPACER });
attrs.push({ type: "label", name: "baseSize", value: item.baseSize });
attrs.push({ type: "label", name: "growthFactor", value: item.growthFactor });
} else {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET });
attrs.push({ type: "relation", name: "template", value: LBTPL_BUILTIN_WIDGET });
}
attrs.push({ type: 'label', name: 'builtinWidget', value: item.builtinWidget });
attrs.push({ type: "label", name: "builtinWidget", value: item.builtinWidget });
} else if (item.targetNoteId) {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_NOTE_LAUNCHER });
attrs.push({ type: 'relation', name: 'target', value: item.targetNoteId });
attrs.push({ type: "relation", name: "template", value: LBTPL_NOTE_LAUNCHER });
attrs.push({ type: "relation", name: "target", value: item.targetNoteId });
} else {
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
}
@@ -410,7 +449,7 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOp
for (const attr of attrs) {
const attrId = note.noteId + "_" + attr.type.charAt(0) + attr.name;
if (!note.getAttributes().find(attr => attr.attributeId === attrId)) {
if (!note.getAttributes().find((attr) => attr.attributeId === attrId)) {
new BAttribute({
attributeId: attrId,
noteId: note.noteId,

View File

@@ -8,9 +8,9 @@ function getHoistedNoteId() {
function isHoistedInHiddenSubtree() {
const hoistedNoteId = getHoistedNoteId();
if (hoistedNoteId === 'root') {
if (hoistedNoteId === "root") {
return false;
} else if (hoistedNoteId === '_hidden') {
} else if (hoistedNoteId === "_hidden") {
return true;
}
@@ -26,7 +26,7 @@ function isHoistedInHiddenSubtree() {
function getWorkspaceNote() {
const hoistedNote = becca.getNote(cls.getHoistedNoteId());
if (hoistedNote && (hoistedNote.isRoot() || hoistedNote.hasLabel('workspace'))) {
if (hoistedNote && (hoistedNote.isRoot() || hoistedNote.hasLabel("workspace"))) {
return hoistedNote;
} else {
return becca.getRoot();

View File

@@ -7,7 +7,7 @@ function getHost() {
return envHost;
}
return config['Network']['host'] || '0.0.0.0';
return config["Network"]["host"] || "0.0.0.0";
}
export default getHost();

View File

@@ -4,17 +4,101 @@ import optionService from "./options.js";
// Default list of allowed HTML tags
export const DEFAULT_ALLOWED_TAGS = [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img',
'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer',
'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time',
'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins',
'en-media', // for ENEX import
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"p",
"a",
"ul",
"ol",
"li",
"b",
"i",
"strong",
"em",
"strike",
"s",
"del",
"abbr",
"code",
"hr",
"br",
"div",
"table",
"thead",
"caption",
"tbody",
"tfoot",
"tr",
"th",
"td",
"pre",
"section",
"img",
"figure",
"figcaption",
"span",
"label",
"input",
"details",
"summary",
"address",
"aside",
"footer",
"header",
"hgroup",
"main",
"nav",
"dl",
"dt",
"menu",
"bdi",
"bdo",
"dfn",
"kbd",
"mark",
"q",
"time",
"var",
"wbr",
"area",
"map",
"track",
"video",
"audio",
"picture",
"del",
"ins",
"en-media", // for ENEX import
// Additional tags (https://github.com/TriliumNext/Notes/issues/567)
'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd',
'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp',
'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt'
"acronym",
"article",
"big",
"button",
"cite",
"col",
"colgroup",
"data",
"dd",
"fieldset",
"form",
"legend",
"meter",
"noscript",
"option",
"progress",
"rp",
"samp",
"small",
"sub",
"sup",
"template",
"textarea",
"tt"
] as const;
// intended mainly as protection against XSS via import
@@ -33,8 +117,7 @@ function sanitize(dirtyHtml: string) {
for (let i = 1; i < 6; ++i) {
if (lowercasedHtml.includes(`<h${i}`)) {
transformTags[`h${i}`] = `h${i + 1}`;
}
else {
} else {
break;
}
}
@@ -42,7 +125,7 @@ function sanitize(dirtyHtml: string) {
// Get allowed tags from options, with fallback to default list if option not yet set
let allowedTags;
try {
allowedTags = JSON.parse(optionService.getOption('allowedHtmlTags'));
allowedTags = JSON.parse(optionService.getOption("allowedHtmlTags"));
} catch (e) {
// Fallback to default list if option doesn't exist or is invalid
allowedTags = DEFAULT_ALLOWED_TAGS;
@@ -52,19 +135,60 @@ function sanitize(dirtyHtml: string) {
return sanitizeHtml(dirtyHtml, {
allowedTags,
allowedAttributes: {
"*": [ 'class', 'style', 'title', 'src', 'href', 'hash', 'disabled', 'align', 'alt', 'center', 'data-*' ],
"input": [ "type", "checked" ]
"*": ["class", "style", "title", "src", "href", "hash", "disabled", "align", "alt", "center", "data-*"],
input: ["type", "checked"]
},
// Be consistent with `allowedSchemes` in `src\public\app\services\link.js`
allowedSchemes: [
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git',
'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo'
],
nonTextTags: [
'head'
"http",
"https",
"ftp",
"ftps",
"mailto",
"data",
"evernote",
"file",
"facetime",
"gemini",
"git",
"gopher",
"imap",
"irc",
"irc6",
"jabber",
"jar",
"lastfm",
"ldap",
"ldaps",
"magnet",
"message",
"mumble",
"nfs",
"onenote",
"pop",
"rmi",
"s3",
"sftp",
"skype",
"sms",
"spotify",
"steam",
"svn",
"udp",
"view-source",
"vlc",
"vnc",
"ws",
"wss",
"xmpp",
"jdbc",
"slack",
"tel",
"smb",
"zotero",
"geo"
],
nonTextTags: ["head"],
transformTags
});
}

View File

@@ -7,34 +7,34 @@ import { getResourceDir } from "./utils.js";
import hidden_subtree from "./hidden_subtree.js";
export async function initializeTranslations() {
const resourceDir = getResourceDir();
const resourceDir = getResourceDir();
// Initialize translations
await i18next.use(Backend).init({
lng: getCurrentLanguage(),
fallbackLng: "en",
ns: "server",
backend: {
loadPath: join(resourceDir, "translations/{{lng}}/{{ns}}.json")
}
});
// Initialize translations
await i18next.use(Backend).init({
lng: getCurrentLanguage(),
fallbackLng: "en",
ns: "server",
backend: {
loadPath: join(resourceDir, "translations/{{lng}}/{{ns}}.json")
}
});
}
function getCurrentLanguage() {
let language;
if (sql_init.isDbInitialized()) {
language = options.getOptionOrNull("locale");
}
let language;
if (sql_init.isDbInitialized()) {
language = options.getOptionOrNull("locale");
}
if (!language) {
console.info("Language option not found, falling back to en.");
language = "en";
}
if (!language) {
console.info("Language option not found, falling back to en.");
language = "en";
}
return language;
return language;
}
export async function changeLanguage(locale: string) {
await i18next.changeLanguage(locale);
hidden_subtree.checkHiddenSubtree(true, { restoreNames: true });
await i18next.changeLanguage(locale);
hidden_subtree.checkHiddenSubtree(true, { restoreNames: true });
}

View File

@@ -19,8 +19,7 @@ async function processImage(uploadBuffer: Buffer, originalName: string, shrinkIm
if (!origImageFormat || !["jpg", "png"].includes(origImageFormat.ext)) {
shrinkImageSwitch = false;
}
else if (isAnimated(uploadBuffer)) {
} else if (isAnimated(uploadBuffer)) {
// recompression of animated images will make them static
shrinkImageSwitch = false;
}
@@ -34,7 +33,7 @@ async function processImage(uploadBuffer: Buffer, originalName: string, shrinkIm
} else {
finalImageBuffer = uploadBuffer;
imageFormat = origImageFormat || {
ext: 'dat'
ext: "dat"
};
}
@@ -46,17 +45,16 @@ async function processImage(uploadBuffer: Buffer, originalName: string, shrinkIm
async function getImageType(buffer: Buffer) {
if (isSvg(buffer.toString())) {
return { ext: 'svg' }
}
else {
return await imageType(buffer) || { ext: "jpg" }; // optimistic JPG default
return { ext: "svg" };
} else {
return (await imageType(buffer)) || { ext: "jpg" }; // optimistic JPG default
}
}
function getImageMimeFromExtension(ext: string) {
ext = ext.toLowerCase();
return `image/${ext === 'svg' ? 'svg+xml' : ext}`;
return `image/${ext === "svg" ? "svg+xml" : ext}`;
}
function updateImage(noteId: string, uploadBuffer: Buffer, originalName: string) {
@@ -65,14 +63,16 @@ function updateImage(noteId: string, uploadBuffer: Buffer, originalName: string)
originalName = htmlSanitizer.sanitize(originalName);
const note = becca.getNote(noteId);
if (!note) { throw new Error("Unable to find note."); }
if (!note) {
throw new Error("Unable to find note.");
}
note.saveRevision();
note.setLabel('originalFileName', originalName);
note.setLabel("originalFileName", originalName);
// resizing images asynchronously since JIMP does not support sync operation
processImage(uploadBuffer, originalName, true).then(({buffer, imageFormat}) => {
processImage(uploadBuffer, originalName, true).then(({ buffer, imageFormat }) => {
sql.transactional(() => {
note.mime = getImageMimeFromExtension(imageFormat.ext);
note.save();
@@ -92,28 +92,30 @@ function saveImage(parentNoteId: string, uploadBuffer: Buffer, originalName: str
const fileName = sanitizeFilename(originalName);
const parentNote = becca.getNote(parentNoteId);
if (!parentNote) { throw new Error("Unable to find parent note."); }
if (!parentNote) {
throw new Error("Unable to find parent note.");
}
const {note} = noteService.createNewNote({
const { note } = noteService.createNewNote({
parentNoteId,
title: fileName,
type: 'image',
mime: 'unknown',
content: '',
type: "image",
mime: "unknown",
content: "",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
note.addLabel('originalFileName', originalName);
note.addLabel("originalFileName", originalName);
// resizing images asynchronously since JIMP does not support sync operation
processImage(uploadBuffer, originalName, shrinkImageSwitch).then(({buffer, imageFormat}) => {
processImage(uploadBuffer, originalName, shrinkImageSwitch).then(({ buffer, imageFormat }) => {
sql.transactional(() => {
note.mime = getImageMimeFromExtension(imageFormat.ext);
if (!originalName.includes(".")) {
originalName += `.${imageFormat.ext}`;
note.setLabel('originalFileName', originalName);
note.setLabel("originalFileName", originalName);
note.title = sanitizeFilename(originalName);
}
@@ -141,8 +143,8 @@ function saveImageToAttachment(noteId: string, uploadBuffer: Buffer, originalNam
const note = becca.getNoteOrThrow(noteId);
let attachment = note.saveAttachment({
role: 'image',
mime: 'unknown',
role: "image",
mime: "unknown",
title: fileName
});
@@ -157,10 +159,12 @@ function saveImageToAttachment(noteId: string, uploadBuffer: Buffer, originalNam
}, 5000);
// resizing images asynchronously since JIMP does not support sync operation
processImage(uploadBuffer, originalName, !!shrinkImageSwitch).then(({buffer, imageFormat}) => {
processImage(uploadBuffer, originalName, !!shrinkImageSwitch).then(({ buffer, imageFormat }) => {
sql.transactional(() => {
// re-read, might be changed in the meantime
if (!attachment.attachmentId) { throw new Error("Missing attachment ID."); }
if (!attachment.attachmentId) {
throw new Error("Missing attachment ID.");
}
attachment = becca.getAttachmentOrThrow(attachment.attachmentId);
attachment.mime = getImageMimeFromExtension(imageFormat.ext);
@@ -178,7 +182,7 @@ function saveImageToAttachment(noteId: string, uploadBuffer: Buffer, originalNam
}
async function shrinkImage(buffer: Buffer, originalName: string) {
let jpegQuality = optionService.getOptionInt('imageJpegQuality', 0);
let jpegQuality = optionService.getOptionInt("imageJpegQuality", 0);
if (jpegQuality < 10 || jpegQuality > 100) {
jpegQuality = 75;
@@ -187,8 +191,7 @@ async function shrinkImage(buffer: Buffer, originalName: string) {
let finalImageBuffer;
try {
finalImageBuffer = await resize(buffer, jpegQuality);
}
catch (e: any) {
} catch (e: any) {
log.error(`Failed to resize image '${originalName}', stack: ${e.stack}`);
finalImageBuffer = buffer;
@@ -204,7 +207,7 @@ async function shrinkImage(buffer: Buffer, originalName: string) {
}
async function resize(buffer: Buffer, quality: number) {
const imageMaxWidthHeight = optionService.getOptionInt('imageMaxWidthHeight');
const imageMaxWidthHeight = optionService.getOptionInt("imageMaxWidthHeight");
const start = Date.now();
@@ -212,13 +215,12 @@ async function resize(buffer: Buffer, quality: number) {
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > imageMaxWidthHeight) {
image.resize({ w: imageMaxWidthHeight });
}
else if (image.bitmap.height > imageMaxWidthHeight) {
} else if (image.bitmap.height > imageMaxWidthHeight) {
image.resize({ h: imageMaxWidthHeight });
}
// when converting PNG to JPG, we lose the alpha channel, this is replaced by white to match Trilium white background
image.background = 0xFFFFFFFF;
image.background = 0xffffffff;
const resultBuffer = await image.getBuffer("image/jpeg", { quality });

View File

@@ -1,6 +1,6 @@
import sax from "sax";
import stream from "stream";
import { Throttle } from 'stream-throttle';
import { Throttle } from "stream-throttle";
import log from "../log.js";
import { md5, escapeHtml, fromBase64 } from "../utils.js";
import sql from "../sql.js";
@@ -23,8 +23,7 @@ function parseDate(text: string) {
text = text.replace(/[-:]/g, "");
// insert - and : to convert it to trilium format
text = text.substr(0, 4) + "-" + text.substr(4, 2) + "-" + text.substr(6, 2)
+ " " + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + ".000Z";
text = text.substr(0, 4) + "-" + text.substr(4, 2) + "-" + text.substr(6, 2) + " " + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + ".000Z";
return text;
}
@@ -50,7 +49,7 @@ interface Note {
noteId: string;
blobId: string;
content: string;
resources: Resource[]
resources: Resource[];
}
let note: Partial<Note> = {};
@@ -59,28 +58,26 @@ let resource: Resource;
function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Promise<BNote> {
const saxStream = sax.createStream(true);
const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex")
? file.originalname.substr(0, file.originalname.length - 5)
: file.originalname;
const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex") ? file.originalname.substr(0, file.originalname.length - 5) : file.originalname;
// root note is new note into all ENEX/notebook's notes will be imported
const rootNote = noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: rootNoteTitle,
content: "",
type: 'text',
mime: 'text/html',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: "text",
mime: "text/html",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
}).note;
function extractContent(content: string) {
const openingNoteIndex = content.indexOf('<en-note>');
const openingNoteIndex = content.indexOf("<en-note>");
if (openingNoteIndex !== -1) {
content = content.substr(openingNoteIndex + 9);
}
const closingNoteIndex = content.lastIndexOf('</en-note>');
const closingNoteIndex = content.lastIndexOf("</en-note>");
if (closingNoteIndex !== -1) {
content = content.substr(0, closingNoteIndex);
@@ -109,15 +106,20 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
// Replace OneNote converted checkboxes with unicode ballot box based
// on known hash of checkboxes for regular, p1, and p2 checkboxes
content = content.replace(/<en-media alt="To Do( priority [12])?" hash="(74de5d3d1286f01bac98d32a09f601d9|4a19d3041585e11643e808d68dd3e72f|8e17580123099ac6515c3634b1f6f9a1)"( type="[a-z\/]*"| width="\d+"| height="\d+")*\/>/g, "\u2610 ");
content = content.replace(/<en-media alt="To Do( priority [12])?" hash="(5069b775461e471a47ce04ace6e1c6ae|7912ee9cec35fc3dba49edb63a9ed158|3a05f4f006a6eaf2627dae5ed8b8013b)"( type="[a-z\/]*"| width="\d+"| height="\d+")*\/>/g, "\u2611 ");
content = content.replace(
/<en-media alt="To Do( priority [12])?" hash="(74de5d3d1286f01bac98d32a09f601d9|4a19d3041585e11643e808d68dd3e72f|8e17580123099ac6515c3634b1f6f9a1)"( type="[a-z\/]*"| width="\d+"| height="\d+")*\/>/g,
"\u2610 "
);
content = content.replace(
/<en-media alt="To Do( priority [12])?" hash="(5069b775461e471a47ce04ace6e1c6ae|7912ee9cec35fc3dba49edb63a9ed158|3a05f4f006a6eaf2627dae5ed8b8013b)"( type="[a-z\/]*"| width="\d+"| height="\d+")*\/>/g,
"\u2611 "
);
content = htmlSanitizer.sanitize(content);
return content;
}
const path: string[] = [];
function getCurrentTag() {
@@ -132,7 +134,7 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
}
}
saxStream.on("error", e => {
saxStream.on("error", (e) => {
// unhandled errors will throw, since this is a proper node event emitter.
log.error(`error when parsing ENEX file: ${e}`);
// clear the error
@@ -140,92 +142,86 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
saxStream._parser.resume();
});
saxStream.on("text", text => {
saxStream.on("text", (text) => {
const currentTag = getCurrentTag();
const previousTag = getPreviousTag();
if (previousTag === 'note-attributes') {
if (previousTag === "note-attributes") {
let labelName = currentTag;
if (labelName === 'source-url') {
labelName = 'pageUrl';
if (labelName === "source-url") {
labelName = "pageUrl";
}
labelName = sanitizeAttributeName(labelName || "");
if (note.attributes) {
note.attributes.push({
type: 'label',
type: "label",
name: labelName,
value: text
});
}
}
else if (previousTag === 'resource-attributes') {
if (currentTag === 'file-name') {
} else if (previousTag === "resource-attributes") {
if (currentTag === "file-name") {
resource.attributes.push({
type: 'label',
name: 'originalFileName',
type: "label",
name: "originalFileName",
value: text
});
resource.title = text;
}
else if (currentTag === 'source-url') {
} else if (currentTag === "source-url") {
resource.attributes.push({
type: 'label',
name: 'pageUrl',
type: "label",
name: "pageUrl",
value: text
});
}
}
else if (previousTag === 'resource') {
if (currentTag === 'data') {
text = text.replace(/\s/g, '');
} else if (previousTag === "resource") {
if (currentTag === "data") {
text = text.replace(/\s/g, "");
// resource can be chunked into multiple events: https://github.com/zadam/trilium/issues/3424
// it would probably make sense to do this in a more global way since it can in theory affect any field,
// not just data
resource.content = (resource.content || "") + text;
}
else if (currentTag === 'mime') {
} else if (currentTag === "mime") {
resource.mime = text.toLowerCase();
}
}
else if (previousTag === 'note') {
if (currentTag === 'title') {
} else if (previousTag === "note") {
if (currentTag === "title") {
note.title = text;
} else if (currentTag === 'created') {
} else if (currentTag === "created") {
note.utcDateCreated = parseDate(text);
} else if (currentTag === 'updated') {
} else if (currentTag === "updated") {
note.utcDateModified = parseDate(text);
} else if (currentTag === 'tag' && note.attributes) {
} else if (currentTag === "tag" && note.attributes) {
note.attributes.push({
type: 'label',
type: "label",
name: sanitizeAttributeName(text),
value: ''
})
value: ""
});
}
// unknown tags are just ignored
}
});
saxStream.on("attribute", attr => {
saxStream.on("attribute", (attr) => {
// an attribute. attr has "name" and "value"
});
saxStream.on("opentag", tag => {
saxStream.on("opentag", (tag) => {
path.push(tag.name);
if (tag.name === 'note') {
if (tag.name === "note") {
note = {
content: "",
// it's an array, not a key-value object because we don't know if attributes can be duplicated
attributes: [],
resources: []
};
}
else if (tag.name === 'resource') {
} else if (tag.name === "resource") {
resource = {
title: "resource",
attributes: []
@@ -239,25 +235,29 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
function updateDates(note: BNote, utcDateCreated?: string, utcDateModified?: string) {
// it's difficult to force custom dateCreated and dateModified to Note entity, so we do it post-creation with SQL
sql.execute(`
sql.execute(
`
UPDATE notes
SET dateCreated = ?,
utcDateCreated = ?,
dateModified = ?,
utcDateModified = ?
WHERE noteId = ?`,
[utcDateCreated, utcDateCreated, utcDateModified, utcDateModified, note.noteId]);
[utcDateCreated, utcDateCreated, utcDateModified, utcDateModified, note.noteId]
);
sql.execute(`
sql.execute(
`
UPDATE blobs
SET utcDateModified = ?
WHERE blobId = ?`,
[utcDateModified, note.blobId]);
[utcDateModified, note.blobId]
);
}
function saveNote() {
// make a copy because stream continues with the next call and note gets overwritten
let {title, content, attributes, resources, utcDateCreated, utcDateModified} = note;
let { title, content, attributes, resources, utcDateCreated, utcDateModified } = note;
if (!title || !content) {
throw new Error("Missing title or content for note.");
@@ -270,9 +270,9 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
title,
content,
utcDateCreated,
type: 'text',
mime: 'text/html',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: "text",
mime: "text/html",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
}).note;
for (const attr of attributes || []) {
@@ -297,16 +297,20 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
const hash = md5(resource.content);
// skip all checked/unchecked checkboxes from OneNote
if (['74de5d3d1286f01bac98d32a09f601d9',
'4a19d3041585e11643e808d68dd3e72f',
'8e17580123099ac6515c3634b1f6f9a1',
'5069b775461e471a47ce04ace6e1c6ae',
'7912ee9cec35fc3dba49edb63a9ed158',
'3a05f4f006a6eaf2627dae5ed8b8013b'].includes(hash)) {
if (
[
"74de5d3d1286f01bac98d32a09f601d9",
"4a19d3041585e11643e808d68dd3e72f",
"8e17580123099ac6515c3634b1f6f9a1",
"5069b775461e471a47ce04ace6e1c6ae",
"7912ee9cec35fc3dba49edb63a9ed158",
"3a05f4f006a6eaf2627dae5ed8b8013b"
].includes(hash)
) {
continue;
}
const mediaRegex = new RegExp(`<en-media [^>]*hash="${hash}"[^>]*>`, 'g');
const mediaRegex = new RegExp(`<en-media [^>]*hash="${hash}"[^>]*>`, "g");
resource.mime = resource.mime || "application/octet-stream";
@@ -319,9 +323,9 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
parentNoteId: noteEntity.noteId,
title: resource.title,
content: resource.content,
type: 'file',
type: "file",
mime: resource.mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
}).note;
for (const attr of resource.attributes) {
@@ -337,11 +341,9 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
content = (content || "").replace(mediaRegex, resourceLink);
};
if (resource.mime && resource.mime.startsWith('image/')) {
if (resource.mime && resource.mime.startsWith("image/")) {
try {
const originalName = (resource.title && resource.title !== 'resource')
? resource.title
: `image.${resource.mime.substr(6)}`; // default if real name is not present
const originalName = resource.title && resource.title !== "resource" ? resource.title : `image.${resource.mime.substr(6)}`; // default if real name is not present
const attachment = imageService.saveImageToAttachment(noteEntity.noteId, resource.content, originalName, !!taskContext.data?.shrinkImages);
@@ -375,10 +377,10 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
updateDates(noteEntity, utcDateCreated, utcDateModified);
}
saxStream.on("closetag", tag => {
saxStream.on("closetag", (tag) => {
path.pop();
if (tag === 'note') {
if (tag === "note") {
saveNote();
}
});
@@ -387,7 +389,7 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
//console.log("opencdata");
});
saxStream.on("cdata", text => {
saxStream.on("cdata", (text) => {
note.content += text;
});
@@ -395,8 +397,7 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
//console.log("closecdata");
});
return new Promise((resolve, reject) =>
{
return new Promise((resolve, reject) => {
// resolve only when we parse the whole document AND saving of all notes have been finished
saxStream.on("end", () => resolve(rootNote));
@@ -405,7 +406,7 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
bufferStream
// rate limiting to improve responsiveness during / after import
.pipe(new Throttle({rate: 500000}))
.pipe(new Throttle({ rate: 500000 }))
.pipe(saxStream);
});
}

View File

@@ -2,46 +2,46 @@
import mimeTypes from "mime-types";
import path from "path";
import { TaskData } from '../task_context_interface.js';
import { TaskData } from "../task_context_interface.js";
const CODE_MIME_TYPES: Record<string, boolean | string> = {
'text/plain': true,
'text/x-csrc': true,
'text/x-c++src': true,
'text/x-csharp': true,
'text/x-clojure': true,
'text/css': true,
'text/x-dockerfile': true,
'text/x-erlang': true,
'text/x-feature': true,
'text/x-go': true,
'text/x-groovy': true,
'text/x-haskell': true,
'text/html': true,
'message/http': true,
'text/x-java': true,
'application/javascript': 'application/javascript;env=frontend',
'application/x-javascript': 'application/javascript;env=frontend',
'application/json': true,
'text/x-kotlin': true,
'text/x-stex': true,
'text/x-lua': true,
"text/plain": true,
"text/x-csrc": true,
"text/x-c++src": true,
"text/x-csharp": true,
"text/x-clojure": true,
"text/css": true,
"text/x-dockerfile": true,
"text/x-erlang": true,
"text/x-feature": true,
"text/x-go": true,
"text/x-groovy": true,
"text/x-haskell": true,
"text/html": true,
"message/http": true,
"text/x-java": true,
"application/javascript": "application/javascript;env=frontend",
"application/x-javascript": "application/javascript;env=frontend",
"application/json": true,
"text/x-kotlin": true,
"text/x-stex": true,
"text/x-lua": true,
// possibly later migrate to text/markdown as primary MIME
'text/markdown': 'text/x-markdown',
'text/x-markdown': true,
'text/x-objectivec': true,
'text/x-pascal': true,
'text/x-perl': true,
'text/x-php': true,
'text/x-python': true,
'text/x-ruby': true,
'text/x-rustsrc': true,
'text/x-scala': true,
'text/x-sh': true,
'text/x-sql': true,
'text/x-swift': true,
'text/xml': true,
'text/x-yaml': true
"text/markdown": "text/x-markdown",
"text/x-markdown": true,
"text/x-objectivec": true,
"text/x-pascal": true,
"text/x-perl": true,
"text/x-php": true,
"text/x-python": true,
"text/x-ruby": true,
"text/x-rustsrc": true,
"text/x-scala": true,
"text/x-sh": true,
"text/x-sql": true,
"text/x-swift": true,
"text/xml": true,
"text/x-yaml": true
};
// extensions missing in mime-db
@@ -67,7 +67,7 @@ const EXTENSION_TO_MIME: Record<string, string> = {
/** @returns false if MIME is not detected */
function getMime(fileName: string) {
if (fileName.toLowerCase() === 'dockerfile') {
if (fileName.toLowerCase() === "dockerfile") {
return "text/x-dockerfile";
}
@@ -81,24 +81,21 @@ function getMime(fileName: string) {
}
function getType(options: TaskData, mime: string) {
mime = mime ? mime.toLowerCase() : '';
mime = mime ? mime.toLowerCase() : "";
if (options.textImportedAsText && (mime === 'text/html' || ['text/markdown', 'text/x-markdown'].includes(mime))) {
return 'text';
}
else if (options.codeImportedAsCode && mime in CODE_MIME_TYPES) {
return 'code';
}
else if (mime.startsWith("image/")) {
return 'image';
}
else {
return 'file';
if (options.textImportedAsText && (mime === "text/html" || ["text/markdown", "text/x-markdown"].includes(mime))) {
return "text";
} else if (options.codeImportedAsCode && mime in CODE_MIME_TYPES) {
return "code";
} else if (mime.startsWith("image/")) {
return "image";
} else {
return "file";
}
}
function normalizeMimeType(mime: string) {
mime = mime ? mime.toLowerCase() : '';
mime = mime ? mime.toLowerCase() : "";
const mappedMime = CODE_MIME_TYPES[mime];
if (mappedMime === true) {

View File

@@ -14,9 +14,9 @@ interface OpmlXml {
interface OpmlBody {
$: {
version: string
}
body: OpmlOutline[]
version: string;
};
body: OpmlOutline[];
}
interface OpmlOutline {
@@ -29,19 +29,17 @@ interface OpmlOutline {
}
async function importOpml(taskContext: TaskContext, fileBuffer: string | Buffer, parentNote: BNote) {
const xml = await new Promise<OpmlXml>(function(resolve, reject)
{
const xml = await new Promise<OpmlXml>(function (resolve, reject) {
parseString(fileBuffer, function (err: any, result: OpmlXml) {
if (err) {
reject(err);
}
else {
} else {
resolve(result);
}
});
});
if (!['1.0', '1.1', '2.0'].includes(xml.opml.$.version)) {
if (!["1.0", "1.1", "2.0"].includes(xml.opml.$.version)) {
return [400, `Unsupported OPML version ${xml.opml.$.version}, 1.0, 1.1 or 2.0 expected instead.`];
}
@@ -57,30 +55,28 @@ async function importOpml(taskContext: TaskContext, fileBuffer: string | Buffer,
if (!title || !title.trim()) {
// https://github.com/zadam/trilium/issues/1862
title = outline.$.text;
content = '';
content = "";
}
}
else if (opmlVersion === 2) {
} else if (opmlVersion === 2) {
title = outline.$.text;
content = outline.$._note; // _note is already HTML
}
else {
} else {
throw new Error(`Unrecognized OPML version ${opmlVersion}`);
}
content = htmlSanitizer.sanitize(content || "");
const {note} = noteService.createNewNote({
const { note } = noteService.createNewNote({
parentNoteId,
title,
content,
type: 'text',
type: "text",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
taskContext.increaseProgressCount();
for (const childOutline of (outline.outline || [])) {
for (const childOutline of outline.outline || []) {
importOutline(childOutline, note.noteId);
}
@@ -102,10 +98,10 @@ async function importOpml(taskContext: TaskContext, fileBuffer: string | Buffer,
function toHtml(text: string) {
if (!text) {
return '';
return "";
}
return `<p>${text.replace(/(?:\r\n|\r|\n)/g, '</p><p>')}</p>`;
return `<p>${text.replace(/(?:\r\n|\r|\n)/g, "</p><p>")}</p>`;
}
export default {

View File

@@ -17,16 +17,16 @@ function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNot
const mime = mimeService.getMime(file.originalname) || file.mimetype;
if (taskContext?.data?.textImportedAsText) {
if (mime === 'text/html') {
if (mime === "text/html") {
return importHtml(taskContext, file, parentNote);
} else if (['text/markdown', 'text/x-markdown'].includes(mime)) {
} else if (["text/markdown", "text/x-markdown"].includes(mime)) {
return importMarkdown(taskContext, file, parentNote);
} else if (mime === 'text/plain') {
} else if (mime === "text/plain") {
return importPlainText(taskContext, file, parentNote);
}
}
if (taskContext?.data?.codeImportedAsCode && mimeService.getType(taskContext.data, mime) === 'code') {
if (taskContext?.data?.codeImportedAsCode && mimeService.getType(taskContext.data, mime) === "code") {
return importCodeNote(taskContext, file, parentNote);
}
@@ -41,7 +41,7 @@ function importImage(file: File, parentNote: BNote, taskContext: TaskContext) {
if (typeof file.buffer === "string") {
throw new Error("Invalid file content for image.");
}
const {note} = imageService.saveImage(parentNote.noteId, file.buffer, file.originalname, !!taskContext.data?.shrinkImages);
const { note } = imageService.saveImage(parentNote.noteId, file.buffer, file.originalname, !!taskContext.data?.shrinkImages);
taskContext.increaseProgressCount();
@@ -51,12 +51,12 @@ function importImage(file: File, parentNote: BNote, taskContext: TaskContext) {
function importFile(taskContext: TaskContext, file: File, parentNote: BNote) {
const originalName = file.originalname;
const {note} = noteService.createNewNote({
const { note } = noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: originalName,
content: file.buffer,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: 'file',
type: "file",
mime: mimeService.getMime(originalName) || file.mimetype
});
@@ -77,7 +77,7 @@ function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote)
parentNoteId: parentNote.noteId,
title,
content,
type: 'code',
type: "code",
mime: mime,
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
@@ -92,13 +92,13 @@ function importPlainText(taskContext: TaskContext, file: File, parentNote: BNote
const plainTextContent = file.buffer.toString("utf-8");
const htmlContent = convertTextToHtml(plainTextContent);
const {note} = noteService.createNewNote({
const { note } = noteService.createNewNote({
parentNoteId: parentNote.noteId,
title,
content: htmlContent,
type: 'text',
mime: 'text/html',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: "text",
mime: "text/html",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
taskContext.increaseProgressCount();
@@ -108,9 +108,7 @@ function importPlainText(taskContext: TaskContext, file: File, parentNote: BNote
function convertTextToHtml(text: string) {
// 1: Plain Text Search
text = text.replace(/&/g, "&amp;").
replace(/</g, "&lt;").
replace(/>/g, "&gt;");
text = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
// 2: Line Breaks
text = text.replace(/\r\n?|\n/g, "<br>");
@@ -134,13 +132,13 @@ function importMarkdown(taskContext: TaskContext, file: File, parentNote: BNote)
htmlContent = htmlSanitizer.sanitize(htmlContent);
}
const {note} = noteService.createNewNote({
const { note } = noteService.createNewNote({
parentNoteId: parentNote.noteId,
title,
content: htmlContent,
type: 'text',
mime: 'text/html',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: "text",
mime: "text/html",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
taskContext.increaseProgressCount();
@@ -162,14 +160,13 @@ function importHtml(taskContext: TaskContext, file: File, parentNote: BNote) {
content = htmlSanitizer.sanitize(content);
}
const {note} = noteService.createNewNote({
const { note } = noteService.createNewNote({
parentNoteId: parentNote.noteId,
title,
content,
type: 'text',
mime: 'text/html',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
type: "text",
mime: "text/html",
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
taskContext.increaseProgressCount();
@@ -188,7 +185,7 @@ function importAttachment(taskContext: TaskContext, file: File, parentNote: BNot
parentNote.saveAttachment({
title: file.originalname,
content: file.buffer,
role: 'file',
role: "file",
mime: mime
});

View File

@@ -19,11 +19,11 @@ import TaskContext from "../task_context.js";
import BNote from "../../becca/entities/bnote.js";
import NoteMeta from "../meta/note_meta.js";
import AttributeMeta from "../meta/attribute_meta.js";
import { Stream } from 'stream';
import { ALLOWED_NOTE_TYPES, NoteType } from '../../becca/entities/rows.js';
import { Stream } from "stream";
import { ALLOWED_NOTE_TYPES, NoteType } from "../../becca/entities/rows.js";
interface MetaFile {
files: NoteMeta[]
files: NoteMeta[];
}
async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRootNote: BNote): Promise<BNote> {
@@ -34,7 +34,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
const attributes: AttributeMeta[] = [];
// path => noteId, used only when meta file is not available
/** path => noteId | attachmentId */
const createdPaths: Record<string, string> = { '/': importRootNote.noteId, '\\': importRootNote.noteId };
const createdPaths: Record<string, string> = { "/": importRootNote.noteId, "\\": importRootNote.noteId };
let metaFile: MetaFile | null = null;
let firstNote: BNote | null = null;
const createdNoteIds = new Set<string>();
@@ -45,7 +45,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
return "empty_note_id";
}
if (origNoteId === 'root' || origNoteId.startsWith("_")) {
if (origNoteId === "root" || origNoteId.startsWith("_")) {
// these "named" noteIds don't differ between Trilium instances
return origNoteId;
}
@@ -108,7 +108,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
parent = cursor;
if (parent.children) {
cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment);
cursor = parent.children.find((file) => file.dataFileName === segment || file.dirFileName === segment);
}
if (!cursor) {
@@ -128,11 +128,10 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
if (parentNoteMeta?.noteId) {
parentNoteId = parentNoteMeta.isImportRoot ? importRootNote.noteId : getNewNoteId(parentNoteMeta.noteId);
}
else {
} else {
const parentPath = path.dirname(filePath);
if (parentPath === '.') {
if (parentPath === ".") {
parentNoteId = importRootNote.noteId;
} else if (parentPath in createdPaths) {
parentNoteId = createdPaths[parentPath];
@@ -180,12 +179,11 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
for (const attr of noteMeta.attributes || []) {
attr.noteId = note.noteId;
if (attr.type === 'label-definition') {
attr.type = 'label';
if (attr.type === "label-definition") {
attr.type = "label";
attr.name = `label:${attr.name}`;
}
else if (attr.type === 'relation-definition') {
attr.type = 'label';
} else if (attr.type === "relation-definition") {
attr.type = "label";
attr.name = `relation:${attr.name}`;
}
@@ -194,12 +192,12 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
continue;
}
if (attr.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(attr.name)) {
if (attr.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(attr.name)) {
// these relations are created automatically and as such don't need to be duplicated in the import
continue;
}
if (attr.type === 'relation') {
if (attr.type === "relation") {
attr.value = getNewNoteId(attr.value);
}
@@ -232,17 +230,17 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
throw new Error("Missing parent note ID.");
}
const {note} = noteService.createNewNote({
const { note } = noteService.createNewNote({
parentNoteId: parentNoteId,
title: noteTitle || "",
content: '',
content: "",
noteId: noteId,
type: resolveNoteType(noteMeta?.type),
mime: noteMeta ? noteMeta.mime : 'text/html',
prefix: noteMeta?.prefix || '',
mime: noteMeta ? noteMeta.mime : "text/html",
prefix: noteMeta?.prefix || "",
isExpanded: !!noteMeta?.isExpanded,
notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined,
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
notePosition: noteMeta && firstNote ? noteMeta.notePosition : undefined,
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
});
createdNoteIds.add(note.noteId);
@@ -267,11 +265,11 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
url = url.substr(3);
}
if (absUrl === '.') {
absUrl = '';
if (absUrl === ".") {
absUrl = "";
}
absUrl += `${absUrl.length > 0 ? '/' : ''}${url}`;
absUrl += `${absUrl.length > 0 ? "/" : ""}${url}`;
const { noteMeta, attachmentMeta } = getMeta(absUrl);
@@ -280,7 +278,8 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
noteId: getNewNoteId(noteMeta.noteId)
};
} else { // don't check for noteMeta since it's not mandatory for notes
} else {
// don't check for noteMeta since it's not mandatory for notes
return {
noteId: getNoteId(noteMeta, absUrl)
};
@@ -345,8 +344,10 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
return `href="${url}"`;
}
if (url.startsWith('#') // already a note path (probably)
|| isUrlAbsolute(url)) {
if (
url.startsWith("#") || // already a note path (probably)
isUrlAbsolute(url)
) {
return match;
}
@@ -362,8 +363,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
});
if (noteMeta) {
const includeNoteLinks = (noteMeta.attributes || [])
.filter(attr => attr.type === 'relation' && attr.name === 'includeNoteLink');
const includeNoteLinks = (noteMeta.attributes || []).filter((attr) => attr.type === "relation" && attr.name === "includeNoteLink");
for (const link of includeNoteLinks) {
// no need to escape the regexp find string since it's a noteId which doesn't contain any special characters
@@ -377,31 +377,25 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
}
function removeTriliumTags(content: string) {
const tagsToRemove = [
'<h1 data-trilium-h1>([^<]*)<\/h1>',
'<title data-trilium-title>([^<]*)<\/title>'
]
const tagsToRemove = ["<h1 data-trilium-h1>([^<]*)<\/h1>", "<title data-trilium-title>([^<]*)<\/title>"];
for (const tag of tagsToRemove) {
let re = new RegExp(tag, "gi");
content = content.replace(re, '');
content = content.replace(re, "");
}
return content;
}
function processNoteContent(noteMeta: NoteMeta | undefined, type: string, mime: string, content: string | Buffer, noteTitle: string, filePath: string) {
if ((noteMeta?.format === 'markdown'
|| (!noteMeta && taskContext.data?.textImportedAsText && ['text/markdown', 'text/x-markdown'].includes(mime)))
&& typeof content === "string") {
if ((noteMeta?.format === "markdown" || (!noteMeta && taskContext.data?.textImportedAsText && ["text/markdown", "text/x-markdown"].includes(mime))) && typeof content === "string") {
content = markdownService.renderToHtml(content, noteTitle);
}
if (type === 'text' && typeof content === "string") {
if (type === "text" && typeof content === "string") {
content = processTextNoteContent(content, noteTitle, filePath, noteMeta);
}
if (type === 'relationMap' && noteMeta && typeof content === "string") {
const relationMapLinks = (noteMeta.attributes || [])
.filter(attr => attr.type === 'relation' && attr.name === 'relationMapLink');
if (type === "relationMap" && noteMeta && typeof content === "string") {
const relationMapLinks = (noteMeta.attributes || []).filter((attr) => attr.type === "relation" && attr.name === "relationMapLink");
// this will replace relation map links
for (const link of relationMapLinks) {
@@ -462,7 +456,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
throw new Error("Unable to resolve mime type.");
}
if (type !== 'file' && type !== 'image') {
if (type !== "file" && type !== "image") {
content = content.toString("utf-8");
}
@@ -496,21 +490,20 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
notePosition: noteMeta?.notePosition
}).save();
}
}
else {
({note} = noteService.createNewNote({
} else {
({ note } = noteService.createNewNote({
parentNoteId: parentNoteId,
title: noteTitle || "",
content: content,
noteId,
type,
mime,
prefix: noteMeta?.prefix || '',
prefix: noteMeta?.prefix || "",
isExpanded: !!noteMeta?.isExpanded,
// root notePosition should be ignored since it relates to the original document
// now import root should be placed after existing notes into new parent
notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined,
isProtected: isProtected,
notePosition: noteMeta && firstNote ? noteMeta.notePosition : undefined,
isProtected: isProtected
}));
createdNoteIds.add(note.noteId);
@@ -520,11 +513,11 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
firstNote = firstNote || note;
}
if (!noteMeta && (type === 'file' || type === 'image')) {
if (!noteMeta && (type === "file" || type === "image")) {
attributes.push({
noteId,
type: 'label',
name: 'originalFileName',
type: "label",
name: "originalFileName",
value: path.basename(filePath)
});
}
@@ -535,7 +528,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
await readZipFile(fileBuffer, async (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => {
const filePath = normalizeFilePath(entry.fileName);
if (filePath === '!!!meta.json') {
if (filePath === "!!!meta.json") {
const content = await readContent(zipfile, entry);
metaFile = JSON.parse(content.toString("utf-8"));
@@ -549,8 +542,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
if (/\/$/.test(entry.fileName)) {
saveDirectory(filePath);
}
else if (filePath !== '!!!meta.json') {
} else if (filePath !== "!!!meta.json") {
const content = await readContent(zipfile, entry);
saveNote(filePath, content);
@@ -568,7 +560,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
if (!metaFile) {
// if there's no meta file, then the notes are created based on the order in that zip file but that
// is usually quite random, so we sort the notes in the way they would appear in the file manager
treeService.sortNotes(noteId, 'title', false, true);
treeService.sortNotes(noteId, "title", false, true);
}
taskContext.increaseProgressCount();
@@ -577,10 +569,9 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
// we're saving attributes and links only now so that all relation and link target notes
// are already in the database (we don't want to have "broken" relations, not even transitionally)
for (const attr of attributes) {
if (attr.type !== 'relation' || attr.value in becca.notes) {
if (attr.type !== "relation" || attr.value in becca.notes) {
new BAttribute(attr).save();
}
else {
} else {
log.info(`Relation not imported since the target note doesn't exist: ${JSON.stringify(attr)}`);
}
}
@@ -609,14 +600,14 @@ function normalizeFilePath(filePath: string): string {
function streamToBuffer(stream: Stream): Promise<Buffer> {
const chunks: Uint8Array[] = [];
stream.on('data', chunk => chunks.push(chunk));
stream.on("data", (chunk) => chunks.push(chunk));
return new Promise((res, rej) => stream.on('end', () => res(Buffer.concat(chunks))));
return new Promise((res, rej) => stream.on("end", () => res(Buffer.concat(chunks))));
}
function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer> {
return new Promise((res, rej) => {
zipfile.openReadStream(entry, function(err, readStream) {
zipfile.openReadStream(entry, function (err, readStream) {
if (err) rej(err);
if (!readStream) throw new Error("Unable to read content.");
@@ -627,12 +618,12 @@ function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer
function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => void) {
return new Promise((res, rej) => {
yauzl.fromBuffer(buffer, {lazyEntries: true, validateEntrySizes: false}, function(err, zipfile) {
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, function (err, zipfile) {
if (err) rej(err);
if (!zipfile) throw new Error("Unable to read zip file.");
zipfile.readEntry();
zipfile.on("entry", async entry => {
zipfile.on("entry", async (entry) => {
try {
await processEntryCallback(zipfile, entry);
} catch (e) {
@@ -646,12 +637,12 @@ function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFi
function resolveNoteType(type: string | undefined): NoteType {
// BC for ZIPs created in Triliun 0.57 and older
if (type === 'relation-map') {
return 'relationMap';
} else if (type === 'note-map') {
return 'noteMap';
} else if (type === 'web-view') {
return 'webView';
if (type === "relation-map") {
return "relationMap";
} else if (type === "note-map") {
return "noteMap";
} else if (type === "web-view") {
return "webView";
}
if (type && (ALLOWED_NOTE_TYPES as readonly string[]).includes(type)) {

View File

@@ -3,7 +3,7 @@
import optionService from "./options.js";
import log from "./log.js";
import { isElectron as getIsElectron, isMac as getIsMac } from "./utils.js";
import { KeyboardShortcut } from './keyboard_actions_interface.js';
import { KeyboardShortcut } from "./keyboard_actions_interface.js";
import { t } from "i18next";
const isMac = getIsMac();
@@ -77,7 +77,6 @@ function getDefaultKeyboardActions() {
scope: "note-tree"
},
{
separator: t("keyboard_actions.creating-and-moving-notes")
},
@@ -197,7 +196,6 @@ function getDefaultKeyboardActions() {
scope: "note-tree"
},
{
separator: t("keyboard_actions.tabs-and-windows")
},
@@ -304,7 +302,6 @@ function getDefaultKeyboardActions() {
scope: "window"
},
{
separator: t("keyboard_actions.dialogs")
},
@@ -351,7 +348,6 @@ function getDefaultKeyboardActions() {
scope: "window"
},
{
separator: t("keyboard_actions.text-note-operations")
},
@@ -602,13 +598,13 @@ function getDefaultKeyboardActions() {
];
/*
* Apply macOS-specific tweaks.
*/
const platformModifier = isMac ? 'Meta' : 'Ctrl';
* Apply macOS-specific tweaks.
*/
const platformModifier = isMac ? "Meta" : "Ctrl";
for (const action of DEFAULT_KEYBOARD_ACTIONS) {
if (action.defaultShortcuts) {
action.defaultShortcuts = action.defaultShortcuts.map(shortcut => shortcut.replace("CommandOrControl", platformModifier));
action.defaultShortcuts = action.defaultShortcuts.map((shortcut) => shortcut.replace("CommandOrControl", platformModifier));
}
}
@@ -623,21 +619,19 @@ function getKeyboardActions() {
}
for (const option of optionService.getOptions()) {
if (option.name.startsWith('keyboardShortcuts')) {
if (option.name.startsWith("keyboardShortcuts")) {
let actionName = option.name.substring(17);
actionName = actionName.charAt(0).toLowerCase() + actionName.slice(1);
const action = actions.find(ea => ea.actionName === actionName);
const action = actions.find((ea) => ea.actionName === actionName);
if (action) {
try {
action.effectiveShortcuts = JSON.parse(option.value);
}
catch (e) {
} catch (e) {
log.error(`Could not parse shortcuts for action ${actionName}`);
}
}
else {
} else {
log.info(`Keyboard action ${actionName} found in database, but not in action definition.`);
}
}

View File

@@ -101,13 +101,13 @@ export interface KeyboardShortcut {
defaultShortcuts?: string[];
effectiveShortcuts?: string[];
/**
* Scope here means on which element the keyboard shortcuts are attached - this means that for the shortcut to work,
* the focus has to be inside the element.
*
* So e.g. shortcuts with "note-tree" scope work only when the focus is in note tree.
* This allows to have the same shortcut have different actions attached based on the context
* e.g. CTRL-C in note tree does something a bit different from CTRL-C in the text editor.
*/
* Scope here means on which element the keyboard shortcuts are attached - this means that for the shortcut to work,
* the focus has to be inside the element.
*
* So e.g. shortcuts with "note-tree" scope work only when the focus is in note tree.
* This allows to have the same shortcut have different actions attached based on the context
* e.g. CTRL-C in note tree does something a bit different from CTRL-C in the text editor.
*/
scope?: "window" | "note-tree" | "text-detail" | "code-detail";
}

View File

@@ -17,7 +17,7 @@ const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const NEW_LINE = isWindows() ? '\r\n' : '\n';
const NEW_LINE = isWindows() ? "\r\n" : "\n";
let todaysMidnight!: Date;
@@ -38,7 +38,7 @@ function initLogFile() {
logFile.end();
}
logFile = fs.createWriteStream(path, {flags: 'a'});
logFile = fs.createWriteStream(path, { flags: "a" });
}
function checkDate(millisSinceMidnight: number) {
@@ -75,7 +75,7 @@ function error(message: string | Error) {
log(`ERROR: ${message}`);
}
const requestBlacklist = [ "/libraries", "/app", "/images", "/stylesheets", "/api/recent-notes" ];
const requestBlacklist = ["/libraries", "/app", "/images", "/stylesheets", "/api/recent-notes"];
function request(req: Request, res: Response, timeMs: number, responseLength: number | string = "?") {
for (const bl of requestBlacklist) {
@@ -88,24 +88,21 @@ function request(req: Request, res: Response, timeMs: number, responseLength: nu
return;
}
info((timeMs >= 10 ? "Slow " : "") +
`${res.statusCode} ${req.method} ${req.url} with ${responseLength} bytes took ${timeMs}ms`);
info((timeMs >= 10 ? "Slow " : "") + `${res.statusCode} ${req.method} ${req.url} with ${responseLength} bytes took ${timeMs}ms`);
}
function pad(num: number) {
num = Math.floor(num);
return num < 10 ? (`0${num}`) : num.toString();
return num < 10 ? `0${num}` : num.toString();
}
function padMilli(num: number) {
if (num < 10) {
return `00${num}`;
}
else if (num < 100) {
} else if (num < 100) {
return `0${num}`;
}
else {
} else {
return num.toString();
}
}

View File

@@ -27,9 +27,7 @@ async function migrate() {
// backup before attempting migration
await backupService.backupNow(
// creating a special backup for version 0.60.4, the changes in 0.61 are major.
currentDbVersion === 214
? `before-migration-v060`
: 'before-migration'
currentDbVersion === 214 ? `before-migration-v060` : "before-migration"
);
const migrationFiles = fs.readdirSync(resourceDir.MIGRATIONS_DIR);
@@ -37,27 +35,29 @@ async function migrate() {
return;
}
const migrations = migrationFiles.map(file => {
const match = file.match(/^([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)$/);
if (!match) {
return null;
}
const migrations = migrationFiles
.map((file) => {
const match = file.match(/^([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)$/);
if (!match) {
return null;
}
const dbVersion = parseInt(match[1]);
if (dbVersion > currentDbVersion) {
const name = match[2];
const type = match[3];
const dbVersion = parseInt(match[1]);
if (dbVersion > currentDbVersion) {
const name = match[2];
const type = match[3];
return {
dbVersion: dbVersion,
name: name,
file: file,
type: type
};
} else {
return null;
}
}).filter((el): el is MigrationInfo => !!el);
return {
dbVersion: dbVersion,
name: name,
file: file,
type: type
};
} else {
return null;
}
})
.filter((el): el is MigrationInfo => !!el);
migrations.sort((a, b) => a.dbVersion - b.dbVersion);
@@ -74,9 +74,12 @@ async function migrate() {
await executeMigration(mig);
sql.execute(`UPDATE options
sql.execute(
`UPDATE options
SET value = ?
WHERE name = ?`, [mig.dbVersion.toString(), "dbVersion"]);
WHERE name = ?`,
[mig.dbVersion.toString(), "dbVersion"]
);
log.info(`Migration to version ${mig.dbVersion} has been successful.`);
} catch (e: any) {
@@ -97,13 +100,13 @@ async function migrate() {
}
async function executeMigration(mig: MigrationInfo) {
if (mig.type === 'sql') {
const migrationSql = fs.readFileSync(`${resourceDir.MIGRATIONS_DIR}/${mig.file}`).toString('utf8');
if (mig.type === "sql") {
const migrationSql = fs.readFileSync(`${resourceDir.MIGRATIONS_DIR}/${mig.file}`).toString("utf8");
console.log(`Migration with SQL script: ${migrationSql}`);
sql.executeScript(migrationSql);
} else if (mig.type === 'js') {
} else if (mig.type === "js") {
console.log("Migration with JS module");
const migrationModule = await import(`${resourceDir.MIGRATIONS_DIR}/${mig.file}`);
@@ -132,8 +135,10 @@ function isDbUpToDate() {
async function migrateIfNecessary() {
const currentDbVersion = getDbVersion();
if (currentDbVersion > appInfo.dbVersion && process.env.TRILIUM_IGNORE_DB_VERSION !== 'true') {
log.error(`Current DB version ${currentDbVersion} is newer than the current DB version ${appInfo.dbVersion}, which means that it was created by a newer and incompatible version of Trilium. Upgrade to the latest version of Trilium to resolve this issue.`);
if (currentDbVersion > appInfo.dbVersion && process.env.TRILIUM_IGNORE_DB_VERSION !== "true") {
log.error(
`Current DB version ${currentDbVersion} is newer than the current DB version ${appInfo.dbVersion}, which means that it was created by a newer and incompatible version of Trilium. Upgrade to the latest version of Trilium to resolve this issue.`
);
await crash();
}

View File

@@ -1,24 +1,24 @@
const noteTypes = [
{ type: 'text', defaultMime: 'text/html' },
{ type: 'code', defaultMime: 'text/plain' },
{ type: 'render', defaultMime: '' },
{ type: 'file', defaultMime: 'application/octet-stream' },
{ type: 'image', defaultMime: '' },
{ type: 'search', defaultMime: '' },
{ type: 'relationMap', defaultMime: 'application/json' },
{ type: 'book', defaultMime: '' },
{ type: 'noteMap', defaultMime: '' },
{ type: 'mermaid', defaultMime: 'text/plain' },
{ type: 'canvas', defaultMime: 'application/json' },
{ type: 'webView', defaultMime: '' },
{ type: 'launcher', defaultMime: '' },
{ type: 'doc', defaultMime: '' },
{ type: 'contentWidget', defaultMime: '' },
{ type: 'mindMap', defaultMime: 'application/json' }
{ type: "text", defaultMime: "text/html" },
{ type: "code", defaultMime: "text/plain" },
{ type: "render", defaultMime: "" },
{ type: "file", defaultMime: "application/octet-stream" },
{ type: "image", defaultMime: "" },
{ type: "search", defaultMime: "" },
{ type: "relationMap", defaultMime: "application/json" },
{ type: "book", defaultMime: "" },
{ type: "noteMap", defaultMime: "" },
{ type: "mermaid", defaultMime: "text/plain" },
{ type: "canvas", defaultMime: "application/json" },
{ type: "webView", defaultMime: "" },
{ type: "launcher", defaultMime: "" },
{ type: "doc", defaultMime: "" },
{ type: "contentWidget", defaultMime: "" },
{ type: "mindMap", defaultMime: "application/json" }
];
function getDefaultMimeForNoteType(typeName: string) {
const typeRec = noteTypes.find(nt => nt.type === typeName);
const typeRec = noteTypes.find((nt) => nt.type === typeName);
if (!typeRec) {
throw new Error(`Cannot find note type '${typeName}'`);
@@ -28,6 +28,6 @@ function getDefaultMimeForNoteType(typeName: string) {
}
export default {
getNoteTypeNames: () => noteTypes.map(nt => nt.type),
getNoteTypeNames: () => noteTypes.map((nt) => nt.type),
getDefaultMimeForNoteType
};

View File

@@ -23,15 +23,15 @@ import noteTypesService from "./note_types.js";
import fs from "fs";
import ws from "./ws.js";
import html2plaintext from "html2plaintext";
import { AttachmentRow, AttributeRow, BranchRow, NoteRow, NoteType } from '../becca/entities/rows.js';
import { AttachmentRow, AttributeRow, BranchRow, NoteRow, NoteType } from "../becca/entities/rows.js";
import TaskContext from "./task_context.js";
import { NoteParams } from './note-interface.js';
import { NoteParams } from "./note-interface.js";
import imageService from "./image.js";
import { t } from "i18next";
interface FoundLink {
name: "imageLink" | "internalLink" | "includeNoteLink" | "relationMapLink",
value: string
name: "imageLink" | "internalLink" | "includeNoteLink" | "relationMapLink";
value: string;
}
interface Attachment {
@@ -40,15 +40,17 @@ interface Attachment {
}
function getNewNotePosition(parentNote: BNote) {
if (parentNote.isLabelTruthy('newNotesOnTop')) {
const minNotePos = parentNote.getChildBranches()
.filter(branch => branch?.noteId !== '_hidden') // has "always last" note position
if (parentNote.isLabelTruthy("newNotesOnTop")) {
const minNotePos = parentNote
.getChildBranches()
.filter((branch) => branch?.noteId !== "_hidden") // has "always last" note position
.reduce((min, note) => Math.min(min, note?.notePosition || 0), 0);
return minNotePos - 10;
} else {
const maxNotePos = parentNote.getChildBranches()
.filter(branch => branch?.noteId !== '_hidden') // has "always last" note position
const maxNotePos = parentNote
.getChildBranches()
.filter((branch) => branch?.noteId !== "_hidden") // has "always last" note position
.reduce((max, note) => Math.max(max, note?.notePosition || 0), 0);
return maxNotePos + 10;
@@ -75,9 +77,9 @@ function copyChildAttributes(parentNote: BNote, childNote: BNote) {
for (const attr of parentNote.getAttributes()) {
if (attr.name.startsWith("child:")) {
const name = attr.name.substr(6);
const hasAlreadyTemplate = childNote.hasRelation('template');
const hasAlreadyTemplate = childNote.hasRelation("template");
if (hasAlreadyTemplate && attr.type === 'relation' && name === 'template') {
if (hasAlreadyTemplate && attr.type === "relation" && name === "template") {
// if the note already has a template, it means the template was chosen by the user explicitly
// in the menu. In that case, we should override the default templates defined in the child: attrs
continue;
@@ -98,7 +100,7 @@ function copyChildAttributes(parentNote: BNote, childNote: BNote) {
function getNewNoteTitle(parentNote: BNote) {
let title = t("notes.new-note");
const titleTemplate = parentNote.getLabelValue('titleTemplate');
const titleTemplate = parentNote.getLabelValue("titleTemplate");
if (titleTemplate !== null) {
try {
@@ -135,19 +137,16 @@ function getAndValidateParent(params: GetValidateParams) {
throw new ValidationError(`Parent note '${params.parentNoteId}' was not found.`);
}
if (parentNote.type === 'launcher' && parentNote.noteId !== '_lbBookmarks') {
if (parentNote.type === "launcher" && parentNote.noteId !== "_lbBookmarks") {
throw new ValidationError(`Creating child notes into launcher notes is not allowed.`);
}
if (['_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(params.parentNoteId) && params.type !== 'launcher') {
if (["_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(params.parentNoteId) && params.type !== "launcher") {
throw new ValidationError(`Only 'launcher' notes can be created in parent '${params.parentNoteId}'`);
}
if (!params.ignoreForbiddenParents) {
if (['_lbRoot', '_hidden'].includes(parentNote.noteId)
|| parentNote.noteId.startsWith("_lbTpl")
|| parentNote.isOptions()) {
if (["_lbRoot", "_hidden"].includes(parentNote.noteId) || parentNote.noteId.startsWith("_lbTpl") || parentNote.isOptions()) {
throw new ValidationError(`Creating child notes into '${parentNote.noteId}' is not allowed.`);
}
}
@@ -170,11 +169,11 @@ function createNewNote(params: NoteParams): {
}
let error;
if (error = dateUtils.validateLocalDateTime(params.dateCreated)) {
if ((error = dateUtils.validateLocalDateTime(params.dateCreated))) {
throw new Error(error);
}
if (error = dateUtils.validateUtcDateTime(params.utcDateCreated)) {
if ((error = dateUtils.validateUtcDateTime(params.utcDateCreated))) {
throw new Error(error);
}
@@ -212,8 +211,7 @@ function createNewNote(params: NoteParams): {
prefix: params.prefix || "",
isExpanded: !!params.isExpanded
}).save();
}
finally {
} finally {
if (!isEntityEventsDisabled) {
// re-enable entity events only if they were previously enabled
// (they can be disabled in case of import)
@@ -228,20 +226,20 @@ function createNewNote(params: NoteParams): {
throw new Error(`Template note '${params.templateNoteId}' does not exist.`);
}
note.addRelation('template', params.templateNoteId);
note.addRelation("template", params.templateNoteId);
// no special handling for ~inherit since it doesn't matter if it's assigned with the note creation or later
}
copyChildAttributes(parentNote, note);
eventService.emit(eventService.ENTITY_CREATED, { entityName: 'notes', entity: note });
eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'notes', entity: note });
eventService.emit(eventService.ENTITY_CREATED, { entityName: "notes", entity: note });
eventService.emit(eventService.ENTITY_CHANGED, { entityName: "notes", entity: note });
triggerNoteTitleChanged(note);
// blobs entity doesn't use "created" event
eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'blobs', entity: note });
eventService.emit(eventService.ENTITY_CREATED, { entityName: 'branches', entity: branch });
eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'branches', entity: branch });
eventService.emit(eventService.ENTITY_CHANGED, { entityName: "blobs", entity: note });
eventService.emit(eventService.ENTITY_CREATED, { entityName: "branches", entity: branch });
eventService.emit(eventService.ENTITY_CHANGED, { entityName: "branches", entity: branch });
eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote: note, parentNote: parentNote });
log.info(`Created new note '${note.noteId}', branch '${branch.branchId}' of type '${note.type}', mime '${note.mime}'`);
@@ -253,24 +251,22 @@ function createNewNote(params: NoteParams): {
});
}
function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: string | undefined, params: NoteParams) {
function createNewNoteWithTarget(target: "into" | "after", targetBranchId: string | undefined, params: NoteParams) {
if (!params.type) {
const parentNote = becca.notes[params.parentNoteId];
// code note type can be inherited, otherwise "text" is the default
params.type = parentNote.type === 'code' ? 'code' : 'text';
params.mime = parentNote.type === 'code' ? parentNote.mime : 'text/html';
params.type = parentNote.type === "code" ? "code" : "text";
params.mime = parentNote.type === "code" ? parentNote.mime : "text/html";
}
if (target === 'into') {
if (target === "into") {
return createNewNote(params);
}
else if (target === 'after' && targetBranchId) {
} else if (target === "after" && targetBranchId) {
const afterBranch = becca.branches[targetBranchId];
// not updating utcDateModified to avoid having to sync whole rows
sql.execute('UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0',
[params.parentNoteId, afterBranch.notePosition]);
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [params.parentNoteId, afterBranch.notePosition]);
params.notePosition = afterBranch.notePosition + 10;
@@ -279,8 +275,7 @@ function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: str
entityChangesService.putNoteReorderingEntityChange(params.parentNoteId);
return retObject;
}
else {
} else {
throw new Error(`Unknown target '${target}'`);
}
}
@@ -318,17 +313,15 @@ function protectNote(note: BNote, protect: boolean) {
const content = attachment.getContent();
attachment.isProtected = protect;
attachment.setContent(content, {forceSave: true});
}
catch (e) {
attachment.setContent(content, { forceSave: true });
} catch (e) {
log.error(`Could not un/protect attachment '${attachment.attachmentId}'`);
throw e;
}
}
}
}
catch (e) {
} catch (e) {
log.error(`Could not un/protect note '${note.noteId}'`);
throw e;
@@ -340,12 +333,12 @@ function checkImageAttachments(note: BNote, content: string) {
let match;
const imgRegExp = /src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image/g;
while (match = imgRegExp.exec(content)) {
while ((match = imgRegExp.exec(content))) {
foundAttachmentIds.add(match[1]);
}
const linkRegExp = /href="[^"]+attachmentId=([a-zA-Z0-9_]+)/g;
while (match = linkRegExp.exec(content)) {
while ((match = linkRegExp.exec(content))) {
foundAttachmentIds.add(match[1]);
}
@@ -363,14 +356,14 @@ function checkImageAttachments(note: BNote, content: string) {
}
}
const existingAttachmentIds = new Set<string | undefined>(attachments.map(att => att.attachmentId));
const unknownAttachmentIds = Array.from(foundAttachmentIds).filter(foundAttId => !existingAttachmentIds.has(foundAttId));
const existingAttachmentIds = new Set<string | undefined>(attachments.map((att) => att.attachmentId));
const unknownAttachmentIds = Array.from(foundAttachmentIds).filter((foundAttId) => !existingAttachmentIds.has(foundAttId));
const unknownAttachments = becca.getAttachments(unknownAttachmentIds);
for (const unknownAttachment of unknownAttachments) {
// the attachment belongs to a different note (was copy-pasted). Attachments can be linked only from the note
// which owns it, so either find an existing attachment having the same content or make a copy.
let localAttachment = note.getAttachments().find(att => att.role === unknownAttachment.role && att.blobId === unknownAttachment.blobId);
let localAttachment = note.getAttachments().find((att) => att.role === unknownAttachment.role && att.blobId === unknownAttachment.blobId);
if (localAttachment) {
if (localAttachment.utcDateScheduledForErasureSince) {
@@ -379,21 +372,25 @@ function checkImageAttachments(note: BNote, content: string) {
localAttachment.save();
}
log.info(`Found equivalent attachment '${localAttachment.attachmentId}' of note '${note.noteId}' for the linked foreign attachment '${unknownAttachment.attachmentId}' of note '${unknownAttachment.ownerId}'`);
log.info(
`Found equivalent attachment '${localAttachment.attachmentId}' of note '${note.noteId}' for the linked foreign attachment '${unknownAttachment.attachmentId}' of note '${unknownAttachment.ownerId}'`
);
} else {
localAttachment = unknownAttachment.copy();
localAttachment.ownerId = note.noteId;
localAttachment.setContent(unknownAttachment.getContent(), {forceSave: true});
localAttachment.setContent(unknownAttachment.getContent(), { forceSave: true });
ws.sendMessageToAllClients({ type: 'toast', message: `Attachment '${localAttachment.title}' has been copied to note '${note.title}'.`});
ws.sendMessageToAllClients({ type: "toast", message: `Attachment '${localAttachment.title}' has been copied to note '${note.title}'.` });
log.info(`Copied attachment '${unknownAttachment.attachmentId}' of note '${unknownAttachment.ownerId}' to new '${localAttachment.attachmentId}' of note '${note.noteId}'`);
}
// replace image links
content = content.replace(`api/attachments/${unknownAttachment.attachmentId}/image`, `api/attachments/${localAttachment.attachmentId}/image`);
// replace reference links
content = content.replace(new RegExp(`href="[^"]+attachmentId=${unknownAttachment.attachmentId}[^"]*"`, "g"),
`href="#root/${localAttachment.ownerId}?viewMode=attachments&amp;attachmentId=${localAttachment.attachmentId}"`);
content = content.replace(
new RegExp(`href="[^"]+attachmentId=${unknownAttachment.attachmentId}[^"]*"`, "g"),
`href="#root/${localAttachment.ownerId}?viewMode=attachments&amp;attachmentId=${localAttachment.attachmentId}"`
);
}
return {
@@ -406,9 +403,9 @@ function findImageLinks(content: string, foundLinks: FoundLink[]) {
const re = /src="[^"]*api\/images\/([a-zA-Z0-9_]+)\//g;
let match;
while (match = re.exec(content)) {
while ((match = re.exec(content))) {
foundLinks.push({
name: 'imageLink',
name: "imageLink",
value: match[1]
});
}
@@ -422,9 +419,9 @@ function findInternalLinks(content: string, foundLinks: FoundLink[]) {
const re = /href="[^"]*#root[a-zA-Z0-9_\/]*\/([a-zA-Z0-9_]+)\/?"/g;
let match;
while (match = re.exec(content)) {
while ((match = re.exec(content))) {
foundLinks.push({
name: 'internalLink',
name: "internalLink",
value: match[1]
});
}
@@ -437,9 +434,9 @@ function findIncludeNoteLinks(content: string, foundLinks: FoundLink[]) {
const re = /<section class="include-note[^>]+data-note-id="([a-zA-Z0-9_]+)"[^>]*>/g;
let match;
while (match = re.exec(content)) {
while ((match = re.exec(content))) {
foundLinks.push({
name: 'includeNoteLink',
name: "includeNoteLink",
value: match[1]
});
}
@@ -453,7 +450,7 @@ function findRelationMapLinks(content: string, foundLinks: FoundLink[]) {
for (const note of obj.notes) {
foundLinks.push({
name: 'relationMapLink',
name: "relationMapLink",
value: note.noteId
});
}
@@ -498,8 +495,7 @@ async function downloadImage(noteId: string, imageUrl: string) {
}
log.info(`Download of '${imageUrl}' succeeded and was saved as image attachment '${attachment.attachmentId}' of note '${noteId}'`);
}
catch (e: any) {
} catch (e: any) {
log.error(`Download of '${imageUrl}' for note '${noteId}' failed with error: ${e.message} ${e.stack}`);
}
}
@@ -514,27 +510,28 @@ function replaceUrl(content: string, url: string, attachment: Attachment) {
}
function downloadImages(noteId: string, content: string) {
const imageRe = /<img[^>]*?\ssrc=['"]([^'">]+)['"]/ig;
const imageRe = /<img[^>]*?\ssrc=['"]([^'">]+)['"]/gi;
let imageMatch;
while (imageMatch = imageRe.exec(content)) {
while ((imageMatch = imageRe.exec(content))) {
const url = imageMatch[1];
const inlineImageMatch = /^data:image\/[a-z]+;base64,/.exec(url);
if (inlineImageMatch) {
const imageBase64 = url.substr(inlineImageMatch[0].length);
const imageBuffer = Buffer.from(imageBase64, 'base64');
const imageBuffer = Buffer.from(imageBase64, "base64");
const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, "inline image", true, true);
const encodedTitle = encodeURIComponent(attachment.title);
content = `${content.substr(0, imageMatch.index)}<img src="api/attachments/${attachment.attachmentId}/image/${encodedTitle}"${content.substr(imageMatch.index + imageMatch[0].length)}`;
}
else if (!url.includes('api/images/') && !/api\/attachments\/.+\/image\/?.*/.test(url)
} else if (
!url.includes("api/images/") &&
!/api\/attachments\/.+\/image\/?.*/.test(url) &&
// this is an exception for the web clipper's "imageId"
&& (url.length !== 20 || url.toLowerCase().startsWith('http'))) {
(url.length !== 20 || url.toLowerCase().startsWith("http"))
) {
if (!optionService.getOptionBool("downloadImagesAutomatically")) {
continue;
}
@@ -544,8 +541,7 @@ function downloadImages(noteId: string, content: string) {
if (!attachment) {
delete imageUrlToAttachmentIdMapping[url];
}
else {
} else {
content = replaceUrl(content, url, attachment);
continue;
}
@@ -591,7 +587,7 @@ function downloadImages(noteId: string, content: string) {
}
for (const url in imageUrlToAttachmentIdMapping) {
const imageNote = imageNotes.find(note => note.noteId === imageUrlToAttachmentIdMapping[url]);
const imageNote = imageNotes.find((note) => note.noteId === imageUrlToAttachmentIdMapping[url]);
if (imageNote) {
updatedContent = replaceUrl(updatedContent, url, imageNote);
@@ -614,19 +610,19 @@ function downloadImages(noteId: string, content: string) {
}
function saveAttachments(note: BNote, content: string) {
const inlineAttachmentRe = /<a[^>]*?\shref=['"]data:([^;'">]+);base64,([^'">]+)['"][^>]*>(.*?)<\/a>/igm;
const inlineAttachmentRe = /<a[^>]*?\shref=['"]data:([^;'">]+);base64,([^'">]+)['"][^>]*>(.*?)<\/a>/gim;
let attachmentMatch;
while (attachmentMatch = inlineAttachmentRe.exec(content)) {
while ((attachmentMatch = inlineAttachmentRe.exec(content))) {
const mime = attachmentMatch[1].toLowerCase();
const base64data = attachmentMatch[2];
const buffer = Buffer.from(base64data, 'base64');
const buffer = Buffer.from(base64data, "base64");
const title = html2plaintext(attachmentMatch[3]);
const attachment = note.saveAttachment({
role: 'file',
role: "file",
mime: mime,
title: title,
content: buffer
@@ -643,8 +639,7 @@ function saveAttachments(note: BNote, content: string) {
}
function saveLinks(note: BNote, content: string | Buffer) {
if ((note.type !== 'text' && note.type !== 'relationMap')
|| (note.isProtected && !protectedSessionService.isProtectedSessionAvailable())) {
if ((note.type !== "text" && note.type !== "relationMap") || (note.isProtected && !protectedSessionService.isProtectedSessionAvailable())) {
return {
forceFrontendReload: false,
content
@@ -654,7 +649,7 @@ function saveLinks(note: BNote, content: string | Buffer) {
const foundLinks: FoundLink[] = [];
let forceFrontendReload = false;
if (note.type === 'text' && typeof content === "string") {
if (note.type === "text" && typeof content === "string") {
content = downloadImages(note.noteId, content);
content = saveAttachments(note, content);
@@ -662,17 +657,14 @@ function saveLinks(note: BNote, content: string | Buffer) {
content = findInternalLinks(content, foundLinks);
content = findIncludeNoteLinks(content, foundLinks);
({forceFrontendReload, content} = checkImageAttachments(note, content));
}
else if (note.type === 'relationMap' && typeof content === "string") {
({ forceFrontendReload, content } = checkImageAttachments(note, content));
} else if (note.type === "relationMap" && typeof content === "string") {
findRelationMapLinks(content, foundLinks);
}
else {
} else {
throw new Error(`Unrecognized type '${note.type}'`);
}
const existingLinks = note.getRelations().filter(rel =>
['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(rel.name));
const existingLinks = note.getRelations().filter((rel) => ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(rel.name));
for (const foundLink of foundLinks) {
const targetNote = becca.notes[foundLink.value];
@@ -680,16 +672,14 @@ function saveLinks(note: BNote, content: string | Buffer) {
continue;
}
const existingLink = existingLinks.find(existingLink =>
existingLink.value === foundLink.value
&& existingLink.name === foundLink.name);
const existingLink = existingLinks.find((existingLink) => existingLink.value === foundLink.value && existingLink.name === foundLink.name);
if (!existingLink) {
const newLink = new BAttribute({
noteId: note.noteId,
type: 'relation',
type: "relation",
name: foundLink.name,
value: foundLink.value,
value: foundLink.value
}).save();
existingLinks.push(newLink);
@@ -698,9 +688,7 @@ function saveLinks(note: BNote, content: string | Buffer) {
}
// marking links as deleted if they are not present on the page anymore
const unusedLinks = existingLinks.filter(existingLink => !foundLinks.some(foundLink =>
existingLink.value === foundLink.value
&& existingLink.name === foundLink.name));
const unusedLinks = existingLinks.filter((existingLink) => !foundLinks.some((foundLink) => existingLink.value === foundLink.value && existingLink.name === foundLink.name));
for (const unusedLink of unusedLinks) {
unusedLink.markAsDeleted();
@@ -711,17 +699,16 @@ function saveLinks(note: BNote, content: string | Buffer) {
function saveRevisionIfNeeded(note: BNote) {
// files and images are versioned separately
if (note.type === 'file' || note.type === 'image' || note.isLabelTruthy('disableVersioning')) {
if (note.type === "file" || note.type === "image" || note.isLabelTruthy("disableVersioning")) {
return;
}
const now = new Date();
const revisionSnapshotTimeInterval = parseInt(optionService.getOption('revisionSnapshotTimeInterval'));
const revisionSnapshotTimeInterval = parseInt(optionService.getOption("revisionSnapshotTimeInterval"));
const revisionCutoff = dateUtils.utcDateTimeStr(new Date(now.getTime() - revisionSnapshotTimeInterval * 1000));
const existingRevisionId = sql.getValue(
"SELECT revisionId FROM revisions WHERE noteId = ? AND utcDateCreated >= ?", [note.noteId, revisionCutoff]);
const existingRevisionId = sql.getValue("SELECT revisionId FROM revisions WHERE noteId = ? AND utcDateCreated >= ?", [note.noteId, revisionCutoff]);
const msSinceDateCreated = now.getTime() - dateUtils.parseDateTime(note.utcDateCreated).getTime();
@@ -744,18 +731,18 @@ function updateNoteData(noteId: string, content: string, attachments: Attachment
note.setContent(newContent, { forceFrontendReload });
if (attachments?.length > 0) {
const existingAttachmentsByTitle = toMap(note.getAttachments({includeContentLength: false}), 'title');
const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title");
for (const {attachmentId, role, mime, title, position, content} of attachments) {
for (const { attachmentId, role, mime, title, position, content } of attachments) {
if (attachmentId || !(title in existingAttachmentsByTitle)) {
note.saveAttachment({attachmentId, role, mime, title, content, position});
note.saveAttachment({ attachmentId, role, mime, title, content, position });
} else {
const existingAttachment = existingAttachmentsByTitle[title];
existingAttachment.role = role;
existingAttachment.mime = mime;
existingAttachment.position = position;
if (content) {
existingAttachment.setContent(content, {forceSave: true});
existingAttachment.setContent(content, { forceSave: true });
}
}
}
@@ -783,7 +770,7 @@ function undeleteNote(noteId: string, taskContext: TaskContext) {
}
function undeleteBranch(branchId: string, deleteId: string, taskContext: TaskContext) {
const branchRow = sql.getRow<BranchRow>("SELECT * FROM branches WHERE branchId = ?", [branchId])
const branchRow = sql.getRow<BranchRow>("SELECT * FROM branches WHERE branchId = ?", [branchId]);
if (!branchRow.isDeleted) {
return;
@@ -809,34 +796,43 @@ function undeleteBranch(branchId: string, deleteId: string, taskContext: TaskCon
noteEntity.updateFromRow(noteRow);
noteEntity.save();
const attributeRows = sql.getRows<AttributeRow>(`
const attributeRows = sql.getRows<AttributeRow>(
`
SELECT * FROM attributes
WHERE isDeleted = 1
AND deleteId = ?
AND (noteId = ?
OR (type = 'relation' AND value = ?))`, [deleteId, noteRow.noteId, noteRow.noteId]);
OR (type = 'relation' AND value = ?))`,
[deleteId, noteRow.noteId, noteRow.noteId]
);
for (const attributeRow of attributeRows) {
// relation might point to a note which hasn't been undeleted yet and would thus throw up
new BAttribute(attributeRow).save({skipValidation: true});
new BAttribute(attributeRow).save({ skipValidation: true });
}
const attachmentRows = sql.getRows<AttachmentRow>(`
const attachmentRows = sql.getRows<AttachmentRow>(
`
SELECT * FROM attachments
WHERE isDeleted = 1
AND deleteId = ?
AND ownerId = ?`, [deleteId, noteRow.noteId]);
AND ownerId = ?`,
[deleteId, noteRow.noteId]
);
for (const attachmentRow of attachmentRows) {
new BAttachment(attachmentRow).save();
}
const childBranchIds = sql.getColumn<string>(`
const childBranchIds = sql.getColumn<string>(
`
SELECT branches.branchId
FROM branches
WHERE branches.isDeleted = 1
AND branches.deleteId = ?
AND branches.parentNoteId = ?`, [deleteId, noteRow.noteId]);
AND branches.parentNoteId = ?`,
[deleteId, noteRow.noteId]
);
for (const childBranchId of childBranchIds) {
undeleteBranch(childBranchId, deleteId, taskContext);
@@ -848,18 +844,21 @@ function undeleteBranch(branchId: string, deleteId: string, taskContext: TaskCon
* @returns return deleted branchIds of an undeleted parent note
*/
function getUndeletedParentBranchIds(noteId: string, deleteId: string) {
return sql.getColumn<string>(`
return sql.getColumn<string>(
`
SELECT branches.branchId
FROM branches
JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId
WHERE branches.noteId = ?
AND branches.isDeleted = 1
AND branches.deleteId = ?
AND parentNote.isDeleted = 0`, [noteId, deleteId]);
AND parentNote.isDeleted = 0`,
[noteId, deleteId]
);
}
function scanForLinks(note: BNote, content: string | Buffer) {
if (!note || !['text', 'relationMap'].includes(note.type)) {
if (!note || !["text", "relationMap"].includes(note.type)) {
return;
}
@@ -871,8 +870,7 @@ function scanForLinks(note: BNote, content: string | Buffer) {
note.setContent(newContent, { forceFrontendReload });
}
});
}
catch (e: any) {
} catch (e: any) {
log.error(`Could not scan for links note '${note.noteId}': ${e.message} ${e.stack}`);
}
}
@@ -899,21 +897,21 @@ function replaceByMap(str: string, mapObj: Record<string, string>) {
return str;
}
const re = new RegExp(Object.keys(mapObj).join("|"),"g");
const re = new RegExp(Object.keys(mapObj).join("|"), "g");
return str.replace(re, matched => mapObj[matched]);
return str.replace(re, (matched) => mapObj[matched]);
}
function duplicateSubtree(origNoteId: string, newParentNoteId: string) {
if (origNoteId === 'root') {
throw new Error('Duplicating root is not possible');
if (origNoteId === "root") {
throw new Error("Duplicating root is not possible");
}
log.info(`Duplicating '${origNoteId}' subtree into '${newParentNoteId}'`);
const origNote = becca.notes[origNoteId];
// might be null if orig note is not in the target newParentNoteId
const origBranch = origNote.getParentBranches().find(branch => branch.parentNoteId === newParentNoteId);
const origBranch = origNote.getParentBranches().find((branch) => branch.parentNoteId === newParentNoteId);
const noteIdMapping = getNoteIdMapping(origNote);
@@ -935,8 +933,8 @@ function duplicateSubtree(origNoteId: string, newParentNoteId: string) {
}
function duplicateSubtreeWithoutRoot(origNoteId: string, newNoteId: string) {
if (origNoteId === 'root') {
throw new Error('Duplicating root is not possible');
if (origNoteId === "root") {
throw new Error("Duplicating root is not possible");
}
const origNote = becca.getNote(origNoteId);
@@ -978,7 +976,7 @@ function duplicateSubtreeInner(origNote: BNote, origBranch: BBranch, newParentNo
let content = origNote.getContent();
if (typeof content === "string" && ['text', 'relationMap', 'search'].includes(origNote.type)) {
if (typeof content === "string" && ["text", "relationMap", "search"].includes(origNote.type)) {
// fix links in the content
content = replaceByMap(content, noteIdMapping);
}
@@ -994,12 +992,12 @@ function duplicateSubtreeInner(origNote: BNote, origBranch: BBranch, newParentNo
// if relation points to within the duplicated tree then replace the target to the duplicated note
// if it points outside of duplicated tree then keep the original target
if (attr.type === 'relation' && attr.value in noteIdMapping) {
if (attr.type === "relation" && attr.value in noteIdMapping) {
attr.value = noteIdMapping[attr.value];
}
// the relation targets may not be created yet, the mapping is pre-generated
attr.save({skipValidation: true});
attr.save({ skipValidation: true });
}
for (const childBranch of origNote.getChildBranches()) {
@@ -1013,20 +1011,20 @@ function duplicateSubtreeInner(origNote: BNote, origBranch: BBranch, newParentNo
const existingNote = becca.notes[newNoteId];
if (existingNote && existingNote.title !== undefined) { // checking that it's not just note's skeleton created because of Branch above
if (existingNote && existingNote.title !== undefined) {
// checking that it's not just note's skeleton created because of Branch above
// note has multiple clones and was already created from another placement in the tree,
// so a branch is all we need for this clone
return {
note: existingNote,
branch: createDuplicatedBranch()
}
}
else {
};
} else {
return {
// order here is important, note needs to be created first to not mess up the becca
note: createDuplicatedNote(),
branch: createDuplicatedBranch()
}
};
}
}

View File

@@ -14,7 +14,7 @@
import becca from "../becca/becca.js";
import BOption from "../becca/entities/boption.js";
import { OptionRow } from '../becca/entities/rows.js';
import { OptionRow } from "../becca/entities/rows.js";
import { FilterOptionsByType, OptionDefinitions, OptionMap, OptionNames } from "./options_interface.js";
import sql from "./sql.js";
@@ -60,11 +60,11 @@ function getOptionInt(name: FilterOptionsByType<number>, defaultValue?: number):
function getOptionBool(name: FilterOptionsByType<boolean>): boolean {
const val = getOption(name);
if (typeof val !== "string" || !['true', 'false'].includes(val)) {
if (typeof val !== "string" || !["true", "false"].includes(val)) {
throw new Error(`Could not parse '${val}' into boolean for option '${name}'`);
}
return val === 'true';
return val === "true";
}
function setOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
@@ -74,8 +74,7 @@ function setOption<T extends OptionNames>(name: T, value: string | OptionDefinit
option.value = value as string;
option.save();
}
else {
} else {
createOption(name, value, false);
}
}

View File

@@ -5,11 +5,11 @@ import { randomSecureToken, isWindows } from "./utils.js";
import log from "./log.js";
import dateUtils from "./date_utils.js";
import keyboardActions from "./keyboard_actions.js";
import { KeyboardShortcutWithRequiredActionName } from './keyboard_actions_interface.js';
import { KeyboardShortcutWithRequiredActionName } from "./keyboard_actions_interface.js";
function initDocumentOptions() {
optionService.createOption('documentId', randomSecureToken(16), false);
optionService.createOption('documentSecret', randomSecureToken(16), false);
optionService.createOption("documentId", randomSecureToken(16), false);
optionService.createOption("documentSecret", randomSecureToken(16), false);
}
/**
@@ -26,10 +26,10 @@ interface NotSyncedOpts {
interface DefaultOption {
name: OptionNames;
/**
* The value to initialize the option with, if the option is not already present in the database.
*
* If a function is passed Gin instead, the function is called if the option does not exist (with access to the current options) and the return value is used instead. Useful to migrate a new option with a value depending on some other option that might be initialized.
*/
* The value to initialize the option with, if the option is not already present in the database.
*
* If a function is passed Gin instead, the function is called if the option does not exist (with access to the current options) and the return value is used instead. Useful to migrate a new option with a value depending on some other option that might be initialized.
*/
value: string | ((options: OptionMap) => string);
isSynced: boolean;
}
@@ -41,95 +41,107 @@ interface DefaultOption {
* @param opts additional options to be initialized, for example the sync configuration.
*/
async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = {}) {
optionService.createOption('openNoteContexts', JSON.stringify([
{
notePath: 'root',
active: true
}
]), false);
optionService.createOption(
"openNoteContexts",
JSON.stringify([
{
notePath: "root",
active: true
}
]),
false
);
optionService.createOption('lastDailyBackupDate', dateUtils.utcNowDateTime(), false);
optionService.createOption('lastWeeklyBackupDate', dateUtils.utcNowDateTime(), false);
optionService.createOption('lastMonthlyBackupDate', dateUtils.utcNowDateTime(), false);
optionService.createOption('dbVersion', appInfo.dbVersion.toString(), false);
optionService.createOption("lastDailyBackupDate", dateUtils.utcNowDateTime(), false);
optionService.createOption("lastWeeklyBackupDate", dateUtils.utcNowDateTime(), false);
optionService.createOption("lastMonthlyBackupDate", dateUtils.utcNowDateTime(), false);
optionService.createOption("dbVersion", appInfo.dbVersion.toString(), false);
optionService.createOption('initialized', initialized ? 'true' : 'false', false);
optionService.createOption("initialized", initialized ? "true" : "false", false);
optionService.createOption('lastSyncedPull', '0', false);
optionService.createOption('lastSyncedPush', '0', false);
optionService.createOption("lastSyncedPull", "0", false);
optionService.createOption("lastSyncedPush", "0", false);
optionService.createOption('theme', 'next', false);
optionService.createOption("theme", "next", false);
optionService.createOption('syncServerHost', opts.syncServerHost || '', false);
optionService.createOption('syncServerTimeout', '120000', false);
optionService.createOption('syncProxy', opts.syncProxy || '', false);
optionService.createOption("syncServerHost", opts.syncServerHost || "", false);
optionService.createOption("syncServerTimeout", "120000", false);
optionService.createOption("syncProxy", opts.syncProxy || "", false);
}
/**
* Contains all the default options that must be initialized on new and existing databases (at startup). The value can also be determined based on other options, provided they have already been initialized.
*/
const defaultOptions: DefaultOption[] = [
{ name: 'revisionSnapshotTimeInterval', value: '600', isSynced: true },
{ name: 'revisionSnapshotNumberLimit', value: '-1', isSynced: true },
{ name: 'protectedSessionTimeout', value: '600', isSynced: true },
{ name: 'zoomFactor', value: isWindows() ? '0.9' : '1.0', isSynced: false },
{ name: 'overrideThemeFonts', value: 'false', isSynced: false },
{ name: 'mainFontFamily', value: 'theme', isSynced: false },
{ name: 'mainFontSize', value: '100', isSynced: false },
{ name: 'treeFontFamily', value: 'theme', isSynced: false },
{ name: 'treeFontSize', value: '100', isSynced: false },
{ name: 'detailFontFamily', value: 'theme', isSynced: false },
{ name: 'detailFontSize', value: '110', isSynced: false },
{ name: 'monospaceFontFamily', value: 'theme', isSynced: false },
{ name: 'monospaceFontSize', value: '110', isSynced: false },
{ name: 'spellCheckEnabled', value: 'true', isSynced: false },
{ name: 'spellCheckLanguageCode', value: 'en-US', isSynced: false },
{ name: 'imageMaxWidthHeight', value: '2000', isSynced: true },
{ name: 'imageJpegQuality', value: '75', isSynced: true },
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
{ name: 'vimKeymapEnabled', value: 'false', isSynced: false },
{ name: 'codeLineWrapEnabled', value: 'true', isSynced: false },
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml","text/x-sh"]', isSynced: true },
{ name: 'leftPaneWidth', value: '25', isSynced: false },
{ name: 'leftPaneVisible', value: 'true', isSynced: false },
{ name: 'rightPaneWidth', value: '25', isSynced: false },
{ name: 'rightPaneVisible', value: 'true', isSynced: false },
{ name: 'nativeTitleBarVisible', value: 'false', isSynced: false },
{ name: 'eraseEntitiesAfterTimeInSeconds', value: '604800', isSynced: true }, // default is 7 days
{ name: 'hideArchivedNotes_main', value: 'false', isSynced: false },
{ name: 'debugModeEnabled', value: 'false', isSynced: false },
{ name: 'headingStyle', value: 'underline', isSynced: true },
{ name: 'autoCollapseNoteTree', value: 'true', isSynced: true },
{ name: 'autoReadonlySizeText', value: '10000', isSynced: false },
{ name: 'autoReadonlySizeCode', value: '30000', isSynced: false },
{ name: 'dailyBackupEnabled', value: 'true', isSynced: false },
{ name: 'weeklyBackupEnabled', value: 'true', isSynced: false },
{ name: 'monthlyBackupEnabled', value: 'true', isSynced: false },
{ name: 'maxContentWidth', value: '1200', isSynced: false },
{ name: 'compressImages', value: 'true', isSynced: true },
{ name: 'downloadImagesAutomatically', value: 'true', isSynced: true },
{ name: 'minTocHeadings', value: '5', isSynced: true },
{ 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 },
{ name: 'customSearchEngineName', value: 'DuckDuckGo', isSynced: true },
{ name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true },
{ name: 'promotedAttributesOpenInRibbon', value: 'true', isSynced: true },
{ name: 'editedNotesOpenInRibbon', value: 'true', isSynced: true },
{ name: "revisionSnapshotTimeInterval", value: "600", isSynced: true },
{ name: "revisionSnapshotNumberLimit", value: "-1", isSynced: true },
{ name: "protectedSessionTimeout", value: "600", isSynced: true },
{ name: "zoomFactor", value: isWindows() ? "0.9" : "1.0", isSynced: false },
{ name: "overrideThemeFonts", value: "false", isSynced: false },
{ name: "mainFontFamily", value: "theme", isSynced: false },
{ name: "mainFontSize", value: "100", isSynced: false },
{ name: "treeFontFamily", value: "theme", isSynced: false },
{ name: "treeFontSize", value: "100", isSynced: false },
{ name: "detailFontFamily", value: "theme", isSynced: false },
{ name: "detailFontSize", value: "110", isSynced: false },
{ name: "monospaceFontFamily", value: "theme", isSynced: false },
{ name: "monospaceFontSize", value: "110", isSynced: false },
{ name: "spellCheckEnabled", value: "true", isSynced: false },
{ name: "spellCheckLanguageCode", value: "en-US", isSynced: false },
{ name: "imageMaxWidthHeight", value: "2000", isSynced: true },
{ name: "imageJpegQuality", value: "75", isSynced: true },
{ name: "autoFixConsistencyIssues", value: "true", isSynced: false },
{ name: "vimKeymapEnabled", value: "false", isSynced: false },
{ name: "codeLineWrapEnabled", value: "true", isSynced: false },
{
name: "codeNotesMimeTypes",
value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml","text/x-sh"]',
isSynced: true
},
{ name: "leftPaneWidth", value: "25", isSynced: false },
{ name: "leftPaneVisible", value: "true", isSynced: false },
{ name: "rightPaneWidth", value: "25", isSynced: false },
{ name: "rightPaneVisible", value: "true", isSynced: false },
{ name: "nativeTitleBarVisible", value: "false", isSynced: false },
{ name: "eraseEntitiesAfterTimeInSeconds", value: "604800", isSynced: true }, // default is 7 days
{ name: "hideArchivedNotes_main", value: "false", isSynced: false },
{ name: "debugModeEnabled", value: "false", isSynced: false },
{ name: "headingStyle", value: "underline", isSynced: true },
{ name: "autoCollapseNoteTree", value: "true", isSynced: true },
{ name: "autoReadonlySizeText", value: "10000", isSynced: false },
{ name: "autoReadonlySizeCode", value: "30000", isSynced: false },
{ name: "dailyBackupEnabled", value: "true", isSynced: false },
{ name: "weeklyBackupEnabled", value: "true", isSynced: false },
{ name: "monthlyBackupEnabled", value: "true", isSynced: false },
{ name: "maxContentWidth", value: "1200", isSynced: false },
{ name: "compressImages", value: "true", isSynced: true },
{ name: "downloadImagesAutomatically", value: "true", isSynced: true },
{ name: "minTocHeadings", value: "5", isSynced: true },
{ 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 },
{ name: "customSearchEngineName", value: "DuckDuckGo", isSynced: true },
{ name: "customSearchEngineUrl", value: "https://duckduckgo.com/?q={keyword}", isSynced: true },
{ name: "promotedAttributesOpenInRibbon", value: "true", isSynced: true },
{ name: "editedNotesOpenInRibbon", value: "true", isSynced: true },
// Internationalization
{ name: 'locale', value: 'en', isSynced: true },
{ name: 'firstDayOfWeek', value: '1', isSynced: true },
{ name: "locale", value: "en", isSynced: true },
{ name: "firstDayOfWeek", value: "1", isSynced: true },
// Code block configuration
{ name: "codeBlockTheme", value: (optionsMap) => {
if (optionsMap.theme === "light") {
return "default:stackoverflow-light";
} else {
return "default:stackoverflow-dark";
}
}, isSynced: false },
{
name: "codeBlockTheme",
value: (optionsMap) => {
if (optionsMap.theme === "light") {
return "default:stackoverflow-light";
} else {
return "default:stackoverflow-dark";
}
},
isSynced: false
},
{ name: "codeBlockWordWrap", value: "false", isSynced: true },
// Text note configuration
@@ -139,18 +151,106 @@ const defaultOptions: DefaultOption[] = [
// HTML import configuration
{ name: "layoutOrientation", value: "vertical", isSynced: false },
{ name: "backgroundEffects", value: "false", isSynced: false },
{ name: "allowedHtmlTags", value: JSON.stringify([
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img',
'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer',
'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time',
'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins',
'en-media',
'acronym', 'article', 'big', 'button', 'cite', 'col', 'colgroup', 'data', 'dd',
'fieldset', 'form', 'legend', 'meter', 'noscript', 'option', 'progress', 'rp',
'samp', 'small', 'sub', 'sup', 'template', 'textarea', 'tt'
]), isSynced: true },
{
name: "allowedHtmlTags",
value: JSON.stringify([
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"p",
"a",
"ul",
"ol",
"li",
"b",
"i",
"strong",
"em",
"strike",
"s",
"del",
"abbr",
"code",
"hr",
"br",
"div",
"table",
"thead",
"caption",
"tbody",
"tfoot",
"tr",
"th",
"td",
"pre",
"section",
"img",
"figure",
"figcaption",
"span",
"label",
"input",
"details",
"summary",
"address",
"aside",
"footer",
"header",
"hgroup",
"main",
"nav",
"dl",
"dt",
"menu",
"bdi",
"bdo",
"dfn",
"kbd",
"mark",
"q",
"time",
"var",
"wbr",
"area",
"map",
"track",
"video",
"audio",
"picture",
"del",
"ins",
"en-media",
"acronym",
"article",
"big",
"button",
"cite",
"col",
"colgroup",
"data",
"dd",
"fieldset",
"form",
"legend",
"meter",
"noscript",
"option",
"progress",
"rp",
"samp",
"small",
"sub",
"sup",
"template",
"textarea",
"tt"
]),
isSynced: true
}
];
/**
@@ -163,7 +263,7 @@ function initStartupOptions() {
const allDefaultOptions = defaultOptions.concat(getKeyboardDefaultOptions());
for (const {name, value, isSynced} of allDefaultOptions) {
for (const { name, value, isSynced } of allDefaultOptions) {
if (!(name in optionsMap)) {
let resolvedValue;
if (typeof value === "function") {
@@ -178,23 +278,24 @@ function initStartupOptions() {
}
if (process.env.TRILIUM_START_NOTE_ID || process.env.TRILIUM_SAFE_MODE) {
optionService.setOption('openNoteContexts', JSON.stringify([
{
notePath: process.env.TRILIUM_START_NOTE_ID || 'root',
active: true
}
]));
optionService.setOption(
"openNoteContexts",
JSON.stringify([
{
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
active: true
}
])
);
}
}
function getKeyboardDefaultOptions() {
return (keyboardActions.getDefaultKeyboardActions()
.filter(ka => !!ka.actionName) as KeyboardShortcutWithRequiredActionName[])
.map(ka => ({
name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`,
value: JSON.stringify(ka.defaultShortcuts),
isSynced: false
})) as DefaultOption[];
return (keyboardActions.getDefaultKeyboardActions().filter((ka) => !!ka.actionName) as KeyboardShortcutWithRequiredActionName[]).map((ka) => ({
name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`,
value: JSON.stringify(ka.defaultShortcuts),
isSynced: false
})) as DefaultOption[];
}
export default {

View File

@@ -9,90 +9,90 @@ export type OptionMap = Record<OptionNames, string>;
* For each keyboard action, there is a corresponding option which identifies the key combination defined by the user.
*/
type KeyboardShortcutsOptions<T extends KeyboardActionNames> = {
[key in T as `keyboardShortcuts${Capitalize<key>}`]: string
[key in T as `keyboardShortcuts${Capitalize<key>}`]: string;
};
export type FontFamily = "theme" | "serif" | "sans-serif" | "monospace" | string;
export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActionNames> {
"openNoteContexts": string;
"lastDailyBackupDate": string;
"lastWeeklyBackupDate": string;
"lastMonthlyBackupDate": string;
"dbVersion": string;
"theme": string;
"syncServerHost": string;
"syncServerTimeout": string;
"syncProxy": string;
"mainFontFamily": FontFamily;
"treeFontFamily": FontFamily;
"detailFontFamily": FontFamily;
"monospaceFontFamily": FontFamily;
"spellCheckLanguageCode": string;
"codeNotesMimeTypes": string;
"headingStyle": string;
"highlightsList": string;
"customSearchEngineName": string;
"customSearchEngineUrl": string;
"locale": string;
"codeBlockTheme": string;
"textNoteEditorType": string;
"layoutOrientation": string;
"allowedHtmlTags": string;
"documentId": string;
"documentSecret": string;
"passwordVerificationHash": string;
"passwordVerificationSalt": string;
"passwordDerivedKeySalt": string;
"encryptedDataKey": string;
openNoteContexts: string;
lastDailyBackupDate: string;
lastWeeklyBackupDate: string;
lastMonthlyBackupDate: string;
dbVersion: string;
theme: string;
syncServerHost: string;
syncServerTimeout: string;
syncProxy: string;
mainFontFamily: FontFamily;
treeFontFamily: FontFamily;
detailFontFamily: FontFamily;
monospaceFontFamily: FontFamily;
spellCheckLanguageCode: string;
codeNotesMimeTypes: string;
headingStyle: string;
highlightsList: string;
customSearchEngineName: string;
customSearchEngineUrl: string;
locale: string;
codeBlockTheme: string;
textNoteEditorType: string;
layoutOrientation: string;
allowedHtmlTags: string;
documentId: string;
documentSecret: string;
passwordVerificationHash: string;
passwordVerificationSalt: string;
passwordDerivedKeySalt: string;
encryptedDataKey: string;
"lastSyncedPull": number;
"lastSyncedPush": number;
"revisionSnapshotTimeInterval": number;
"revisionSnapshotNumberLimit": number;
"protectedSessionTimeout": number;
"zoomFactor": number;
"mainFontSize": number;
"treeFontSize": number;
"detailFontSize": number;
"monospaceFontSize": number;
"imageMaxWidthHeight": number;
"imageJpegQuality": number;
"leftPaneWidth": number;
"rightPaneWidth": number;
"eraseEntitiesAfterTimeInSeconds": number;
"autoReadonlySizeText": number;
"autoReadonlySizeCode": number;
"maxContentWidth": number;
"minTocHeadings": number;
"eraseUnusedAttachmentsAfterSeconds": number;
"firstDayOfWeek": number;
lastSyncedPull: number;
lastSyncedPush: number;
revisionSnapshotTimeInterval: number;
revisionSnapshotNumberLimit: number;
protectedSessionTimeout: number;
zoomFactor: number;
mainFontSize: number;
treeFontSize: number;
detailFontSize: number;
monospaceFontSize: number;
imageMaxWidthHeight: number;
imageJpegQuality: number;
leftPaneWidth: number;
rightPaneWidth: number;
eraseEntitiesAfterTimeInSeconds: number;
autoReadonlySizeText: number;
autoReadonlySizeCode: number;
maxContentWidth: number;
minTocHeadings: number;
eraseUnusedAttachmentsAfterSeconds: number;
firstDayOfWeek: number;
"initialized": boolean;
"overrideThemeFonts": boolean;
"spellCheckEnabled": boolean;
"autoFixConsistencyIssues": boolean;
"vimKeymapEnabled": boolean;
"codeLineWrapEnabled": boolean;
"leftPaneVisible": boolean;
"rightPaneVisible": boolean;
"nativeTitleBarVisible": boolean;
"hideArchivedNotes_main": boolean;
"debugModeEnabled": boolean;
"autoCollapseNoteTree": boolean;
"dailyBackupEnabled": boolean;
"weeklyBackupEnabled": boolean;
"monthlyBackupEnabled": boolean;
"compressImages": boolean;
"downloadImagesAutomatically": boolean;
"checkForUpdates": boolean;
"disableTray": boolean;
"promotedAttributesOpenInRibbon": boolean;
"editedNotesOpenInRibbon": boolean;
"codeBlockWordWrap": boolean;
"textNoteEditorMultilineToolbar": boolean;
"backgroundEffects": boolean;
};
initialized: boolean;
overrideThemeFonts: boolean;
spellCheckEnabled: boolean;
autoFixConsistencyIssues: boolean;
vimKeymapEnabled: boolean;
codeLineWrapEnabled: boolean;
leftPaneVisible: boolean;
rightPaneVisible: boolean;
nativeTitleBarVisible: boolean;
hideArchivedNotes_main: boolean;
debugModeEnabled: boolean;
autoCollapseNoteTree: boolean;
dailyBackupEnabled: boolean;
weeklyBackupEnabled: boolean;
monthlyBackupEnabled: boolean;
compressImages: boolean;
downloadImagesAutomatically: boolean;
checkForUpdates: boolean;
disableTray: boolean;
promotedAttributesOpenInRibbon: boolean;
editedNotesOpenInRibbon: boolean;
codeBlockWordWrap: boolean;
textNoteEditorMultilineToolbar: boolean;
backgroundEffects: boolean;
}
export type OptionNames = keyof OptionDefinitions;

View File

@@ -21,7 +21,7 @@ if (process.env.TRILIUM_PORT) {
} else if (isElectron()) {
port = env.isDev() ? 37740 : 37840;
} else {
port = parseAndValidate(config['Network']['port'] || '3000', `Network.port in ${dataDir.CONFIG_INI_PATH}`);
port = parseAndValidate(config["Network"]["port"] || "3000", `Network.port in ${dataDir.CONFIG_INI_PATH}`);
}
export default port;

View File

@@ -1,35 +1,29 @@
import { DefinitionObject } from "./promoted_attribute_definition_interface.js";
function parse(value: string): DefinitionObject {
const tokens = value.split(',').map(t => t.trim());
const tokens = value.split(",").map((t) => t.trim());
const defObj: DefinitionObject = {};
for (const token of tokens) {
if (token === 'promoted') {
if (token === "promoted") {
defObj.isPromoted = true;
}
else if (['text', 'number', 'boolean', 'date', 'datetime', 'time', 'url'].includes(token)) {
} else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
defObj.labelType = token;
}
else if (['single', 'multi'].includes(token)) {
} else if (["single", "multi"].includes(token)) {
defObj.multiplicity = token;
}
else if (token.startsWith('precision')) {
const chunks = token.split('=');
} else if (token.startsWith("precision")) {
const chunks = token.split("=");
defObj.numberPrecision = parseInt(chunks[1]);
}
else if (token.startsWith('alias')) {
const chunks = token.split('=');
} else if (token.startsWith("alias")) {
const chunks = token.split("=");
defObj.promotedAlias = chunks[1];
}
else if (token.startsWith('inverse')) {
const chunks = token.split('=');
} else if (token.startsWith("inverse")) {
const chunks = token.split("=");
defObj.inverseRelation = chunks[1].replace(/[^\p{L}\p{N}_:]/ug, "")
}
else {
defObj.inverseRelation = chunks[1].replace(/[^\p{L}\p{N}_:]/gu, "");
} else {
console.log("Unrecognized attribute definition token:", token);
}
}

View File

@@ -58,11 +58,8 @@ function touchProtectedSession() {
}
function checkProtectedSessionExpiration() {
const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout');
if (isProtectedSessionAvailable()
&& lastProtectedSessionOperationDate
&& Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) {
const protectedSessionTimeout = options.getOptionInt("protectedSessionTimeout");
if (isProtectedSessionAvailable() && lastProtectedSessionOperationDate && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) {
resetDataKey();
log.info("Expiring protected session");

View File

@@ -4,7 +4,7 @@ import { isElectron } from "./utils.js";
import log from "./log.js";
import url from "url";
import syncOptions from "./sync_options.js";
import { ExecOpts } from './request_interface.js';
import { ExecOpts } from "./request_interface.js";
// this service provides abstraction over node's HTTP/HTTPS and electron net.client APIs
// this allows supporting system proxy
@@ -22,7 +22,7 @@ interface ClientOpts {
proxy?: string | null;
}
type RequestEvent = ("error" | "response" | "abort");
type RequestEvent = "error" | "response" | "abort";
interface Request {
on(event: RequestEvent, cb: (e: any) => void): void;
@@ -37,14 +37,14 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
const client = getClient(opts);
// hack for cases where electron.net does not work, but we don't want to set proxy
if (opts.proxy === 'noproxy') {
if (opts.proxy === "noproxy") {
opts.proxy = null;
}
const paging = opts.paging || {
pageCount: 1,
pageIndex: 0,
requestId: 'n/a'
requestId: "n/a"
};
const proxyAgent = await getProxyAgent(opts);
@@ -54,14 +54,14 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
try {
const headers: Record<string, string | number> = {
Cookie: (opts.cookieJar && opts.cookieJar.header) || "",
'Content-Type': paging.pageCount === 1 ? 'application/json' : 'text/plain',
"Content-Type": paging.pageCount === 1 ? "application/json" : "text/plain",
pageCount: paging.pageCount,
pageIndex: paging.pageIndex,
requestId: paging.requestId
};
if (opts.auth) {
headers['trilium-cred'] = Buffer.from(`dummy:${opts.auth.password}`).toString('base64');
headers["trilium-cred"] = Buffer.from(`dummy:${opts.auth.password}`).toString("base64");
}
const request = (await client).request({
@@ -78,22 +78,22 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
agent: proxyAgent
});
request.on('error', err => reject(generateError(opts, err)));
request.on("error", (err) => reject(generateError(opts, err)));
request.on('response', response => {
if (opts.cookieJar && response.headers['set-cookie']) {
opts.cookieJar.header = response.headers['set-cookie'];
request.on("response", (response) => {
if (opts.cookieJar && response.headers["set-cookie"]) {
opts.cookieJar.header = response.headers["set-cookie"];
}
let responseStr = '';
let responseStr = "";
let chunks: Buffer[] = [];
response.on('data', (chunk: Buffer) => chunks.push(chunk));
response.on("data", (chunk: Buffer) => chunks.push(chunk));
response.on('end', () => {
response.on("end", () => {
// use Buffer instead of string concatenation to avoid implicit decoding for each chunk
// decode the entire data chunks explicitly as utf-8
responseStr = Buffer.concat(chunks).toString('utf-8')
responseStr = Buffer.concat(chunks).toString("utf-8");
if ([200, 201, 204].includes(response.statusCode)) {
try {
@@ -111,7 +111,7 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
try {
const jsonObj = JSON.parse(responseStr);
errorMessage = jsonObj?.message || '';
errorMessage = jsonObj?.message || "";
} catch (e: any) {
errorMessage = responseStr.substr(0, Math.min(responseStr.length, 100));
}
@@ -124,14 +124,11 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
let payload;
if (opts.body) {
payload = typeof opts.body === 'object'
? JSON.stringify(opts.body)
: opts.body;
payload = typeof opts.body === "object" ? JSON.stringify(opts.body) : opts.body;
}
request.end(payload as string);
}
catch (e: any) {
} catch (e: any) {
reject(generateError(opts, e.message));
}
});
@@ -140,7 +137,7 @@ async function exec<T>(opts: ExecOpts): Promise<T> {
async function getImage(imageUrl: string): Promise<Buffer> {
const proxyConf = syncOptions.getSyncProxy();
const opts: ClientOpts = {
method: 'GET',
method: "GET",
url: imageUrl,
proxy: proxyConf !== "noproxy" ? proxyConf : null
};
@@ -165,19 +162,19 @@ async function getImage(imageUrl: string): Promise<Buffer> {
agent: proxyAgent
});
request.on('error', err => reject(generateError(opts, err)));
request.on("error", (err) => reject(generateError(opts, err)));
request.on('abort', err => reject(generateError(opts, err)));
request.on("abort", (err) => reject(generateError(opts, err)));
request.on('response', response => {
request.on("response", (response) => {
if (![200, 201, 204].includes(response.statusCode)) {
reject(generateError(opts, `${response.statusCode} ${response.statusMessage}`));
}
const chunks: Buffer[] = []
const chunks: Buffer[] = [];
response.on('data', (chunk: Buffer) => chunks.push(chunk));
response.on('end', () => resolve(Buffer.concat(chunks)));
response.on("data", (chunk: Buffer) => chunks.push(chunk));
response.on("end", () => resolve(Buffer.concat(chunks)));
});
request.end(undefined);
@@ -187,22 +184,21 @@ async function getImage(imageUrl: string): Promise<Buffer> {
});
}
const HTTP = 'http:', HTTPS = 'https:';
const HTTP = "http:",
HTTPS = "https:";
async function getProxyAgent(opts: ClientOpts) {
if (!opts.proxy) {
return null;
}
const {protocol} = url.parse(opts.url);
const { protocol } = url.parse(opts.url);
if (!protocol || ![HTTP, HTTPS].includes(protocol)) {
return null;
}
const AgentClass = HTTP === protocol
? (await import('http-proxy-agent')).HttpProxyAgent
: (await import('https-proxy-agent')).HttpsProxyAgent;
const AgentClass = HTTP === protocol ? (await import("http-proxy-agent")).HttpProxyAgent : (await import("https-proxy-agent")).HttpsProxyAgent;
return new AgentClass(opts.proxy);
}
@@ -211,11 +207,11 @@ async function getClient(opts: ClientOpts): Promise<Client> {
// it's not clear how to explicitly configure proxy (as opposed to system proxy),
// so in that case, we always use node's modules
if (isElectron() && !opts.proxy) {
return (await import('electron')).net as Client;
return (await import("electron")).net as Client;
} else {
const {protocol} = url.parse(opts.url);
const { protocol } = url.parse(opts.url);
if (protocol === 'http:' || protocol === 'https:') {
if (protocol === "http:" || protocol === "https:") {
return await import(protocol.substr(0, protocol.length - 1));
} else {
throw new Error(`Unrecognized protocol '${protocol}'`);
@@ -223,10 +219,13 @@ async function getClient(opts: ClientOpts): Promise<Client> {
}
}
function generateError(opts: {
method: string;
url: string;
}, message: string) {
function generateError(
opts: {
method: string;
url: string;
},
message: string
) {
return new Error(`Request to ${opts.method} ${opts.url} failed, error: ${message}`);
}

View File

@@ -14,7 +14,7 @@ export interface ExecOpts {
cookieJar?: CookieJar;
auth?: {
password?: string;
},
};
timeout: number;
body?: string | {};
}

View File

@@ -19,7 +19,7 @@ function protectRevisions(note: BNote) {
revision.isProtected = !!note.isProtected;
// this will force de/encryption
revision.setContent(content, {forceSave: true});
revision.setContent(content, { forceSave: true });
} catch (e) {
log.error(`Could not un/protect note revision '${revision.revisionId}'`);
@@ -33,7 +33,7 @@ function protectRevisions(note: BNote) {
const content = attachment.getContent();
attachment.isProtected = note.isProtected;
attachment.setContent(content, {forceSave: true});
attachment.setContent(content, { forceSave: true });
} catch (e) {
log.error(`Could not un/protect attachment '${attachment.attachmentId}'`);

View File

@@ -1,8 +1,6 @@
export default function sanitizeAttributeName(origName: string) {
const fixedName = (origName === '')
? "unnamed"
: origName.replace(/[^\p{L}\p{N}_:]/ug, "_");
// any not allowed character should be replaced with underscore
const fixedName = origName === "" ? "unnamed" : origName.replace(/[^\p{L}\p{N}_:]/gu, "_");
// any not allowed character should be replaced with underscore
return fixedName;
}
}

View File

@@ -10,7 +10,7 @@ import BNote from "../becca/entities/bnote.js";
function getRunAtHours(note: BNote): number[] {
try {
return note.getLabelValues('runAtHour').map(hour => parseInt(hour));
return note.getLabelValues("runAtHour").map((hour) => parseInt(hour));
} catch (e: any) {
log.error(`Could not parse runAtHour for note ${note.noteId}: ${e.message}`);
@@ -21,15 +21,13 @@ function getRunAtHours(note: BNote): number[] {
function runNotesWithLabel(runAttrValue: string) {
const instanceName = config.General ? config.General.instanceName : null;
const currentHours = new Date().getHours();
const notes = attributeService.getNotesWithLabel('run', runAttrValue);
const notes = attributeService.getNotesWithLabel("run", runAttrValue);
for (const note of notes) {
const runOnInstances = note.getLabelValues('runOnInstance');
const runOnInstances = note.getLabelValues("runOnInstance");
const runAtHours = getRunAtHours(note);
if ((runOnInstances.length === 0 || runOnInstances.includes(instanceName))
&& (runAtHours.length === 0 || runAtHours.includes(currentHours))
) {
if ((runOnInstances.length === 0 || runOnInstances.includes(instanceName)) && (runAtHours.length === 0 || runAtHours.includes(currentHours))) {
scriptService.executeNoteNoException(note, { originEntity: note });
}
}
@@ -41,13 +39,25 @@ sqlInit.dbReady.then(() => {
});
if (!process.env.TRILIUM_SAFE_MODE) {
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
setTimeout(
cls.wrap(() => runNotesWithLabel("backendStartup")),
10 * 1000
);
setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000);
setInterval(
cls.wrap(() => runNotesWithLabel("hourly")),
3600 * 1000
);
setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000);
setInterval(
cls.wrap(() => runNotesWithLabel("daily")),
24 * 3600 * 1000
);
setInterval(cls.wrap(() => hiddenSubtreeService.checkHiddenSubtree()), 7 * 3600 * 1000);
setInterval(
cls.wrap(() => hiddenSubtreeService.checkHiddenSubtree()),
7 * 3600 * 1000
);
}
setInterval(() => protectedSessionService.checkProtectedSessionExpiration(), 30000);

View File

@@ -3,7 +3,7 @@ import cls from "./cls.js";
import log from "./log.js";
import becca from "../becca/becca.js";
import BNote from "../becca/entities/bnote.js";
import { ApiParams } from './backend_script_api_interface.js';
import { ApiParams } from "./backend_script_api_interface.js";
interface Bundle {
note?: BNote;
@@ -17,13 +17,13 @@ interface Bundle {
type ScriptParams = any[];
function executeNote(note: BNote, apiParams: ApiParams) {
if (!note.isJavaScript() || note.getScriptEnv() !== 'backend' || !note.isContentAvailable()) {
if (!note.isJavaScript() || note.getScriptEnv() !== "backend" || !note.isContentAvailable()) {
log.info(`Cannot execute note ${note.noteId} "${note.title}", note must be of type "Code: JS backend"`);
return;
}
const bundle = getScriptBundle(note, true, 'backend');
const bundle = getScriptBundle(note, true, "backend");
if (!bundle) {
throw new Error("Unable to determine bundle.");
}
@@ -34,8 +34,7 @@ function executeNote(note: BNote, apiParams: ApiParams) {
function executeNoteNoException(note: BNote, apiParams: ApiParams) {
try {
executeNote(note, apiParams);
}
catch (e) {
} catch (e) {
// just swallow, exception is logged already in executeNote
}
}
@@ -46,10 +45,10 @@ function executeBundle(bundle: Bundle, apiParams: ApiParams = {}) {
apiParams.startNote = bundle.note;
}
const originalComponentId = cls.get('componentId');
const originalComponentId = cls.get("componentId");
cls.set('componentId', 'script');
cls.set('bundleNoteId', bundle.note?.noteId);
cls.set("componentId", "script");
cls.set("bundleNoteId", bundle.note?.noteId);
// last \r\n is necessary if the script contains line comment on its last line
const script = `function() {\r
@@ -59,14 +58,12 @@ ${bundle.script}\r
try {
return execute(ctx, script);
}
catch (e: any) {
} catch (e: any) {
log.error(`Execution of script "${bundle.note?.title}" (${bundle.note?.noteId}) failed with error: ${e.message}`);
throw e;
}
finally {
cls.set('componentId', originalComponentId);
} finally {
cls.set("componentId", originalComponentId);
}
}
@@ -90,7 +87,7 @@ function executeScript(script: string, params: ScriptParams, startNoteId: string
// override normal note's content, and it's mime type / script environment
const overrideContent = `return (${script}\r\n)(${getParams(params)})`;
const bundle = getScriptBundle(currentNote, true, 'backend', [], overrideContent);
const bundle = getScriptBundle(currentNote, true, "backend", [], overrideContent);
if (!bundle) {
throw new Error("Unable to determine script bundle.");
}
@@ -99,7 +96,9 @@ function executeScript(script: string, params: ScriptParams, startNoteId: string
}
function execute(ctx: ScriptContext, script: string) {
return function () { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx);
return function () {
return eval(`const apiContext = this;\r\n(${script}\r\n)()`);
}.call(ctx);
}
function getParams(params?: ScriptParams) {
@@ -107,14 +106,15 @@ function getParams(params?: ScriptParams) {
return params;
}
return params.map(p => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13);
}
else {
return JSON.stringify(p);
}
}).join(",");
return params
.map((p) => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13);
} else {
return JSON.stringify(p);
}
})
.join(",");
}
function getScriptBundleForFrontend(note: BNote, script?: string, params?: ScriptParams) {
@@ -124,7 +124,7 @@ function getScriptBundleForFrontend(note: BNote, script?: string, params?: Scrip
overrideContent = `return (${script}\r\n)(${getParams(params)})`;
}
const bundle = getScriptBundle(note, true, 'frontend', [], overrideContent);
const bundle = getScriptBundle(note, true, "frontend", [], overrideContent);
if (!bundle) {
return;
@@ -134,7 +134,7 @@ function getScriptBundleForFrontend(note: BNote, script?: string, params?: Scrip
bundle.noteId = bundle.note?.noteId;
delete bundle.note;
bundle.allNoteIds = bundle.allNotes?.map(note => note.noteId);
bundle.allNoteIds = bundle.allNotes?.map((note) => note.noteId);
delete bundle.allNotes;
return bundle;
@@ -149,18 +149,18 @@ function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: string |
return;
}
if (!root && note.hasOwnedLabel('disableInclusion')) {
if (!root && note.hasOwnedLabel("disableInclusion")) {
return;
}
if (note.type !== 'file' && !root && scriptEnv !== note.getScriptEnv()) {
if (note.type !== "file" && !root && scriptEnv !== note.getScriptEnv()) {
return;
}
const bundle: Bundle = {
note: note,
script: '',
html: '',
script: "",
html: "",
allNotes: [note]
};
@@ -187,24 +187,23 @@ function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: string |
}
}
const moduleNoteIds = modules.map(mod => mod.noteId);
const moduleNoteIds = modules.map((mod) => mod.noteId);
// only frontend scripts are async. Backend cannot be async because of transaction management.
const isFrontend = scriptEnv === 'frontend';
const isFrontend = scriptEnv === "frontend";
if (note.isJavaScript()) {
bundle.script += `
apiContext.modules['${note.noteId}'] = { exports: {} };
${root ? 'return ' : ''}${isFrontend ? 'await' : ''} ((${isFrontend ? 'async' : ''} function(exports, module, require, api${modules.length > 0 ? ', ' : ''}${modules.map(child => sanitizeVariableName(child.title)).join(', ')}) {
${root ? "return " : ""}${isFrontend ? "await" : ""} ((${isFrontend ? "async" : ""} function(exports, module, require, api${modules.length > 0 ? ", " : ""}${modules.map((child) => sanitizeVariableName(child.title)).join(", ")}) {
try {
${overrideContent || note.getContent()};
} catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); }
for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];
return module.exports;
}).call({}, {}, apiContext.modules['${note.noteId}'], apiContext.require(${JSON.stringify(moduleNoteIds)}), apiContext.apis['${note.noteId}']${modules.length > 0 ? ', ' : ''}${modules.map(mod => `apiContext.modules['${mod.noteId}'].exports`).join(', ')}));
}).call({}, {}, apiContext.modules['${note.noteId}'], apiContext.require(${JSON.stringify(moduleNoteIds)}), apiContext.apis['${note.noteId}']${modules.length > 0 ? ", " : ""}${modules.map((mod) => `apiContext.modules['${mod.noteId}'].exports`).join(", ")}));
`;
}
else if (note.isHtml()) {
} else if (note.isHtml()) {
bundle.html += note.getContent();
}

View File

@@ -1,7 +1,7 @@
import { toObject } from "./utils.js";
import BackendScriptApi from "./backend_script_api.js";
import BNote from "../becca/entities/bnote.js";
import { ApiParams } from './backend_script_api_interface.js';
import { ApiParams } from "./backend_script_api_interface.js";
type Module = {
exports: any[];
@@ -16,22 +16,22 @@ class ScriptContext {
constructor(allNotes: BNote[], apiParams: ApiParams) {
this.allNotes = allNotes;
this.modules = {};
this.notes = toObject(allNotes, note => [note.noteId, note]);
this.apis = toObject(allNotes, note => [note.noteId, new BackendScriptApi(note, apiParams)]);
this.notes = toObject(allNotes, (note) => [note.noteId, note]);
this.apis = toObject(allNotes, (note) => [note.noteId, new BackendScriptApi(note, apiParams)]);
}
require(moduleNoteIds: string[]) {
return (moduleName: string) => {
const candidates = this.allNotes.filter(note => moduleNoteIds.includes(note.noteId));
const note = candidates.find(c => c.title === moduleName);
const candidates = this.allNotes.filter((note) => moduleNoteIds.includes(note.noteId));
const note = candidates.find((c) => c.title === moduleName);
if (!note) {
return require(moduleName);
}
return this.modules[note.noteId].exports;
}
};
};
}
}
export default ScriptContext;

View File

@@ -7,10 +7,9 @@ import becca from "../../../becca/becca.js";
import SearchContext from "../search_context.js";
class AncestorExp extends Expression {
private ancestorNoteId: string;
private ancestorDepthComparator;
ancestorDepth?: string;
constructor(ancestorNoteId: string, ancestorDepth?: string) {
@@ -59,15 +58,12 @@ class AncestorExp extends Expression {
const comparedDepth = parseInt(depthCondition.substr(2));
if (depthCondition.startsWith("eq")) {
return depth => depth === comparedDepth;
}
else if (depthCondition.startsWith("gt")) {
return depth => depth > comparedDepth;
}
else if (depthCondition.startsWith("lt")) {
return depth => depth < comparedDepth;
}
else {
return (depth) => depth === comparedDepth;
} else if (depthCondition.startsWith("gt")) {
return (depth) => depth > comparedDepth;
} else if (depthCondition.startsWith("lt")) {
return (depth) => depth < comparedDepth;
} else {
log.error(`Unrecognized depth condition value ${depthCondition}`);
return null;
}

View File

@@ -7,7 +7,6 @@ import becca from "../../../becca/becca.js";
import Expression from "./expression.js";
class AttributeExistsExp extends Expression {
private attributeType: string;
private attributeName: string;
private isTemplateLabel: boolean;
@@ -19,14 +18,12 @@ class AttributeExistsExp extends Expression {
this.attributeType = attributeType;
this.attributeName = attributeName;
// template attr is used as a marker for templates, but it's not meant to be inherited
this.isTemplateLabel = this.attributeType === 'label' && (this.attributeName === 'template' || this.attributeName === 'workspacetemplate');
this.isTemplateLabel = this.attributeType === "label" && (this.attributeName === "template" || this.attributeName === "workspacetemplate");
this.prefixMatch = prefixMatch;
}
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const attrs = this.prefixMatch
? becca.findAttributesWithPrefix(this.attributeType, this.attributeName)
: becca.findAttributes(this.attributeType, this.attributeName);
const attrs = this.prefixMatch ? becca.findAttributesWithPrefix(this.attributeType, this.attributeName) : becca.findAttributes(this.attributeType, this.attributeName);
const resultNoteSet = new NoteSet();
@@ -35,11 +32,9 @@ class AttributeExistsExp extends Expression {
if (attr.isInheritable && !this.isTemplateLabel) {
resultNoteSet.addAll(note.getSubtreeNotesIncludingTemplated());
}
else if (note.isInherited() && !this.isTemplateLabel) {
} else if (note.isInherited() && !this.isTemplateLabel) {
resultNoteSet.addAll(note.getInheritingNotes());
}
else {
} else {
resultNoteSet.add(note);
}
}

View File

@@ -5,7 +5,6 @@ import NoteSet from "../note_set.js";
import SearchContext from "../search_context.js";
class ChildOfExp extends Expression {
private subExpression: Expression;
constructor(subExpression: Expression) {

View File

@@ -8,7 +8,6 @@ import SearchContext from "../search_context.js";
type Comparator = (value: string) => boolean;
class LabelComparisonExp extends Expression {
private attributeType: string;
private attributeName: string;
private comparator: Comparator;
@@ -32,11 +31,9 @@ class LabelComparisonExp extends Expression {
if (inputNoteSet.hasNoteId(note.noteId) && this.comparator(value)) {
if (attr.isInheritable) {
resultNoteSet.addAll(note.getSubtreeNotesIncludingTemplated());
}
else if (note.isInherited()) {
} else if (note.isInherited()) {
resultNoteSet.addAll(note.getInheritingNotes());
}
else {
} else {
resultNoteSet.add(note);
}
}

View File

@@ -12,14 +12,13 @@ import striptags from "striptags";
import { normalize } from "../../utils.js";
import sql from "../../sql.js";
const ALLOWED_OPERATORS = new Set(['=', '!=', '*=*', '*=', '=*', '%=']);
const ALLOWED_OPERATORS = new Set(["=", "!=", "*=*", "*=", "=*", "%="]);
const cachedRegexes: Record<string, RegExp> = {};
function getRegex(str: string): RegExp {
if (!(str in cachedRegexes)) {
cachedRegexes[str] = new RegExp(str, 'ms'); // multiline, dot-all
cachedRegexes[str] = new RegExp(str, "ms"); // multiline, dot-all
}
return cachedRegexes[str];
@@ -34,13 +33,12 @@ interface ConstructorOpts {
type SearchRow = Pick<NoteRow, "noteId" | "type" | "mime" | "content" | "isProtected">;
class NoteContentFulltextExp extends Expression {
private operator: string;
private tokens: string[];
private raw: boolean;
private flatText: boolean;
constructor(operator: string, {tokens, raw, flatText}: ConstructorOpts) {
constructor(operator: string, { tokens, raw, flatText }: ConstructorOpts) {
super();
this.operator = operator;
@@ -57,19 +55,18 @@ class NoteContentFulltextExp extends Expression {
}
const resultNoteSet = new NoteSet();
for (const row of sql.iterateRows<SearchRow>(`
SELECT noteId, type, mime, content, isProtected
FROM notes JOIN blobs USING (blobId)
WHERE type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap') AND isDeleted = 0`)) {
this.findInText(row, inputNoteSet, resultNoteSet);
}
return resultNoteSet;
}
findInText({noteId, isProtected, content, type, mime}: SearchRow, inputNoteSet: NoteSet, resultNoteSet: NoteSet) {
findInText({ noteId, isProtected, content, type, mime }: SearchRow, inputNoteSet: NoteSet, resultNoteSet: NoteSet) {
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) {
return;
}
@@ -96,24 +93,23 @@ class NoteContentFulltextExp extends Expression {
if (this.tokens.length === 1) {
const [token] = this.tokens;
if ((this.operator === '=' && token === content)
|| (this.operator === '!=' && token !== content)
|| (this.operator === '*=' && content.endsWith(token))
|| (this.operator === '=*' && content.startsWith(token))
|| (this.operator === '*=*' && content.includes(token))
|| (this.operator === '%=' && getRegex(token).test(content))) {
if (
(this.operator === "=" && token === content) ||
(this.operator === "!=" && token !== content) ||
(this.operator === "*=" && content.endsWith(token)) ||
(this.operator === "=*" && content.startsWith(token)) ||
(this.operator === "*=*" && content.includes(token)) ||
(this.operator === "%=" && getRegex(token).test(content))
) {
resultNoteSet.add(becca.notes[noteId]);
}
} else {
const nonMatchingToken = this.tokens.find(token =>
!content?.includes(token) &&
(
const nonMatchingToken = this.tokens.find(
(token) =>
!content?.includes(token) &&
// in case of default fulltext search, we should consider both title, attrs and content
// so e.g. "hello world" should match when "hello" is in title and "world" in content
!this.flatText
|| !becca.notes[noteId].getFlatText().includes(token)
)
(!this.flatText || !becca.notes[noteId].getFlatText().includes(token))
);
if (!nonMatchingToken) {
@@ -127,18 +123,17 @@ class NoteContentFulltextExp extends Expression {
preprocessContent(content: string | Buffer, type: string, mime: string) {
content = normalize(content.toString());
if (type === 'text' && mime === 'text/html') {
if (!this.raw && content.length < 20000) { // striptags is slow for very large notes
if (type === "text" && mime === "text/html") {
if (!this.raw && content.length < 20000) {
// striptags is slow for very large notes
content = this.stripTags(content);
}
content = content.replace(/&nbsp;/g, ' ');
}
else if (type === 'mindMap' && mime === 'application/json') {
let mindMapcontent = JSON.parse (content);
content = content.replace(/&nbsp;/g, " ");
} else if (type === "mindMap" && mime === "application/json") {
let mindMapcontent = JSON.parse(content);
// Define interfaces for the JSON structure
// Define interfaces for the JSON structure
interface MindmapNode {
id: string;
topic: string;
@@ -146,62 +141,58 @@ class NoteContentFulltextExp extends Expression {
direction?: number;
expanded?: boolean;
}
interface MindmapData {
nodedata: MindmapNode;
arrows: any[]; // If you know the structure, replace `any` with the correct type
summaries: any[];
direction: number;
theme: {
name: string;
type: string;
palette: string[];
cssvar: Record<string, string>; // Object with string keys and string values
name: string;
type: string;
palette: string[];
cssvar: Record<string, string>; // Object with string keys and string values
};
}
// Recursive function to collect all topics
function collectTopics(node: MindmapNode): string[] {
// Recursive function to collect all topics
function collectTopics(node: MindmapNode): string[] {
// Collect the current node's topic
let topics = [node.topic];
// If the node has children, collect topics recursively
if (node.children && node.children.length > 0) {
for (const child of node.children) {
topics = topics.concat(collectTopics(child));
for (const child of node.children) {
topics = topics.concat(collectTopics(child));
}
}
}
return topics;
}
// Start extracting from the root node
const topicsArray = collectTopics(mindMapcontent.nodedata);
// Combine topics into a single string
const topicsString = topicsArray.join(", ");
content = normalize(topicsString.toString());
}
else if (type === 'canvas' && mime === 'application/json') {
} else if (type === "canvas" && mime === "application/json") {
interface Element {
type: string;
text?: string; // Optional since not all objects have a `text` property
id: string;
[key: string]: any; // Other properties that may exist
}
let canvasContent = JSON.parse (content);
const elements: Element [] = canvasContent.elements;
let canvasContent = JSON.parse(content);
const elements: Element[] = canvasContent.elements;
const texts = elements
.filter((element: Element) => element.type === 'text' && element.text) // Filter for 'text' type elements with a 'text' property
.filter((element: Element) => element.type === "text" && element.text) // Filter for 'text' type elements with a 'text' property
.map((element: Element) => element.text!); // Use `!` to assert `text` is defined after filtering
content = normalize(texts.toString())
}
content = normalize(texts.toString());
}
return content.trim();
}
@@ -210,17 +201,17 @@ class NoteContentFulltextExp extends Expression {
// we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
// we want to insert space in place of block tags (because they imply text separation)
// but we don't want to insert text for typical formatting inline tags which can occur within one word
const linkTag = 'a';
const inlineFormattingTags = ['b', 'strong', 'em', 'i', 'span', 'big', 'small', 'font', 'sub', 'sup'];
const linkTag = "a";
const inlineFormattingTags = ["b", "strong", "em", "i", "span", "big", "small", "font", "sub", "sup"];
// replace tags which imply text separation with a space
content = striptags(content, [linkTag, ...inlineFormattingTags], ' ');
content = striptags(content, [linkTag, ...inlineFormattingTags], " ");
// replace the inline formatting tags (but not links) without a space
content = striptags(content, [linkTag], '');
content = striptags(content, [linkTag], "");
// at least the closing link tag can be easily stripped
return content.replace(/<\/a>/ig, "");
return content.replace(/<\/a>/gi, "");
}
}

View File

@@ -47,7 +47,7 @@ class NoteFlatTextExp extends Expression {
return;
}
if (note.parents.length === 0 || note.noteId === 'root') {
if (note.parents.length === 0 || note.noteId === "root") {
// we've reached root, but there are still remaining tokens -> this candidate note produced no result
return;
}
@@ -82,15 +82,14 @@ class NoteFlatTextExp extends Expression {
}
if (foundTokens.length > 0) {
const newRemainingTokens = remainingTokens.filter(token => !foundTokens.includes(token));
const newRemainingTokens = remainingTokens.filter((token) => !foundTokens.includes(token));
searchPathTowardsRoot(parentNote, newRemainingTokens, [note.noteId, ...takenPath]);
}
else {
} else {
searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId, ...takenPath]);
}
}
}
};
const candidateNotes = this.getCandidateNotes(inputNoteSet);
@@ -109,9 +108,7 @@ class NoteFlatTextExp extends Expression {
}
for (const attribute of note.ownedAttributes) {
if (normalize(attribute.name).includes(token)
|| normalize(attribute.value).includes(token)) {
if (normalize(attribute.name).includes(token) || normalize(attribute.value).includes(token)) {
foundAttrTokens.push(token);
}
}
@@ -128,7 +125,7 @@ class NoteFlatTextExp extends Expression {
}
if (foundTokens.length > 0) {
const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token));
const remainingTokens = this.tokens.filter((token) => !foundTokens.includes(token));
searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId]);
}

View File

@@ -9,15 +9,13 @@ class OrExp extends Expression {
private subExpressions: Expression[];
static of(subExpressions: Expression[]) {
subExpressions = subExpressions.filter(exp => !!exp);
subExpressions = subExpressions.filter((exp) => !!exp);
if (subExpressions.length === 1) {
return subExpressions[0];
}
else if (subExpressions.length > 0) {
} else if (subExpressions.length > 0) {
return new OrExp(subExpressions);
}
else {
} else {
return new TrueExp();
}
}

View File

@@ -17,7 +17,6 @@ interface OrderDefinition {
}
class OrderByAndLimitExp extends Expression {
private orderDefinitions: OrderDefinition[];
private limit: number;
subExpression: Expression | null;
@@ -42,10 +41,10 @@ class OrderByAndLimitExp extends Expression {
throw new Error("Missing subexpression");
}
let {notes} = this.subExpression.execute(inputNoteSet, executionContext, searchContext);
let { notes } = this.subExpression.execute(inputNoteSet, executionContext, searchContext);
notes.sort((a, b) => {
for (const {valueExtractor, smaller, larger} of this.orderDefinitions) {
for (const { valueExtractor, smaller, larger } of this.orderDefinitions) {
let valA: string | number | Date | null = valueExtractor.extract(a);
let valB: string | number | Date | null = valueExtractor.extract(b);
@@ -60,25 +59,20 @@ class OrderByAndLimitExp extends Expression {
if (valA === null && valB === null) {
// neither has attribute at all
continue;
}
else if (valB === null) {
} else if (valB === null) {
return smaller;
}
else if (valA === null) {
} else if (valA === null) {
return larger;
}
// if both are dates, then parse them for dates comparison
if (typeof valA === "string" && this.isDate(valA) &&
typeof valB === "string" && this.isDate(valB)) {
if (typeof valA === "string" && this.isDate(valA) && typeof valB === "string" && this.isDate(valB)) {
valA = new Date(valA);
valB = new Date(valB);
}
// if both are numbers, then parse them for numerical comparison
else if (typeof valA === "string" && this.isNumber(valA) &&
typeof valB === "string" && this.isNumber(valB)) {
else if (typeof valA === "string" && this.isNumber(valA) && typeof valB === "string" && this.isNumber(valB)) {
valA = parseFloat(valA);
valB = parseFloat(valB);
}
@@ -108,13 +102,13 @@ class OrderByAndLimitExp extends Expression {
}
isDate(date: number | string) {
return !isNaN(new Date(date).getTime());
return !isNaN(new Date(date).getTime());
}
isNumber(x: number | string) {
if (typeof x === 'number') {
if (typeof x === "number") {
return true;
} else if (typeof x === 'string') {
} else if (typeof x === "string") {
// isNaN will return false for blank string
return x.trim() !== "" && !isNaN(parseInt(x, 10));
} else {

View File

@@ -9,31 +9,31 @@ import buildComparator from "../services/build_comparator.js";
* we need the case-sensitive form, so we have this translation object.
*/
const PROP_MAPPING: Record<string, string> = {
"noteid": "noteId",
"title": "title",
"type": "type",
"mime": "mime",
"isprotected": "isProtected",
"isarchived": "isArchived",
"datecreated": "dateCreated",
"datemodified": "dateModified",
"utcdatecreated": "utcDateCreated",
"utcdatemodified": "utcDateModified",
"parentcount": "parentCount",
"childrencount": "childrenCount",
"attributecount": "attributeCount",
"labelcount": "labelCount",
"ownedlabelcount": "ownedLabelCount",
"relationcount": "relationCount",
"ownedrelationcount": "ownedRelationCount",
"relationcountincludinglinks": "relationCountIncludingLinks",
"ownedrelationcountincludinglinks": "ownedRelationCountIncludingLinks",
"targetrelationcount": "targetRelationCount",
"targetrelationcountincludinglinks": "targetRelationCountIncludingLinks",
"contentsize": "contentSize",
"contentandattachmentssize": "contentAndAttachmentsSize",
"contentandattachmentsandrevisionssize": "contentAndAttachmentsAndRevisionsSize",
"revisioncount": "revisionCount"
noteid: "noteId",
title: "title",
type: "type",
mime: "mime",
isprotected: "isProtected",
isarchived: "isArchived",
datecreated: "dateCreated",
datemodified: "dateModified",
utcdatecreated: "utcDateCreated",
utcdatemodified: "utcDateModified",
parentcount: "parentCount",
childrencount: "childrenCount",
attributecount: "attributeCount",
labelcount: "labelCount",
ownedlabelcount: "ownedLabelCount",
relationcount: "relationCount",
ownedrelationcount: "ownedRelationCount",
relationcountincludinglinks: "relationCountIncludingLinks",
ownedrelationcountincludinglinks: "ownedRelationCountIncludingLinks",
targetrelationcount: "targetRelationCount",
targetrelationcountincludinglinks: "targetRelationCountIncludingLinks",
contentsize: "contentSize",
contentandattachmentssize: "contentAndAttachmentsSize",
contentandattachmentsandrevisionssize: "contentAndAttachmentsAndRevisionsSize",
revisioncount: "revisionCount"
};
interface SearchContext {
@@ -41,7 +41,6 @@ interface SearchContext {
}
class PropertyComparisonExp extends Expression {
private propertyName: string;
private operator: string;
private comparedValue: string;
@@ -59,7 +58,7 @@ class PropertyComparisonExp extends Expression {
this.comparedValue = comparedValue; // for DEBUG mode
this.comparator = buildComparator(operator, comparedValue);
if (['contentsize', 'contentandattachmentssize', 'contentandattachmentsandrevisionssize', 'revisioncount'].includes(this.propertyName)) {
if (["contentsize", "contentandattachmentssize", "contentandattachmentsandrevisionssize", "revisioncount"].includes(this.propertyName)) {
searchContext.dbLoadNeeded = true;
}
}
@@ -70,7 +69,7 @@ class PropertyComparisonExp extends Expression {
for (const note of inputNoteSet.notes) {
let value = (note as any)[this.propertyName];
if (value !== undefined && value !== null && typeof value !== 'string') {
if (value !== undefined && value !== null && typeof value !== "string") {
value = value.toString();
}

View File

@@ -19,7 +19,7 @@ class RelationWhereExp extends Expression {
execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext) {
const candidateNoteSet = new NoteSet();
for (const attr of becca.findAttributes('relation', this.relationName)) {
for (const attr of becca.findAttributes("relation", this.relationName)) {
const note = attr.note;
if (inputNoteSet.hasNoteId(note.noteId) && attr.targetNote) {

View File

@@ -3,7 +3,6 @@
import BNote from "../../becca/entities/bnote.js";
class NoteSet {
private noteIdSet: Set<string>;
notes: BNote[];
@@ -11,7 +10,7 @@ class NoteSet {
constructor(notes: BNote[] = []) {
this.notes = notes;
this.noteIdSet = new Set(notes.map(note => note.noteId));
this.noteIdSet = new Set(notes.map((note) => note.noteId));
this.sorted = false;
}

View File

@@ -1,10 +1,9 @@
"use strict";
import hoistedNoteService from "../hoisted_note.js";
import { SearchParams } from './services/types.js';
import { SearchParams } from "./services/types.js";
class SearchContext {
fastSearch: boolean;
includeArchivedNotes: boolean;
includeHiddenNotes: boolean;

View File

@@ -16,7 +16,7 @@ class SearchResult {
}
get notePath() {
return this.notePathArray.join('/');
return this.notePathArray.join("/");
}
get noteId() {
@@ -38,18 +38,14 @@ class SearchResult {
// Title matching scores, make sure to always win
if (normalizedTitle === normalizedQuery) {
this.score += 2000; // Increased from 1000 to ensure exact matches always win
}
else if (normalizedTitle.startsWith(normalizedQuery)) {
this.score += 500; // Increased to give more weight to prefix matches
}
else if (normalizedTitle.includes(` ${normalizedQuery} `) ||
normalizedTitle.startsWith(`${normalizedQuery} `) ||
normalizedTitle.endsWith(` ${normalizedQuery}`)) {
this.score += 300; // Increased to better distinguish word matches
} else if (normalizedTitle.startsWith(normalizedQuery)) {
this.score += 500; // Increased to give more weight to prefix matches
} else if (normalizedTitle.includes(` ${normalizedQuery} `) || normalizedTitle.startsWith(`${normalizedQuery} `) || normalizedTitle.endsWith(` ${normalizedQuery}`)) {
this.score += 300; // Increased to better distinguish word matches
}
// Add scores for partial matches with adjusted weights
this.addScoreForStrings(tokens, note.title, 2.0); // Increased to give more weight to title matches
this.addScoreForStrings(tokens, note.title, 2.0); // Increased to give more weight to title matches
this.addScoreForStrings(tokens, this.notePathTitle, 0.3); // Reduced to further de-emphasize path matches
if (note.isInHiddenSubtree()) {

View File

@@ -8,26 +8,26 @@ function getRegex(str: string) {
return cachedRegexes[str];
}
type Comparator<T> = (comparedValue: T) => ((val: string) => boolean);
type Comparator<T> = (comparedValue: T) => (val: string) => boolean;
const stringComparators: Record<string, Comparator<string>> = {
"=": comparedValue => (val => val === comparedValue),
"!=": comparedValue => (val => val !== comparedValue),
">": comparedValue => (val => val > comparedValue),
">=": comparedValue => (val => val >= comparedValue),
"<": comparedValue => (val => val < comparedValue),
"<=": comparedValue => (val => val <= comparedValue),
"*=": comparedValue => (val => !!val && val.endsWith(comparedValue)),
"=*": comparedValue => (val => !!val && val.startsWith(comparedValue)),
"*=*": comparedValue => (val => !!val && val.includes(comparedValue)),
"%=": comparedValue => (val => !!val && !!getRegex(comparedValue).test(val)),
"=": (comparedValue) => (val) => val === comparedValue,
"!=": (comparedValue) => (val) => val !== comparedValue,
">": (comparedValue) => (val) => val > comparedValue,
">=": (comparedValue) => (val) => val >= comparedValue,
"<": (comparedValue) => (val) => val < comparedValue,
"<=": (comparedValue) => (val) => val <= comparedValue,
"*=": (comparedValue) => (val) => !!val && val.endsWith(comparedValue),
"=*": (comparedValue) => (val) => !!val && val.startsWith(comparedValue),
"*=*": (comparedValue) => (val) => !!val && val.includes(comparedValue),
"%=": (comparedValue) => (val) => !!val && !!getRegex(comparedValue).test(val)
};
const numericComparators: Record<string, Comparator<number>> = {
">": comparedValue => (val => parseFloat(val) > comparedValue),
">=": comparedValue => (val => parseFloat(val) >= comparedValue),
"<": comparedValue => (val => parseFloat(val) < comparedValue),
"<=": comparedValue => (val => parseFloat(val) <= comparedValue)
">": (comparedValue) => (val) => parseFloat(val) > comparedValue,
">=": (comparedValue) => (val) => parseFloat(val) >= comparedValue,
"<": (comparedValue) => (val) => parseFloat(val) < comparedValue,
"<=": (comparedValue) => (val) => parseFloat(val) <= comparedValue
};
function buildComparator(operator: string, comparedValue: string) {

View File

@@ -9,28 +9,28 @@ function handleParens(tokens: TokenStructure) {
}
while (true) {
const leftIdx = tokens.findIndex(token => "token" in token && token.token === '(');
const leftIdx = tokens.findIndex((token) => "token" in token && token.token === "(");
if (leftIdx === -1) {
return tokens;
}
let rightIdx;
let parensLevel = 0
let parensLevel = 0;
for (rightIdx = leftIdx; rightIdx < tokens.length; rightIdx++) {
const token = tokens[rightIdx];
if (!("token" in token)) {
continue;
}
if (token.token === ')') {
if (token.token === ")") {
parensLevel--;
if (parensLevel === 0) {
break;
}
} else if (token.token === '(') {
} else if (token.token === "(") {
parensLevel++;
}
}
@@ -39,11 +39,7 @@ function handleParens(tokens: TokenStructure) {
throw new Error("Did not find matching right parenthesis.");
}
tokens = [
...tokens.slice(0, leftIdx),
handleParens(tokens.slice(leftIdx + 1, rightIdx)),
...tokens.slice(rightIdx + 1)
] as (TokenData | TokenData[])[];
tokens = [...tokens.slice(0, leftIdx), handleParens(tokens.slice(leftIdx + 1, rightIdx)), ...tokens.slice(rightIdx + 1)] as (TokenData | TokenData[])[];
}
}

View File

@@ -9,23 +9,22 @@ function lex(str: string) {
let quotes: boolean | string = false; // otherwise contains used quote - ', " or `
let fulltextEnded = false;
let currentWord = '';
let currentWord = "";
function isSymbolAnOperator(chr: string) {
return ['=', '*', '>', '<', '!', "-", "+", '%', ','].includes(chr);
return ["=", "*", ">", "<", "!", "-", "+", "%", ","].includes(chr);
}
function isPreviousSymbolAnOperator() {
if (currentWord.length === 0) {
return false;
}
else {
} else {
return isSymbolAnOperator(currentWord[currentWord.length - 1]);
}
}
function finishWord(endIndex: number, createAlsoForEmptyWords = false) {
if (currentWord === '' && !createAlsoForEmptyWords) {
if (currentWord === "" && !createAlsoForEmptyWords) {
return;
}
@@ -44,84 +43,70 @@ function lex(str: string) {
fulltextQuery = str.substr(0, endIndex + 1);
}
currentWord = '';
currentWord = "";
}
for (let i = 0; i < str.length; i++) {
const chr = str[i];
if (chr === '\\') {
if (chr === "\\") {
if (i + 1 < str.length) {
i++;
currentWord += str[i];
}
else {
} else {
currentWord += chr;
}
continue;
}
else if (['"', "'", '`'].includes(chr)) {
} else if (['"', "'", "`"].includes(chr)) {
if (!quotes) {
if (currentWord.length === 0 || isPreviousSymbolAnOperator()) {
finishWord(i - 1);
quotes = chr;
}
else {
} else {
// quote inside a word does not have special meening and does not break word
// e.g. d'Artagnan is kept as a single token
currentWord += chr;
}
}
else if (quotes === chr) {
} else if (quotes === chr) {
finishWord(i - 1, true);
quotes = false;
}
else {
} else {
// it's a quote, but within other kind of quotes, so it's valid as a literal character
currentWord += chr;
}
continue;
}
else if (!quotes) {
if (!fulltextEnded && currentWord === 'note' && chr === '.' && i + 1 < str.length) {
} else if (!quotes) {
if (!fulltextEnded && currentWord === "note" && chr === "." && i + 1 < str.length) {
fulltextEnded = true;
}
if (chr === '#' || chr === '~') {
if (chr === "#" || chr === "~") {
if (!fulltextEnded) {
fulltextEnded = true;
}
else {
} else {
finishWord(i - 1);
}
currentWord = chr;
continue;
}
else if (['#', '~'].includes(currentWord) && chr === '!') {
} else if (["#", "~"].includes(currentWord) && chr === "!") {
currentWord += chr;
continue;
}
else if (chr === ' ') {
} else if (chr === " ") {
finishWord(i - 1);
continue;
}
else if (fulltextEnded && ['(', ')', '.'].includes(chr)) {
} else if (fulltextEnded && ["(", ")", "."].includes(chr)) {
finishWord(i - 1);
currentWord += chr;
finishWord(i);
continue;
}
else if (fulltextEnded
&& !['#!', '~!'].includes(currentWord)
&& isPreviousSymbolAnOperator() !== isSymbolAnOperator(chr)) {
} else if (fulltextEnded && !["#!", "~!"].includes(currentWord) && isPreviousSymbolAnOperator() !== isSymbolAnOperator(chr)) {
finishWord(i - 1);
currentWord += chr;
@@ -129,7 +114,7 @@ function lex(str: string) {
}
}
if (chr === ',') {
if (chr === ",") {
continue;
}
@@ -144,7 +129,7 @@ function lex(str: string) {
fulltextQuery,
fulltextTokens,
expressionTokens
}
};
}
export default lex;

View File

@@ -25,7 +25,7 @@ import { TokenData, TokenStructure } from "./types.js";
import Expression from "../expressions/expression.js";
function getFulltext(_tokens: TokenData[], searchContext: SearchContext) {
const tokens: string[] = _tokens.map(t => removeDiacritic(t.token));
const tokens: string[] = _tokens.map((t) => removeDiacritic(t.token));
searchContext.highlightedTokens.push(...tokens);
@@ -34,28 +34,13 @@ function getFulltext(_tokens: TokenData[], searchContext: SearchContext) {
}
if (!searchContext.fastSearch) {
return new OrExp([
new NoteFlatTextExp(tokens),
new NoteContentFulltextExp('*=*', {tokens, flatText: true})
]);
}
else {
return new OrExp([new NoteFlatTextExp(tokens), new NoteContentFulltextExp("*=*", { tokens, flatText: true })]);
} else {
return new NoteFlatTextExp(tokens);
}
}
const OPERATORS = new Set([
"=",
"!=",
"*=*",
"*=",
"=*",
">",
">=",
"<",
"<=",
"%="
]);
const OPERATORS = new Set(["=", "!=", "*=*", "*=", "=*", ">", ">=", "<", "<=", "%="]);
function isOperator(token: TokenData) {
if (Array.isArray(token)) {
@@ -76,7 +61,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
let i: number;
function context(i: number) {
let {startIndex, endIndex} = tokens[i];
let { startIndex, endIndex } = tokens[i];
startIndex = Math.max(0, (startIndex || 0) - 20);
endIndex = Math.min(searchContext.originalQuery.length, (endIndex || Number.MAX_SAFE_INTEGER) + 20);
@@ -86,8 +71,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
const resolveConstantOperand = () => {
const operand = tokens[i];
if (!operand.inQuotes
&& (operand.token.startsWith('#') || operand.token.startsWith('~') || operand.token === 'note')) {
if (!operand.inQuotes && (operand.token.startsWith("#") || operand.token.startsWith("~") || operand.token === "note")) {
searchContext.addError(`Error near token "${operand.token}" in ${context(i)}, it's possible to compare with constant only.`);
return null;
}
@@ -99,12 +83,11 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
let delta = 0;
if (i + 2 < tokens.length) {
if (tokens[i + 1].token === '+') {
if (tokens[i + 1].token === "+") {
i += 2;
delta += parseInt(tokens[i].token);
}
else if (tokens[i + 1].token === '-') {
} else if (tokens[i + 1].token === "-") {
i += 2;
delta -= parseInt(tokens[i].token);
@@ -113,39 +96,35 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
let format, date;
if (operand.token === 'now') {
date = dayjs().add(delta, 'second');
if (operand.token === "now") {
date = dayjs().add(delta, "second");
format = "YYYY-MM-DD HH:mm:ss";
}
else if (operand.token === 'today') {
date = dayjs().add(delta, 'day');
} else if (operand.token === "today") {
date = dayjs().add(delta, "day");
format = "YYYY-MM-DD";
}
else if (operand.token === 'month') {
date = dayjs().add(delta, 'month');
} else if (operand.token === "month") {
date = dayjs().add(delta, "month");
format = "YYYY-MM";
}
else if (operand.token === 'year') {
date = dayjs().add(delta, 'year');
} else if (operand.token === "year") {
date = dayjs().add(delta, "year");
format = "YYYY";
}
else {
} else {
throw new Error(`Unrecognized keyword: ${operand.token}`);
}
return date.format(format);
}
};
const parseNoteProperty: () => Expression | undefined | null = () => {
if (tokens[i].token !== '.') {
if (tokens[i].token !== ".") {
searchContext.addError('Expected "." to separate field path');
return;
}
i++;
if (['content', 'rawcontent'].includes(tokens[i].token)) {
const raw = tokens[i].token === 'rawcontent';
if (["content", "rawcontent"].includes(tokens[i].token)) {
const raw = tokens[i].token === "rawcontent";
i += 1;
@@ -158,35 +137,41 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
i++;
return new NoteContentFulltextExp(operator.token, {tokens: [tokens[i].token], raw});
return new NoteContentFulltextExp(operator.token, { tokens: [tokens[i].token], raw });
}
if (tokens[i].token === 'parents') {
if (tokens[i].token === "parents") {
i += 1;
const expression = parseNoteProperty();
if (!expression) { return; }
if (!expression) {
return;
}
return new ChildOfExp(expression);
}
if (tokens[i].token === 'children') {
if (tokens[i].token === "children") {
i += 1;
const expression = parseNoteProperty();
if (!expression) { return; }
if (!expression) {
return;
}
return new ParentOfExp(expression);
}
if (tokens[i].token === 'ancestors') {
if (tokens[i].token === "ancestors") {
i += 1;
const expression = parseNoteProperty();
if (!expression) { return; }
if (!expression) {
return;
}
return new DescendantOfExp(expression);
}
if (tokens[i].token === 'labels') {
if (tokens[i + 1].token !== '.') {
if (tokens[i].token === "labels") {
if (tokens[i + 1].token !== ".") {
searchContext.addError(`Expected "." to separate field path, got "${tokens[i + 1].token}" in ${context(i)}`);
return;
}
@@ -196,8 +181,8 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
return parseLabel(tokens[i].token);
}
if (tokens[i].token === 'relations') {
if (tokens[i + 1].token !== '.') {
if (tokens[i].token === "relations") {
if (tokens[i + 1].token !== ".") {
searchContext.addError(`Expected "." to separate field path, got "${tokens[i + 1].token}" in ${context(i)}`);
return;
}
@@ -207,18 +192,15 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
return parseRelation(tokens[i].token);
}
if (tokens[i].token === 'text') {
if (tokens[i + 1].token !== '*=*') {
if (tokens[i].token === "text") {
if (tokens[i + 1].token !== "*=*") {
searchContext.addError(`Virtual attribute "note.text" supports only *=* operator, instead given "${tokens[i + 1].token}" in ${context(i)}`);
return;
}
i += 2;
return new OrExp([
new PropertyComparisonExp(searchContext, 'title', '*=*', tokens[i].token),
new NoteContentFulltextExp('*=*', {tokens: [tokens[i].token]})
]);
return new OrExp([new PropertyComparisonExp(searchContext, "title", "*=*", tokens[i].token), new NoteContentFulltextExp("*=*", { tokens: [tokens[i].token] })]);
}
if (PropertyComparisonExp.isProperty(tokens[i].token)) {
@@ -237,14 +219,14 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
}
searchContext.addError(`Unrecognized note property "${tokens[i].token}" in ${context(i)}`);
}
};
function parseAttribute(name: string) {
const isLabel = name.startsWith('#');
const isLabel = name.startsWith("#");
name = name.substr(1);
const isNegated = name.startsWith('!');
const isNegated = name.startsWith("!");
if (isNegated) {
name = name.substr(1);
@@ -271,8 +253,8 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
searchContext.highlightedTokens.push(comparedValue);
if (searchContext.fuzzyAttributeSearch && operator === '=') {
operator = '*=*';
if (searchContext.fuzzyAttributeSearch && operator === "=") {
operator = "*=*";
}
const comparator = buildComparator(operator, comparedValue);
@@ -280,41 +262,41 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
if (!comparator) {
searchContext.addError(`Can't find operator '${operator}' in ${context(i - 1)}`);
} else {
return new LabelComparisonExp('label', labelName, comparator);
return new LabelComparisonExp("label", labelName, comparator);
}
} else {
return new AttributeExistsExp('label', labelName, searchContext.fuzzyAttributeSearch);
return new AttributeExistsExp("label", labelName, searchContext.fuzzyAttributeSearch);
}
}
function parseRelation(relationName: string) {
searchContext.highlightedTokens.push(relationName);
if (i < tokens.length - 2 && tokens[i + 1].token === '.') {
if (i < tokens.length - 2 && tokens[i + 1].token === ".") {
i += 1;
const expression = parseNoteProperty();
if (!expression) { return; }
if (!expression) {
return;
}
return new RelationWhereExp(relationName, expression);
}
else if (i < tokens.length - 2 && isOperator(tokens[i + 1])) {
} else if (i < tokens.length - 2 && isOperator(tokens[i + 1])) {
searchContext.addError(`Relation can be compared only with property, e.g. ~relation.title=hello in ${context(i)}`);
return null;
}
else {
return new AttributeExistsExp('relation', relationName, searchContext.fuzzyAttributeSearch);
} else {
return new AttributeExistsExp("relation", relationName, searchContext.fuzzyAttributeSearch);
}
}
function parseOrderByAndLimit() {
const orderDefinitions: {
valueExtractor: ValueExtractor,
direction: string
valueExtractor: ValueExtractor;
direction: string;
}[] = [];
let limit;
if (tokens[i].token === 'orderby') {
if (tokens[i].token === "orderby") {
do {
const propertyPath = [];
let direction = "asc";
@@ -325,7 +307,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
propertyPath.push(tokens[i].token);
i++;
} while (i < tokens.length && tokens[i].token === '.');
} while (i < tokens.length && tokens[i].token === ".");
if (i < tokens.length && ["asc", "desc"].includes(tokens[i].token)) {
direction = tokens[i].token;
@@ -343,10 +325,10 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
valueExtractor,
direction
});
} while (i < tokens.length && tokens[i].token === ',');
} while (i < tokens.length && tokens[i].token === ",");
}
if (i < tokens.length && tokens[i].token === 'limit') {
if (i < tokens.length && tokens[i].token === "limit") {
limit = parseInt(tokens[i + 1].token);
}
@@ -354,13 +336,11 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
}
function getAggregateExpression() {
if (op === null || op === 'and') {
if (op === null || op === "and") {
return AndExp.of(expressions);
}
else if (op === 'or') {
} else if (op === "or") {
return OrExp.of(expressions);
}
else {
} else {
throw new Error(`Unrecognized op=${op}`);
}
}
@@ -376,19 +356,18 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
const token = tokens[i].token;
if (token === '#' || token === '~') {
if (token === "#" || token === "~") {
continue;
}
if (token.startsWith('#') || token.startsWith('~')) {
if (token.startsWith("#") || token.startsWith("~")) {
const attribute = parseAttribute(token);
if (attribute) {
expressions.push(attribute);
}
}
else if (['orderby', 'limit'].includes(token)) {
} else if (["orderby", "limit"].includes(token)) {
if (level !== 0) {
searchContext.addError('orderBy can appear only on the top expression level');
searchContext.addError("orderBy can appear only on the top expression level");
continue;
}
@@ -400,8 +379,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
exp.subExpression = getAggregateExpression();
return exp;
}
else if (token === 'not') {
} else if (token === "not") {
i += 1;
if (!Array.isArray(tokens[i])) {
@@ -411,53 +389,46 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
const tokenArray = tokens[i] as unknown as TokenData[];
const expression = getExpression(tokenArray, searchContext, level++);
if (!expression) { return; }
if (!expression) {
return;
}
expressions.push(new NotExp(expression));
}
else if (token === 'note') {
} else if (token === "note") {
i++;
const expression = parseNoteProperty();
if (!expression) { return; }
if (!expression) {
return;
}
expressions.push(expression);
continue;
}
else if (['and', 'or'].includes(token)) {
} else if (["and", "or"].includes(token)) {
if (!op) {
op = token;
} else if (op !== token) {
searchContext.addError("Mixed usage of AND/OR - always use parenthesis to group AND/OR expressions.");
}
else if (op !== token) {
searchContext.addError('Mixed usage of AND/OR - always use parenthesis to group AND/OR expressions.');
}
}
else if (isOperator({token: token})) {
} else if (isOperator({ token: token })) {
searchContext.addError(`Misplaced or incomplete expression "${token}"`);
}
else {
} else {
searchContext.addError(`Unrecognized expression "${token}"`);
}
if (!op && expressions.length > 1) {
op = 'and';
op = "and";
}
}
return getAggregateExpression();
}
function parse({fulltextTokens, expressionTokens, searchContext}: {
fulltextTokens: TokenData[],
expressionTokens: TokenStructure,
searchContext: SearchContext,
originalQuery: string
}) {
function parse({ fulltextTokens, expressionTokens, searchContext }: { fulltextTokens: TokenData[]; expressionTokens: TokenStructure; searchContext: SearchContext; originalQuery: string }) {
let expression: Expression | undefined | null;
try {
expression = getExpression(expressionTokens as TokenData[], searchContext);
}
catch (e: any) {
} catch (e: any) {
searchContext.addError(e.message);
expression = new TrueExp();
@@ -470,13 +441,18 @@ function parse({fulltextTokens, expressionTokens, searchContext}: {
expression
]);
if (searchContext.orderBy && searchContext.orderBy !== 'relevancy') {
if (searchContext.orderBy && searchContext.orderBy !== "relevancy") {
const filterExp = exp;
exp = new OrderByAndLimitExp([{
valueExtractor: new ValueExtractor(searchContext, ['note', searchContext.orderBy]),
direction: searchContext.orderDirection
}], searchContext.limit || undefined);
exp = new OrderByAndLimitExp(
[
{
valueExtractor: new ValueExtractor(searchContext, ["note", searchContext.orderBy]),
direction: searchContext.orderDirection
}
],
searchContext.limit || undefined
);
(exp as any).subExpression = filterExp;
}
@@ -484,8 +460,8 @@ function parse({fulltextTokens, expressionTokens, searchContext}: {
return exp;
}
function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}: SearchContext) {
if (ancestorNoteId && ancestorNoteId !== 'root') {
function getAncestorExp({ ancestorNoteId, ancestorDepth, includeHiddenNotes }: SearchContext) {
if (ancestorNoteId && ancestorNoteId !== "root") {
return new AncestorExp(ancestorNoteId, ancestorDepth);
} else if (!includeHiddenNotes) {
return new NotExp(new IsHiddenExp());

View File

@@ -34,28 +34,27 @@ function searchFromNote(note: BNote): SearchNoteResult {
let searchResultNoteIds;
let highlightedTokens: string[];
const searchScript = note.getRelationValue('searchScript');
const searchString = note.getLabelValue('searchString') || "";
const searchScript = note.getRelationValue("searchScript");
const searchString = note.getLabelValue("searchString") || "";
let error = null;
if (searchScript) {
searchResultNoteIds = searchFromRelation(note, 'searchScript');
searchResultNoteIds = searchFromRelation(note, "searchScript");
highlightedTokens = [];
} else {
const searchContext = new SearchContext({
fastSearch: note.hasLabel('fastSearch'),
ancestorNoteId: note.getRelationValue('ancestor') || undefined,
ancestorDepth: note.getLabelValue('ancestorDepth') || undefined,
includeArchivedNotes: note.hasLabel('includeArchivedNotes'),
orderBy: note.getLabelValue('orderBy') || undefined,
orderDirection: note.getLabelValue('orderDirection') || undefined,
limit: parseInt(note.getLabelValue('limit') || "0", 10),
debug: note.hasLabel('debug'),
fastSearch: note.hasLabel("fastSearch"),
ancestorNoteId: note.getRelationValue("ancestor") || undefined,
ancestorDepth: note.getLabelValue("ancestorDepth") || undefined,
includeArchivedNotes: note.hasLabel("includeArchivedNotes"),
orderBy: note.getLabelValue("orderBy") || undefined,
orderDirection: note.getLabelValue("orderDirection") || undefined,
limit: parseInt(note.getLabelValue("limit") || "0", 10),
debug: note.hasLabel("debug"),
fuzzyAttributeSearch: false
});
searchResultNoteIds = findResultsWithQuery(searchString, searchContext)
.map(sr => sr.noteId);
searchResultNoteIds = findResultsWithQuery(searchString, searchContext).map((sr) => sr.noteId);
highlightedTokens = searchContext.highlightedTokens;
error = searchContext.getError();
@@ -64,7 +63,7 @@ function searchFromNote(note: BNote): SearchNoteResult {
// we won't return search note's own noteId
// also don't allow root since that would force infinite cycle
return {
searchResultNoteIds: searchResultNoteIds.filter(resultNoteId => !['root', note.noteId].includes(resultNoteId)),
searchResultNoteIds: searchResultNoteIds.filter((resultNoteId) => !["root", note.noteId].includes(resultNoteId)),
highlightedTokens,
error: error
};
@@ -79,7 +78,7 @@ function searchFromRelation(note: BNote, relationName: string) {
return [];
}
if (!scriptNote.isJavaScript() || scriptNote.getScriptEnv() !== 'backend') {
if (!scriptNote.isJavaScript() || scriptNote.getScriptEnv() !== "backend") {
log.info(`Note ${scriptNote.noteId} is not executable.`);
return [];
@@ -91,7 +90,7 @@ function searchFromRelation(note: BNote, relationName: string) {
return [];
}
const result = scriptService.executeNote(scriptNote, {originEntity: note});
const result = scriptService.executeNote(scriptNote, { originEntity: note });
if (!Array.isArray(result)) {
log.info(`Result from ${scriptNote.noteId} is not an array.`);
@@ -104,7 +103,7 @@ function searchFromRelation(note: BNote, relationName: string) {
}
// we expect either array of noteIds (strings) or notes, in that case we extract noteIds ourselves
return typeof result[0] === 'string' ? result : result.map(item => item.noteId);
return typeof result[0] === "string" ? result : result.map((item) => item.noteId);
}
function loadNeededInfoFromDatabase() {
@@ -131,7 +130,7 @@ function loadNeededInfoFromDatabase() {
JOIN blobs USING(blobId)
WHERE notes.isDeleted = 0`);
for (const {noteId, blobId, length} of noteContentLengths) {
for (const { noteId, blobId, length } of noteContentLengths) {
if (!(noteId in becca.notes)) {
log.error(`Note '${noteId}' not found in becca.`);
continue;
@@ -159,7 +158,7 @@ function loadNeededInfoFromDatabase() {
WHERE attachments.isDeleted = 0
AND notes.isDeleted = 0`);
for (const {noteId, blobId, length} of attachmentContentLengths) {
for (const { noteId, blobId, length } of attachmentContentLengths) {
if (!(noteId in becca.notes)) {
log.error(`Note '${noteId}' not found in becca.`);
continue;
@@ -205,7 +204,7 @@ function loadNeededInfoFromDatabase() {
JOIN blobs ON attachments.blobId = blobs.blobId
WHERE notes.isDeleted = 0`);
for (const {noteId, blobId, length, isNoteRevision} of revisionContentLengths) {
for (const { noteId, blobId, length, isNoteRevision } of revisionContentLengths) {
if (!(noteId in becca.notes)) {
log.error(`Note '${noteId}' not found in becca.`);
continue;
@@ -218,7 +217,7 @@ function loadNeededInfoFromDatabase() {
noteBlobs[noteId][blobId] = length;
if (isNoteRevision) {
if (isNoteRevision) {
const noteRevision = becca.notes[noteId];
if (noteRevision && noteRevision.revisionCount) {
noteRevision.revisionCount++;
@@ -245,16 +244,15 @@ function findResultsWithExpression(expression: Expression, searchContext: Search
const noteSet = expression.execute(allNoteSet, executionContext, searchContext);
const searchResults = noteSet.notes
.map(note => {
const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();
const searchResults = noteSet.notes.map((note) => {
const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();
if (!notePathArray) {
throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`);
}
if (!notePathArray) {
throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`);
}
return new SearchResult(notePathArray);
});
return new SearchResult(notePathArray);
});
for (const res of searchResults) {
res.computeScore(searchContext.fulltextQuery, searchContext.highlightedTokens);
@@ -282,15 +280,14 @@ function findResultsWithExpression(expression: Expression, searchContext: Search
}
function parseQueryToExpression(query: string, searchContext: SearchContext) {
const {fulltextQuery, fulltextTokens, expressionTokens} = lex(query);
const { fulltextQuery, fulltextTokens, expressionTokens } = lex(query);
searchContext.fulltextQuery = fulltextQuery;
let structuredExpressionTokens: TokenStructure;
try {
structuredExpressionTokens = handleParens(expressionTokens);
}
catch (e: any) {
} catch (e: any) {
structuredExpressionTokens = [];
searchContext.addError(e.message);
}
@@ -318,7 +315,7 @@ function parseQueryToExpression(query: string, searchContext: SearchContext) {
function searchNotes(query: string, params: SearchParams = {}): BNote[] {
const searchResults = findResultsWithQuery(query, new SearchContext(params));
return searchResults.map(sr => becca.notes[sr.noteId]);
return searchResults.map((sr) => becca.notes[sr.noteId]);
}
function findResultsWithQuery(query: string, searchContext: SearchContext): SearchResult[] {
@@ -347,9 +344,7 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
includeHiddenNotes: true,
fuzzyAttributeSearch: true,
ignoreInternalAttributes: true,
ancestorNoteId: hoistedNoteService.isHoistedInHiddenSubtree()
? 'root'
: hoistedNoteService.getHoistedNoteId()
ancestorNoteId: hoistedNoteService.isHoistedInHiddenSubtree() ? "root" : hoistedNoteService.getHoistedNoteId()
});
const allSearchResults = findResultsWithQuery(query, searchContext);
@@ -358,7 +353,7 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
highlightSearchResults(trimmed, searchContext.highlightedTokens, searchContext.ignoreInternalAttributes);
return trimmed.map(result => {
return trimmed.map((result) => {
return {
notePath: result.notePath,
noteTitle: beccaService.getNoteTitle(result.noteId),
@@ -378,23 +373,21 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
// which would make the resulting HTML string invalid.
// { and } are used for marking <b> and </b> tag (to avoid matches on single 'b' character)
// < and > are used for marking <small> and </small>
highlightedTokens = highlightedTokens
.map(token => token.replace('/[<\{\}]/g', ''))
.filter(token => !!token?.trim());
highlightedTokens = highlightedTokens.map((token) => token.replace("/[<\{\}]/g", "")).filter((token) => !!token?.trim());
// sort by the longest, so we first highlight the longest matches
highlightedTokens.sort((a, b) => a.length > b.length ? -1 : 1);
highlightedTokens.sort((a, b) => (a.length > b.length ? -1 : 1));
for (const result of searchResults) {
const note = becca.notes[result.noteId];
result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, '');
result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, "");
if (highlightedTokens.find(token => note.type.includes(token))) {
if (highlightedTokens.find((token) => note.type.includes(token))) {
result.highlightedNotePathTitle += ` "type: ${note.type}'`;
}
if (highlightedTokens.find(token => note.mime.includes(token))) {
if (highlightedTokens.find((token) => note.mime.includes(token))) {
result.highlightedNotePathTitle += ` "mime: ${note.mime}'`;
}
@@ -403,9 +396,7 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
continue;
}
if (highlightedTokens.find(token => normalize(attr.name).includes(token)
|| normalize(attr.value).includes(token))) {
if (highlightedTokens.find((token) => normalize(attr.name).includes(token) || normalize(attr.value).includes(token))) {
result.highlightedNotePathTitle += ` "${formatAttribute(attr)}'`;
}
}
@@ -427,7 +418,9 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
let match;
// Find all matches
if (!result.highlightedNotePathTitle) { continue; }
if (!result.highlightedNotePathTitle) {
continue;
}
while ((match = tokenRegex.exec(normalizeString(result.highlightedNotePathTitle))) !== null) {
result.highlightedNotePathTitle = wrapText(result.highlightedNotePathTitle, match.index, token.length, "{", "}");
@@ -438,20 +431,17 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
}
for (const result of searchResults) {
if (!result.highlightedNotePathTitle) { continue; }
result.highlightedNotePathTitle = result.highlightedNotePathTitle
.replace(/"/g, "<small>")
.replace(/'/g, "</small>")
.replace(/{/g, "<b>")
.replace(/}/g, "</b>");
if (!result.highlightedNotePathTitle) {
continue;
}
result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/"/g, "<small>").replace(/'/g, "</small>").replace(/{/g, "<b>").replace(/}/g, "</b>");
}
}
function formatAttribute(attr: BAttribute) {
if (attr.type === 'relation') {
if (attr.type === "relation") {
return `~${escapeHtml(attr.name)}=…`;
}
else if (attr.type === 'label') {
} else if (attr.type === "label") {
let label = `#${escapeHtml(attr.name)}`;
if (attr.value) {

View File

@@ -21,4 +21,4 @@ export interface SearchParams {
limit?: number | null;
debug?: boolean;
fuzzyAttributeSearch?: boolean;
}
}

View File

@@ -7,31 +7,31 @@ import BNote from "../../becca/entities/bnote.js";
* we need a case-sensitive form, so we have this translation object.
*/
const PROP_MAPPING: Record<string, string> = {
"noteid": "noteId",
"title": "title",
"type": "type",
"mime": "mime",
"isprotected": "isProtected",
"isarchived": "isArchived",
"datecreated": "dateCreated",
"datemodified": "dateModified",
"utcdatecreated": "utcDateCreated",
"utcdatemodified": "utcDateModified",
"parentcount": "parentCount",
"childrencount": "childrenCount",
"attributecount": "attributeCount",
"labelcount": "labelCount",
"ownedlabelcount": "ownedLabelCount",
"relationcount": "relationCount",
"ownedrelationcount": "ownedRelationCount",
"relationcountincludinglinks": "relationCountIncludingLinks",
"ownedrelationcountincludinglinks": "ownedRelationCountIncludingLinks",
"targetrelationcount": "targetRelationCount",
"targetrelationcountincludinglinks": "targetRelationCountIncludingLinks",
"contentsize": "contentSize",
"contentandattachmentssize": "contentAndAttachmentsSize",
"contentandattachmentsandrevisionssize": "contentAndAttachmentsAndRevisionsSize",
"revisioncount": "revisionCount"
noteid: "noteId",
title: "title",
type: "type",
mime: "mime",
isprotected: "isProtected",
isarchived: "isArchived",
datecreated: "dateCreated",
datemodified: "dateModified",
utcdatecreated: "utcDateCreated",
utcdatemodified: "utcDateModified",
parentcount: "parentCount",
childrencount: "childrenCount",
attributecount: "attributeCount",
labelcount: "labelCount",
ownedlabelcount: "ownedLabelCount",
relationcount: "relationCount",
ownedrelationcount: "ownedRelationCount",
relationcountincludinglinks: "relationCountIncludingLinks",
ownedrelationcountincludinglinks: "ownedRelationCountIncludingLinks",
targetrelationcount: "targetRelationCount",
targetrelationcountincludinglinks: "targetRelationCountIncludingLinks",
contentsize: "contentSize",
contentandattachmentssize: "contentAndAttachmentsSize",
contentandattachmentsandrevisionssize: "contentAndAttachmentsAndRevisionsSize",
revisioncount: "revisionCount"
};
interface SearchContext {
@@ -42,48 +42,44 @@ class ValueExtractor {
private propertyPath: string[];
constructor(searchContext: SearchContext, propertyPath: string[]) {
this.propertyPath = propertyPath.map(pathEl => pathEl.toLowerCase());
this.propertyPath = propertyPath.map((pathEl) => pathEl.toLowerCase());
if (this.propertyPath[0].startsWith('#')) {
this.propertyPath = ['note', 'labels', this.propertyPath[0].substr(1), ...this.propertyPath.slice( 1, this.propertyPath.length)];
}
else if (this.propertyPath[0].startsWith('~')) {
this.propertyPath = ['note', 'relations', this.propertyPath[0].substr(1), ...this.propertyPath.slice(1, this.propertyPath.length)];
if (this.propertyPath[0].startsWith("#")) {
this.propertyPath = ["note", "labels", this.propertyPath[0].substr(1), ...this.propertyPath.slice(1, this.propertyPath.length)];
} else if (this.propertyPath[0].startsWith("~")) {
this.propertyPath = ["note", "relations", this.propertyPath[0].substr(1), ...this.propertyPath.slice(1, this.propertyPath.length)];
}
if (['contentsize', 'contentandattachmentssize', 'contentandattachmentsandrevisionssize', 'revisioncount'].includes(this.propertyPath[this.propertyPath.length - 1])) {
if (["contentsize", "contentandattachmentssize", "contentandattachmentsandrevisionssize", "revisioncount"].includes(this.propertyPath[this.propertyPath.length - 1])) {
searchContext.dbLoadNeeded = true;
}
}
validate() {
if (this.propertyPath[0] !== 'note') {
if (this.propertyPath[0] !== "note") {
return `property specifier must start with 'note', but starts with '${this.propertyPath[0]}'`;
}
for (let i = 1; i < this.propertyPath.length; i++) {
const pathEl = this.propertyPath[i];
if (pathEl === 'labels') {
if (pathEl === "labels") {
if (i !== this.propertyPath.length - 2) {
return `label is a terminal property specifier and must be at the end`;
}
i++;
}
else if (pathEl === 'relations') {
} else if (pathEl === "relations") {
if (i >= this.propertyPath.length - 2) {
return `relation name or property name is missing`;
}
i++;
}
else if (pathEl in PROP_MAPPING || pathEl === 'random') {
} else if (pathEl in PROP_MAPPING || pathEl === "random") {
if (i !== this.propertyPath.length - 1) {
return `${pathEl} is a terminal property specifier and must be at the end`;
}
}
else if (!["parents", "children"].includes(pathEl)) {
} else if (!["parents", "children"].includes(pathEl)) {
return `Unrecognized property specifier ${pathEl}`;
}
}
@@ -101,33 +97,28 @@ class ValueExtractor {
return cursor;
}
if (cur() === 'labels') {
if (cur() === "labels") {
i++;
const attr = cursor.getAttributeCaseInsensitive('label', cur());
const attr = cursor.getAttributeCaseInsensitive("label", cur());
return attr ? attr.value : null;
}
if (cur() === 'relations') {
if (cur() === "relations") {
i++;
const attr = cursor.getAttributeCaseInsensitive('relation', cur());
const attr = cursor.getAttributeCaseInsensitive("relation", cur());
cursor = attr?.targetNote || null;
}
else if (cur() === 'parents') {
} else if (cur() === "parents") {
cursor = cursor.parents[0];
}
else if (cur() === 'children') {
} else if (cur() === "children") {
cursor = cursor.children[0];
}
else if (cur() === 'random') {
} else if (cur() === "random") {
return Math.random().toString(); // string is expected for comparison
}
else if (cur() in PROP_MAPPING) {
} else if (cur() in PROP_MAPPING) {
return (cursor as any)[PROP_MAPPING[cur()]];
}
else {
} else {
// FIXME
}
}

View File

@@ -3,7 +3,7 @@
import fs from "fs";
import dataDir from "./data_dir.js";
import log from "./log.js";
import { randomSecureToken } from "./utils.js"
import { randomSecureToken } from "./utils.js";
const sessionSecretPath = `${dataDir.TRILIUM_DATA_DIR}/session_secret.txt`;
@@ -17,8 +17,7 @@ if (!fs.existsSync(sessionSecretPath)) {
log.info("Generated session secret");
fs.writeFileSync(sessionSecretPath, sessionSecret, ENCODING);
}
else {
} else {
sessionSecret = fs.readFileSync(sessionSecretPath, ENCODING);
}

View File

@@ -7,13 +7,15 @@ import request from "./request.js";
import appInfo from "./app_info.js";
import { timeLimit } from "./utils.js";
import becca from "../becca/becca.js";
import { SetupStatusResponse, SetupSyncSeedResponse } from './api-interface.js';
import { SetupStatusResponse, SetupSyncSeedResponse } from "./api-interface.js";
async function hasSyncServerSchemaAndSeed() {
const response = await requestToSyncServer<SetupStatusResponse>('GET', '/api/setup/status');
const response = await requestToSyncServer<SetupStatusResponse>("GET", "/api/setup/status");
if (response.syncVersion !== appInfo.syncVersion) {
throw new Error(`Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${response.syncVersion}. To fix this issue, use same Trilium version on all instances.`);
throw new Error(
`Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${response.syncVersion}. To fix this issue, use same Trilium version on all instances.`
);
}
return response.schemaExists;
@@ -23,7 +25,7 @@ function triggerSync() {
log.info("Triggering sync.");
// it's ok to not wait for it here
syncService.sync().then(res => {
syncService.sync().then((res) => {
if (res.success) {
sqlInit.setDbAsInitialized();
}
@@ -33,34 +35,37 @@ function triggerSync() {
async function sendSeedToSyncServer() {
log.info("Initiating sync to server");
await requestToSyncServer<void>('POST', '/api/setup/sync-seed', {
await requestToSyncServer<void>("POST", "/api/setup/sync-seed", {
options: getSyncSeedOptions(),
syncVersion: appInfo.syncVersion
});
// this is a completely new sync, need to reset counters. If this was not a new sync,
// the previous request would have failed.
optionService.setOption('lastSyncedPush', 0);
optionService.setOption('lastSyncedPull', 0);
optionService.setOption("lastSyncedPush", 0);
optionService.setOption("lastSyncedPull", 0);
}
async function requestToSyncServer<T>(method: string, path: string, body?: string | {}): Promise<T> {
const timeout = syncOptions.getSyncTimeout();
return await timeLimit(request.exec({
method,
url: syncOptions.getSyncServerHost() + path,
body,
proxy: syncOptions.getSyncProxy(),
timeout: timeout
}), timeout) as T;
return (await timeLimit(
request.exec({
method,
url: syncOptions.getSyncServerHost() + path,
body,
proxy: syncOptions.getSyncProxy(),
timeout: timeout
}),
timeout
)) as T;
}
async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string, password: string) {
if (sqlInit.isDbInitialized()) {
return {
result: 'failure',
error: 'DB is already initialized.'
result: "failure",
error: "DB is already initialized."
};
}
@@ -69,7 +74,7 @@ async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string
// the response is expected to contain documentId and documentSecret options
const resp = await request.exec<SetupSyncSeedResponse>({
method: 'get',
method: "get",
url: `${syncServerHost}/api/setup/sync-seed`,
auth: { password },
proxy: syncProxy,
@@ -82,32 +87,28 @@ async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string
log.error(message);
return {
result: 'failure',
result: "failure",
error: message
}
};
}
await sqlInit.createDatabaseForSync(resp.options, syncServerHost, syncProxy);
triggerSync();
return { result: 'success' };
}
catch (e: any) {
return { result: "success" };
} catch (e: any) {
log.error(`Sync failed: '${e.message}', stack: ${e.stack}`);
return {
result: 'failure',
result: "failure",
error: e.message
};
}
}
function getSyncSeedOptions() {
return [
becca.getOption('documentId'),
becca.getOption('documentSecret')
];
return [becca.getOption("documentId"), becca.getOption("documentSecret")];
}
export default {

View File

@@ -1,7 +1,6 @@
type Updater = () => void;
class SpacedUpdate {
private updater: Updater;
private lastUpdated: number;
private changed: boolean;
@@ -29,8 +28,7 @@ class SpacedUpdate {
try {
await this.updater();
}
catch (e) {
} catch (e) {
this.changed = true;
throw e;
@@ -55,8 +53,7 @@ class SpacedUpdate {
this.updater();
this.lastUpdated = Date.now();
this.changed = false;
}
else {
} else {
// update isn't triggered but changes are still pending, so we need to schedule another check
this.scheduleUpdate();
}
@@ -67,8 +64,7 @@ class SpacedUpdate {
try {
await callback();
}
finally {
} finally {
this.changeForbidden = false;
}
}

View File

@@ -20,35 +20,33 @@ function getInboxNote(date: string) {
let inbox;
if (!workspaceNote.isRoot()) {
inbox = workspaceNote.searchNoteInSubtree('#workspaceInbox');
inbox = workspaceNote.searchNoteInSubtree("#workspaceInbox");
if (!inbox) {
inbox = workspaceNote.searchNoteInSubtree('#inbox');
inbox = workspaceNote.searchNoteInSubtree("#inbox");
}
if (!inbox) {
inbox = workspaceNote;
}
}
else {
inbox = attributeService.getNoteWithLabel('inbox')
|| dateNoteService.getDayNote(date);
} else {
inbox = attributeService.getNoteWithLabel("inbox") || dateNoteService.getDayNote(date);
}
return inbox;
}
function createSqlConsole() {
const {note} = noteService.createNewNote({
parentNoteId: getMonthlyParentNoteId('_sqlConsole', 'sqlConsole'),
title: 'SQL Console - ' + dateUtils.localNowDate(),
const { note } = noteService.createNewNote({
parentNoteId: getMonthlyParentNoteId("_sqlConsole", "sqlConsole"),
title: "SQL Console - " + dateUtils.localNowDate(),
content: "SELECT title, isDeleted, isProtected FROM notes WHERE noteId = ''\n\n\n\n",
type: 'code',
mime: 'text/x-sqlite;schema=trilium'
type: "code",
mime: "text/x-sqlite;schema=trilium"
});
note.setLabel('iconClass', 'bx bx-data');
note.setLabel('keepCurrentHoisting');
note.setLabel("iconClass", "bx bx-data");
note.setLabel("keepCurrentHoisting");
return note;
}
@@ -58,14 +56,12 @@ function saveSqlConsole(sqlConsoleNoteId: string) {
if (!sqlConsoleNote) throw new Error(`Unable to find SQL console note ID: ${sqlConsoleNoteId}`);
const today = dateUtils.localNowDate();
const sqlConsoleHome =
attributeService.getNoteWithLabel('sqlConsoleHome')
|| dateNoteService.getDayNote(today);
const sqlConsoleHome = attributeService.getNoteWithLabel("sqlConsoleHome") || dateNoteService.getDayNote(today);
const result = sqlConsoleNote.cloneTo(sqlConsoleHome.noteId);
for (const parentBranch of sqlConsoleNote.getParentBranches()) {
if (parentBranch.parentNote?.hasAncestor('_hidden')) {
if (parentBranch.parentNote?.hasAncestor("_hidden")) {
parentBranch.markAsDeleted();
}
}
@@ -74,19 +70,19 @@ function saveSqlConsole(sqlConsoleNoteId: string) {
}
function createSearchNote(searchString: string, ancestorNoteId: string) {
const {note} = noteService.createNewNote({
parentNoteId: getMonthlyParentNoteId('_search', 'search'),
const { note } = noteService.createNewNote({
parentNoteId: getMonthlyParentNoteId("_search", "search"),
title: `${t("special_notes.search_prefix")} ${searchString}`,
content: "",
type: 'search',
mime: 'application/json'
type: "search",
mime: "application/json"
});
note.setLabel('searchString', searchString);
note.setLabel('keepCurrentHoisting');
note.setLabel("searchString", searchString);
note.setLabel("keepCurrentHoisting");
if (ancestorNoteId) {
note.setRelation('ancestor', ancestorNoteId);
note.setRelation("ancestor", ancestorNoteId);
}
return note;
@@ -99,14 +95,11 @@ function getSearchHome() {
}
if (!workspaceNote.isRoot()) {
return workspaceNote.searchNoteInSubtree('#workspaceSearchHome')
|| workspaceNote.searchNoteInSubtree('#searchHome')
|| workspaceNote;
return workspaceNote.searchNoteInSubtree("#workspaceSearchHome") || workspaceNote.searchNoteInSubtree("#searchHome") || workspaceNote;
} else {
const today = dateUtils.localNowDate();
return workspaceNote.searchNoteInSubtree('#searchHome')
|| dateNoteService.getDayNote(today);
return workspaceNote.searchNoteInSubtree("#searchHome") || dateNoteService.getDayNote(today);
}
}
@@ -121,7 +114,7 @@ function saveSearchNote(searchNoteId: string) {
const result = searchNote.cloneTo(searchHome.noteId);
for (const parentBranch of searchNote.getParentBranches()) {
if (parentBranch.parentNote?.hasAncestor('_hidden')) {
if (parentBranch.parentNote?.hasAncestor("_hidden")) {
parentBranch.markAsDeleted();
}
}
@@ -133,17 +126,16 @@ function getMonthlyParentNoteId(rootNoteId: string, prefix: string) {
const month = dateUtils.localNowDate().substring(0, 7);
const labelName = `${prefix}MonthNote`;
let monthNote = searchService.findFirstNoteWithQuery(`#${labelName}="${month}"`,
new SearchContext({ancestorNoteId: rootNoteId}));
let monthNote = searchService.findFirstNoteWithQuery(`#${labelName}="${month}"`, new SearchContext({ ancestorNoteId: rootNoteId }));
if (!monthNote) {
monthNote = noteService.createNewNote({
parentNoteId: rootNoteId,
title: month,
content: '',
content: "",
isProtected: false,
type: 'book'
}).note
type: "book"
}).note;
monthNote.addLabel(labelName, month);
}
@@ -155,12 +147,12 @@ function createScriptLauncher(parentNoteId: string, forceNoteId?: string) {
const note = noteService.createNewNote({
noteId: forceNoteId,
title: "Script Launcher",
type: 'launcher',
content: '',
type: "launcher",
content: "",
parentNoteId: parentNoteId
}).note;
note.addRelation('template', LBTPL_SCRIPT);
note.addRelation("template", LBTPL_SCRIPT);
return note;
}
@@ -173,39 +165,39 @@ interface LauncherConfig {
function createLauncher({ parentNoteId, launcherType, noteId }: LauncherConfig) {
let note;
if (launcherType === 'note') {
if (launcherType === "note") {
note = noteService.createNewNote({
noteId: noteId,
title: "Note Launcher",
type: 'launcher',
content: '',
type: "launcher",
content: "",
parentNoteId: parentNoteId
}).note;
note.addRelation('template', LBTPL_NOTE_LAUNCHER);
} else if (launcherType === 'script') {
note.addRelation("template", LBTPL_NOTE_LAUNCHER);
} else if (launcherType === "script") {
note = createScriptLauncher(parentNoteId, noteId);
} else if (launcherType === 'customWidget') {
} else if (launcherType === "customWidget") {
note = noteService.createNewNote({
noteId: noteId,
title: "Widget Launcher",
type: 'launcher',
content: '',
type: "launcher",
content: "",
parentNoteId: parentNoteId
}).note;
note.addRelation('template', LBTPL_CUSTOM_WIDGET);
} else if (launcherType === 'spacer') {
note.addRelation("template", LBTPL_CUSTOM_WIDGET);
} else if (launcherType === "spacer") {
note = noteService.createNewNote({
noteId: noteId,
branchId: noteId,
title: "Spacer",
type: 'launcher',
content: '',
type: "launcher",
content: "",
parentNoteId: parentNoteId
}).note;
note.addRelation('template', LBTPL_SPACER);
note.addRelation("template", LBTPL_SPACER);
} else {
throw new Error(`Unrecognized launcher type '${launcherType}'`);
}
@@ -221,7 +213,7 @@ function resetLauncher(noteId: string) {
if (note?.isLaunchBarConfig()) {
if (note) {
if (noteId === '_lbRoot') {
if (noteId === "_lbRoot") {
// deleting hoisted notes are not allowed, so we just reset the children
for (const childNote of note.getChildNotes()) {
childNote.deleteNote();
@@ -247,42 +239,35 @@ function resetLauncher(noteId: string) {
* Another use case was for script-packages (e.g. demo Task manager) which could this way register automatically/easily
* into the launchbar - for this it's recommended to use backend API's createOrUpdateLauncher()
*/
function createOrUpdateScriptLauncherFromApi(opts: {
id: string;
title: string;
action: string;
icon?: string;
shortcut?: string;
}) {
function createOrUpdateScriptLauncherFromApi(opts: { id: string; title: string; action: string; icon?: string; shortcut?: string }) {
if (opts.id && !/^[a-z0-9]+$/i.test(opts.id)) {
throw new Error(`Launcher ID can be alphanumeric only, '${opts.id}' given`);
}
const launcherId = opts.id || (`tb_${opts.title.toLowerCase().replace(/[^[a-z0-9]/gi, "")}`);
const launcherId = opts.id || `tb_${opts.title.toLowerCase().replace(/[^[a-z0-9]/gi, "")}`;
if (!opts.title) {
throw new Error("Title is mandatory property to create or update a launcher.");
}
const launcherNote = becca.getNote(launcherId)
|| createScriptLauncher('_lbVisibleLaunchers', launcherId);
const launcherNote = becca.getNote(launcherId) || createScriptLauncher("_lbVisibleLaunchers", launcherId);
launcherNote.title = opts.title;
launcherNote.setContent(`(${opts.action})()`);
launcherNote.setLabel('scriptInLauncherContent'); // there's no target note, the script is in the launcher's content
launcherNote.mime = 'application/javascript;env=frontend';
launcherNote.setLabel("scriptInLauncherContent"); // there's no target note, the script is in the launcher's content
launcherNote.mime = "application/javascript;env=frontend";
launcherNote.save();
if (opts.shortcut) {
launcherNote.setLabel('keyboardShortcut', opts.shortcut);
launcherNote.setLabel("keyboardShortcut", opts.shortcut);
} else {
launcherNote.removeLabel('keyboardShortcut');
launcherNote.removeLabel("keyboardShortcut");
}
if (opts.icon) {
launcherNote.setLabel('iconClass', `bx bx-${opts.icon}`);
launcherNote.setLabel("iconClass", `bx bx-${opts.icon}`);
} else {
launcherNote.removeLabel('iconClass');
launcherNote.removeLabel("iconClass");
}
return launcherNote;

View File

@@ -36,16 +36,15 @@ function rebuildIntegrationTestDatabase() {
statementCache = {};
}
if (!process.env.TRILIUM_INTEGRATION_TEST) {
dbConnection.pragma('journal_mode = WAL');
dbConnection.pragma("journal_mode = WAL");
}
const LOG_ALL_QUERIES = false;
type Params = any;
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((eventType) => {
process.on(eventType, () => {
if (dbConnection) {
// closing connection is especially important to fold -wal file into the main DB file
@@ -63,7 +62,7 @@ function insert<T extends {}>(tableName: string, rec: T, replace = false) {
}
const columns = keys.join(", ");
const questionMarks = keys.map(p => "?").join(", ");
const questionMarks = keys.map((p) => "?").join(", ");
const query = `INSERT
${replace ? "OR REPLACE" : ""} INTO
@@ -91,9 +90,9 @@ function upsert<T extends {}>(tableName: string, primaryKey: string, rec: T) {
const columns = keys.join(", ");
const questionMarks = keys.map(colName => `@${colName}`).join(", ");
const questionMarks = keys.map((colName) => `@${colName}`).join(", ");
const updateMarks = keys.map(colName => `${colName} = @${colName}`).join(", ");
const updateMarks = keys.map((colName) => `${colName} = @${colName}`).join(", ");
const query = `INSERT INTO ${tableName} (${columns}) VALUES (${questionMarks})
ON CONFLICT (${primaryKey}) DO UPDATE SET ${updateMarks}`;
@@ -116,7 +115,7 @@ function stmt(sql: string) {
}
function getRow<T>(query: string, params: Params = []): T {
return wrap(query, s => s.get(params)) as T;
return wrap(query, (s) => s.get(params)) as T;
}
function getRowOrNull<T>(query: string, params: Params = []): T | null {
@@ -125,11 +124,11 @@ function getRowOrNull<T>(query: string, params: Params = []): T | null {
return null;
}
return (all.length > 0 ? all[0] : null) as (T | null);
return (all.length > 0 ? all[0] : null) as T | null;
}
function getValue<T>(query: string, params: Params = []): T {
return wrap(query, s => s.pluck().get(params)) as T;
return wrap(query, (s) => s.pluck().get(params)) as T;
}
// smaller values can result in better performance due to better usage of statement cache
@@ -146,30 +145,28 @@ function getManyRows<T>(query: string, params: Params): T[] {
let j = 1;
for (const param of curParams) {
curParamsObj['param' + j++] = param;
curParamsObj["param" + j++] = param;
}
let i = 1;
const questionMarks = curParams.map(() => ":param" + i++).join(",");
const curQuery = query.replace(/\?\?\?/g, questionMarks);
const statement = curParams.length === PARAM_LIMIT
? stmt(curQuery)
: dbConnection.prepare(curQuery);
const statement = curParams.length === PARAM_LIMIT ? stmt(curQuery) : dbConnection.prepare(curQuery);
const subResults = statement.all(curParamsObj);
results = results.concat(subResults);
}
return (results as (T[] | null) || []);
return (results as T[] | null) || [];
}
function getRows<T>(query: string, params: Params = []): T[] {
return wrap(query, s => s.all(params)) as T[];
return wrap(query, (s) => s.all(params)) as T[];
}
function getRawRows<T extends {} | unknown[]>(query: string, params: Params = []): T[] {
return (wrap(query, s => s.raw().all(params)) as T[]) || [];
return (wrap(query, (s) => s.raw().all(params)) as T[]) || [];
}
function iterateRows<T>(query: string, params: Params = []): IterableIterator<T> {
@@ -192,11 +189,11 @@ function getMap<K extends string | number | symbol, V>(query: string, params: Pa
}
function getColumn<T>(query: string, params: Params = []): T[] {
return wrap(query, s => s.pluck().all(params)) as T[];
return wrap(query, (s) => s.pluck().all(params)) as T[];
}
function execute(query: string, params: Params = []): RunResult {
return wrap(query, s => s.run(params)) as RunResult;
return wrap(query, (s) => s.run(params)) as RunResult;
}
function executeMany(query: string, params: Params) {
@@ -212,7 +209,7 @@ function executeMany(query: string, params: Params) {
let j = 1;
for (const param of curParams) {
curParamsObj['param' + j++] = param;
curParamsObj["param" + j++] = param;
}
let i = 1;
@@ -241,8 +238,7 @@ function wrap(query: string, func: (statement: Statement) => unknown): unknown {
try {
result = func(stmt(query));
}
catch (e: any) {
} catch (e: any) {
if (e.message.includes("The database connection is not open")) {
// this often happens on killing the app which puts these alerts in front of user
// in these cases error should be simply ignored.
@@ -259,8 +255,7 @@ function wrap(query: string, func: (statement: Statement) => unknown): unknown {
if (milliseconds >= 20 && !cls.isSlowQueryLoggingDisabled()) {
if (query.includes("WITH RECURSIVE")) {
log.info(`Slow recursive query took ${milliseconds}ms.`);
}
else {
} else {
log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`);
}
}
@@ -272,13 +267,13 @@ function transactional<T>(func: (statement: Statement) => T) {
try {
const ret = (dbConnection.transaction(func) as any).deferred();
if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released)
if (!dbConnection.inTransaction) {
// i.e. transaction was really committed (and not just savepoint released)
ws.sendTransactionEntityChangesToAllClients();
}
return ret;
}
catch (e) {
} catch (e) {
const entityChangeIds = cls.getAndClearEntityChangeIds();
if (entityChangeIds.length > 0) {
@@ -312,7 +307,7 @@ function fillParamList(paramIds: string[] | Set<string>, truncate = true) {
}
// doing it manually to avoid this showing up on the slow query list
const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map(paramId => `(?)`).join(',')}`);
const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map((paramId) => `(?)`).join(",")}`);
s.run(paramIds);
}
@@ -320,8 +315,7 @@ function fillParamList(paramIds: string[] | Set<string>, truncate = true) {
async function copyDatabase(targetFilePath: string) {
try {
fs.unlinkSync(targetFilePath);
} catch (e) {
} // unlink throws exception if the file did not exist
} catch (e) {} // unlink throws exception if the file did not exist
await dbConnection.backup(targetFilePath);
}
@@ -333,8 +327,7 @@ function disableSlowQueryLogging<T>(cb: () => T) {
cls.disableSlowQueryLogging(true);
return cb();
}
finally {
} finally {
cls.disableSlowQueryLogging(orig);
}
}
@@ -345,60 +338,60 @@ export default {
replace,
/**
* Get single value from the given query - first column from first returned row.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns single value
*/
* Get single value from the given query - first column from first returned row.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns single value
*/
getValue,
/**
* Get first returned row.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - map of column name to column value
*/
* Get first returned row.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - map of column name to column value
*/
getRow,
getRowOrNull,
/**
* Get all returned rows.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - array of all rows, each row is a map of column name to column value
*/
* Get all returned rows.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - array of all rows, each row is a map of column name to column value
*/
getRows,
getRawRows,
iterateRows,
getManyRows,
/**
* Get a map of first column mapping to second column.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - map of first column to second column
*/
* Get a map of first column mapping to second column.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - map of first column to second column
*/
getMap,
/**
* Get a first column in an array.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns array of first column of all returned rows
*/
* Get a first column in an array.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns array of first column of all returned rows
*/
getColumn,
/**
* Execute SQL
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
*/
* Execute SQL
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
*/
execute,
executeMany,
executeScript,

View File

@@ -10,7 +10,7 @@ import TaskContext from "./task_context.js";
import migrationService from "./migration.js";
import cls from "./cls.js";
import config from "./config.js";
import { OptionRow } from '../becca/entities/rows.js';
import { OptionRow } from "../becca/entities/rows.js";
import BNote from "../becca/entities/bnote.js";
import BBranch from "../becca/entities/bbranch.js";
import zipImportService from "./import/zip.js";
@@ -32,13 +32,12 @@ function isDbInitialized() {
const initialized = sql.getValue("SELECT value FROM options WHERE name = 'initialized'");
return initialized === 'true';
return initialized === "true";
}
async function initDbConnection() {
if (!isDbInitialized()) {
log.info(`DB not initialized, please visit setup page` +
(isElectron() ? '' : ` - http://[your-server-host]:${port} to see instructions on how to initialize Trilium.`));
log.info(`DB not initialized, please visit setup page` + (isElectron() ? "" : ` - http://[your-server-host]:${port} to see instructions on how to initialize Trilium.`));
return;
}
@@ -73,17 +72,17 @@ async function createInitialDatabase() {
log.info("Creating root note ...");
rootNote = new BNote({
noteId: 'root',
title: 'root',
type: 'text',
mime: 'text/html'
noteId: "root",
title: "root",
type: "text",
mime: "text/html"
}).save();
rootNote.setContent('');
rootNote.setContent("");
new BBranch({
noteId: 'root',
parentNoteId: 'none',
noteId: "root",
parentNoteId: "none",
isExpanded: true,
notePosition: 10
}).save();
@@ -96,7 +95,7 @@ async function createInitialDatabase() {
log.info("Importing demo content ...");
const dummyTaskContext = new TaskContext("no-progress-reporting", 'import', false);
const dummyTaskContext = new TaskContext("no-progress-reporting", "import", false);
await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
@@ -107,12 +106,15 @@ async function createInitialDatabase() {
const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
optionService.setOption('openNoteContexts', JSON.stringify([
{
notePath: startNoteId,
active: true
}
]));
optionService.setOption(
"openNoteContexts",
JSON.stringify([
{
notePath: startNoteId,
active: true
}
])
);
});
log.info("Schema and initial content generated.");
@@ -120,7 +122,7 @@ async function createInitialDatabase() {
initDbConnection();
}
async function createDatabaseForSync(options: OptionRow[], syncServerHost = '', syncProxy = '') {
async function createDatabaseForSync(options: OptionRow[], syncServerHost = "", syncProxy = "") {
log.info("Creating database for sync");
if (isDbInitialized()) {
@@ -148,7 +150,7 @@ async function createDatabaseForSync(options: OptionRow[], syncServerHost = '',
function setDbAsInitialized() {
if (!isDbInitialized()) {
optionService.setOption('initialized', 'true');
optionService.setOption("initialized", "true");
initDbConnection();
}

View File

@@ -17,8 +17,8 @@ import ws from "./ws.js";
import entityChangesService from "./entity_changes.js";
import entityConstructor from "../becca/entity_constructor.js";
import becca from "../becca/becca.js";
import { EntityChange, EntityChangeRecord, EntityRow } from './entity_changes_interface.js';
import { CookieJar, ExecOpts } from './request_interface.js';
import { EntityChange, EntityChangeRecord, EntityRow } from "./entity_changes_interface.js";
import { CookieJar, ExecOpts } from "./request_interface.js";
import setupService from "./setup.js";
import consistency_checks from "./consistency_checks.js";
import becca_loader from "../becca/becca_loader.js";
@@ -29,7 +29,7 @@ let outstandingPullCount = 0;
interface CheckResponse {
maxEntityChangeId: number;
entityHashes: Record<string, Record<string, string>>
entityHashes: Record<string, Record<string, string>>;
}
interface SyncResponse {
@@ -52,7 +52,7 @@ async function sync() {
try {
return await syncMutexService.doExclusively(async () => {
if (!syncOptions.isSyncSetup()) {
return { success: false, errorCode: 'NOT_CONFIGURED', message: 'Sync not configured' };
return { success: false, errorCode: "NOT_CONFIGURED", message: "Sync not configured" };
}
let continueSync = false;
@@ -69,8 +69,7 @@ async function sync() {
await syncFinished(syncContext);
continueSync = await checkContentHash(syncContext);
}
while (continueSync);
} while (continueSync);
ws.syncFinished();
@@ -78,15 +77,15 @@ async function sync() {
success: true
};
});
}
catch (e: any) {
} catch (e: any) {
// we're dynamically switching whether we're using proxy or not based on whether we encountered error with the current method
proxyToggle = !proxyToggle;
if (e.message?.includes('ECONNREFUSED') ||
e.message?.includes('ERR_') || // node network errors
e.message?.includes('Bad Gateway')) {
if (
e.message?.includes("ECONNREFUSED") ||
e.message?.includes("ERR_") || // node network errors
e.message?.includes("Bad Gateway")
) {
ws.syncFailed();
log.info("No connection to sync server.");
@@ -95,8 +94,7 @@ async function sync() {
success: false,
message: "No connection to sync server."
};
}
else {
} else {
log.info(`Sync failed: '${e.message}', stack: ${e.stack}`);
ws.syncFailed();
@@ -104,13 +102,13 @@ async function sync() {
return {
success: false,
message: e.message
}
};
}
}
}
async function login() {
if (!await setupService.hasSyncServerSchemaAndSeed()) {
if (!(await setupService.hasSyncServerSchemaAndSeed())) {
await setupService.sendSeedToSyncServer();
}
@@ -120,11 +118,11 @@ async function login() {
async function doLogin(): Promise<SyncContext> {
const timestamp = dateUtils.utcNowDateTime();
const documentSecret = optionService.getOption('documentSecret');
const documentSecret = optionService.getOption("documentSecret");
const hash = hmac(documentSecret, timestamp);
const syncContext: SyncContext = { cookieJar: {} };
const resp = await syncRequest<SyncResponse>(syncContext, 'POST', '/api/login/sync', {
const resp = await syncRequest<SyncResponse>(syncContext, "POST", "/api/login/sync", {
timestamp: timestamp,
syncVersion: appInfo.syncVersion,
hash: hash
@@ -135,7 +133,9 @@ async function doLogin(): Promise<SyncContext> {
}
if (resp.instanceId === instanceId) {
throw new Error(`Sync server has instance ID '${resp.instanceId}' which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`);
throw new Error(
`Sync server has instance ID '${resp.instanceId}' which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`
);
}
syncContext.instanceId = resp.instanceId;
@@ -161,11 +161,11 @@ async function pullChanges(syncContext: SyncContext) {
const startDate = Date.now();
const resp = await syncRequest<ChangesResponse>(syncContext, 'GET', changesUri);
const resp = await syncRequest<ChangesResponse>(syncContext, "GET", changesUri);
if (!resp) {
throw new Error("Request failed.");
}
const {entityChanges, lastEntityChangeId} = resp;
const { entityChanges, lastEntityChangeId } = resp;
outstandingPullCount = resp.outstandingPullCount;
@@ -184,12 +184,14 @@ async function pullChanges(syncContext: SyncContext) {
if (entityChanges.length === 0) {
break;
} else {
try { // https://github.com/zadam/trilium/issues/4310
try {
// https://github.com/zadam/trilium/issues/4310
const sizeInKb = Math.round(JSON.stringify(resp).length / 1024);
log.info(`Sync ${logMarkerId}: Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`);
}
catch (e: any) {
log.info(
`Sync ${logMarkerId}: Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`
);
} catch (e: any) {
log.error(`Error occurred ${e.message} ${e.stack}`);
}
}
@@ -202,7 +204,7 @@ async function pushChanges(syncContext: SyncContext) {
let lastSyncedPush: number | null | undefined = getLastSyncedPush();
while (true) {
const entityChanges = sql.getRows<EntityChange>('SELECT * FROM entity_changes WHERE isSynced = 1 AND id > ? LIMIT 1000', [lastSyncedPush]);
const entityChanges = sql.getRows<EntityChange>("SELECT * FROM entity_changes WHERE isSynced = 1 AND id > ? LIMIT 1000", [lastSyncedPush]);
if (entityChanges.length === 0) {
log.info("Nothing to push");
@@ -210,15 +212,14 @@ async function pushChanges(syncContext: SyncContext) {
break;
}
const filteredEntityChanges = entityChanges.filter(entityChange => {
const filteredEntityChanges = entityChanges.filter((entityChange) => {
if (entityChange.instanceId === syncContext.instanceId) {
// this may set lastSyncedPush beyond what's actually sent (because of size limit)
// so this is applied to the database only if there's no actual update
lastSyncedPush = entityChange.id;
return false;
}
else {
} else {
return true;
}
});
@@ -236,7 +237,7 @@ async function pushChanges(syncContext: SyncContext) {
const logMarkerId = randomString(10); // to easily pair sync events between client and server logs
await syncRequest(syncContext, 'PUT', `/api/sync/update?logMarkerId=${logMarkerId}`, {
await syncRequest(syncContext, "PUT", `/api/sync/update?logMarkerId=${logMarkerId}`, {
entities: entityChangesRecords,
instanceId
});
@@ -254,11 +255,11 @@ async function pushChanges(syncContext: SyncContext) {
}
async function syncFinished(syncContext: SyncContext) {
await syncRequest(syncContext, 'POST', '/api/sync/finished');
await syncRequest(syncContext, "POST", "/api/sync/finished");
}
async function checkContentHash(syncContext: SyncContext) {
const resp = await syncRequest<CheckResponse>(syncContext, 'GET', '/api/sync/check');
const resp = await syncRequest<CheckResponse>(syncContext, "GET", "/api/sync/check");
if (!resp) {
throw new Error("Got no response.");
}
@@ -285,13 +286,13 @@ async function checkContentHash(syncContext: SyncContext) {
// before re-queuing sectors, make sure the entity changes are correct
consistency_checks.runEntityChangesChecks();
await syncRequest(syncContext, 'POST', `/api/sync/check-entity-changes`);
await syncRequest(syncContext, "POST", `/api/sync/check-entity-changes`);
}
for (const {entityName, sector} of failedChecks) {
for (const { entityName, sector } of failedChecks) {
entityChangesService.addEntityChangesForSector(entityName, sector);
await syncRequest(syncContext, 'POST', `/api/sync/queue-sector/${entityName}/${sector}`);
await syncRequest(syncContext, "POST", `/api/sync/queue-sector/${entityName}/${sector}`);
}
return failedChecks.length > 0;
@@ -300,11 +301,11 @@ async function checkContentHash(syncContext: SyncContext) {
const PAGE_SIZE = 1000000;
interface SyncContext {
cookieJar: CookieJar
cookieJar: CookieJar;
}
async function syncRequest<T extends {}>(syncContext: SyncContext, method: string, requestPath: string, _body?: {}) {
const body = _body ? JSON.stringify(_body) : '';
const body = _body ? JSON.stringify(_body) : "";
const timeout = syncOptions.getSyncTimeout();
@@ -328,19 +329,18 @@ async function syncRequest<T extends {}>(syncContext: SyncContext, method: strin
proxy: proxyToggle ? syncOptions.getSyncProxy() : null
};
response = await timeLimit(request.exec(opts), timeout) as T;
response = (await timeLimit(request.exec(opts), timeout)) as T;
}
return response;
}
function getEntityChangeRow(entityChange: EntityChange) {
const {entityName, entityId} = entityChange;
const { entityName, entityId } = entityChange;
if (entityName === 'note_reordering') {
if (entityName === "note_reordering") {
return sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [entityId]);
}
else {
} else {
const primaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
if (!primaryKey) {
@@ -354,9 +354,9 @@ function getEntityChangeRow(entityChange: EntityChange) {
return null;
}
if (entityName === 'blobs' && entityRow.content !== null) {
if (typeof entityRow.content === 'string') {
entityRow.content = Buffer.from(entityRow.content, 'utf-8');
if (entityName === "blobs" && entityRow.content !== null) {
if (typeof entityRow.content === "string") {
entityRow.content = Buffer.from(entityRow.content, "utf-8");
}
if (entityRow.content) {
@@ -374,7 +374,7 @@ function getEntityChangeRecords(entityChanges: EntityChange[]) {
for (const entityChange of entityChanges) {
if (entityChange.isErased) {
records.push({entityChange});
records.push({ entityChange });
continue;
}
@@ -400,22 +400,23 @@ function getEntityChangeRecords(entityChanges: EntityChange[]) {
}
function getLastSyncedPull() {
return parseInt(optionService.getOption('lastSyncedPull'));
return parseInt(optionService.getOption("lastSyncedPull"));
}
function setLastSyncedPull(entityChangeId: number) {
const lastSyncedPullOption = becca.getOption('lastSyncedPull');
const lastSyncedPullOption = becca.getOption("lastSyncedPull");
if (lastSyncedPullOption) { // might be null in initial sync when becca is not loaded
if (lastSyncedPullOption) {
// might be null in initial sync when becca is not loaded
lastSyncedPullOption.value = `${entityChangeId}`;
}
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPull']);
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, "lastSyncedPull"]);
}
function getLastSyncedPush() {
const lastSyncedPush = parseInt(optionService.getOption('lastSyncedPush'));
const lastSyncedPush = parseInt(optionService.getOption("lastSyncedPush"));
ws.setLastSyncedPush(lastSyncedPush);
@@ -425,18 +426,19 @@ function getLastSyncedPush() {
function setLastSyncedPush(entityChangeId: number) {
ws.setLastSyncedPush(entityChangeId);
const lastSyncedPushOption = becca.getOption('lastSyncedPush');
const lastSyncedPushOption = becca.getOption("lastSyncedPush");
if (lastSyncedPushOption) { // might be null in initial sync when becca is not loaded
if (lastSyncedPushOption) {
// might be null in initial sync when becca is not loaded
lastSyncedPushOption.value = `${entityChangeId}`;
}
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPush']);
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, "lastSyncedPush"]);
}
function getMaxEntityChangeId() {
return sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes');
return sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes");
}
function getOutstandingPullCount() {

View File

@@ -11,8 +11,7 @@ async function doExclusively<T>(func: () => T) {
try {
return await func();
}
finally {
} finally {
releaseMutex();
}
}

View File

@@ -12,20 +12,20 @@ import config from "./config.js";
*/
function get(name: OptionNames) {
return (config['Sync'] && config['Sync'][name]) || optionService.getOption(name);
return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name);
}
export default {
// env variable is the easiest way to guarantee we won't overwrite prod data during development
// after copying prod document/data directory
getSyncServerHost: () => process.env.TRILIUM_SYNC_SERVER_HOST || get('syncServerHost'),
getSyncServerHost: () => process.env.TRILIUM_SYNC_SERVER_HOST || get("syncServerHost"),
isSyncSetup: () => {
const syncServerHost = get('syncServerHost');
const syncServerHost = get("syncServerHost");
// special value "disabled" is here to support a use case where the document is configured with sync server,
// and we need to override it with config from config.ini
return !!syncServerHost && syncServerHost !== 'disabled';
return !!syncServerHost && syncServerHost !== "disabled";
},
getSyncTimeout: () => parseInt(get('syncServerTimeout')) || 120000,
getSyncProxy: () => get('syncProxy')
getSyncTimeout: () => parseInt(get("syncServerTimeout")) || 120000,
getSyncProxy: () => get("syncProxy")
};

View File

@@ -4,12 +4,12 @@ import entityChangesService from "./entity_changes.js";
import eventService from "./events.js";
import entityConstructor from "../becca/entity_constructor.js";
import ws from "./ws.js";
import { EntityChange, EntityChangeRecord, EntityRow } from './entity_changes_interface.js';
import { EntityChange, EntityChangeRecord, EntityRow } from "./entity_changes_interface.js";
interface UpdateContext {
alreadyErased: number;
erased: number;
updated: Record<string, string[]>
updated: Record<string, string[]>;
}
function updateEntities(entityChanges: EntityChangeRecord[], instanceId: string) {
@@ -25,9 +25,8 @@ function updateEntities(entityChanges: EntityChangeRecord[], instanceId: string)
alreadyErased: 0
};
for (const {entityChange, entity} of entityChanges) {
const changeAppliedAlready = entityChange.changeId
&& !!sql.getValue("SELECT 1 FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
for (const { entityChange, entity } of entityChanges) {
const changeAppliedAlready = entityChange.changeId && !!sql.getValue("SELECT 1 FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
if (changeAppliedAlready) {
updateContext.alreadyUpdated++;
@@ -35,7 +34,8 @@ function updateEntities(entityChanges: EntityChangeRecord[], instanceId: string)
continue;
}
if (!atLeastOnePullApplied) { // avoid spamming and send only for first
if (!atLeastOnePullApplied) {
// avoid spamming and send only for first
ws.syncPullInProgress();
atLeastOnePullApplied = true;
@@ -48,13 +48,11 @@ function updateEntities(entityChanges: EntityChangeRecord[], instanceId: string)
}
function updateEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow | undefined, instanceId: string, updateContext: UpdateContext) {
if (!remoteEntityRow && remoteEC.entityName === 'options') {
if (!remoteEntityRow && remoteEC.entityName === "options") {
return; // can be undefined for options with isSynced=false
}
const updated = remoteEC.entityName === 'note_reordering'
? updateNoteReordering(remoteEC, remoteEntityRow, instanceId)
: updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext);
const updated = remoteEC.entityName === "note_reordering" ? updateNoteReordering(remoteEC, remoteEntityRow, instanceId) : updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext);
if (updated) {
if (remoteEntityRow?.isDeleted) {
@@ -62,8 +60,7 @@ function updateEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow | undef
entityName: remoteEC.entityName,
entityId: remoteEC.entityId
});
}
else if (!remoteEC.isErased) {
} else if (!remoteEC.isErased) {
eventService.emit(eventService.ENTITY_CHANGE_SYNCED, {
entityName: remoteEC.entityName,
entityRow: remoteEntityRow
@@ -74,9 +71,7 @@ function updateEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow | undef
function updateNormalEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow | undefined, instanceId: string, updateContext: UpdateContext) {
const localEC = sql.getRow<EntityChange | undefined>(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]);
const localECIsOlderOrSameAsRemote = (
localEC && localEC.utcDateChanged && remoteEC.utcDateChanged &&
localEC.utcDateChanged <= remoteEC.utcDateChanged);
const localECIsOlderOrSameAsRemote = localEC && localEC.utcDateChanged && remoteEC.utcDateChanged && localEC.utcDateChanged <= remoteEC.utcDateChanged;
if (!localEC || localECIsOlderOrSameAsRemote) {
if (remoteEC.isErased) {
@@ -100,17 +95,12 @@ function updateNormalEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow |
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
}
if (!localEC
|| localECIsOlderOrSameAsRemote
|| localEC.hash !== remoteEC.hash
|| localEC.isErased !== remoteEC.isErased
) {
if (!localEC || localECIsOlderOrSameAsRemote || localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased) {
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
}
return true;
} else if ((localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased)
&& !localECIsOlderOrSameAsRemote) {
} else if ((localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased) && !localECIsOlderOrSameAsRemote) {
// the change on our side is newer than on the other side, so the other side should update
entityChangesService.putEntityChangeForOtherInstances(localEC);
@@ -121,12 +111,12 @@ function updateNormalEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow |
}
function preProcessContent(remoteEC: EntityChange, remoteEntityRow: EntityRow) {
if (remoteEC.entityName === 'blobs' && remoteEntityRow.content !== null) {
if (remoteEC.entityName === "blobs" && remoteEntityRow.content !== null) {
// we always use a Buffer object which is different from normal saving - there we use a simple string type for
// "string notes". The problem is that in general, it's not possible to detect whether a blob content
// is string note or note (syncs can arrive out of order)
if (typeof remoteEntityRow.content === "string") {
remoteEntityRow.content = Buffer.from(remoteEntityRow.content, 'base64');
remoteEntityRow.content = Buffer.from(remoteEntityRow.content, "base64");
if (remoteEntityRow.content.byteLength === 0) {
// there seems to be a bug which causes empty buffer to be stored as NULL which is then picked up as inconsistency
@@ -152,16 +142,9 @@ function updateNoteReordering(remoteEC: EntityChange, remoteEntityRow: EntityRow
}
function eraseEntity(entityChange: EntityChange) {
const {entityName, entityId} = entityChange;
const { entityName, entityId } = entityChange;
const entityNames = [
"notes",
"branches",
"attributes",
"revisions",
"attachments",
"blobs"
];
const entityNames = ["notes", "branches", "attributes", "revisions", "attachments", "blobs"];
if (!entityNames.includes(entityName)) {
log.error(`Cannot erase ${entityName} '${entityId}'.`);
@@ -174,10 +157,7 @@ function eraseEntity(entityChange: EntityChange) {
}
function logUpdateContext(updateContext: UpdateContext) {
const message = JSON.stringify(updateContext)
.replaceAll('"', '')
.replaceAll(":", ": ")
.replaceAll(",", ", ");
const message = JSON.stringify(updateContext).replaceAll('"', "").replaceAll(":", ": ").replaceAll(",", ", ");
log.info(message.substr(1, message.length - 2));
}

View File

@@ -1,13 +1,12 @@
"use strict";
import { TaskData } from './task_context_interface.js';
import { TaskData } from "./task_context_interface.js";
import ws from "./ws.js";
// taskId => TaskContext
const taskContexts: Record<string, TaskContext> = {};
class TaskContext {
private taskId: string;
private taskType: string | null;
private progressCount: number;
@@ -43,11 +42,11 @@ class TaskContext {
increaseProgressCount() {
this.progressCount++;
if (Date.now() - this.lastSentCountTs >= 300 && this.taskId !== 'no-progress-reporting') {
if (Date.now() - this.lastSentCountTs >= 300 && this.taskId !== "no-progress-reporting") {
this.lastSentCountTs = Date.now();
ws.sendMessageToAllClients({
type: 'taskProgressCount',
type: "taskProgressCount",
taskId: this.taskId,
taskType: this.taskType,
data: this.data,
@@ -58,7 +57,7 @@ class TaskContext {
reportError(message: string) {
ws.sendMessageToAllClients({
type: 'taskError',
type: "taskError",
taskId: this.taskId,
taskType: this.taskType,
data: this.data,
@@ -68,7 +67,7 @@ class TaskContext {
taskSucceeded(result?: string | Record<string, string | undefined>) {
ws.sendMessageToAllClients({
type: 'taskSucceeded',
type: "taskSucceeded",
taskId: this.taskId,
taskType: this.taskType,
data: this.data,

View File

@@ -1,4 +1,4 @@
import { Menu, Tray } from 'electron';
import { Menu, Tray } from "electron";
import path from "path";
import windowService from "./window.js";
import optionService from "./options.js";
@@ -9,56 +9,52 @@ let tray: Tray;
// is minimized
let isVisible = true;
// Inspired by https://github.com/signalapp/Signal-Desktop/blob/dcb5bb672635c4b29a51adec8a5658e3834ec8fc/app/tray_icon.ts#L20
const getIconSize = () => {
switch (process.platform) {
case 'darwin':
case "darwin":
return 16;
case 'win32':
case "win32":
return 32;
default:
return 256;
}
}
};
const getIconPath = () => {
const iconSize = getIconSize();
return path.join(
path.dirname(fileURLToPath(import.meta.url)),
"../..",
"images",
"app-icons",
"png",
`${iconSize}x${iconSize}.png`
)
}
return path.join(path.dirname(fileURLToPath(import.meta.url)), "../..", "images", "app-icons", "png", `${iconSize}x${iconSize}.png`);
};
const registerVisibilityListener = () => {
const mainWindow = windowService.getMainWindow();
if (!mainWindow) { return; }
if (!mainWindow) {
return;
}
// They need to be registered before the tray updater is registered
mainWindow.on('show', () => {
mainWindow.on("show", () => {
isVisible = true;
updateTrayMenu();
});
mainWindow.on('hide', () => {
mainWindow.on("hide", () => {
isVisible = false;
updateTrayMenu();
});
mainWindow.on("minimize", updateTrayMenu);
mainWindow.on("maximize", updateTrayMenu);
}
};
const updateTrayMenu = () => {
const mainWindow = windowService.getMainWindow();
if (!mainWindow) { return; }
if (!mainWindow) {
return;
}
const contextMenu = Menu.buildFromTemplate([
{
label: isVisible ? 'Hide' : 'Show',
type: 'normal',
label: isVisible ? "Hide" : "Show",
type: "normal",
click: () => {
if (isVisible) {
mainWindow.hide();
@@ -69,22 +65,24 @@ const updateTrayMenu = () => {
}
},
{
type: 'separator'
type: "separator"
},
{
label: 'Quit',
type: 'normal',
label: "Quit",
type: "normal",
click: () => {
mainWindow.close();
}
},
}
]);
tray?.setContextMenu(contextMenu);
}
};
const changeVisibility = () => {
const window = windowService.getMainWindow();
if (!window) { return; }
if (!window) {
return;
}
if (isVisible) {
window.hide();
@@ -92,7 +90,7 @@ const changeVisibility = () => {
window.show();
window.focus();
}
}
};
function createTray() {
if (optionService.getOptionBool("disableTray")) {
@@ -100,9 +98,9 @@ function createTray() {
}
tray = new Tray(getIconPath());
tray.setToolTip('TriliumNext Notes')
tray.setToolTip("TriliumNext Notes");
// Restore focus
tray.on('click', changeVisibility)
tray.on("click", changeVisibility);
updateTrayMenu();
registerVisibilityListener();
@@ -110,4 +108,4 @@ function createTray() {
export default {
createTray
}
};

Some files were not shown because too many files have changed in this diff Show More