Compare commits

...

22 Commits

Author SHA1 Message Date
zadam
d6046efa1b release 0.62.5 2024-01-08 00:05:13 +01:00
zadam
ee608fcf46 unescape HTML before downloading images, #4566 2024-01-08 00:03:11 +01:00
zadam
894b08a1b8 correctly save attachment URL, #4566 2024-01-07 23:51:38 +01:00
zadam
4e549baedc fix auto-download of images, closes #4566 2024-01-07 23:45:40 +01:00
zadam
6b6e42e9ba document attachment ETAPI APIs in OpenAPI spec, fixes #4559 2024-01-07 23:11:55 +01:00
zadam
0404b78fb8 fix loading katex in share #4558 2024-01-07 22:52:16 +01:00
zadam
439743d2b0 convert absolute image attachment URLs to relative without domain, fixes #4509 2023-12-27 23:22:40 +01:00
zadam
0ac397e7ff fix setNoteToParent API breakage, closes #4505 2023-12-11 23:05:05 +01:00
zadam
d243880099 release 0.62.4 2023-12-07 00:03:59 +01:00
zadam
2e23c521c3 electron upgrade 2023-12-06 23:54:17 +01:00
zadam
eb761b286f electron upgrade 2023-12-04 00:17:00 +01:00
zadam
d0f6ff5f98 fix erasing revisions 2023-12-04 00:11:24 +01:00
zadam
84feaabc52 release 0.62.3 2023-11-27 23:37:24 +01:00
zadam
a6036859b8 normalize strings before calculation hashes, #4435 2023-11-27 23:23:55 +01:00
zadam
93dcce2217 dragging notes from note tree will automatically insert them as images where appropriate (image, canvas, mermaid) 2023-11-27 10:38:19 +01:00
zadam
686af0c6a1 when canvas and mermaid are inserted using "include note", we insert them as images 2023-11-27 10:22:54 +01:00
zadam
d07f02b95f contrary to what I believed encodeURIComponent() is available also in node.js, #4478 2023-11-27 10:15:29 +01:00
zadam
ad74952194 fix thumbnails with chinese titles, closes #4478 2023-11-27 10:10:27 +01:00
zadam
10f3df3ed4 make sure content is string for post-processing 2023-11-26 23:51:04 +01:00
zadam
18e2e6779b remove title to fix closing highlights list, fixes #4471 2023-11-24 23:51:12 +01:00
zadam
ed129c307b fix printing math, closes #4470 2023-11-24 00:17:20 +01:00
zadam
8742e4bfe9 make sure the promoted attributes don't take the whole screen in mobile, #4468 2023-11-24 00:04:49 +01:00
25 changed files with 267 additions and 70 deletions

