diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index 4b6f0ebd2..7c9d9844e 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -251,8 +251,8 @@ export default class NoteTreeWidget extends TabAwareWidget { this.triggerCommand('setActiveScreen', {screen:'detail'}); } }, - expand: (event, data) => this.setExpandedToServer(data.node.data.branchId, true), - collapse: (event, data) => this.setExpandedToServer(data.node.data.branchId, false), + expand: (event, data) => this.setExpanded(data.node.data.branchId, true), + collapse: (event, data) => this.setExpanded(data.node.data.branchId, false), hotkeys: utils.isMobile() ? undefined : { keydown: await this.getHotKeys() }, dnd5: { autoExpandMS: 600, @@ -932,12 +932,13 @@ export default class NoteTreeWidget extends TabAwareWidget { } } - async setExpandedToServer(branchId, isExpanded) { + async setExpanded(branchId, isExpanded) { utils.assertArguments(branchId); - const expandedNum = isExpanded ? 1 : 0; + const branch = treeCache.getBranch(branchId); + branch.isExpanded = isExpanded; - await server.put('branches/' + branchId + '/expanded/' + expandedNum); + await server.put(`branches/${branchId}/expanded/${isExpanded ? 1 : 0}`); } async reloadTreeFromCache() { @@ -997,7 +998,7 @@ export default class NoteTreeWidget extends TabAwareWidget { return false; } }; - + for (const action of actions) { for (const shortcut of action.effectiveShortcuts) { hotKeyMap[utils.normalizeShortcut(shortcut)] = node => { @@ -1022,83 +1023,83 @@ export default class NoteTreeWidget extends TabAwareWidget { async deleteNotesCommand({node}) { const branchIds = this.getSelectedOrActiveBranchIds(node); - + await branchService.deleteNotes(branchIds); this.clearSelectedNodes(); } - + moveNoteUpCommand({node}) { const beforeNode = node.getPrevSibling(); - + if (beforeNode !== null) { branchService.moveBeforeBranch([node.data.branchId], beforeNode.data.branchId); } } - + moveNoteDownCommand({node}) { const afterNode = node.getNextSibling(); if (afterNode !== null) { branchService.moveAfterBranch([node.data.branchId], afterNode.data.branchId); } } - + moveNoteUpInHierarchyCommand({node}) { branchService.moveNodeUpInHierarchy(node); } - + moveNoteDownInHierarchyCommand({node}) { const toNode = node.getPrevSibling(); - + if (toNode !== null) { branchService.moveToParentNote([node.data.branchId], toNode.data.noteId); } } - + addNoteAboveToSelectionCommand() { const node = this.getFocusedNode(); - + if (!node) { return; } - + if (node.isActive()) { node.setSelected(true); } - + const prevSibling = node.getPrevSibling(); - + if (prevSibling) { prevSibling.setActive(true, {noEvents: true}); - + if (prevSibling.isSelected()) { node.setSelected(false); } - + prevSibling.setSelected(true); } } addNoteBelowToSelectionCommand() { const node = this.getFocusedNode(); - + if (!node) { return; } - + if (node.isActive()) { node.setSelected(true); } - + const nextSibling = node.getNextSibling(); - + if (nextSibling) { nextSibling.setActive(true, {noEvents: true}); - + if (nextSibling.isSelected()) { node.setSelected(false); } - + nextSibling.setSelected(true); } } @@ -1182,4 +1183,4 @@ export default class NoteTreeWidget extends TabAwareWidget { noteCreateService.duplicateNote(node.data.noteId, branch.parentNoteId); } -} \ No newline at end of file +} diff --git a/src/routes/api/files.js b/src/routes/api/files.js index 54ba741fb..88c18354d 100644 --- a/src/routes/api/files.js +++ b/src/routes/api/files.js @@ -1,6 +1,5 @@ "use strict"; -const noteService = require('../../services/notes'); const protectedSessionService = require('../../services/protected_session'); const repository = require('../../services/repository'); const utils = require('../../services/utils'); @@ -45,7 +44,9 @@ async function downloadNoteFile(noteId, res, contentDisposition = true) { if (contentDisposition) { // (one) reason we're not using the originFileName (available as label) is that it's not // available for older note revisions and thus would be inconsistent - res.setHeader('Content-Disposition', utils.getContentDisposition(note.title || "untitled")); + const filename = utils.formatDownloadTitle(note.title, note.type, note.mime); + + res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); } res.setHeader('Content-Type', note.mime); @@ -70,4 +71,4 @@ module.exports = { openFile, downloadFile, downloadNoteFile -}; \ No newline at end of file +}; diff --git a/src/routes/api/note_revisions.js b/src/routes/api/note_revisions.js index 023ba30ee..7f325f725 100644 --- a/src/routes/api/note_revisions.js +++ b/src/routes/api/note_revisions.js @@ -38,13 +38,7 @@ async function getNoteRevision(req) { * @return {string} */ function getRevisionFilename(noteRevision) { - let filename = noteRevision.title || "untitled"; - - if (noteRevision.type === 'text') { - filename += '.html'; - } else if (['relation-map', 'search'].includes(noteRevision.type)) { - filename += '.json'; - } + let filename = utils.formatDownloadTitle(noteRevision.title, noteRevision.type, noteRevision.mime); const extension = path.extname(filename); const date = noteRevision.dateCreated @@ -158,4 +152,4 @@ module.exports = { eraseAllNoteRevisions, eraseNoteRevision, restoreNoteRevision -}; \ No newline at end of file +}; diff --git a/src/services/notes.js b/src/services/notes.js index 152c72ec4..a04164ef5 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -13,6 +13,7 @@ const Attribute = require('../entities/attribute'); const hoistedNoteService = require('../services/hoisted_note'); 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'); @@ -276,9 +277,9 @@ async function downloadImage(noteId, imageUrl) { const downloadImagePromises = {}; function replaceUrl(content, url, imageNote) { - const quoted = url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); + const quotedUrl = utils.quoteRegex(url); - return content.replace(new RegExp(`\\s+src=[\"']${quoted}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`); + return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`); } async function downloadImages(noteId, content) { diff --git a/src/services/utils.js b/src/services/utils.js index 57af4218e..7cb02ae40 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -5,6 +5,7 @@ const randtoken = require('rand-token').generator({source: 'crypto'}); const unescape = require('unescape'); const escape = require('escape-html'); const sanitize = require("sanitize-filename"); +const mimeTypes = require('mime-types'); function newEntityId() { return randomString(12); @@ -166,10 +167,46 @@ function isStringNote(type, mime) { || STRING_MIME_TYPES.includes(mime); } -function replaceAll(string, replaceWhat, replaceWith) { - const escapedWhat = replaceWhat.replace(/([\/,!\\^${}\[\]().*+?|<>\-&])/g, "\\$&"); +function quoteRegex(url) { + return url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); +} - return string.replace(new RegExp(escapedWhat, "g"), replaceWith); +function replaceAll(string, replaceWhat, replaceWith) { + const quotedReplaceWhat = quoteRegex(replaceWhat); + + return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith); +} + +function formatDownloadTitle(filename, type, mime) { + if (!filename) { + filename = "untitled"; + } + + if (type === 'text') { + return filename + '.html'; + } else if (['relation-map', 'search'].includes(type)) { + return filename + '.json'; + } else { + if (!mime) { + return filename; + } + + mime = mime.toLowerCase(); + const filenameLc = filename.toLowerCase(); + const extensions = mimeTypes.extensions[mime]; + + if (!extensions || extensions.length === 0) { + return filename; + } + + for (const ext of extensions) { + if (filenameLc.endsWith('.' + ext)) { + return filename; + } + } + + return filename + '.' + extensions[0]; + } } module.exports = { @@ -198,5 +235,7 @@ module.exports = { sanitizeFilenameForHeader, getContentDisposition, isStringNote, - replaceAll -}; \ No newline at end of file + quoteRegex, + replaceAll, + formatDownloadTitle +};