Merge branch 'TriliumNext:develop' into develop

This commit is contained in:
CobriMediaJulien
2024-12-15 15:36:44 +01:00
committed by GitHub
227 changed files with 10872 additions and 62622 deletions

View File

@@ -8,11 +8,10 @@ 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 { AppRequest } from '../routes/route-interface.js';
const noAuthentication = config.General && config.General.noAuthentication === true;
function checkAuth(req: AppRequest, res: Response, next: NextFunction) {
function checkAuth(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
res.redirect("setup");
}
@@ -26,7 +25,7 @@ function checkAuth(req: AppRequest, res: Response, next: NextFunction) {
// for electron things which need network stuff
// currently, we're doing that for file upload because handling form data seems to be difficult
function checkApiAuthOrElectron(req: AppRequest, res: Response, next: NextFunction) {
function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) {
if (!req.session.loggedIn && !utils.isElectron() && !noAuthentication) {
reject(req, res, "Logged in session not found");
}
@@ -35,7 +34,7 @@ function checkApiAuthOrElectron(req: AppRequest, res: Response, next: NextFuncti
}
}
function checkApiAuth(req: AppRequest, 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");
}
@@ -44,7 +43,7 @@ function checkApiAuth(req: AppRequest, res: Response, next: NextFunction) {
}
}
function checkAppInitialized(req: AppRequest, res: Response, next: NextFunction) {
function checkAppInitialized(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
res.redirect("setup");
}
@@ -53,7 +52,7 @@ function checkAppInitialized(req: AppRequest, res: Response, next: NextFunction)
}
}
function checkPasswordSet(req: AppRequest, res: Response, next: NextFunction) {
function checkPasswordSet(req: Request, res: Response, next: NextFunction) {
if (!utils.isElectron() && !passwordService.isPasswordSet()) {
res.redirect("set-password");
} else {
@@ -61,7 +60,7 @@ function checkPasswordSet(req: AppRequest, res: Response, next: NextFunction) {
}
}
function checkPasswordNotSet(req: AppRequest, res: Response, next: NextFunction) {
function checkPasswordNotSet(req: Request, res: Response, next: NextFunction) {
if (!utils.isElectron() && passwordService.isPasswordSet()) {
res.redirect("login");
} else {
@@ -69,7 +68,7 @@ function checkPasswordNotSet(req: AppRequest, res: Response, next: NextFunction)
}
}
function checkAppNotInitialized(req: AppRequest, res: Response, next: NextFunction) {
function checkAppNotInitialized(req: Request, res: Response, next: NextFunction) {
if (sqlInit.isDbInitialized()) {
reject(req, res, "App already initialized.");
}
@@ -78,7 +77,7 @@ function checkAppNotInitialized(req: AppRequest, res: Response, next: NextFuncti
}
}
function checkEtapiToken(req: AppRequest, res: Response, next: NextFunction) {
function checkEtapiToken(req: Request, res: Response, next: NextFunction) {
if (etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
next();
}
@@ -87,7 +86,7 @@ function checkEtapiToken(req: AppRequest, res: Response, next: NextFunction) {
}
}
function reject(req: AppRequest, res: Response, message: string) {
function reject(req: Request, res: Response, message: string) {
log.info(`${req.method} ${req.path} rejected with 401 ${message}`);
res.setHeader("Content-Type", "text/plain")
@@ -95,7 +94,7 @@ function reject(req: AppRequest, res: Response, message: string) {
.send(message);
}
function checkCredentials(req: AppRequest, res: Response, next: NextFunction) {
function checkCredentials(req: Request, res: Response, next: NextFunction) {
if (!sqlInit.isDbInitialized()) {
res.setHeader("Content-Type", "text/plain")
.status(400)
@@ -111,6 +110,13 @@ function checkCredentials(req: AppRequest, res: Response, next: NextFunction) {
}
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.');
return;
}
const auth = Buffer.from(header, 'base64').toString();
const colonIndex = auth.indexOf(':');
const password = colonIndex === -1 ? "" : auth.substr(colonIndex + 1);

View File

@@ -9,6 +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 cloningService from "./cloning.js";
import appInfo from "./app_info.js";
import searchService from "./search/services/search.js";
@@ -94,6 +95,12 @@ interface Api {
*/
xml2js: typeof xml2js;
/**
* 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
@@ -397,6 +404,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
this.axios = axios;
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);

View File

@@ -8,6 +8,7 @@ export default [
{ 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' },

View File

@@ -54,7 +54,7 @@ enum Command {
* duplicate subtrees. This way, all instances will generate the same structure with the same IDs.
*/
let HIDDEN_SUBTREE_DEFINITION = buildHiddenSubtreeDefinition();
let hiddenSubtreeDefinition: Item;
function buildHiddenSubtreeDefinition(): Item {
return {
@@ -288,11 +288,11 @@ function checkHiddenSubtree(force = false, extraOpts: CheckHiddenExtraOpts = {})
return;
}
if (force) {
HIDDEN_SUBTREE_DEFINITION = buildHiddenSubtreeDefinition();
if (!hiddenSubtreeDefinition || force) {
hiddenSubtreeDefinition = buildHiddenSubtreeDefinition();
}
checkHiddenSubtreeRecursively('root', HIDDEN_SUBTREE_DEFINITION, extraOpts);
checkHiddenSubtreeRecursively('root', hiddenSubtreeDefinition, extraOpts);
}
function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOpts: CheckHiddenExtraOpts = {}) {

View File

@@ -52,13 +52,15 @@ function sanitize(dirtyHtml: string) {
return sanitizeHtml(dirtyHtml, {
allowedTags,
allowedAttributes: {
'*': [ 'class', 'style', 'title', 'src', 'href', 'hash', 'disabled', 'align', 'alt', 'center', 'data-*' ]
"*": [ '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', 'irc', 'gemini', 'git',
'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', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack'
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero'
],
nonTextTags: [
'head'

View File

@@ -6,7 +6,7 @@ import protectedSessionService from "./protected_session.js";
import noteService from "./notes.js";
import optionService from "./options.js";
import sql from "./sql.js";
import jimp from "jimp";
import { Jimp } from "jimp";
import imageType from "image-type";
import sanitizeFilename from "sanitize-filename";
import isSvg from "is-svg";
@@ -15,7 +15,7 @@ import htmlSanitizer from "./html_sanitizer.js";
async function processImage(uploadBuffer: Buffer, originalName: string, shrinkImageSwitch: boolean) {
const compressImages = optionService.getOptionBool("compressImages");
const origImageFormat = getImageType(uploadBuffer);
const origImageFormat = await getImageType(uploadBuffer);
if (!origImageFormat || !["jpg", "png"].includes(origImageFormat.ext)) {
shrinkImageSwitch = false;
@@ -30,7 +30,7 @@ async function processImage(uploadBuffer: Buffer, originalName: string, shrinkIm
if (compressImages && shrinkImageSwitch) {
finalImageBuffer = await shrinkImage(uploadBuffer, originalName);
imageFormat = getImageType(finalImageBuffer);
imageFormat = await getImageType(finalImageBuffer);
} else {
finalImageBuffer = uploadBuffer;
imageFormat = origImageFormat || {
@@ -44,16 +44,12 @@ async function processImage(uploadBuffer: Buffer, originalName: string, shrinkIm
};
}
function getImageType(buffer: Buffer) {
if (isSvg(buffer)) {
return {
ext: 'svg'
}
async function getImageType(buffer: Buffer) {
if (isSvg(buffer.toString())) {
return { ext: 'svg' }
}
else {
return imageType(buffer) || {
ext: "jpg"
}; // optimistic JPG default
return await imageType(buffer) || { ext: "jpg" }; // optimistic JPG default
}
}
@@ -212,21 +208,19 @@ async function resize(buffer: Buffer, quality: number) {
const start = Date.now();
const image = await jimp.read(buffer);
const image = await Jimp.read(buffer);
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > imageMaxWidthHeight) {
image.resize(imageMaxWidthHeight, jimp.AUTO);
image.resize({ w: imageMaxWidthHeight });
}
else if (image.bitmap.height > imageMaxWidthHeight) {
image.resize(jimp.AUTO, imageMaxWidthHeight);
image.resize({ h: imageMaxWidthHeight });
}
image.quality(quality);
// 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.getBufferAsync(jimp.MIME_JPEG);
const resultBuffer = await image.getBuffer("image/jpeg", { quality });
log.info(`Resizing image of ${resultBuffer.byteLength} took ${Date.now() - start}ms`);

View File

@@ -134,9 +134,11 @@ const defaultOptions: DefaultOption[] = [
// Text note configuration
{ name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true },
{ name: "textNoteEditorMultilineToolbar", value: "false", isSynced: true },
// 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',

View File

@@ -9,6 +9,8 @@ class SearchContext {
includeArchivedNotes: boolean;
includeHiddenNotes: boolean;
ignoreHoistedNote: boolean;
/** Whether to ignore certain attributes from the search such as ~internalLink. */
ignoreInternalAttributes: boolean;
ancestorNoteId?: string;
ancestorDepth?: string;
orderBy?: string;
@@ -28,6 +30,7 @@ class SearchContext {
this.includeArchivedNotes = !!params.includeArchivedNotes;
this.includeHiddenNotes = !!params.includeHiddenNotes;
this.ignoreHoistedNote = !!params.ignoreHoistedNote;
this.ignoreInternalAttributes = !!params.ignoreInternalAttributes;
this.ancestorNoteId = params.ancestorNoteId;
if (!this.ancestorNoteId && !this.ignoreHoistedNote) {

View File

@@ -83,7 +83,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
return `"${startIndex !== 0 ? "..." : ""}${searchContext.originalQuery.substr(startIndex, endIndex - startIndex)}${endIndex !== searchContext.originalQuery.length ? "..." : ""}"`;
}
function resolveConstantOperand() {
const resolveConstantOperand = () => {
const operand = tokens[i];
if (!operand.inQuotes
@@ -136,7 +136,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
return date.format(format);
}
function parseNoteProperty(): Expression | undefined | null {
const parseNoteProperty: () => Expression | undefined | null = () => {
if (tokens[i].token !== '.') {
searchContext.addError('Expected "." to separate field path');
return;

View File

@@ -346,6 +346,7 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
includeArchivedNotes: false,
includeHiddenNotes: true,
fuzzyAttributeSearch: true,
ignoreInternalAttributes: true,
ancestorNoteId: hoistedNoteService.isHoistedInHiddenSubtree()
? 'root'
: hoistedNoteService.getHoistedNoteId()
@@ -355,7 +356,7 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
const trimmed = allSearchResults.slice(0, 200);
highlightSearchResults(trimmed, searchContext.highlightedTokens);
highlightSearchResults(trimmed, searchContext.highlightedTokens, searchContext.ignoreInternalAttributes);
return trimmed.map(result => {
return {
@@ -367,7 +368,10 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
});
}
function highlightSearchResults(searchResults: SearchResult[], highlightedTokens: string[]) {
/**
* @param ignoreInternalAttributes whether to ignore certain attributes from the search such as ~internalLink.
*/
function highlightSearchResults(searchResults: SearchResult[], highlightedTokens: string[], ignoreInternalAttributes = false) {
highlightedTokens = Array.from(new Set(highlightedTokens));
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
@@ -395,6 +399,10 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
}
for (const attr of note.getAttributes()) {
if (attr.type === "relation" && attr.name === "internalLink" && ignoreInternalAttributes) {
continue;
}
if (highlightedTokens.find(token => utils.normalize(attr.name).includes(token)
|| utils.normalize(attr.value).includes(token))) {

View File

@@ -12,6 +12,8 @@ export interface SearchParams {
includeArchivedNotes?: boolean;
includeHiddenNotes?: boolean;
ignoreHoistedNote?: boolean;
/** Whether to ignore certain attributes from the search such as ~internalLink. */
ignoreInternalAttributes?: boolean;
ancestorNoteId?: string;
ancestorDepth?: string;
orderBy?: string;

View File

@@ -347,7 +347,6 @@ export default {
/**
* Get single value from the given query - first column from first returned row.
*
* @method
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns single value
@@ -357,7 +356,6 @@ export default {
/**
* Get first returned row.
*
* @method
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - map of column name to column value
@@ -368,7 +366,6 @@ export default {
/**
* Get all returned rows.
*
* @method
* @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
@@ -381,7 +378,6 @@ export default {
/**
* Get a map of first column mapping to second column.
*
* @method
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - map of first column to second column
@@ -391,7 +387,6 @@ export default {
/**
* Get a first column in an array.
*
* @method
* @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
@@ -401,7 +396,6 @@ export default {
/**
* Execute SQL
*
* @method
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
*/

View File

@@ -8,7 +8,7 @@ import sqlInit from "./sql_init.js";
import cls from "./cls.js";
import keyboardActionsService from "./keyboard_actions.js";
import remoteMain from "@electron/remote/main/index.js";
import { App, BrowserWindow, WebContents, ipcMain } from 'electron';
import { App, BrowserWindow, BrowserWindowConstructorOptions, WebContents, ipcMain } from 'electron';
import { fileURLToPath } from "url";
import { dirname } from "path";
@@ -31,7 +31,7 @@ async function createExtraWindow(extraWindowHash: string) {
contextIsolation: false,
spellcheck: spellcheckEnabled
},
frame: optionService.getOptionBool('nativeTitleBarVisible'),
...getWindowExtraOpts(),
icon: getIcon()
});
@@ -71,6 +71,8 @@ async function createMainWindow(app: App) {
const { BrowserWindow } = (await import('electron')); // should not be statically imported
mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
@@ -82,9 +84,9 @@ async function createMainWindow(app: App) {
contextIsolation: false,
spellcheck: spellcheckEnabled,
webviewTag: true
},
frame: optionService.getOptionBool('nativeTitleBarVisible'),
icon: getIcon()
},
icon: getIcon(),
...getWindowExtraOpts()
});
mainWindowState.manage(mainWindow);
@@ -110,6 +112,33 @@ async function createMainWindow(app: App) {
});
}
function getWindowExtraOpts() {
const extraOpts: Partial<BrowserWindowConstructorOptions> = {};
const isMac = (process.platform === "darwin");
const isWindows = (process.platform === "win32");
if (!optionService.getOptionBool('nativeTitleBarVisible')) {
if (isMac) {
extraOpts.titleBarStyle = "hiddenInset";
extraOpts.titleBarOverlay = true;
} else if (isWindows) {
extraOpts.titleBarStyle = "hidden";
extraOpts.titleBarOverlay = true;
} else {
// Linux or other platforms.
extraOpts.frame = false;
}
}
// Window effects (Mica)
if (optionService.getOptionBool('backgroundEffects') && isWindows) {
extraOpts.backgroundMaterial = "auto";
}
return extraOpts;
}
function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) {
remoteMain.enable(webContents);

View File

@@ -18,7 +18,7 @@ if (env.isDev()) {
const debounce = (await import("debounce")).default;
const debouncedReloadFrontend = debounce(() => reloadFrontend("source code change"), 200);
chokidar
.watch('src/public')
.watch(utils.isElectron() ? 'dist/src/public' : 'src/public')
.on('add', debouncedReloadFrontend)
.on('change', debouncedReloadFrontend)
.on('unlink', debouncedReloadFrontend);