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 +};