18
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.62.1-beta", "version": "0.62.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "trilium", "name": "trilium",
"version": "0.62.1-beta", "version": "0.62.4",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
@@ -81,7 +81,7 @@
}, },
"devDependencies": { "devDependencies": {
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron": "25.9.5", "electron": "25.9.8",
"electron-builder": "24.6.4", "electron-builder": "24.6.4",
"electron-packager": "17.1.2", "electron-packager": "17.1.2",
"electron-rebuild": "3.2.9", "electron-rebuild": "3.2.9",
@@ -4366,9 +4366,9 @@
} }
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "25.9.5", "version": "25.9.8",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.5.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-25.9.8.tgz",
"integrity": "sha512-gM7GXUSd3JVRcYbBnNOtZeNnE5MCJjtZTT8QyIxJvpQ0Dh9dz3hTuEL62dOwnMFW/l47ACQ6es/8qi01P4QGZA==", "integrity": "sha512-PGgp6PH46QVENHuAHc2NT1Su8Q1qov7qIl2jI5tsDpTibwV2zD8539AeWBQySeBU4dhbj9onIl7+1bXQ0wefBg==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
@@ -16964,9 +16964,9 @@
} }
}, },
"electron": { "electron": {
"version": "25.9.5", "version": "25.9.8",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.5.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-25.9.8.tgz",
"integrity": "sha512-gM7GXUSd3JVRcYbBnNOtZeNnE5MCJjtZTT8QyIxJvpQ0Dh9dz3hTuEL62dOwnMFW/l47ACQ6es/8qi01P4QGZA==", "integrity": "sha512-PGgp6PH46QVENHuAHc2NT1Su8Q1qov7qIl2jI5tsDpTibwV2zD8539AeWBQySeBU4dhbj9onIl7+1bXQ0wefBg==",
"requires": { "requires": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",

View File

@@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.62.2", "version": "0.62.5",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@@ -104,7 +104,7 @@
}, },
"devDependencies": { "devDependencies": {
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron": "25.9.5", "electron": "25.9.8",
"electron-builder": "24.6.4", "electron-builder": "24.6.4",
"electron-packager": "17.1.2", "electron-packager": "17.1.2",
"electron-rebuild": "3.2.9", "electron-rebuild": "3.2.9",

View File

@@ -427,6 +427,116 @@ paths:
application/json; charset=utf-8: application/json; charset=utf-8:
schema: schema:
$ref: '#/components/schemas/Error' $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: /attributes:
post: post:
description: create an attribute for a given note description: create an attribute for a given note
@@ -474,7 +584,7 @@ paths:
schema: schema:
$ref: '#/components/schemas/Error' $ref: '#/components/schemas/Error'
patch: 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 operationId: patchAttributeById
requestBody: requestBody:
required: true required: true
@@ -496,7 +606,7 @@ paths:
schema: schema:
$ref: '#/components/schemas/Error' $ref: '#/components/schemas/Error'
delete: delete:
description: deletes a attribute based on the attributeId supplied. description: deletes an attribute based on the attributeId supplied.
operationId: deleteAttributeById operationId: deleteAttributeById
responses: responses:
'204': '204':
@@ -884,6 +994,57 @@ components:
$ref: '#/components/schemas/Note' $ref: '#/components/schemas/Note'
branch: branch:
$ref: '#/components/schemas/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: Attribute:
type: object type: object
description: Attribute (Label, Relation) is a key-value record attached to a note. description: Attribute (Label, Relation) is a key-value record attached to a note.

View File

@@ -117,14 +117,14 @@ async function renderCode(note, $renderedContent) {
} }
function renderImage(entity, $renderedContent, options = {}) { function renderImage(entity, $renderedContent, options = {}) {
const sanitizedTitle = entity.title.replace(/[^a-z0-9-.]/gi, ""); const encodedTitle = encodeURIComponent(entity.title);
let url; let url;
if (entity instanceof FNote) { if (entity instanceof FNote) {
url = `api/images/${entity.noteId}/${sanitizedTitle}?${Math.random()}`; url = `api/images/${entity.noteId}/${encodedTitle}?${Math.random()}`;
} else if (entity instanceof FAttachment) { } else if (entity instanceof FAttachment) {
url = `api/attachments/${entity.attachmentId}/image/${sanitizedTitle}?${entity.utcDateModified}">`; url = `api/attachments/${entity.attachmentId}/image/${encodedTitle}?${entity.utcDateModified}">`;
} }
$renderedContent // styles needed for the zoom to work well $renderedContent // styles needed for the zoom to work well

View File

@@ -42,6 +42,7 @@ async function createLink(notePath, options = {}) {
const showNotePath = options.showNotePath === undefined ? false : options.showNotePath; const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;
const showNoteIcon = options.showNoteIcon === undefined ? false : options.showNoteIcon; const showNoteIcon = options.showNoteIcon === undefined ? false : options.showNoteIcon;
const referenceLink = options.referenceLink === undefined ? false : options.referenceLink; const referenceLink = options.referenceLink === undefined ? false : options.referenceLink;
const autoConvertToImage = options.autoConvertToImage === undefined ? false : options.autoConvertToImage;
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath); const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
const viewScope = options.viewScope || {}; const viewScope = options.viewScope || {};
@@ -58,6 +59,16 @@ async function createLink(notePath, options = {}) {
} }
} }
const note = await froca.getNote(noteId);
if (autoConvertToImage && ['image', 'canvas', 'mermaid'].includes(note.type) && viewMode === 'default') {
const encodedTitle = encodeURIComponent(linkTitle);
return $("<img>")
.attr("src", `api/images/${noteId}/${encodedTitle}?${Math.random()}`)
.attr("alt", linkTitle);
}
const $container = $("<span>"); const $container = $("<span>");
if (showNoteIcon) { if (showNoteIcon) {

View File

@@ -97,7 +97,7 @@ export default class IncludeNoteDialog extends BasicWidget {
const boxSize = $("input[name='include-note-box-size']:checked").val(); const boxSize = $("input[name='include-note-box-size']:checked").val();
if (note.type === 'image') { if (['image', 'canvas', 'mermaid'].includes(note.type)) {
// there's no benefit to use insert note functionlity for images, // there's no benefit to use insert note functionlity for images,
// so we'll just add an IMG tag // so we'll just add an IMG tag
this.textTypeWidget.addImage(noteId); this.textTypeWidget.addImage(noteId);

View File

@@ -274,16 +274,16 @@ export default class RevisionsDialog extends BasicWidget {
this.$content.html($table); this.$content.html($table);
} else if (revisionItem.type === 'canvas') { } else if (revisionItem.type === 'canvas') {
const sanitizedTitle = revisionItem.title.replace(/[^a-z0-9-.]/gi, ""); const encodedTitle = encodeURIComponent(revisionItem.title);
this.$content.html($("<img>") this.$content.html($("<img>")
.attr("src", `api/revisions/${revisionItem.revisionId}/image/${sanitizedTitle}?${Math.random()}`) .attr("src", `api/revisions/${revisionItem.revisionId}/image/${encodedTitle}?${Math.random()}`)
.css("max-width", "100%")); .css("max-width", "100%"));
} else if (revisionItem.type === 'mermaid') { } else if (revisionItem.type === 'mermaid') {
const sanitizedTitle = revisionItem.title.replace(/[^a-z0-9-.]/gi, ""); const encodedTitle = encodeURIComponent(revisionItem.title);
this.$content.html($("<img>") this.$content.html($("<img>")
.attr("src", `api/revisions/${revisionItem.revisionId}/image/${sanitizedTitle}?${Math.random()}`) .attr("src", `api/revisions/${revisionItem.revisionId}/image/${encodedTitle}?${Math.random()}`)
.css("max-width", "100%")); .css("max-width", "100%"));
this.$content.append($("<pre>").text(fullRevision.content)); this.$content.append($("<pre>").text(fullRevision.content));

View File

@@ -56,7 +56,6 @@ export default class HighlightsListWidget extends RightPanelWidget {
.class("icon-action"), .class("icon-action"),
new OnClickButtonWidget() new OnClickButtonWidget()
.icon("bx-x") .icon("bx-x")
.title("Close Highlights List")
.titlePlacement("left") .titlePlacement("left")
.onClick(widget => widget.triggerCommand("closeHlt")) .onClick(widget => widget.triggerCommand("closeHlt"))
.class("icon-action") .class("icon-action")

View File

@@ -258,10 +258,11 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
.append($("<h2>").text(this.note.title)) .append($("<h2>").text(this.note.title))
.append($promotedAttributes) .append($promotedAttributes)
.prop('outerHTML'), .prop('outerHTML'),
footer: ` footer: `
<script src="${assetPath}/libraries/katex/katex.min.js"></script> <script src="${assetPath}/node_modules/katex/dist/katex.min.js"></script>
<script src="${assetPath}/libraries/katex/mhchem.min.js"></script> <script src="${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script>
<script src="${assetPath}/libraries/katex/auto-render.min.js"></script> <script src="${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
<script> <script>
document.body.className += ' ck-content printed-content'; document.body.className += ' ck-content printed-content';
@@ -273,7 +274,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
`${assetPath}/libraries/codemirror/codemirror.css`, `${assetPath}/libraries/codemirror/codemirror.css`,
`${assetPath}/libraries/ckeditor/ckeditor-content.css`, `${assetPath}/libraries/ckeditor/ckeditor-content.css`,
`${assetPath}/libraries/bootstrap/css/bootstrap.min.css`, `${assetPath}/libraries/bootstrap/css/bootstrap.min.css`,
`${assetPath}/libraries/katex/katex.min.css`, `${assetPath}/node_modules/katex/dist/katex.min.css`,
`${assetPath}/stylesheets/print.css`, `${assetPath}/stylesheets/print.css`,
`${assetPath}/stylesheets/relation_map.css`, `${assetPath}/stylesheets/relation_map.css`,
`${assetPath}/stylesheets/ckeditor-theme.css` `${assetPath}/stylesheets/ckeditor-theme.css`

View File

@@ -402,11 +402,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
})); }));
if (notes.length === 1) { if (notes.length === 1) {
linkService.createLink(notes[0].noteId, {referenceLink: true}) linkService.createLink(notes[0].noteId, {referenceLink: true, autoConvertToImage: true})
.then($link => data.dataTransfer.setData("text/html", $link[0].outerHTML)); .then($link => data.dataTransfer.setData("text/html", $link[0].outerHTML));
} }
else { else {
Promise.all(notes.map(note => linkService.createLink(note.noteId, {referenceLink: true}))).then(links => { Promise.all(notes.map(note => linkService.createLink(note.noteId, {referenceLink: true, autoConvertToImage: true}))).then(links => {
const $list = $("<ul>").append(...links.map($link => $("<li>").append($link))); const $list = $("<ul>").append(...links.map($link => $("<li>").append($link)));
data.dataTransfer.setData("text/html", $list[0].outerHTML); data.dataTransfer.setData("text/html", $list[0].outerHTML);

View File

@@ -8,8 +8,14 @@ import options from "../../services/options.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
const TPL = ` const TPL = `
<div> <div class="promoted-attributes-widget">
<style> <style>
body.mobile .promoted-attributes-widget {
/* https://github.com/zadam/trilium/issues/4468 */
flex-shrink: 0.4;
overflow: auto;
}
.promoted-attributes-container { .promoted-attributes-container {
margin: auto; margin: auto;
display: flex; display: flex;

View File

@@ -68,7 +68,6 @@ export default class TocWidget extends RightPanelWidget {
.class("icon-action"), .class("icon-action"),
new OnClickButtonWidget() new OnClickButtonWidget()
.icon("bx-x") .icon("bx-x")
.title("Close Table of Contents")
.titlePlacement("left") .titlePlacement("left")
.onClick(widget => widget.triggerCommand("closeToc")) .onClick(widget => widget.triggerCommand("closeToc"))
.class("icon-action") .class("icon-action")

View File

@@ -365,12 +365,10 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const note = await froca.getNote(noteId); const note = await froca.getNote(noteId);
this.watchdog.editor.model.change( writer => { this.watchdog.editor.model.change( writer => {
const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, ""); const encodedTitle = encodeURIComponent(note.title);
const src = `api/images/${note.noteId}/${sanitizedTitle}`; const src = `api/images/${note.noteId}/${encodedTitle}`;
const imageElement = writer.createElement( 'image', { 'src': src } ); this.watchdog.editor.execute( 'insertImage', { source: src } );
this.watchdog.editor.model.insertContent(imageElement, this.watchdog.editor.model.document.selection);
} ); } );
} }

View File

@@ -153,8 +153,9 @@ function processContent(images, note, content) {
const buffer = Buffer.from(dataUrl.split(",")[1], 'base64'); const buffer = Buffer.from(dataUrl.split(",")[1], 'base64');
const attachment = imageService.saveImageToAttachment(note.noteId, buffer, filename, true); const attachment = imageService.saveImageToAttachment(note.noteId, buffer, filename, true);
const sanitizedTitle = attachment.title.replace(/[^a-z0-9-.]/gi, "");
const url = `api/attachments/${attachment.attachmentId}/image/${sanitizedTitle}`; const encodedTitle = encodeURIComponent(attachment.title);
const url = `api/attachments/${attachment.attachmentId}/image/${encodedTitle}`;
log.info(`Replacing '${imageId}' with '${url}' in note '${note.noteId}'`); log.info(`Replacing '${imageId}' with '${url}' in note '${note.noteId}'`);

View File

@@ -8,6 +8,7 @@ const cls = require('../../services/cls');
const path = require('path'); const path = require('path');
const becca = require("../../becca/becca"); const becca = require("../../becca/becca");
const blobService = require("../../services/blob"); const blobService = require("../../services/blob");
const eraseService = require("../../services/erase.js");
function getRevisionBlob(req) { function getRevisionBlob(req) {
const preview = req.query.preview === 'true'; const preview = req.query.preview === 'true';
@@ -88,11 +89,11 @@ function eraseAllRevisions(req) {
const revisionIdsToErase = sql.getColumn('SELECT revisionId FROM revisions WHERE noteId = ?', const revisionIdsToErase = sql.getColumn('SELECT revisionId FROM revisions WHERE noteId = ?',
[req.params.noteId]); [req.params.noteId]);
revisionService.eraseRevisions(revisionIdsToErase); eraseService.eraseRevisions(revisionIdsToErase);
} }
function eraseRevision(req) { function eraseRevision(req) {
revisionService.eraseRevisions([req.params.revisionId]); eraseService.eraseRevisions([req.params.revisionId]);
} }
function restoreRevision(req) { function restoreRevision(req) {

View File

@@ -1 +1 @@
module.exports = { buildDate:"2023-11-21T20:49:24+01:00", buildRevision: "e2b1421bf3d764ffe444a103c118e67d8c563673" }; module.exports = { buildDate:"2024-01-08T00:05:13+01:00", buildRevision: "ee608fcf46a2052f21d94a8a72fc023dcaf955f7" };

View File

@@ -4,6 +4,7 @@ const becca = require("../becca/becca");
const cloningService = require("./cloning"); const cloningService = require("./cloning");
const branchService = require("./branches"); const branchService = require("./branches");
const utils = require("./utils"); const utils = require("./utils");
const eraseService = require("./erase.js");
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
addLabel: (action, note) => { addLabel: (action, note) => {
@@ -18,7 +19,7 @@ const ACTION_HANDLERS = {
note.deleteNote(deleteId); note.deleteNote(deleteId);
}, },
deleteRevisions: (action, note) => { deleteRevisions: (action, note) => {
revisionService.eraseRevisions(note.getRevisions().map(rev => rev.revisionId)); eraseService.eraseRevisions(note.getRevisions().map(rev => rev.revisionId));
}, },
deleteLabel: (action, note) => { deleteLabel: (action, note) => {
for (const label of note.getOwnedLabels(action.labelName)) { for (const label of note.getOwnedLabels(action.labelName)) {

View File

@@ -467,7 +467,7 @@ class ConsistencyChecks {
WHERE blobs.blobId IS NULL`, WHERE blobs.blobId IS NULL`,
({revisionId, blobId}) => { ({revisionId, blobId}) => {
if (this.autoFix) { if (this.autoFix) {
revisionService.eraseRevisions([revisionId]); eraseService.eraseRevisions([revisionId]);
this.reloadNeeded = true; this.reloadNeeded = true;

View File

@@ -29,7 +29,7 @@ function eraseNotes(noteIdsToErase) {
const revisionIdsToErase = sql.getManyRows(`SELECT revisionId FROM revisions WHERE noteId IN (???)`, noteIdsToErase) const revisionIdsToErase = sql.getManyRows(`SELECT revisionId FROM revisions WHERE noteId IN (???)`, noteIdsToErase)
.map(row => row.revisionId); .map(row => row.revisionId);
revisionService.eraseRevisions(revisionIdsToErase); eraseRevisions(revisionIdsToErase);
log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`); log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`);
} }
@@ -79,6 +79,18 @@ function eraseAttachments(attachmentIdsToErase) {
log.info(`Erased attachments: ${JSON.stringify(attachmentIdsToErase)}`); log.info(`Erased attachments: ${JSON.stringify(attachmentIdsToErase)}`);
} }
function eraseRevisions(revisionIdsToErase) {
if (revisionIdsToErase.length === 0) {
return;
}
sql.executeMany(`DELETE FROM revisions WHERE revisionId IN (???)`, revisionIdsToErase);
setEntityChangesAsErased(sql.getManyRows(`SELECT * FROM entity_changes WHERE entityName = 'revisions' AND entityId IN (???)`, revisionIdsToErase));
log.info(`Removed revisions: ${JSON.stringify(revisionIdsToErase)}`);
}
function eraseUnusedBlobs() { function eraseUnusedBlobs() {
const unusedBlobIds = sql.getColumn(` const unusedBlobIds = sql.getColumn(`
SELECT blobs.blobId SELECT blobs.blobId
@@ -184,5 +196,6 @@ module.exports = {
eraseUnusedAttachmentsNow, eraseUnusedAttachmentsNow,
eraseNotesWithDeleteId, eraseNotesWithDeleteId,
eraseUnusedBlobs, eraseUnusedBlobs,
eraseAttachments eraseAttachments,
eraseRevisions
}; };

View File

@@ -303,8 +303,8 @@ function importEnex(taskContext, file, parentNote) {
const attachment = imageService.saveImageToAttachment(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages); const attachment = imageService.saveImageToAttachment(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages);
const sanitizedTitle = attachment.title.replace(/[^a-z0-9-.]/gi, ""); const encodedTitle = encodeURIComponent(attachment.title);
const url = `api/attachments/${attachment.attachmentId}/image/${sanitizedTitle}`; const url = `api/attachments/${attachment.attachmentId}/image/${encodedTitle}`;
const imageLink = `<img src="${url}">`; const imageLink = `<img src="${url}">`;
content = content.replace(mediaRegex, imageLink); content = content.replace(mediaRegex, imageLink);

View File

@@ -494,7 +494,7 @@ async function downloadImage(noteId, imageUrl) {
const title = path.basename(parsedUrl.pathname); const title = path.basename(parsedUrl.pathname);
const imageService = require('../services/image'); const imageService = require('../services/image');
const {attachment} = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true); const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true);
imageUrlToAttachmentIdMapping[imageUrl] = attachment.attachmentId; imageUrlToAttachmentIdMapping[imageUrl] = attachment.attachmentId;
@@ -511,7 +511,7 @@ const downloadImagePromises = {};
function replaceUrl(content, url, attachment) { function replaceUrl(content, url, attachment) {
const quotedUrl = utils.quoteRegex(url); 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) { function downloadImages(noteId, content) {
@@ -519,7 +519,7 @@ function downloadImages(noteId, content) {
let imageMatch; let imageMatch;
while (imageMatch = imageRe.exec(content)) { while (imageMatch = imageRe.exec(content)) {
const url = imageMatch[1]; let url = imageMatch[1];
const inlineImageMatch = /^data:image\/[a-z]+;base64,/.exec(url); const inlineImageMatch = /^data:image\/[a-z]+;base64,/.exec(url);
if (inlineImageMatch) { if (inlineImageMatch) {
@@ -529,9 +529,9 @@ function downloadImages(noteId, content) {
const imageService = require('../services/image'); const imageService = require('../services/image');
const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, "inline image", true, true); const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, "inline image", true, true);
const sanitizedTitle = attachment.title.replace(/[^a-z0-9-.]/gi, ""); const encodedTitle = encodeURIComponent(attachment.title);
content = `${content.substr(0, imageMatch.index)}<img src="api/attachments/${attachment.attachmentId}/image/${sanitizedTitle}"${content.substr(imageMatch.index + imageMatch[0].length)}`; 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" // this is an exception for the web clipper's "imageId"
@@ -541,6 +541,8 @@ function downloadImages(noteId, content) {
continue; continue;
} }
url = utils.unescapeHtml(url);
if (url in imageUrlToAttachmentIdMapping) { if (url in imageUrlToAttachmentIdMapping) {
const attachment = becca.getAttachment(imageUrlToAttachmentIdMapping[url]); const attachment = becca.getAttachment(imageUrlToAttachmentIdMapping[url]);
@@ -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)}`; 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; return content;
} }
@@ -889,6 +895,10 @@ function scanForLinks(note, content) {
* Things which have to be executed after updating content, but asynchronously (separate transaction) * Things which have to be executed after updating content, but asynchronously (separate transaction)
*/ */
async function asyncPostProcessContent(note, content) { async function asyncPostProcessContent(note, content) {
if (note.hasStringContent() && !utils.isString(content)) {
content = content.toString();
}
scanForLinks(note, content); scanForLinks(note, content);
} }

View File

@@ -46,18 +46,6 @@ function protectRevisions(note) {
} }
} }
function eraseRevisions(revisionIdsToErase) {
if (revisionIdsToErase.length === 0) {
return;
}
log.info(`Removing revisions: ${JSON.stringify(revisionIdsToErase)}`);
sql.executeMany(`DELETE FROM revisions WHERE revisionId IN (???)`, revisionIdsToErase);
sql.executeMany(`UPDATE entity_changes SET isErased = 1, utcDateChanged = '${dateUtils.utcNowDateTime()}' WHERE entityName = 'revisions' AND entityId IN (???)`, revisionIdsToErase);
}
module.exports = { module.exports = {
protectRevisions, protectRevisions
eraseRevisions
}; };

View File

@@ -209,8 +209,9 @@ function sortNotesIfNeeded(parentNoteId) {
function setNoteToParent(noteId, prefix, parentNoteId) { function setNoteToParent(noteId, prefix, parentNoteId) {
const parentNote = becca.getNote(parentNoteId); const parentNote = becca.getNote(parentNoteId);
if (!parentNote) { if (parentNoteId && !parentNote) {
throw new Error(`Cannot move note to deleted parent note '${parentNoteId}'`); // null parentNoteId is a valid value
throw new Error(`Cannot move note to deleted / missing parent note '${parentNoteId}'`);
} }
// case where there might be more such branches is ignored. It's expected there should be just one // case where there might be more such branches is ignored. It's expected there should be just one

View File

@@ -63,6 +63,8 @@ function isElectron() {
} }
function hash(text) { function hash(text) {
text = text.normalize();
return crypto.createHash('sha1').update(text).digest('base64'); return crypto.createHash('sha1').update(text).digest('base64');
} }
@@ -303,6 +305,10 @@ function toMap(list, key) {
return map; return map;
} }
function isString(x) {
return Object.prototype.toString.call(x) === "[object String]";
}
module.exports = { module.exports = {
randomSecureToken, randomSecureToken,
randomString, randomString,
@@ -335,4 +341,5 @@ module.exports = {
normalize, normalize,
hashedBlobId, hashedBlobId,
toMap, toMap,
isString
}; };

View File

@@ -105,10 +105,10 @@ function renderText(result, note) {
if (result.content.includes(`<span class="math-tex">`)) { if (result.content.includes(`<span class="math-tex">`)) {
result.header += ` result.header += `
<script src="../../${assetPath}/libraries/katex/katex.min.js"></script> <script src="../../${assetPath}/node_modules/katex/dist/katex.min.js"></script>
<link rel="stylesheet" href="../../${assetPath}/libraries/katex/katex.min.css"> <link rel="stylesheet" href="../../${assetPath}/node_modules/katex/dist/katex.min.css">
<script src="../../${assetPath}/libraries/katex/auto-render.min.js"></script> <script src="../../${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
<script src="../../${assetPath}/libraries/katex/mhchem.min.js"></script> <script src="../../${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.getElementById('content')); renderMathInElement(document.getElementById('content'));