diff --git a/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml b/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml
index 7585ab8d0..4dcc60b75 100644
--- a/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml
+++ b/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml
@@ -16,586 +16,671 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
+
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
-
+
4
INT|0s
1
0
-
+
5
TEXT|0s
1
""
-
+
1
apiTokenId
1
-
+
apiTokenId
1
sqlite_autoindex_api_tokens_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
-
+
4
TEXT|0s
1
-
+
5
TEXT|0s
1
''
-
+
6
INT|0s
1
0
-
+
7
TEXT|0s
1
-
+
8
TEXT|0s
1
-
+
9
INT|0s
1
-
+
10
TEXT|0s
1
""
-
+
11
int|0s
0
-
+
1
attributeId
1
-
+
+ noteId
+
+
+
+ noteId
+
+
+
+ name
+value
+
+
+
+ name
+
+
+
+ name
+
+
+
+ value
+
+
+
+ value
+
+
+
attributeId
1
sqlite_autoindex_attributes_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
-
+
4
INTEGER|0s
1
-
+
5
TEXT|0s
-
+
6
BOOLEAN|0s
-
+
7
INTEGER|0s
1
0
-
+
8
TEXT|0s
1
-
+
9
TEXT|0s
1
""
-
+
10
TEXT|0s
1
'1970-01-01T00:00:00.000Z'
-
+
1
branchId
1
-
+
noteId
parentNoteId
-
+
noteId
-
+
parentNoteId
-
+
branchId
1
sqlite_autoindex_branches_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
-
+
3
TEXT|0s
-
+
4
TEXT|0s
1
-
+
1
eventId
1
-
+
+ noteId
+
+
+
eventId
1
sqlite_autoindex_event_log_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
-
+
4
TEXT|0s
1
-
+
5
- INTEGER|0s
- 1
- 0
-
-
- 6
- TEXT|0s
- 1
-
-
- 7
- TEXT|0s
- 1
-
-
- 8
TEXT|0s
1
""
-
+
+ 6
+ INTEGER|0s
+ 1
+ 0
+
+
+ 7
+ TEXT|0s
+ 1
+
+
+ 8
+ TEXT|0s
+ 1
+
+
1
linkId
1
-
+
+ noteId
+
+
+
+ noteId
+
+
+
+ targetNoteId
+
+
+
+ targetNoteId
+
+
+
linkId
1
sqlite_autoindex_links_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
+ 3
+ INT|0s
+ 1
+ 0
+
+
+ 4
+ TEXT|0s
+ NULL
+
+
+ 1
+ noteContentId
+
+ 1
+
+
+ noteId
+
+
+
+ noteContentId
+ 1
+ sqlite_autoindex_note_contents_1
+
+
+ 1
+ TEXT|0s
+ 1
+
+
+ 2
+ TEXT|0s
+ 1
+
+
3
TEXT|0s
-
+
4
TEXT|0s
-
+
5
INT|0s
1
0
-
+
6
TEXT|0s
1
-
+
7
TEXT|0s
1
-
+
8
TEXT|0s
1
''
-
+
9
TEXT|0s
1
''
-
+
10
TEXT|0s
1
""
-
+
1
noteRevisionId
1
-
+
noteId
-
+
dateModifiedFrom
-
+
dateModifiedTo
-
+
noteRevisionId
1
sqlite_autoindex_note_revisions_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
"note"
-
+
3
- TEXT|0s
- NULL
-
-
- 4
INT|0s
1
0
-
- 5
+
+ 4
TEXT|0s
1
'text'
-
- 6
+
+ 5
TEXT|0s
1
'text/html'
-
- 7
+
+ 6
TEXT|0s
1
""
-
- 8
+
+ 7
INT|0s
1
0
-
+
+ 8
+ TEXT|0s
+ 1
+
+
9
TEXT|0s
1
-
- 10
- TEXT|0s
- 1
-
-
+
1
noteId
1
-
+
+ isDeleted
+
+
+
noteId
1
sqlite_autoindex_notes_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
-
+
3
INT|0s
-
+
4
INTEGER|0s
1
0
-
+
5
TEXT|0s
1
""
-
+
6
TEXT|0s
1
'1970-01-01T00:00:00.000Z'
-
+
1
name
1
-
+
name
1
sqlite_autoindex_options_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
""
-
+
4
TEXT|0s
1
-
+
5
INT|0s
-
+
1
branchId
1
-
+
branchId
1
sqlite_autoindex_recent_notes_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
1
sourceId
1
-
+
sourceId
1
sqlite_autoindex_source_ids_1
-
+
1
text|0s
-
+
2
text|0s
-
+
3
text|0s
-
+
4
integer|0s
-
+
5
text|0s
-
+
1
-
+
2
-
+
1
INTEGER|0s
1
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
-
+
4
TEXT|0s
1
-
+
5
TEXT|0s
1
-
+
entityName
entityId
1
-
+
syncDate
-
+
id
1
diff --git a/db/migrations/0125__create_note_content_table.sql b/db/migrations/0125__create_note_content_table.sql
new file mode 100644
index 000000000..1bba63b5f
--- /dev/null
+++ b/db/migrations/0125__create_note_content_table.sql
@@ -0,0 +1,34 @@
+CREATE TABLE IF NOT EXISTS "note_contents" (
+ `noteContentId` TEXT NOT NULL,
+ `noteId` TEXT NOT NULL,
+ `isProtected` INT NOT NULL DEFAULT 0,
+ `content` TEXT NULL DEFAULT NULL,
+ PRIMARY KEY(`noteContentId`)
+);
+
+CREATE UNIQUE INDEX `IDX_note_contents_noteId` ON `note_contents` (`noteId`);
+
+INSERT INTO note_contents (noteContentId, noteId, isProtected, content)
+ SELECT 'C' || SUBSTR(noteId, 2), noteId, isProtected, content FROM notes;
+
+CREATE TABLE IF NOT EXISTS "notes_mig" (
+ `noteId` TEXT NOT NULL,
+ `title` TEXT NOT NULL DEFAULT "note",
+ `isProtected` INT NOT NULL DEFAULT 0,
+ `type` TEXT NOT NULL DEFAULT 'text',
+ `mime` TEXT NOT NULL DEFAULT 'text/html',
+ `hash` TEXT DEFAULT "" NOT NULL,
+ `isDeleted` INT NOT NULL DEFAULT 0,
+ `dateCreated` TEXT NOT NULL,
+ `dateModified` TEXT NOT NULL,
+ PRIMARY KEY(`noteId`)
+);
+
+INSERT INTO notes_mig (noteId, title, isProtected, isDeleted, dateCreated, dateModified, type, mime, hash)
+SELECT noteId, title, isProtected, isDeleted, dateCreated, dateModified, type, mime, hash FROM notes;
+
+DROP TABLE notes;
+
+ALTER TABLE notes_mig RENAME TO notes;
+
+CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
\ No newline at end of file
diff --git a/db/migrations/0126__create_missing_indexes.sql b/db/migrations/0126__create_missing_indexes.sql
new file mode 100644
index 000000000..1d7a1be87
--- /dev/null
+++ b/db/migrations/0126__create_missing_indexes.sql
@@ -0,0 +1,8 @@
+CREATE INDEX `IDX_attributes_noteId` ON `attributes` (`noteId`);
+CREATE INDEX `IDX_attributes_name` ON `attributes` (`name`);
+CREATE INDEX `IDX_attributes_value` ON `attributes` (`value`);
+
+CREATE INDEX `IDX_event_log_noteId` ON `event_log` (`noteId`);
+
+CREATE INDEX `IDX_links_noteId` ON `links` (`noteId`);
+CREATE INDEX `IDX_links_targetNoteId` ON `links` (`targetNoteId`);
diff --git a/src/entities/entity_constructor.js b/src/entities/entity_constructor.js
index b7ee4b313..906ae5b99 100644
--- a/src/entities/entity_constructor.js
+++ b/src/entities/entity_constructor.js
@@ -1,4 +1,5 @@
const Note = require('../entities/note');
+const NoteContent = require('../entities/note_content');
const NoteRevision = require('../entities/note_revision');
const Link = require('../entities/link');
const Branch = require('../entities/branch');
@@ -12,6 +13,7 @@ const ENTITY_NAME_TO_ENTITY = {
"attributes": Attribute,
"branches": Branch,
"notes": Note,
+ "note_contents": NoteContent,
"note_revisions": NoteRevision,
"recent_notes": RecentNote,
"options": Option,
@@ -48,6 +50,9 @@ function createEntityFromRow(row) {
else if (row.branchId) {
entity = new Branch(row);
}
+ else if (row.noteContentId) {
+ entity = new NoteContent(row);
+ }
else if (row.noteId) {
entity = new Note(row);
}
diff --git a/src/entities/note.js b/src/entities/note.js
index da4640379..914aec87d 100644
--- a/src/entities/note.js
+++ b/src/entities/note.js
@@ -19,7 +19,6 @@ const RELATION_DEFINITION = 'relation-definition';
* @property {string} type - one of "text", "code", "file" or "render"
* @property {string} mime - MIME type, e.g. "text/html"
* @property {string} title - note title
- * @property {string} content - note content - e.g. HTML text for text notes, file payload for files
* @property {boolean} isProtected - true if note is protected
* @property {boolean} isDeleted - true if note is deleted
* @property {string} dateCreated
@@ -30,7 +29,7 @@ const RELATION_DEFINITION = 'relation-definition';
class Note extends Entity {
static get entityName() { return "notes"; }
static get primaryKeyName() { return "noteId"; }
- static get hashedProperties() { return ["noteId", "title", "content", "type", "isProtected", "isDeleted"]; }
+ static get hashedProperties() { return ["noteId", "title", "type", "isProtected", "isDeleted"]; }
/**
* @param row - object containing database row from "notes" table
@@ -54,26 +53,18 @@ class Note extends Entity {
// saving ciphertexts in case we do want to update protected note outside of protected session
// (which is allowed)
this.titleCipherText = this.title;
- this.contentCipherText = this.content;
-
this.title = "[protected]";
- this.content = "";
}
}
-
- this.setContent(this.content);
}
- setContent(content) {
- this.content = content;
-
- // if parsing below is not successful then there's no jsonContent as opposed to still having the old unupdated ones
- delete this.jsonContent;
-
- try {
- this.jsonContent = JSON.parse(this.content);
+ /** @returns {Promise} */
+ async getNoteContent() {
+ if (!this.noteContent) {
+ this.noteContent = await repository.getEntity(`SELECT * FROM note_contents WHERE noteId = ?`, [this.noteId]);
}
- catch(e) {}
+
+ return this.noteContent;
}
/** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
diff --git a/src/entities/note_content.js b/src/entities/note_content.js
new file mode 100644
index 000000000..904f5d9c1
--- /dev/null
+++ b/src/entities/note_content.js
@@ -0,0 +1,82 @@
+"use strict";
+
+const Entity = require('./entity');
+const protectedSessionService = require('../services/protected_session');
+const repository = require('../services/repository');
+
+/**
+ * This represents a Note which is a central object in the Trilium Notes project.
+ *
+ * @property {string} noteContentId - primary key
+ * @property {string} noteId - reference to owning note
+ * @property {boolean} isProtected - true if note content is protected
+ * @property {blob} content - note content - e.g. HTML text for text notes, file payload for files
+ *
+ * @extends Entity
+ */
+class NoteContent extends Entity {
+ static get entityName() {
+ return "note_contents";
+ }
+
+ static get primaryKeyName() {
+ return "noteContentId";
+ }
+
+ static get hashedProperties() {
+ return ["noteContentId", "noteId", "isProtected", "content"];
+ }
+
+ /**
+ * @param row - object containing database row from "note_contents" table
+ */
+ constructor(row) {
+ super(row);
+
+ this.isProtected = !!this.isProtected;
+ /* true if content (meaning any kind of potentially encrypted content) is either not encrypted
+ * or encrypted, but with available protected session (so effectively decrypted) */
+ this.isContentAvailable = true;
+
+ // check if there's noteContentId, otherwise this is a new entity which wasn't encrypted yet
+ if (this.isProtected && this.noteContentId) {
+ this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable();
+
+ if (this.isContentAvailable) {
+ protectedSessionService.decryptNoteContent(this);
+ }
+ else {
+ // saving ciphertexts in case we do want to update protected note outside of protected session
+ // (which is allowed)
+ this.contentCipherText = this.content;
+ this.content = "";
+ }
+ }
+ }
+
+ /**
+ * @returns {Promise}
+ */
+ async getNote() {
+ return await repository.getNote(this.noteId);
+ }
+
+ // cannot be static!
+ updatePojo(pojo) {
+ if (pojo.isProtected) {
+ if (this.isContentAvailable) {
+ protectedSessionService.encryptNoteContent(pojo);
+ }
+ else {
+ // updating protected note outside of protected session means we will keep original ciphertext
+ pojo.content = pojo.contentCipherText;
+ }
+ }
+
+ delete pojo.jsonContent;
+ delete pojo.isContentAvailable;
+ delete pojo.contentCipherText;
+ }
+}
+
+module.exports = NoteContent;
\ No newline at end of file
diff --git a/src/public/javascripts/entities/note_full.js b/src/public/javascripts/entities/note_full.js
index 735e43526..3fd6cc530 100644
--- a/src/public/javascripts/entities/note_full.js
+++ b/src/public/javascripts/entities/note_full.js
@@ -8,15 +8,15 @@ class NoteFull extends NoteShort {
super(treeCache, row);
/** @param {string} */
- this.content = row.content;
+ this.noteContent = row.noteContent;
- if (this.content !== "" && this.isJson()) {
- try {
- /** @param {object} */
- this.jsonContent = JSON.parse(this.content);
- }
- catch(e) {}
- }
+ // if (this.content !== "" && this.isJson()) {
+ // try {
+ // /** @param {object} */
+ // this.jsonContent = JSON.parse(this.content);
+ // }
+ // catch(e) {}
+ // }
}
}
diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js
index 7384fdd78..3f47ab698 100644
--- a/src/public/javascripts/services/note_detail.js
+++ b/src/public/javascripts/services/note_detail.js
@@ -357,6 +357,7 @@ export default {
updateNoteView,
loadNote,
getCurrentNote,
+ getCurrentNoteContent,
getCurrentNoteType,
getCurrentNoteId,
focusOnTitle,
@@ -364,7 +365,6 @@ export default {
saveNote,
saveNoteIfChanged,
noteChanged,
- getCurrentNoteContent,
onNoteChange,
addDetailLoadedListener
};
\ No newline at end of file
diff --git a/src/public/javascripts/services/note_detail_code.js b/src/public/javascripts/services/note_detail_code.js
index 1f638d1e4..e06e9e8db 100644
--- a/src/public/javascripts/services/note_detail_code.js
+++ b/src/public/javascripts/services/note_detail_code.js
@@ -49,7 +49,7 @@ async function show() {
// this needs to happen after the element is shown, otherwise the editor won't be refreshed
// CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check)
// we provide fallback
- codeEditor.setValue(currentNote.content || "");
+ codeEditor.setValue(currentNote.noteContent.content || "");
const info = CodeMirror.findModeByMIME(currentNote.mime);
diff --git a/src/public/javascripts/services/note_detail_file.js b/src/public/javascripts/services/note_detail_file.js
index df8a7f657..e0d98adc0 100644
--- a/src/public/javascripts/services/note_detail_file.js
+++ b/src/public/javascripts/services/note_detail_file.js
@@ -27,8 +27,8 @@ async function show() {
$fileSize.text((attributeMap.fileSize || "?") + " bytes");
$fileType.text(currentNote.mime);
- $previewRow.toggle(!!currentNote.content);
- $previewContent.text(currentNote.content);
+ $previewRow.toggle(!!currentNote.noteContent.content);
+ $previewContent.text(currentNote.noteContent.content);
}
$downloadButton.click(() => utils.download(getFileUrl()));
diff --git a/src/public/javascripts/services/note_detail_relation_map.js b/src/public/javascripts/services/note_detail_relation_map.js
index 6a2bd38b5..2835a94ce 100644
--- a/src/public/javascripts/services/note_detail_relation_map.js
+++ b/src/public/javascripts/services/note_detail_relation_map.js
@@ -93,9 +93,9 @@ function loadMapData() {
}
};
- if (currentNote.content) {
+ if (currentNote.noteContent.content) {
try {
- mapData = JSON.parse(currentNote.content);
+ mapData = JSON.parse(currentNote.noteContent.content);
} catch (e) {
console.log("Could not parse content: ", e);
}
diff --git a/src/public/javascripts/services/note_detail_search.js b/src/public/javascripts/services/note_detail_search.js
index bdbde236b..d340005cd 100644
--- a/src/public/javascripts/services/note_detail_search.js
+++ b/src/public/javascripts/services/note_detail_search.js
@@ -16,7 +16,7 @@ function show() {
$component.show();
try {
- const json = JSON.parse(noteDetailService.getCurrentNote().content);
+ const json = JSON.parse(noteDetailService.getCurrentNote().noteContent.content);
$searchString.val(json.searchString);
}
diff --git a/src/public/javascripts/services/note_detail_text.js b/src/public/javascripts/services/note_detail_text.js
index fb8e2e4a3..3d9a48457 100644
--- a/src/public/javascripts/services/note_detail_text.js
+++ b/src/public/javascripts/services/note_detail_text.js
@@ -22,7 +22,7 @@ async function show() {
textEditor.isReadOnly = await isReadOnly();
- textEditor.setData(noteDetailService.getCurrentNote().content);
+ textEditor.setData(noteDetailService.getCurrentNote().noteContent.content);
$component.show();
}
diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js
index 326ab86be..7112e8ef4 100644
--- a/src/routes/api/notes.js
+++ b/src/routes/api/notes.js
@@ -12,18 +12,10 @@ async function getNote(req) {
return [404, "Note " + noteId + " has not been found."];
}
- if (note.type === 'file' || note.type === 'image') {
- if (note.type === 'file' && note.mime.startsWith('text/')) {
- note.content = note.content.toString("UTF-8");
+ if (note.mime.startsWith('text/')) {
+ const noteContent = await note.getNoteContent();
- if (note.content.length > 10000) {
- note.content = note.content.substr(0, 10000) + "...";
- }
- }
- else {
- // no need to transfer (potentially large) file/image payload for this request
- note.content = null;
- }
+ noteContent.content = noteContent.content.toString("UTF-8");
}
return note;
diff --git a/src/services/app_info.js b/src/services/app_info.js
index 2dd364356..66bf0d6fa 100644
--- a/src/services/app_info.js
+++ b/src/services/app_info.js
@@ -4,8 +4,8 @@ const build = require('./build');
const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
-const APP_DB_VERSION = 124;
-const SYNC_VERSION = 4;
+const APP_DB_VERSION = 125;
+const SYNC_VERSION = 5;
module.exports = {
appVersion: packageJson.version,
diff --git a/src/services/consistency_checks.js b/src/services/consistency_checks.js
index 63fa78919..fc24a0ef1 100644
--- a/src/services/consistency_checks.js
+++ b/src/services/consistency_checks.js
@@ -234,6 +234,7 @@ async function findLogicIssues() {
await findIssues(`
SELECT noteId
FROM notes
+ JOIN note_contents USING(noteId)
WHERE
isDeleted = 0
AND content IS NULL`,
diff --git a/src/services/protected_session.js b/src/services/protected_session.js
index 41e240ba8..be0b60c97 100644
--- a/src/services/protected_session.js
+++ b/src/services/protected_session.js
@@ -47,28 +47,25 @@ function decryptNoteTitle(noteId, encryptedTitle) {
}
function decryptNote(note) {
- const dataKey = getDataKey();
-
if (!note.isProtected) {
return;
}
- try {
- if (note.title) {
- note.title = dataEncryptionService.decryptString(dataKey, note.title);
- }
+ if (note.title) {
+ note.title = decryptNoteTitle(note.noteId)
+ }
+}
- if (note.content) {
- if (note.type === 'file' || note.type === 'image') {
- note.content = dataEncryptionService.decrypt(dataKey, note.content);
- }
- else {
- note.content = dataEncryptionService.decryptString(dataKey, note.content);
- }
- }
+function decryptNoteContent(noteContent) {
+ if (!noteContent.isProtected) {
+ return;
+ }
+
+ try {
+ noteContent.content = dataEncryptionService.decrypt(getDataKey(), noteContent.content);
}
catch (e) {
- e.message = `Cannot decrypt note for noteId=${note.noteId}: ` + e.message;
+ e.message = `Cannot decrypt note content for noteContentId=${noteContent.noteContentId}: ` + e.message;
throw e;
}
}
@@ -96,10 +93,11 @@ function decryptNoteRevision(hist) {
}
function encryptNote(note) {
- const dataKey = getDataKey();
+ note.title = dataEncryptionService.encrypt(getDataKey(), note.title);
+}
- note.title = dataEncryptionService.encrypt(dataKey, note.title);
- note.content = dataEncryptionService.encrypt(dataKey, note.content);
+function encryptNoteContent(noteContent) {
+ noteContent.content = dataEncryptionService.encrypt(getDataKey(), noteContent.content);
}
function encryptNoteRevision(revision) {
@@ -115,9 +113,11 @@ module.exports = {
isProtectedSessionAvailable,
decryptNoteTitle,
decryptNote,
+ decryptNoteContent,
decryptNotes,
decryptNoteRevision,
encryptNote,
+ encryptNoteContent,
encryptNoteRevision,
setProtectedSessionId
};
\ No newline at end of file
diff --git a/src/services/repository.js b/src/services/repository.js
index e1e65e119..e6cb4fc27 100644
--- a/src/services/repository.js
+++ b/src/services/repository.js
@@ -37,27 +37,32 @@ async function getEntity(query, params = []) {
return entityConstructor.createEntityFromRow(row);
}
-/** @returns {Note|null} */
+/** @returns {Promise} */
async function getNote(noteId) {
return await getEntity("SELECT * FROM notes WHERE noteId = ?", [noteId]);
}
-/** @returns {Branch|null} */
+/** @returns {Promise} */
+async function getNoteContent(noteContentId) {
+ return await getEntity("SELECT * FROM note_contents WHERE noteContentId = ?", [noteContentId]);
+}
+
+/** @returns {Promise} */
async function getBranch(branchId) {
return await getEntity("SELECT * FROM branches WHERE branchId = ?", [branchId]);
}
-/** @returns {Attribute|null} */
+/** @returns {Promise} */
async function getAttribute(attributeId) {
return await getEntity("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]);
}
-/** @returns {Option|null} */
+/** @returns {Promise