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
- +
1
- - +
+ 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} */ async function getOption(name) { return await getEntity("SELECT * FROM options WHERE name = ?", [name]); } -/** @returns {Link|null} */ +/** @returns {Promise} */ async function getLink(linkId) { return await getEntity("SELECT * FROM links WHERE linkId = ?", [linkId]); } @@ -121,6 +126,7 @@ module.exports = { getEntities, getEntity, getNote, + getNoteContent, getBranch, getAttribute, getOption,