From f01657e1ddbd8ba64a49ae496c128ee19bc4c39a Mon Sep 17 00:00:00 2001 From: zadam Date: Fri, 14 Apr 2023 16:49:06 +0200 Subject: [PATCH] refactorings --- src/becca/entities/battachment.js | 53 ++++ src/becca/entities/bnote.js | 62 +---- src/becca/entities/bnote_revision.js | 6 + .../app/services/attribute_autocomplete.js | 4 +- src/public/app/services/open.js | 2 +- .../attribute_widgets/attribute_editor.js | 4 +- src/public/app/widgets/dialogs/export.js | 2 +- .../app/widgets/dialogs/note_revisions.js | 6 +- .../ribbon_widgets/promoted_attributes.js | 2 +- .../app/widgets/type_widgets/relation_map.js | 2 +- src/routes/api/attachments.js | 33 +-- src/routes/api/note_revisions.js | 8 +- src/routes/api/notes.js | 65 +---- src/routes/api/relation-map.js | 69 +++++ src/routes/routes.js | 255 +++++++++--------- src/services/hoisted_note.js | 7 +- src/services/note_revisions.js | 35 ++- src/services/notes.js | 28 +- src/services/special_notes.js | 1 - src/services/tree.js | 85 +----- 20 files changed, 336 insertions(+), 393 deletions(-) create mode 100644 src/routes/api/relation-map.js diff --git a/src/becca/entities/battachment.js b/src/becca/entities/battachment.js index 09af5041b..9fc0ed27a 100644 --- a/src/becca/entities/battachment.js +++ b/src/becca/entities/battachment.js @@ -6,6 +6,10 @@ const becca = require('../becca'); const AbstractBeccaEntity = require("./abstract_becca_entity"); const sql = require("../../services/sql"); +const attachmentRoleToNoteTypeMapping = { + 'image': 'image' +}; + /** * Attachment represent data related/attached to the note. Conceptually similar to attributes, but intended for * larger amounts of data and generally not accessible to the user. @@ -92,6 +96,55 @@ class BAttachment extends AbstractBeccaEntity { this._setContent(content, opts); } + /** + * @returns {{note: BNote, branch: BBranch}} + */ + convertToNote() { + if (this.type === 'search') { + throw new Error(`Note of type search cannot have child notes`); + } + + if (!this.getNote()) { + throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " + + "Converting note revision's attachments to note is not (yet) supported."); + } + + if (!(this.role in attachmentRoleToNoteTypeMapping)) { + throw new Error(`Mapping from attachment role '${this.role}' to note's type is not defined`); + } + + if (!this.isContentAvailable()) { // isProtected is same for attachment + throw new Error(`Cannot convert protected attachment outside of protected session`); + } + + const noteService = require('../../services/notes'); + + const { note, branch } = noteService.createNewNote({ + parentNoteId: this.parentId, + title: this.title, + type: attachmentRoleToNoteTypeMapping[this.role], + mime: this.mime, + content: this.getContent(), + isProtected: this.isProtected + }); + + this.markAsDeleted(); + + if (this.role === 'image' && this.type === 'text') { + const origContent = this.getContent(); + const oldAttachmentUrl = `api/attachment/${this.attachmentId}/image/`; + const newNoteUrl = `api/images/${note.noteId}/`; + + const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl); + + if (origContent !== fixedContent) { + this.setContent(fixedContent); + } + } + + return { note, branch }; + } + beforeSaving() { super.beforeSaving(); diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 7aa467a8c..8c87dc09e 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -947,7 +947,7 @@ class BNote extends AbstractBeccaEntity { }; } - /** @returns {String[]} - includes the subtree node as well */ + /** @returns {String[]} - includes the subtree root note as well */ getSubtreeNoteIds({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) { return this.getSubtree({includeArchived, includeHidden, resolveSearch}) .notes @@ -1033,6 +1033,11 @@ class BNote extends AbstractBeccaEntity { return this.ancestorCache; } + /** @returns {string[]} */ + getAncestorNoteIds() { + return this.getAncestors().map(note => note.noteId); + } + /** @returns {boolean} */ hasAncestor(ancestorNoteId) { for (const ancestorNote of this.getAncestors()) { @@ -1408,61 +1413,6 @@ class BNote extends AbstractBeccaEntity { return attachment; } - /** - * @param attachmentId - * @returns {{note: BNote, branch: BBranch}} - */ - convertAttachmentToChildNote(attachmentId) { - if (this.type === 'search') { - throw new Error(`Note of type search cannot have child notes`); - } - - const attachment = this.getAttachmentById(attachmentId); - - if (!attachment) { - throw new NotFoundError(`Attachment '${attachmentId} of note '${this.noteId}' doesn't exist.`); - } - - const attachmentRoleToNoteTypeMapping = { - 'image': 'image' - }; - - if (!(attachment.role in attachmentRoleToNoteTypeMapping)) { - throw new Error(`Mapping from attachment role '${attachment.role}' to note's type is not defined`); - } - - if (!this.isContentAvailable()) { // isProtected is same for attachment - throw new Error(`Cannot convert protected attachment outside of protected session`); - } - - const noteService = require('../../services/notes'); - - const {note, branch} = noteService.createNewNote({ - parentNoteId: this.noteId, - title: attachment.title, - type: attachmentRoleToNoteTypeMapping[attachment.role], - mime: attachment.mime, - content: attachment.getContent(), - isProtected: this.isProtected - }); - - attachment.markAsDeleted(); - - if (attachment.role === 'image' && this.type === 'text') { - const origContent = this.getContent(); - const oldAttachmentUrl = `api/notes/${this.noteId}/images/${attachment.attachmentId}/`; - const newNoteUrl = `api/images/${note.noteId}/`; - - const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl); - - if (origContent !== fixedContent) { - this.setContent(fixedContent); - } - } - - return { note, branch }; - } - /** * (Soft) delete a note and all its descendants. * diff --git a/src/becca/entities/bnote_revision.js b/src/becca/entities/bnote_revision.js index 6457a37da..368a65ade 100644 --- a/src/becca/entities/bnote_revision.js +++ b/src/becca/entities/bnote_revision.js @@ -63,6 +63,12 @@ class BNoteRevision extends AbstractBeccaEntity { return utils.isStringNote(this.type, this.mime); } + isContentAvailable() { + return !this.noteRevisionId // new note which was not encrypted yet + || !this.isProtected + || protectedSessionService.isProtectedSessionAvailable() + } + /* * Note revision content has quite special handling - it's not a separate entity, but a lazily loaded * part of NoteRevision entity with its own sync. Reason behind this hybrid design is that diff --git a/src/public/app/services/attribute_autocomplete.js b/src/public/app/services/attribute_autocomplete.js index 6780ec7af..bd0bc3af2 100644 --- a/src/public/app/services/attribute_autocomplete.js +++ b/src/public/app/services/attribute_autocomplete.js @@ -20,7 +20,7 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) { source: async (term, cb) => { const type = typeof attributeType === "function" ? attributeType() : attributeType; - const names = await server.get(`attributes/names/?type=${type}&query=${encodeURIComponent(term)}`); + const names = await server.get(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`); const result = names.map(name => ({name})); cb(result); @@ -52,7 +52,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }) { return; } - const attributeValues = (await server.get(`attributes/values/${encodeURIComponent(attributeName)}`)) + const attributeValues = (await server.get(`attribute-values/${encodeURIComponent(attributeName)}`)) .map(attribute => ({ value: attribute })); if (attributeValues.length === 0) { diff --git a/src/public/app/services/open.js b/src/public/app/services/open.js index 2d0ef6091..b1df01dd7 100644 --- a/src/public/app/services/open.js +++ b/src/public/app/services/open.js @@ -48,7 +48,7 @@ async function openNoteExternally(noteId, mime) { } function downloadNoteRevision(noteId, noteRevisionId) { - const url = getUrlForDownload(`api/notes/${noteId}/revisions/${noteRevisionId}/download`); + const url = getUrlForDownload(`api/revisions/${noteRevisionId}/download`); download(url); } diff --git a/src/public/app/widgets/attribute_widgets/attribute_editor.js b/src/public/app/widgets/attribute_widgets/attribute_editor.js index 217717f05..bb69f3b50 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_editor.js +++ b/src/public/app/widgets/attribute_widgets/attribute_editor.js @@ -95,7 +95,7 @@ const mentionSetup = { { marker: '#', feed: async queryText => { - const names = await server.get(`attributes/names/?type=label&query=${encodeURIComponent(queryText)}`); + const names = await server.get(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`); return names.map(name => { return { @@ -110,7 +110,7 @@ const mentionSetup = { { marker: '~', feed: async queryText => { - const names = await server.get(`attributes/names/?type=relation&query=${encodeURIComponent(queryText)}`); + const names = await server.get(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`); return names.map(name => { return { diff --git a/src/public/app/widgets/dialogs/export.js b/src/public/app/widgets/dialogs/export.js index 5a13f2746..9fcb098ba 100644 --- a/src/public/app/widgets/dialogs/export.js +++ b/src/public/app/widgets/dialogs/export.js @@ -219,7 +219,7 @@ export default class ExportDialog extends BasicWidget { exportBranch(branchId, type, format, version) { this.taskId = utils.randomString(10); - const url = openService.getUrlForDownload(`api/notes/${branchId}/export/${type}/${format}/${version}/${this.taskId}`); + const url = openService.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${this.taskId}`); openService.download(url); } diff --git a/src/public/app/widgets/dialogs/note_revisions.js b/src/public/app/widgets/dialogs/note_revisions.js index 226bc3f1c..dbcca9b2e 100644 --- a/src/public/app/widgets/dialogs/note_revisions.js +++ b/src/public/app/widgets/dialogs/note_revisions.js @@ -188,7 +188,7 @@ export default class NoteRevisionsDialog extends BasicWidget { const text = 'Do you want to restore this revision? This will overwrite current title/content of the note with this revision.'; if (await dialogService.confirm(text)) { - await server.put(`notes/${revisionItem.noteId}/restore-revision/${revisionItem.noteRevisionId}`); + await server.post(`revisions/${revisionItem.noteRevisionId}/restore`); this.$widget.modal('hide'); @@ -202,7 +202,7 @@ export default class NoteRevisionsDialog extends BasicWidget { const text = 'Do you want to delete this revision? This action will delete revision title and content, but still preserve revision metadata.'; if (await dialogService.confirm(text)) { - await server.remove(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`); + await server.remove(`revisions/${revisionItem.noteRevisionId}`); this.loadNoteRevisions(revisionItem.noteId); @@ -232,7 +232,7 @@ export default class NoteRevisionsDialog extends BasicWidget { async renderContent(revisionItem) { this.$content.empty(); - const fullNoteRevision = await server.get(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`); + const fullNoteRevision = await server.get(`revisions/${revisionItem.noteRevisionId}`); if (revisionItem.type === 'text') { this.$content.html(fullNoteRevision.content); diff --git a/src/public/app/widgets/ribbon_widgets/promoted_attributes.js b/src/public/app/widgets/ribbon_widgets/promoted_attributes.js index 46b199509..fa3edc52c 100644 --- a/src/public/app/widgets/ribbon_widgets/promoted_attributes.js +++ b/src/public/app/widgets/ribbon_widgets/promoted_attributes.js @@ -142,7 +142,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { $input.prop("type", "text"); // no need to await for this, can be done asynchronously - server.get(`attributes/values/${encodeURIComponent(valueAttr.name)}`).then(attributeValues => { + server.get(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then(attributeValues => { if (attributeValues.length === 0) { return; } diff --git a/src/public/app/widgets/type_widgets/relation_map.js b/src/public/app/widgets/type_widgets/relation_map.js index 696ad250d..c8619d8a1 100644 --- a/src/public/app/widgets/type_widgets/relation_map.js +++ b/src/public/app/widgets/type_widgets/relation_map.js @@ -237,7 +237,7 @@ export default class RelationMapTypeWidget extends TypeWidget { async loadNotesAndRelations() { const noteIds = this.mapData.notes.map(note => note.noteId); - const data = await server.post("notes/relation-map", {noteIds, relationMapNoteId: this.noteId}); + const data = await server.post("relation-map", {noteIds, relationMapNoteId: this.noteId}); this.relations = []; diff --git a/src/routes/api/attachments.js b/src/routes/api/attachments.js index 818a254d1..2eebaa4d3 100644 --- a/src/routes/api/attachments.js +++ b/src/routes/api/attachments.js @@ -1,7 +1,6 @@ const becca = require("../../becca/becca"); const NotFoundError = require("../../errors/not_found_error"); const utils = require("../../services/utils"); -const noteService = require("../../services/notes"); function getAttachments(req) { const includeContent = req.query.includeContent === 'true'; @@ -19,18 +18,12 @@ function getAttachments(req) { function getAttachment(req) { const includeContent = req.query.includeContent === 'true'; - const {noteId, attachmentId} = req.params; + const {attachmentId} = req.params; - const note = becca.getNote(noteId); - - if (!note) { - throw new NotFoundError(`Note '${noteId}' doesn't exist.`); - } - - const attachment = note.getAttachmentById(attachmentId); + const attachment = becca.getAttachment(attachmentId); if (!attachment) { - throw new NotFoundError(`Attachment '${attachmentId} of note '${noteId}' doesn't exist.`); + throw new NotFoundError(`Attachment '${attachmentId}' doesn't exist.`); } return processAttachment(attachment, includeContent); @@ -72,15 +65,9 @@ function saveAttachment(req) { } function deleteAttachment(req) { - const {noteId, attachmentId} = req.params; + const {attachmentId} = req.params; - const note = becca.getNote(noteId); - - if (!note) { - throw new NotFoundError(`Note '${noteId}' doesn't exist.`); - } - - const attachment = note.getAttachmentById(attachmentId); + const attachment = becca.getAttachment(attachmentId); if (attachment) { attachment.markAsDeleted(); @@ -88,15 +75,15 @@ function deleteAttachment(req) { } function convertAttachmentToNote(req) { - const {noteId, attachmentId} = req.params; + const {attachmentId} = req.params; - const note = becca.getNote(noteId); + const attachment = becca.getAttachment(attachmentId); - if (!note) { - throw new NotFoundError(`Note '${noteId}' doesn't exist.`); + if (!attachment) { + throw new NotFoundError(`Attachment '${attachmentId}' doesn't exist.`); } - return note.convertAttachmentToChildNote(attachmentId); + return attachment.convertToNote(); } module.exports = { diff --git a/src/routes/api/note_revisions.js b/src/routes/api/note_revisions.js index e6eb5079b..cf7bb9174 100644 --- a/src/routes/api/note_revisions.js +++ b/src/routes/api/note_revisions.js @@ -64,13 +64,7 @@ function getRevisionFilename(noteRevision) { function downloadNoteRevision(req, res) { const noteRevision = becca.getNoteRevision(req.params.noteRevisionId); - if (noteRevision.noteId !== req.params.noteId) { - return res.setHeader("Content-Type", "text/plain") - .status(400) - .send(`Note revision ${req.params.noteRevisionId} does not belong to note ${req.params.noteId}`); - } - - if (noteRevision.isProtected && !protectedSessionService.isProtectedSessionAvailable()) { + if (!noteRevision.isContentAvailable()) { return res.setHeader("Content-Type", "text/plain") .status(401) .send("Protected session not available"); diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index 81b300063..9ea7a1a25 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.js @@ -127,69 +127,6 @@ function setNoteTypeMime(req) { note.save(); } -function getRelationMap(req) { - const {relationMapNoteId, noteIds} = req.body; - - const resp = { - // noteId => title - noteTitles: {}, - relations: [], - // relation name => inverse relation name - inverseRelations: { - 'internalLink': 'internalLink' - } - }; - - if (noteIds.length === 0) { - return resp; - } - - const questionMarks = noteIds.map(noteId => '?').join(','); - - const relationMapNote = becca.getNote(relationMapNoteId); - - const displayRelationsVal = relationMapNote.getLabelValue('displayRelations'); - const displayRelations = !displayRelationsVal ? [] : displayRelationsVal - .split(",") - .map(token => token.trim()); - - const hideRelationsVal = relationMapNote.getLabelValue('hideRelations'); - const hideRelations = !hideRelationsVal ? [] : hideRelationsVal - .split(",") - .map(token => token.trim()); - - const foundNoteIds = sql.getColumn(`SELECT noteId FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); - const notes = becca.getNotes(foundNoteIds); - - for (const note of notes) { - resp.noteTitles[note.noteId] = note.title; - - resp.relations = resp.relations.concat(note.getRelations() - .filter(relation => !relation.isAutoLink() || displayRelations.includes(relation.name)) - .filter(relation => displayRelations.length > 0 - ? displayRelations.includes(relation.name) - : !hideRelations.includes(relation.name)) - .filter(relation => noteIds.includes(relation.value)) - .map(relation => ({ - attributeId: relation.attributeId, - sourceNoteId: relation.noteId, - targetNoteId: relation.value, - name: relation.name - }))); - - for (const relationDefinition of note.getRelationDefinitions()) { - const def = relationDefinition.getDefinition(); - - if (def.inverseRelation) { - resp.inverseRelations[relationDefinition.getDefinedName()] = def.inverseRelation; - resp.inverseRelations[def.inverseRelation] = relationDefinition.getDefinedName(); - } - } - } - - return resp; -} - function changeTitle(req) { const noteId = req.params.noteId; const title = req.body.title; @@ -273,6 +210,7 @@ function getDeleteNotesPreview(req) { if (noteIdsToBeDeleted.size > 0) { sql.fillParamList(noteIdsToBeDeleted); + // FIXME: No need to do this in database, can be done with becca data brokenRelations = sql.getRows(` SELECT attr.noteId, attr.name, attr.value FROM attributes attr @@ -334,7 +272,6 @@ module.exports = { sortChildNotes, protectNote, setNoteTypeMime, - getRelationMap, changeTitle, duplicateSubtree, eraseDeletedNotesNow, diff --git a/src/routes/api/relation-map.js b/src/routes/api/relation-map.js new file mode 100644 index 000000000..2f4cfce26 --- /dev/null +++ b/src/routes/api/relation-map.js @@ -0,0 +1,69 @@ +const becca = require("../../becca/becca.js"); +const sql = require("../../services/sql.js"); + +function getRelationMap(req) { + const {relationMapNoteId, noteIds} = req.body; + + const resp = { + // noteId => title + noteTitles: {}, + relations: [], + // relation name => inverse relation name + inverseRelations: { + 'internalLink': 'internalLink' + } + }; + + if (noteIds.length === 0) { + return resp; + } + + const questionMarks = noteIds.map(noteId => '?').join(','); + + const relationMapNote = becca.getNote(relationMapNoteId); + + const displayRelationsVal = relationMapNote.getLabelValue('displayRelations'); + const displayRelations = !displayRelationsVal ? [] : displayRelationsVal + .split(",") + .map(token => token.trim()); + + const hideRelationsVal = relationMapNote.getLabelValue('hideRelations'); + const hideRelations = !hideRelationsVal ? [] : hideRelationsVal + .split(",") + .map(token => token.trim()); + + const foundNoteIds = sql.getColumn(`SELECT noteId FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); + const notes = becca.getNotes(foundNoteIds); + + for (const note of notes) { + resp.noteTitles[note.noteId] = note.title; + + resp.relations = resp.relations.concat(note.getRelations() + .filter(relation => !relation.isAutoLink() || displayRelations.includes(relation.name)) + .filter(relation => displayRelations.length > 0 + ? displayRelations.includes(relation.name) + : !hideRelations.includes(relation.name)) + .filter(relation => noteIds.includes(relation.value)) + .map(relation => ({ + attributeId: relation.attributeId, + sourceNoteId: relation.noteId, + targetNoteId: relation.value, + name: relation.name + }))); + + for (const relationDefinition of note.getRelationDefinitions()) { + const def = relationDefinition.getDefinition(); + + if (def.inverseRelation) { + resp.inverseRelations[relationDefinition.getDefinedName()] = def.inverseRelation; + resp.inverseRelations[def.inverseRelation] = relationDefinition.getDefinedName(); + } + } + } + + return resp; +} + +module.exports = { + getRelationMap +}; diff --git a/src/routes/routes.js b/src/routes/routes.js index d1b0bc58f..5338de029 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -57,8 +57,10 @@ const backendLogRoute = require('./api/backend_log'); const statsRoute = require('./api/stats'); const fontsRoute = require('./api/fonts'); const etapiTokensApiRoutes = require('./api/etapi_tokens'); +const relationMapApiRoute = require('./api/relation-map'); const otherRoute = require('./api/other'); const shareRoutes = require('../share/routes'); + const etapiAuthRoutes = require('../etapi/auth'); const etapiAppInfoRoutes = require('../etapi/app_info'); const etapiAttributeRoutes = require('../etapi/attributes'); @@ -73,7 +75,7 @@ const csrfMiddleware = csurf({ }); const MAX_ALLOWED_FILE_SIZE_MB = 250; -const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete'; +const GET = 'get', PST = 'post', PUT = 'put', PATCH = 'patch', DEL = 'delete'; const uploadMiddleware = createUploadMiddleware(); @@ -101,63 +103,32 @@ function register(app) { skipSuccessfulRequests: true // successful auth to rate-limited ETAPI routes isn't counted. However, successful auth to /login is still counted! }); - route(POST, '/login', [loginRateLimiter], loginRoute.login); - route(POST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout); - route(POST, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword); + route(PST, '/login', [loginRateLimiter], loginRoute.login); + route(PST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout); + route(PST, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword); route(GET, '/setup', [], setupRoute.setupPage); apiRoute(GET, '/api/tree', treeApiRoute.getTree); - apiRoute(POST, '/api/tree/load', treeApiRoute.load); - apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix); - - apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent); - apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote); - apiRoute(PUT, '/api/branches/:branchId/move-after/:afterBranchId', branchesApiRoute.moveBranchAfterNote); - apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded); - apiRoute(PUT, '/api/branches/:branchId/expanded-subtree/:expanded', branchesApiRoute.setExpandedForSubtree); - apiRoute(DELETE, '/api/branches/:branchId', branchesApiRoute.deleteBranch); - - apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete); + apiRoute(PST, '/api/tree/load', treeApiRoute.load); apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote); apiRoute(PUT, '/api/notes/:noteId/data', notesApiRoute.updateNoteData); - apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote); + apiRoute(DEL, '/api/notes/:noteId', notesApiRoute.deleteNote); apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote); - apiRoute(POST, '/api/notes/:noteId/revision', notesApiRoute.forceSaveNoteRevision); - apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); + apiRoute(PST, '/api/notes/:noteId/revision', notesApiRoute.forceSaveNoteRevision); + apiRoute(PST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes); apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote); apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime); - apiRoute(GET, '/api/notes/:noteId/attachments', attachmentsApiRoute.getAttachments); - apiRoute(GET, '/api/notes/:noteId/attachments/:attachmentId', attachmentsApiRoute.getAttachment); - apiRoute(POST, '/api/notes/:noteId/attachments', attachmentsApiRoute.saveAttachment); - apiRoute(POST, '/api/notes/:noteId/attachments/:attachmentId/convert-to-note', attachmentsApiRoute.convertAttachmentToNote); - apiRoute(DELETE, '/api/notes/:noteId/attachments/:attachmentId', attachmentsApiRoute.deleteAttachment); - apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions); - apiRoute(DELETE, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions); - apiRoute(GET, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision); - apiRoute(DELETE, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision); - route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision); - apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision); - apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap); - apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow); apiRoute(PUT, '/api/notes/:noteId/title', notesApiRoute.changeTitle); - apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree); - apiRoute(POST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile); - - apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate); - + apiRoute(PST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree); + apiRoute(PST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile); apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch); apiRoute(PUT, '/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present', cloningApiRoute.toggleNoteInParent); apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote); apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter); - - route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch); - route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importToBranch, apiResultHandler); - route(PUT, '/api/notes/:noteId/file', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], filesRoute.updateFile, apiResultHandler); - route(GET, '/api/notes/:noteId/open', [auth.checkApiAuthOrElectron], filesRoute.openFile); route(GET, '/api/notes/:noteId/open-partial', [auth.checkApiAuthOrElectron], createPartialContentHandler(filesRoute.fileContentProvider, { @@ -166,69 +137,73 @@ function register(app) { route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); // this "hacky" path is used for easier referencing of CSS resources route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); - apiRoute(POST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveToTmpDir); + apiRoute(PST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveToTmpDir); + + apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent); + apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote); + apiRoute(PUT, '/api/branches/:branchId/move-after/:afterBranchId', branchesApiRoute.moveBranchAfterNote); + apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded); + apiRoute(PUT, '/api/branches/:branchId/expanded-subtree/:expanded', branchesApiRoute.setExpandedForSubtree); + apiRoute(DEL, '/api/branches/:branchId', branchesApiRoute.deleteBranch); + apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix); + + apiRoute(GET, '/api/notes/:noteId/attachments', attachmentsApiRoute.getAttachments); + apiRoute(PST, '/api/notes/:noteId/attachments', attachmentsApiRoute.saveAttachment); + apiRoute(GET, '/api/attachments/:attachmentId', attachmentsApiRoute.getAttachment); + apiRoute(PST, '/api/attachments/:attachmentId/convert-to-note', attachmentsApiRoute.convertAttachmentToNote); + apiRoute(DEL, '/api/attachments/:attachmentId', attachmentsApiRoute.deleteAttachment); + route(GET, '/api/attachments/:attachmentId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage); + + apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions); + apiRoute(DEL, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions); + apiRoute(GET, '/api/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision); + apiRoute(DEL, '/api/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision); + apiRoute(PST, '/api/revisions/:noteRevisionId/restore', noteRevisionsApiRoute.restoreNoteRevision); + route(GET, '/api/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision); + + + route(GET, '/api/branches/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch); + route(PST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importToBranch, apiResultHandler); apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes); - apiRoute(POST, '/api/notes/:noteId/attributes', attributesRoute.addNoteAttribute); + apiRoute(PST, '/api/notes/:noteId/attributes', attributesRoute.addNoteAttribute); apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes); apiRoute(PUT, '/api/notes/:noteId/attribute', attributesRoute.updateNoteAttribute); apiRoute(PUT, '/api/notes/:noteId/set-attribute', attributesRoute.setNoteAttribute); apiRoute(PUT, '/api/notes/:noteId/relations/:name/to/:targetNoteId', attributesRoute.createRelation); - apiRoute(DELETE, '/api/notes/:noteId/relations/:name/to/:targetNoteId', attributesRoute.deleteRelation); - apiRoute(DELETE, '/api/notes/:noteId/attributes/:attributeId', attributesRoute.deleteNoteAttribute); - apiRoute(GET, '/api/attributes/names', attributesRoute.getAttributeNames); - apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); - - apiRoute(POST, '/api/note-map/:noteId/tree', noteMapRoute.getTreeMap); - apiRoute(POST, '/api/note-map/:noteId/link', noteMapRoute.getLinkMap); - apiRoute(GET, '/api/note-map/:noteId/backlink-count', noteMapRoute.getBacklinkCount); - apiRoute(GET, '/api/note-map/:noteId/backlinks', noteMapRoute.getBacklinks); - - apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); - apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote); - apiRoute(GET, '/api/special-notes/weeks/:date', specialNotesRoute.getWeekNote); - apiRoute(GET, '/api/special-notes/months/:month', specialNotesRoute.getMonthNote); - apiRoute(GET, '/api/special-notes/years/:year', specialNotesRoute.getYearNote); - apiRoute(GET, '/api/special-notes/notes-for-month/:month', specialNotesRoute.getDayNotesForMonth); - apiRoute(POST, '/api/special-notes/sql-console', specialNotesRoute.createSqlConsole); - apiRoute(POST, '/api/special-notes/save-sql-console', specialNotesRoute.saveSqlConsole); - apiRoute(POST, '/api/special-notes/search-note', specialNotesRoute.createSearchNote); - apiRoute(POST, '/api/special-notes/save-search-note', specialNotesRoute.saveSearchNote); - apiRoute(POST, '/api/special-notes/launchers/:noteId/reset', specialNotesRoute.resetLauncher); - apiRoute(POST, '/api/special-notes/launchers/:parentNoteId/:launcherType', specialNotesRoute.createLauncher); - apiRoute(PUT, '/api/special-notes/api-script-launcher', specialNotesRoute.createOrUpdateScriptLauncherFromApi); + apiRoute(DEL, '/api/notes/:noteId/relations/:name/to/:targetNoteId', attributesRoute.deleteRelation); + apiRoute(DEL, '/api/notes/:noteId/attributes/:attributeId', attributesRoute.deleteNoteAttribute); + apiRoute(GET, '/api/attribute-names', attributesRoute.getAttributeNames); + apiRoute(GET, '/api/attribute-values/:attributeName', attributesRoute.getValuesForAttribute); // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage); - route(GET, '/api/notes/:noteId/images/:attachmentId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage); - route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.uploadImage, apiResultHandler); + route(PST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.uploadImage, apiResultHandler); route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler); - apiRoute(GET, '/api/recent-changes/:ancestorNoteId', recentChangesApiRoute.getRecentChanges); - apiRoute(GET, '/api/options', optionsApiRoute.getOptions); // FIXME: possibly change to sending value in the body to avoid host of HTTP server issues with slashes apiRoute(PUT, '/api/options/:name/:value*', optionsApiRoute.updateOption); apiRoute(PUT, '/api/options', optionsApiRoute.updateOptions); apiRoute(GET, '/api/options/user-themes', optionsApiRoute.getUserThemes); - apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword); - apiRoute(POST, '/api/password/reset', passwordApiRoute.resetPassword); + apiRoute(PST, '/api/password/change', passwordApiRoute.changePassword); + apiRoute(PST, '/api/password/reset', passwordApiRoute.resetPassword); - apiRoute(POST, '/api/sync/test', syncApiRoute.testSync); - apiRoute(POST, '/api/sync/now', syncApiRoute.syncNow); - apiRoute(POST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges); - apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); - apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); + apiRoute(PST, '/api/sync/test', syncApiRoute.testSync); + apiRoute(PST, '/api/sync/now', syncApiRoute.syncNow); + apiRoute(PST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges); + apiRoute(PST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); + apiRoute(PST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); route(GET, '/api/sync/check', [auth.checkApiAuth], syncApiRoute.checkSync, apiResultHandler); route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler); route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler); - route(POST, '/api/sync/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler); - route(POST, '/api/sync/check-entity-changes', [auth.checkApiAuth], syncApiRoute.checkEntityChanges, apiResultHandler); - route(POST, '/api/sync/queue-sector/:entityName/:sector', [auth.checkApiAuth], syncApiRoute.queueSector, apiResultHandler); + route(PST, '/api/sync/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler); + route(PST, '/api/sync/check-entity-changes', [auth.checkApiAuth], syncApiRoute.checkEntityChanges, apiResultHandler); + route(PST, '/api/sync/queue-sector/:entityName/:sector', [auth.checkApiAuth], syncApiRoute.queueSector, apiResultHandler); route(GET, '/api/sync/stats', [], syncApiRoute.getStats, apiResultHandler); - apiRoute(POST, '/api/recent-notes', recentNotesRoute.addRecentNote); + apiRoute(PST, '/api/recent-notes', recentNotesRoute.addRecentNote); apiRoute(GET, '/api/app-info', appInfoRoute.getAppInfo); // docker health check @@ -236,82 +211,102 @@ function register(app) { // group of services below are meant to be executed from outside route(GET, '/api/setup/status', [], setupApiRoute.getStatus, apiResultHandler); - route(POST, '/api/setup/new-document', [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler, false); - route(POST, '/api/setup/sync-from-server', [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler, false); + route(PST, '/api/setup/new-document', [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler, false); + route(PST, '/api/setup/sync-from-server', [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler, false); route(GET, '/api/setup/sync-seed', [auth.checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler); - route(POST, '/api/setup/sync-seed', [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler, false); + route(PST, '/api/setup/sync-seed', [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler, false); + + apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete); + apiRoute(GET, '/api/quick-search/:searchString', searchRoute.quickSearch); + apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); + apiRoute(PST, '/api/search-and-execute-note/:noteId', searchRoute.searchAndExecute); + apiRoute(PST, '/api/search-related', searchRoute.getRelatedNotes); + apiRoute(GET, '/api/search/:searchString', searchRoute.search); + apiRoute(GET, '/api/search-templates', searchRoute.searchTemplates); + + apiRoute(PST, '/api/bulk-action/execute', bulkActionRoute.execute); + apiRoute(PST, '/api/bulk-action/affected-notes', bulkActionRoute.getAffectedNoteCount); + + route(PST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); + // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) + apiRoute(PST, '/api/login/protected', loginApiRoute.loginToProtectedSession); + apiRoute(PST, '/api/login/protected/touch', loginApiRoute.touchProtectedSession); + apiRoute(PST, '/api/logout/protected', loginApiRoute.logoutFromProtectedSession); + + route(PST, '/api/login/token', [loginRateLimiter], loginApiRoute.token, apiResultHandler); + + apiRoute(GET, '/api/etapi-tokens', etapiTokensApiRoutes.getTokens); + apiRoute(PST, '/api/etapi-tokens', etapiTokensApiRoutes.createToken); + apiRoute(PATCH, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.patchToken); + apiRoute(DEL, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.deleteToken); + + // in case of local electron, local calls are allowed unauthenticated, for server they need auth + const clipperMiddleware = utils.isElectron() ? [] : [auth.checkEtapiToken]; + + route(GET, '/api/clipper/handshake', clipperMiddleware, clipperRoute.handshake, apiResultHandler); + route(PST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler); + route(PST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler); + route(PST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler); + + apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); + apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote); + apiRoute(GET, '/api/special-notes/weeks/:date', specialNotesRoute.getWeekNote); + apiRoute(GET, '/api/special-notes/months/:month', specialNotesRoute.getMonthNote); + apiRoute(GET, '/api/special-notes/years/:year', specialNotesRoute.getYearNote); + apiRoute(GET, '/api/special-notes/notes-for-month/:month', specialNotesRoute.getDayNotesForMonth); + apiRoute(PST, '/api/special-notes/sql-console', specialNotesRoute.createSqlConsole); + apiRoute(PST, '/api/special-notes/save-sql-console', specialNotesRoute.saveSqlConsole); + apiRoute(PST, '/api/special-notes/search-note', specialNotesRoute.createSearchNote); + apiRoute(PST, '/api/special-notes/save-search-note', specialNotesRoute.saveSearchNote); + apiRoute(PST, '/api/special-notes/launchers/:noteId/reset', specialNotesRoute.resetLauncher); + apiRoute(PST, '/api/special-notes/launchers/:parentNoteId/:launcherType', specialNotesRoute.createLauncher); + apiRoute(PUT, '/api/special-notes/api-script-launcher', specialNotesRoute.createOrUpdateScriptLauncherFromApi); apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema); - apiRoute(POST, '/api/sql/execute/:noteId', sqlRoute.execute); - route(POST, '/api/database/anonymize/:type', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false); + apiRoute(PST, '/api/sql/execute/:noteId', sqlRoute.execute); + route(PST, '/api/database/anonymize/:type', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false); // backup requires execution outside of transaction - route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false); + route(PST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false); // VACUUM requires execution outside of transaction - route(POST, '/api/database/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.vacuumDatabase, apiResultHandler, false); + route(PST, '/api/database/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.vacuumDatabase, apiResultHandler, false); - route(POST, '/api/database/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler, false); + route(PST, '/api/database/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler, false); apiRoute(GET, '/api/database/check-integrity', databaseRoute.checkIntegrity); - apiRoute(POST, '/api/script/exec', scriptRoute.exec); - apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run); + apiRoute(PST, '/api/script/exec', scriptRoute.exec); + apiRoute(PST, '/api/script/run/:noteId', scriptRoute.run); apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles); apiRoute(GET, '/api/script/widgets', scriptRoute.getWidgetBundles); apiRoute(GET, '/api/script/bundle/:noteId', scriptRoute.getBundle); apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles); // no CSRF since this is called from android app - route(POST, '/api/sender/login', [loginRateLimiter], loginApiRoute.token, apiResultHandler); - route(POST, '/api/sender/image', [auth.checkEtapiToken, uploadMiddlewareWithErrorHandling], senderRoute.uploadImage, apiResultHandler); - route(POST, '/api/sender/note', [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler); - - apiRoute(GET, '/api/quick-search/:searchString', searchRoute.quickSearch); - apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); - apiRoute(POST, '/api/search-and-execute-note/:noteId', searchRoute.searchAndExecute); - apiRoute(POST, '/api/search-related', searchRoute.getRelatedNotes); - apiRoute(GET, '/api/search/:searchString', searchRoute.search); - apiRoute(GET, '/api/search-templates', searchRoute.searchTemplates); - - apiRoute(POST, '/api/bulk-action/execute', bulkActionRoute.execute); - apiRoute(POST, '/api/bulk-action/affected-notes', bulkActionRoute.getAffectedNoteCount); - - route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); - // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) - apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession); - apiRoute(POST, '/api/login/protected/touch', loginApiRoute.touchProtectedSession); - apiRoute(POST, '/api/logout/protected', loginApiRoute.logoutFromProtectedSession); - - route(POST, '/api/login/token', [loginRateLimiter], loginApiRoute.token, apiResultHandler); - - // in case of local electron, local calls are allowed unauthenticated, for server they need auth - const clipperMiddleware = utils.isElectron() ? [] : [auth.checkEtapiToken]; - - route(GET, '/api/clipper/handshake', clipperMiddleware, clipperRoute.handshake, apiResultHandler); - route(POST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler); - route(POST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler); - route(POST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler); - - apiRoute(GET, '/api/similar-notes/:noteId', similarNotesRoute.getSimilarNotes); + route(PST, '/api/sender/login', [loginRateLimiter], loginApiRoute.token, apiResultHandler); + route(PST, '/api/sender/image', [auth.checkEtapiToken, uploadMiddlewareWithErrorHandling], senderRoute.uploadImage, apiResultHandler); + route(PST, '/api/sender/note', [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler); apiRoute(GET, '/api/keyboard-actions', keysRoute.getKeyboardActions); apiRoute(GET, '/api/keyboard-shortcuts-for-notes', keysRoute.getShortcutsForNotes); + apiRoute(PST, '/api/relation-map', relationMapApiRoute.getRelationMap); + apiRoute(PST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow); + apiRoute(GET, '/api/similar-notes/:noteId', similarNotesRoute.getSimilarNotes); apiRoute(GET, '/api/backend-log', backendLogRoute.getBackendLog); - apiRoute(GET, '/api/stats/note-size/:noteId', statsRoute.getNoteSize); apiRoute(GET, '/api/stats/subtree-size/:noteId', statsRoute.getSubtreeSize); - - apiRoute(POST, '/api/delete-notes-preview', notesApiRoute.getDeleteNotesPreview); - + apiRoute(PST, '/api/delete-notes-preview', notesApiRoute.getDeleteNotesPreview); route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss); apiRoute(GET, '/api/other/icon-usage', otherRoute.getIconUsage); + apiRoute(GET, '/api/recent-changes/:ancestorNoteId', recentChangesApiRoute.getRecentChanges); + apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate); - apiRoute(GET, '/api/etapi-tokens', etapiTokensApiRoutes.getTokens); - apiRoute(POST, '/api/etapi-tokens', etapiTokensApiRoutes.createToken); - apiRoute(PATCH, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.patchToken); - apiRoute(DELETE, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.deleteToken); + apiRoute(PST, '/api/note-map/:noteId/tree', noteMapRoute.getTreeMap); + apiRoute(PST, '/api/note-map/:noteId/link', noteMapRoute.getLinkMap); + apiRoute(GET, '/api/note-map/:noteId/backlink-count', noteMapRoute.getBacklinkCount); + apiRoute(GET, '/api/note-map/:noteId/backlinks', noteMapRoute.getBacklinks); shareRoutes.register(router); diff --git a/src/services/hoisted_note.js b/src/services/hoisted_note.js index 06c7c3394..ea12a17f5 100644 --- a/src/services/hoisted_note.js +++ b/src/services/hoisted_note.js @@ -23,12 +23,8 @@ function isHoistedInHiddenSubtree() { return hoistedNote.isHiddenCompletely(); } -function getHoistedNote() { - return becca.getNote(cls.getHoistedNoteId()); -} - function getWorkspaceNote() { - const hoistedNote = getHoistedNote(); + const hoistedNote = becca.getNote(cls.getHoistedNoteId()); if (hoistedNote.isRoot() || hoistedNote.hasLabel('workspace')) { return hoistedNote; @@ -39,7 +35,6 @@ function getWorkspaceNote() { module.exports = { getHoistedNoteId, - getHoistedNote, getWorkspaceNote, isHoistedInHiddenSubtree }; diff --git a/src/services/note_revisions.js b/src/services/note_revisions.js index 012884c4c..af6e1a1dc 100644 --- a/src/services/note_revisions.js +++ b/src/services/note_revisions.js @@ -2,35 +2,32 @@ const log = require('./log'); const sql = require('./sql'); -const protectedSession = require("./protected_session"); +const protectedSessionService = require("./protected_session"); /** * @param {BNote} note */ function protectNoteRevisions(note) { + if (!protectedSessionService.isProtectedSessionAvailable()) { + throw new Error(`Cannot (un)protect revisions of note '${note.noteId}' without active protected session`); + } + for (const revision of note.getNoteRevisions()) { - if (note.isProtected !== revision.isProtected) { - if (!protectedSession.isProtectedSessionAvailable()) { - log.error("Protected session is not available to fix note revisions."); + if (note.isProtected === revision.isProtected) { + continue; + } - return; - } + try { + const content = revision.getContent(); - try { - const content = revision.getContent(); + revision.isProtected = note.isProtected; - revision.isProtected = note.isProtected; + // this will force de/encryption + revision.setContent(content, {forceSave: true}); + } catch (e) { + log.error(`Could not un/protect note revision '${revision.noteRevisionId}'`); - // this will force de/encryption - revision.setContent(content); - - revision.save(); - } - catch (e) { - log.error(`Could not un/protect note revision ID = ${revision.noteRevisionId}`); - - throw e; - } + throw e; } } } diff --git a/src/services/notes.js b/src/services/notes.js index 37838c6a8..5b92ee6d0 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -9,7 +9,6 @@ const protectedSessionService = require('../services/protected_session'); const log = require('../services/log'); const utils = require('../services/utils'); const noteRevisionService = require('../services/note_revisions'); -const attributeService = require('../services/attributes'); const request = require('./request'); const path = require('path'); const url = require('url'); @@ -21,8 +20,8 @@ const dayjs = require("dayjs"); const htmlSanitizer = require("./html_sanitizer"); const ValidationError = require("../errors/validation_error"); const noteTypesService = require("./note_types"); -const {attach} = require("jsdom/lib/jsdom/living/helpers/svg/basic-types.js"); +/** @param {BNote} parentNote */ function getNewNotePosition(parentNote) { if (parentNote.isLabelTruthy('newNotesOnTop')) { const minNotePos = parentNote.getChildBranches() @@ -37,6 +36,7 @@ function getNewNotePosition(parentNote) { } } +/** @param {BNote} note */ function triggerNoteTitleChanged(note) { eventService.emit(eventService.NOTE_TITLE_CHANGED, note); } @@ -53,6 +53,10 @@ function deriveMime(type, mime) { return noteTypesService.getDefaultMimeForNoteType(type); } +/** + * @param {BNote} parentNote + * @param {BNote} childNote + */ function copyChildAttributes(parentNote, childNote) { const hasAlreadyTemplate = childNote.hasRelation('template'); @@ -78,6 +82,7 @@ function copyChildAttributes(parentNote, childNote) { } } +/** @param {BNote} parentNote */ function getNewNoteTitle(parentNote) { let title = "new note"; @@ -278,6 +283,12 @@ function createNewNoteWithTarget(target, targetBranchId, params) { } } +/** + * @param {BNote} note + * @param {boolean} protect + * @param {boolean} includingSubTree + * @param {TaskContext} taskContext + */ function protectNoteRecursively(note, protect, includingSubTree, taskContext) { protectNote(note, protect); @@ -290,7 +301,15 @@ function protectNoteRecursively(note, protect, includingSubTree, taskContext) { } } +/** + * @param {BNote} note + * @param {boolean} protect + */ function protectNote(note, protect) { + if (!protectedSessionService.isProtectedSessionAvailable()) { + throw new Error(`Cannot (un)protect note '${note.noteId}' with protect flag '${protect}' without active protected session`); + } + try { if (protect !== note.isProtected) { const content = note.getContent(); @@ -310,7 +329,7 @@ function protectNote(note, protect) { noteRevisionService.protectNoteRevisions(note); } catch (e) { - log.error(`Could not un/protect note ID = ${note.noteId}`); + log.error(`Could not un/protect note '${note.noteId}'`); throw e; } @@ -565,6 +584,7 @@ function saveLinks(note, content) { return content; } +/** @param {BNote} note */ function saveNoteRevisionIfNeeded(note) { // files and images are versioned separately if (note.type === 'file' || note.type === 'image' || note.hasLabel('disableVersioning')) { @@ -709,6 +729,8 @@ function scanForLinks(note, content) { } /** + * @param {BNote} note + * @param {string} content * Things which have to be executed after updating content, but asynchronously (separate transaction) */ async function asyncPostProcessContent(note, content) { diff --git a/src/services/special_notes.js b/src/services/special_notes.js index bbc1bc978..20426eb40 100644 --- a/src/services/special_notes.js +++ b/src/services/special_notes.js @@ -4,7 +4,6 @@ const becca = require("../becca/becca"); const noteService = require("./notes"); const dateUtils = require("./date_utils"); const log = require("./log"); -const hiddenSubtreeService = require("./hidden_subtree"); const hoistedNoteService = require("./hoisted_note"); const searchService = require("./search/services/search"); const SearchContext = require("./search/search_context"); diff --git a/src/services/tree.js b/src/services/tree.js index 1038e8772..371b777f4 100644 --- a/src/services/tree.js +++ b/src/services/tree.js @@ -4,31 +4,8 @@ const sql = require('./sql'); const log = require('./log'); const BBranch = require('../becca/entities/bbranch'); const entityChangesService = require('./entity_changes'); -const protectedSessionService = require('./protected_session'); const becca = require('../becca/becca'); -function getNotes(noteIds) { - // we return also deleted notes which have been specifically asked for - const notes = sql.getManyRows(` - SELECT - noteId, - title, - isProtected, - type, - mime, - isDeleted - FROM notes - WHERE noteId IN (???)`, noteIds); - - protectedSessionService.decryptNotes(notes); - - notes.forEach(note => { - note.isProtected = !!note.isProtected - }); - - return notes; -} - function validateParentChild(parentNoteId, childNoteId, branchId = null) { if (['root', '_hidden', '_share', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(childNoteId)) { return { success: false, message: `Cannot change this note's location.`}; @@ -39,7 +16,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) { return { success: false, message: `Cannot move anything into 'none' parent.` }; } - const existing = getExistingBranch(parentNoteId, childNoteId); + const existing = becca.getBranchFromChildAndParent(childNoteId, parentNoteId); if (existing && (branchId === null || existing.branchId !== branchId)) { const parentNote = becca.getNote(parentNoteId); @@ -51,7 +28,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) { }; } - if (!checkTreeCycle(parentNoteId, childNoteId)) { + if (wouldAddingBranchCreateCycle(parentNoteId, childNoteId)) { return { success: false, message: 'Moving/cloning note here would create cycle.' @@ -68,59 +45,22 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) { return { success: true }; } -function getExistingBranch(parentNoteId, childNoteId) { - const branchId = sql.getValue(` - SELECT branchId - FROM branches - WHERE noteId = ? - AND parentNoteId = ? - AND isDeleted = 0`, [childNoteId, parentNoteId]); - - return becca.getBranch(branchId); -} - /** * Tree cycle can be created when cloning or when moving existing clone. This method should detect both cases. */ -function checkTreeCycle(parentNoteId, childNoteId) { - const subtreeNoteIds = []; +function wouldAddingBranchCreateCycle(parentNoteId, childNoteId) { + const childNote = becca.getNote(childNoteId); + const parentNote = becca.getNote(parentNoteId); + + if (!childNote || !parentNote) { + return false; + } // we'll load the whole subtree - because the cycle can start in one of the notes in the subtree - loadSubtreeNoteIds(childNoteId, subtreeNoteIds); + const childSubtreeNoteIds = new Set(childNote.getSubtreeNoteIds()); + const parentAncestorNoteIds = parentNote.getAncestorNoteIds(); - function checkTreeCycleInner(parentNoteId) { - if (parentNoteId === 'root') { - return true; - } - - if (subtreeNoteIds.includes(parentNoteId)) { - // while towards the root of the tree we encountered noteId which is already present in the subtree - // joining parentNoteId with childNoteId would then clearly create a cycle - return false; - } - - const parentNoteIds = sql.getColumn("SELECT DISTINCT parentNoteId FROM branches WHERE noteId = ? AND isDeleted = 0", [parentNoteId]); - - for (const pid of parentNoteIds) { - if (!checkTreeCycleInner(pid)) { - return false; - } - } - - return true; - } - - return checkTreeCycleInner(parentNoteId); -} - -function loadSubtreeNoteIds(parentNoteId, subtreeNoteIds) { - subtreeNoteIds.push(parentNoteId); - - const children = sql.getColumn("SELECT noteId FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]); - - for (const childNoteId of children) { - loadSubtreeNoteIds(childNoteId, subtreeNoteIds); - } + return parentAncestorNoteIds.some(parentAncestorNoteId => childSubtreeNoteIds.has(parentAncestorNoteId)); } function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, foldersFirst = false, sortNatural = false, sortLocale) { @@ -295,7 +235,6 @@ function setNoteToParent(noteId, prefix, parentNoteId) { } module.exports = { - getNotes, validateParentChild, sortNotes, sortNotesIfNeeded,