Merge branch 'master' into excali-17-2

This commit is contained in:
zadam
2024-03-06 06:53:19 +01:00
56 changed files with 2802 additions and 2331 deletions

View File

@@ -161,6 +161,13 @@ class BRevision extends AbstractBeccaEntity {
return this.getAttachments().filter(attachment => attachment.title === title)[0];
}
/**
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
*/
eraseRevision() {
require("../../services/erase.js").eraseRevisions([this.revisionId]);
}
beforeSaving() {
super.beforeSaving();

View File

@@ -427,6 +427,116 @@ paths:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
/attachments:
post:
description: create an attachment
operationId: postAttachment
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateAttachment'
responses:
'201':
description: attachment created
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Attachment'
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
/attachments/{attachmentId}:
parameters:
- name: attachmentId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
get:
description: Returns an attachment identified by its ID
operationId: getAttachmentById
responses:
'200':
description: attachment response
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Attachment'
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
patch:
description: patch an attachment identified by the attachmentId with changes in the body. Only role, mime, title, and position are patchable.
operationId: patchAttachmentById
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Attachment'
responses:
'200':
description: attribute updated
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Attachment'
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
delete:
description: deletes an attachment based on the attachmentId supplied.
operationId: deleteAttachmentById
responses:
'204':
description: attachment deleted
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
/attachments/{attachmentId}/content:
parameters:
- name: attachmentId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
get:
description: Returns attachment content identified by its ID
operationId: getAttachmentContent
responses:
'200':
description: attachment content response
content:
text/html:
schema:
type: string
put:
description: Updates attachment content identified by its ID
operationId: putAttachmentContentById
requestBody:
description: html content of attachment
required: true
content:
text/plain:
schema:
type: string
responses:
'204':
description: attachment content updated
/attributes:
post:
description: create an attribute for a given note
@@ -474,7 +584,7 @@ paths:
schema:
$ref: '#/components/schemas/Error'
patch:
description: patch a attribute identified by the attributeId with changes in the body. For labels, only value and position can be updated. For relations, only position can be updated. If you want to modify other properties, you need to delete the old attribute and create a new one.
description: patch an attribute identified by the attributeId with changes in the body. For labels, only value and position can be updated. For relations, only position can be updated. If you want to modify other properties, you need to delete the old attribute and create a new one.
operationId: patchAttributeById
requestBody:
required: true
@@ -496,7 +606,7 @@ paths:
schema:
$ref: '#/components/schemas/Error'
delete:
description: deletes a attribute based on the attributeId supplied.
description: deletes an attribute based on the attributeId supplied.
operationId: deleteAttributeById
responses:
'204':
@@ -884,6 +994,57 @@ components:
$ref: '#/components/schemas/Note'
branch:
$ref: '#/components/schemas/Branch'
Attachment:
type: object
description: Attachment is owned by a note, has title and content
properties:
attachmentId:
$ref: '#/components/schemas/EntityId'
readOnly: true
ownerId:
$ref: '#/components/schemas/EntityId'
description: identifies the owner of the attachment, is either noteId or revisionId
role:
type: string
mime:
type: string
title:
type: string
position:
type: integer
format: int32
blobId:
type: string
description: ID of the blob object which effectively serves as a content hash
dateModified:
$ref: '#/components/schemas/LocalDateTime'
readOnly: true
utcDateModified:
$ref: '#/components/schemas/UtcDateTime'
readOnly: true
utcDateScheduledForErasureSince:
$ref: '#/components/schemas/UtcDateTime'
readOnly: true
contentLength:
type: integer
format: int32
CreateAttachment:
type: object
properties:
ownerId:
$ref: '#/components/schemas/EntityId'
description: identifies the owner of the attachment, is either noteId or revisionId
role:
type: string
mime:
type: string
title:
type: string
content:
type: string
position:
type: integer
format: int32
Attribute:
type: object
description: Attribute (Label, Relation) is a key-value record attached to a note.

View File

@@ -0,0 +1,44 @@
import utils from "../services/utils.js";
import contextMenu from "./context_menu.js";
import imageService from "../services/image.js";
const PROP_NAME = "imageContextMenuInstalled";
function setupContextMenu($image) {
if (!utils.isElectron() || $image.prop(PROP_NAME)) {
return;
}
$image.prop(PROP_NAME, true);
$image.on('contextmenu', e => {
e.preventDefault();
contextMenu.show({
x: e.pageX,
y: e.pageY,
items: [
{
title: "Copy reference to clipboard",
command: "copyImageReferenceToClipboard",
uiIcon: "bx bx-empty"
},
{title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty"},
],
selectMenuItemHandler: ({command}) => {
if (command === 'copyImageReferenceToClipboard') {
imageService.copyImageReferenceToClipboard($image);
} else if (command === 'copyImageToClipboard') {
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
utils.dynamicRequire('electron');
webContents.copyImageAt(e.pageX, e.pageY);
} else {
throw new Error(`Unrecognized command '${command}'`);
}
}
});
});
}
export default {
setupContextMenu
};

View File

@@ -107,6 +107,13 @@ async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
return false;
}
try {
await activateParentNotePath();
}
catch (e) {
console.error(e);
}
const taskId = utils.randomString(10);
let counter = 0;
@@ -134,6 +141,16 @@ async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
return true;
}
async function activateParentNotePath() {
// this is not perfect, maybe we should find the next/previous sibling, but that's more complex
const activeContext = appContext.tabManager.getActiveContext();
const parentNotePathArr = activeContext.notePathArray.slice(0, -1);
if (parentNotePathArr.length > 0) {
activeContext.setNote(parentNotePathArr.join("/"));
}
}
async function moveNodeUpInHierarchy(node) {
if (hoistedNoteService.isHoistedNode(node)
|| hoistedNoteService.isTopLevelNode(node)

View File

@@ -9,6 +9,7 @@ import linkService from "./link.js";
import treeService from "./tree.js";
import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js";
let idCounter = 1;
@@ -148,6 +149,8 @@ function renderImage(entity, $renderedContent, options = {}) {
});
});
}
imageContextMenuService.setupContextMenu($img);
}
function renderFile(entity, type, $renderedContent) {

View File

@@ -48,11 +48,11 @@ async function checkNoteAccess(notePath, noteContext) {
const hoistedNoteId = noteContext.hoistedNoteId;
if (!resolvedNotePath.includes(hoistedNoteId) && !resolvedNotePath.includes('_hidden')) {
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes('_hidden') || resolvedNotePath.includes('_lbBookmarks'))) {
const requestedNote = await froca.getNote(treeService.getNoteIdFromUrl(resolvedNotePath));
const hoistedNote = await froca.getNote(hoistedNoteId);
if (!hoistedNote.hasAncestor('_hidden')
if ((!hoistedNote.hasAncestor('_hidden') || resolvedNotePath.includes('_lbBookmarks'))
&& !await dialogService.confirm(`Requested note '${requestedNote.title}' is outside of hoisted note '${hoistedNote.title}' subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?`)) {
return false;
}

View File

@@ -4,6 +4,7 @@ import froca from "./froca.js";
import attributeRenderer from "./attribute_renderer.js";
import libraryLoader from "./library_loader.js";
import treeService from "./tree.js";
import utils from "./utils.js";
const TPL = `
<div class="note-list">
@@ -215,7 +216,11 @@ class NoteListRenderer {
if (highlightedTokens.length > 0) {
await libraryLoader.requireLibrary(libraryLoader.MARKJS);
this.highlightRegex = new RegExp(highlightedTokens.join("|"), 'gi');
const regex = highlightedTokens
.map(token => utils.escapeRegExp(token))
.join("|");
this.highlightRegex = new RegExp(regex, 'gi');
} else {
this.highlightRegex = null;
}

View File

@@ -200,7 +200,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
this.attributeDetailWidget.hide();
});
this.$editor.on('blur', () => this.save());
this.$editor.on('blur', () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160
this.$addNewAttributeButton = this.$widget.find('.add-new-attribute-button');
this.$addNewAttributeButton.on('click', e => this.addNewAttribute(e));

View File

@@ -8,10 +8,13 @@ export default class RightPaneContainer extends FlexContainer {
this.id('right-pane');
this.css('height', '100%');
this.collapsible();
this.rightPaneHidden = false;
}
isEnabled() {
return super.isEnabled()
&& !this.rightPaneHidden
&& this.children.length > 0
&& !!this.children.find(ch => ch.isEnabled() && ch.canBeShown());
}
@@ -44,4 +47,10 @@ export default class RightPaneContainer extends FlexContainer {
splitService.setupRightPaneResizer();
}
}
toggleRightPaneEvent() {
this.rightPaneHidden = !this.rightPaneHidden;
this.reEvaluateRightPaneVisibilityCommand();
}
}

View File

@@ -1091,7 +1091,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return;
}
const nodeCtx = this.#getActiveNodeCtx();
const activeNode = this.getActiveNode();
const activeNodeFocused = activeNode?.hasFocus();
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const refreshCtx = {
noteIdsToUpdate: new Set(),
@@ -1108,7 +1110,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
await this.#executeTreeUpdates(refreshCtx, loadResults);
await this.#setActiveNode(nodeCtx, movedActiveNode, parentsOfAddedNodes);
await this.#setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes);
if (refreshCtx.noteIdsToReload.size > 0 || refreshCtx.noteIdsToUpdate.size > 0) {
// workaround for https://github.com/mar10/fancytree/issues/1054
@@ -1257,73 +1259,42 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
}
#getActiveNodeCtx() {
const nodeCtx = {
activeNotePath: null,
activeNodeFocused: null,
nextNotePath: null
};
const activeNode = this.getActiveNode();
nodeCtx.activeNodeFocused = activeNode?.hasFocus();
nodeCtx.activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
nodeCtx.nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
return nodeCtx;
}
async #setActiveNode(nodeCtx, movedActiveNode, parentsOfAddedNodes) {
async #setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes) {
if (movedActiveNode) {
for (const parentNode of parentsOfAddedNodes) {
const foundNode = (parentNode.getChildren() || []).find(child => child.data.noteId === movedActiveNode.data.noteId);
if (foundNode) {
nodeCtx.activeNotePath = treeService.getNotePath(foundNode);
activeNotePath = treeService.getNotePath(foundNode);
break;
}
}
}
if (!nodeCtx.activeNotePath) {
if (!activeNotePath) {
return;
}
let node = await this.expandToNote(nodeCtx.activeNotePath, false);
if (node && node.data.noteId !== treeService.getNoteIdFromUrl(nodeCtx.activeNotePath)) {
let node = await this.expandToNote(activeNotePath, false);
if (node && node.data.noteId !== treeService.getNoteIdFromUrl(activeNotePath)) {
// if the active note has been moved elsewhere then it won't be found by the path,
// so we switch to the alternative of trying to find it by noteId
const notesById = this.getNodesByNoteId(treeService.getNoteIdFromUrl(nodeCtx.activeNotePath));
const notesById = this.getNodesByNoteId(treeService.getNoteIdFromUrl(activeNotePath));
// if there are multiple clones, then we'd rather not activate anyone
node = notesById.length === 1 ? notesById[0] : null;
}
if (node) {
if (nodeCtx.activeNodeFocused) {
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
this.tree.$container.focus();
}
await node.setActive(true, {noEvents: true, noFocus: !nodeCtx.activeNodeFocused});
} else {
// this is used when the original note has been deleted, and we want to move the focus to the note above/below
node = await this.expandToNote(nodeCtx.nextNotePath, false);
if (node) {
// FIXME: this is conceptually wrong
// here note tree is responsible for updating global state of the application
// this should be done by NoteContext / TabManager and note tree should only listen to
// changes in active note and just set the "active" state
// We don't await since that can bring up infinite cycles when e.g. custom widget does some backend requests which wait for max sync ID processed
appContext.tabManager.getActiveContext().setNote(nodeCtx.nextNotePath).then(() => {
const newActiveNode = this.getActiveNode();
// return focus if the previously active node was also focused
if (newActiveNode && nodeCtx.activeNodeFocused) {
newActiveNode.setFocus(true);
}
});
}
if (!node) {
return;
}
if (activeNodeFocused) {
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
this.tree.$container.focus();
}
await node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
}
sortChildren(node) {

View File

@@ -30,9 +30,14 @@ const TPL = `
height: 40px;
width: 40px;
}
.title-bar-buttons .top-btn.active{
background-color:var(--accented-background-color);
}
.title-bar-buttons .btn.focus, .title-bar-buttons .btn:focus {
box-shadow: none;
}
</style>
<!-- divs act as a hitbox for the buttons, making them clickable on corners -->

View File

@@ -1,7 +1,7 @@
import utils from "../../services/utils.js";
import TypeWidget from "./type_widget.js";
import libraryLoader from "../../services/library_loader.js";
import contextMenu from "../../menus/context_menu.js";
import imageContextMenuService from "../../menus/image_context_menu.js";
import imageService from "../../services/image.js";
const TPL = `
@@ -55,36 +55,7 @@ class ImageTypeWidget extends TypeWidget {
});
});
if (utils.isElectron()) {
// for browser, we want to let the native menu
this.$imageView.on('contextmenu', e => {
e.preventDefault();
contextMenu.show({
x: e.pageX,
y: e.pageY,
items: [
{
title: "Copy reference to clipboard",
command: "copyImageReferenceToClipboard",
uiIcon: "bx bx-empty"
},
{title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty"},
],
selectMenuItemHandler: ({command}) => {
if (command === 'copyImageReferenceToClipboard') {
imageService.copyImageReferenceToClipboard(this.$imageWrapper);
} else if (command === 'copyImageToClipboard') {
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
utils.dynamicRequire('electron');
webContents.copyImageAt(e.pageX, e.pageY);
} else {
throw new Error(`Unrecognized command '${command}'`);
}
}
});
});
}
imageContextMenuService.setupContextMenu(this.$imageView);
super.doRender();
}

View File

@@ -18,6 +18,8 @@ const TPL = `
<h5>Highlights List visibility</h5>
<p>You can hide the highlights widget per-note by adding a <code>#hideHighlightWidget</code> label.</p>
<p>You can configure a keyboard shortcut for quickly toggling the right pane (including Highlights) in the Options -> Shortcuts (name "toggleRightPane").</p>
</div>`;
export default class HighlightsListOptions extends OptionsWidget {

View File

@@ -11,6 +11,8 @@ const TPL = `
</div>
<p>You can also use this option to effectively disable TOC by setting a very high number.</p>
<p>You can configure a keyboard shortcut for quickly toggling the right pane (including TOC) in the Options -> Shortcuts (name "toggleRightPane").</p>
</div>`;
export default class TableOfContentsOptions extends OptionsWidget {

View File

@@ -9,14 +9,9 @@
"start_url": "/",
"icons": [
{
"src": "/assets/vX/images/app-icons/ios/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "/assets/vX/images/app-icons/png/512x512.png",
"sizes": "512x512",
"type": "image/png"
"src": "favicon.ico",
"sizes": "180x180 512x512",
"type": "image/x-icon"
}
]
}

View File

@@ -3,6 +3,7 @@ body {
--ck-color-base-text: var(--main-text-color);
--ck-color-base-foreground: var(--accented-background-color);
--ck-color-base-background: var(--main-background-color);
--ck-color-focus-border: var(--main-border-color);
--ck-color-text: var(--main-text-color);
--ck-color-shadow-drop: var(--main-background-color);

View File

@@ -88,3 +88,7 @@ body .CodeMirror {
.excalidraw.theme--dark {
--theme-filter: invert(80%) hue-rotate(180deg) !important;
}
body .todo-list input[type="checkbox"]:not(:checked):before {
border-color: var(--muted-text-color) !important;
}

View File

@@ -39,7 +39,13 @@ function getLightAnonymizationScript() {
SELECT blobId FROM notes WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend')
UNION ALL
SELECT blobId FROM revisions WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend')
);`;
);
UPDATE options SET value = 'anonymized' WHERE name IN
('documentId', 'documentSecret', 'encryptedDataKey',
'passwordVerificationHash', 'passwordVerificationSalt',
'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')
AND value != '';`;
}
async function createAnonymizedCopy(type) {

View File

@@ -4,8 +4,8 @@ const build = require('./build.js');
const packageJson = require('../../package.json');
const {TRILIUM_DATA_DIR} = require('./data_dir.js');
const APP_DB_VERSION = 227;
const SYNC_VERSION = 31;
const APP_DB_VERSION = 228;
const SYNC_VERSION = 32;
const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = {

View File

@@ -43,12 +43,16 @@ const optionsService = require('./options.js');
*/
function BackendScriptApi(currentNote, apiParams) {
/**
* Note where the script started executing
* 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.
* @type {BNote}
*/
this.startNote = apiParams.startNote;
/**
* Note where the script is currently executing. 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.
* @type {BNote}
*/
this.currentNote = currentNote;
@@ -288,7 +292,7 @@ function BackendScriptApi(currentNote, apiParams) {
* @param {string} params.parentNoteId
* @param {string} params.title
* @param {string|Buffer} params.content
* @param {NoteType} params.type - text, code, file, image, search, book, relationMap, canvas
* @param {NoteType} params.type - text, code, file, image, search, book, relationMap, canvas, webView
* @param {string} [params.mime] - value is derived from default mimes for type
* @param {boolean} [params.isProtected=false]
* @param {boolean} [params.isExpanded=false]

View File

@@ -5,12 +5,14 @@ const utils = require('./utils.js');
function getBlobPojo(entityName, entityId) {
const entity = becca.getEntity(entityName, entityId);
if (!entity) {
throw new NotFoundError(`Entity ${entityName} '${entityId}' was not found.`);
}
const blob = becca.getBlob(entity);
if (!blob) {
throw new NotFoundError(`Blob ${entity.blobId} for ${entityName} '${entityId}' was not found.`);
}
const pojo = blob.getPojo();

View File

@@ -1 +1 @@
module.exports = { buildDate:"2023-12-07T00:03:59+01:00", buildRevision: "2e23c521c356c2305124f5df0f474532fa5f34ce" };
module.exports = { buildDate:"2024-03-03T06:58:18+01:00", buildRevision: "0ad337c8e806ba84d48d7b97aa46df52d9f236a8" };

View File

@@ -48,6 +48,14 @@ function isEntityEventsDisabled() {
return !!namespace.get('disableEntityEvents');
}
function setMigrationRunning(running) {
namespace.set('migrationRunning', !!running);
}
function isMigrationRunning() {
return !!namespace.get('migrationRunning');
}
function disableSlowQueryLogging(disable) {
namespace.set('disableSlowQueryLogging', disable);
}
@@ -102,5 +110,7 @@ module.exports = {
putEntityChange,
ignoreEntityChangeIds,
disableSlowQueryLogging,
isSlowQueryLoggingDisabled
isSlowQueryLoggingDisabled,
setMigrationRunning,
isMigrationRunning
};

View File

@@ -17,6 +17,9 @@ const {sanitizeAttributeName} = require('./sanitize_attribute_name.js');
const noteTypes = require('../services/note_types.js').getNoteTypeNames();
class ConsistencyChecks {
/**
* @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast)
*/
constructor(autoFix) {
this.autoFix = autoFix;
this.unrecoveredConsistencyErrors = false;

View File

@@ -37,6 +37,8 @@ function eraseNotes(noteIdsToErase) {
function setEntityChangesAsErased(entityChanges) {
for (const ec of entityChanges) {
ec.isErased = true;
// we're not changing hash here, not sure if good or not
// content hash check takes isErased flag into account, though
ec.utcDateChanged = dateUtils.utcNowDateTime();
entityChangesService.putEntityChangeWithForcedChange(ec);

View File

@@ -181,6 +181,8 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
noteIdToMeta[note.noteId] = meta;
// sort children for having a stable / reproducible export format
note.sortChildren();
const childBranches = note.getChildBranches()
.filter(branch => branch.noteId !== '_hidden');

View File

@@ -219,7 +219,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
{
actionName: "reopenLastTab",
defaultShortcuts: isElectron ? ["CommandOrControl+Shift+T"] : [],
description: "Repoens the last closed tab",
description: "Reopens the last closed tab",
scope: "window"
},
{
@@ -291,7 +291,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
{
actionName: "eigthTab",
defaultShortcuts: ["CommandOrControl+8"],
description: "Activates the eigth tab in the list",
description: "Activates the eighth tab in the list",
scope: "window"
},
{
@@ -302,7 +302,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
},
{
actionName: "lastTab",
defaultShortcuts: ["CommandOrControl+0"],
defaultShortcuts: [],
description: "Activates the last tab in the list",
scope: "window"
},
@@ -494,9 +494,16 @@ const DEFAULT_KEYBOARD_ACTIONS = [
separator: "Other"
},
{
actionName: "toggleRightPane",
defaultShortcuts: [],
description: "Toggle the display of the right pane, which includes Table of Contents and Highlights",
scope: "window"
},
{
actionName: "printActiveNote",
defaultShortcuts: [],
description: "Print active note",
scope: "window"
},
{

View File

@@ -5,12 +5,13 @@ const log = require('./log.js');
const utils = require('./utils.js');
const resourceDir = require('./resource_dir.js');
const appInfo = require('./app_info.js');
const cls = require('./cls.js');
async function migrate() {
const currentDbVersion = getDbVersion();
if (currentDbVersion < 214) {
log.error("Direct migration from your current version is not supported. Please upgrade to the latest v0.60.X first and only then to this version.");
log.error("Direct migration from your current version is not supported. Please upgrade to the latest v0.60.4 first and only then to this version.");
utils.crash();
return;
@@ -18,7 +19,7 @@ async function migrate() {
// backup before attempting migration
await backupService.backupNow(
// creating a special backup for versions 0.60.X, the changes in 0.61 are major.
// creating a special backup for version 0.60.4, the changes in 0.61 are major.
currentDbVersion === 214
? `before-migration-v060`
: 'before-migration'
@@ -51,6 +52,9 @@ async function migrate() {
// all migrations are executed in one transaction - upgrade either succeeds, or the user can stay at the old version
// otherwise if half of the migrations succeed, user can't use any version - DB is too "new" for the old app,
// and too old for the new app version.
cls.setMigrationRunning(true);
sql.transactional(() => {
for (const mig of migrations) {
try {
@@ -73,8 +77,11 @@ async function migrate() {
}
});
log.info("VACUUMing database, this might take a while ...");
sql.execute("VACUUM");
if (currentDbVersion === 214) {
// special VACUUM after the big migration
log.info("VACUUMing database, this might take a while ...");
sql.execute("VACUUM");
}
}
function executeMigration(mig) {

View File

@@ -471,6 +471,8 @@ function findRelationMapLinks(content, foundLinks) {
const imageUrlToAttachmentIdMapping = {};
async function downloadImage(noteId, imageUrl) {
const unescapedUrl = utils.unescapeHtml(imageUrl);
try {
let imageBuffer;
@@ -487,14 +489,14 @@ async function downloadImage(noteId, imageUrl) {
});
});
} else {
imageBuffer = await request.getImage(imageUrl);
imageBuffer = await request.getImage(unescapedUrl);
}
const parsedUrl = url.parse(imageUrl);
const parsedUrl = url.parse(unescapedUrl);
const title = path.basename(parsedUrl.pathname);
const imageService = require('../services/image.js');
const {attachment} = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true);
const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true);
imageUrlToAttachmentIdMapping[imageUrl] = attachment.attachmentId;
@@ -511,7 +513,7 @@ const downloadImagePromises = {};
function replaceUrl(content, url, attachment) {
const quotedUrl = utils.quoteRegex(url);
return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "ig"), ` src="api/attachments/${encodeURIComponent(attachment.title)}/image"`);
return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "ig"), ` src="api/attachments/${attachment.attachmentId}/image/${encodeURIComponent(attachment.title)}"`);
}
function downloadImages(noteId, content) {
@@ -636,6 +638,10 @@ function saveAttachments(note, content) {
content = `${content.substr(0, attachmentMatch.index)}<a class="reference-link" href="#root/${note.noteId}?viewMode=attachments&attachmentId=${attachment.attachmentId}">${title}</a>${content.substr(attachmentMatch.index + attachmentMatch[0].length)}`;
}
// removing absolute references to server to keep it working between instances,
// we also omit / at the beginning to keep the paths relative
content = content.replace(/src="[^"]*\/api\/attachments\//g, 'src="api/attachments/');
return content;
}
@@ -889,6 +895,11 @@ function scanForLinks(note, content) {
* Things which have to be executed after updating content, but asynchronously (separate transaction)
*/
async function asyncPostProcessContent(note, content) {
if (cls.isMigrationRunning()) {
// this is rarely needed for migrations, but can cause trouble by e.g. triggering downloads
return;
}
if (note.hasStringContent() && !utils.isString(content)) {
content = content.toString();
}

View File

@@ -63,10 +63,15 @@ function exec(opts) {
}
let responseStr = '';
let chunks = [];
response.on('data', chunk => responseStr += chunk);
response.on('data', chunk => chunks.push(chunk));
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')
if ([200, 201, 204].includes(response.statusCode)) {
try {
const jsonObj = responseStr.trim() ? JSON.parse(responseStr) : null;

View File

@@ -111,11 +111,7 @@ class NoteContentFulltextExp extends Expression {
if (type === 'text' && mime === 'text/html') {
if (!this.raw && content.length < 20000) { // striptags is slow for very large notes
// allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
content = striptags(content, ['a'], ' ');
// at least the closing tag can be easily stripped
content = content.replace(/<\/a>/ig, "");
content = this.stripTags(content);
}
content = content.replace(/&nbsp;/g, ' ');
@@ -123,6 +119,23 @@ class NoteContentFulltextExp extends Expression {
return content.trim();
}
stripTags(content) {
// 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'];
// replace tags which imply text separation with a space
content = striptags(content, [linkTag, ...inlineFormattingTags], ' ');
// replace the inline formatting tags (but not links) without a space
content = striptags(content, [linkTag], '');
// at least the closing link tag can be easily stripped
return content.replace(/<\/a>/ig, "");
}
}
module.exports = NoteContentFulltextExp;

View File

@@ -73,7 +73,6 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext
if (localEC?.isErased) {
eraseEntity(remoteEC); // make sure it's erased anyway
updateContext.alreadyErased++;
return false; // we won't save entitychange in this case
} else {
eraseEntity(remoteEC);
updateContext.erased++;
@@ -91,12 +90,17 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
}
if (!localEC || localEC.utcDateChanged < remoteEC.utcDateChanged || localEC.hash !== remoteEC.hash) {
if (!localEC
|| localEC.utcDateChanged < remoteEC.utcDateChanged
|| localEC.hash !== remoteEC.hash
|| localEC.isErased !== remoteEC.isErased
) {
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
}
return true;
} else if (localEC.hash !== remoteEC.hash && localEC.utcDateChanged > remoteEC.utcDateChanged) {
} else if ((localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased)
&& localEC.utcDateChanged > remoteEC.utcDateChanged) {
// the change on our side is newer than on the other side, so the other side should update
entityChangesService.putEntityChangeForOtherInstances(localEC);
@@ -148,7 +152,7 @@ function eraseEntity(entityChange) {
];
if (!entityNames.includes(entityName)) {
log.error(`Cannot erase entity '${entityName}', id '${entityId}'.`);
log.error(`Cannot erase ${entityName} '${entityId}'.`);
return;
}

View File

@@ -34,8 +34,8 @@ function hashedBlobId(content) {
// we don't want such + and / in the IDs
const kindaBase62Hash = base64Hash
.replace('+', 'X')
.replace('/', 'Y');
.replaceAll('+', 'X')
.replaceAll('/', 'Y');
// 20 characters of base62 gives us ~120 bit of entropy which is plenty enough
return kindaBase62Hash.substr(0, 20);

View File

@@ -105,10 +105,10 @@ function renderText(result, note) {
if (result.content.includes(`<span class="math-tex">`)) {
result.header += `
<script src="../../${assetPath}/libraries/katex/katex.min.js"></script>
<link rel="stylesheet" href="../../${assetPath}/libraries/katex/katex.min.css">
<script src="../../${assetPath}/libraries/katex/auto-render.min.js"></script>
<script src="../../${assetPath}/libraries/katex/mhchem.min.js"></script>
<script src="../../${assetPath}/node_modules/katex/dist/katex.min.js"></script>
<link rel="stylesheet" href="../../${assetPath}/node_modules/katex/dist/katex.min.css">
<script src="../../${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
<script src="../../${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.getElementById('content'));
@@ -137,7 +137,7 @@ function renderCode(result) {
function renderMermaid(result, note) {
result.content = `
<img src="api/images/${note.noteId}/${note.escapedTitle}?${note.utcDateModified}">
<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">
<hr>
<details>
<summary>Chart source</summary>
@@ -146,7 +146,7 @@ function renderMermaid(result, note) {
}
function renderImage(result, note) {
result.content = `<img src="api/images/${note.noteId}/${note.escapedTitle}?${note.utcDateModified}">`;
result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">`;
}
function renderFile(note, result) {

View File

@@ -490,6 +490,10 @@ class SNote extends AbstractShacaEntity {
return escape(this.title);
}
get encodedTitle() {
return encodeURIComponent(this.title);
}
getPojo() {
return {
noteId: this.noteId,

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<link rel="manifest" href="manifest.webmanifest">
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>Trilium Notes</title>
</head>
<body class="desktop heading-style-<%= headingStyle %>">

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="theme-color" content="#fff">
<title>Trilium Notes</title>
<link rel="manifest" href="manifest.webmanifest">
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<style>
.lds-roller {