"use strict"; const Attribute = require('../../entities/attribute'); const utils = require('../../services/utils'); const log = require('../../services/log'); const repository = require('../../services/repository'); const noteService = require('../../services/notes'); const attributeService = require('../../services/attributes'); const Branch = require('../../entities/branch'); const path = require('path'); const commonmark = require('commonmark'); const protectedSessionService = require('../protected_session'); const mimeService = require("./mime"); const treeService = require("../tree"); const yauzl = require("yauzl"); const htmlSanitizer = require('../html_sanitizer'); /** * @param {TaskContext} taskContext * @param {Buffer} fileBuffer * @param {Note} importRootNote * @return {Promise<*>} */ async function importZip(taskContext, fileBuffer, importRootNote) { // maps from original noteId (in tar file) to newly generated noteId const noteIdMap = {}; const attributes = []; // path => noteId, used only when meta file is not available const createdPaths = { '/': importRootNote.noteId, '\\': importRootNote.noteId }; const mdReader = new commonmark.Parser(); const mdWriter = new commonmark.HtmlRenderer(); let metaFile = null; let firstNote = null; const createdNoteIds = {}; function getNewNoteId(origNoteId) { // in case the original noteId is empty. This probably shouldn't happen, but still good to have this precaution if (!origNoteId.trim()) { return ""; } if (!noteIdMap[origNoteId]) { noteIdMap[origNoteId] = utils.newEntityId(); } return noteIdMap[origNoteId]; } function getMeta(filePath) { if (!metaFile) { return {}; } const pathSegments = filePath.split(/[\/\\]/g); let cursor = { isImportRoot: true, children: metaFile.files }; let parent; for (const segment of pathSegments) { if (!cursor || !cursor.children || cursor.children.length === 0) { return {}; } parent = cursor; cursor = cursor.children.find(file => file.dataFileName === segment || file.dirFileName === segment); } return { parentNoteMeta: parent, noteMeta: cursor }; } function getParentNoteId(filePath, parentNoteMeta) { let parentNoteId; if (parentNoteMeta) { parentNoteId = parentNoteMeta.isImportRoot ? importRootNote.noteId : getNewNoteId(parentNoteMeta.noteId); } else { const parentPath = path.dirname(filePath); if (parentPath === '.') { parentNoteId = importRootNote.noteId; } else if (parentPath in createdPaths) { parentNoteId = createdPaths[parentPath]; } else { // tar allows creating out of order records - i.e. file in a directory can appear in the tar stream before actual directory // (out-of-order-directory-records.tar in test set) parentNoteId = saveDirectory(parentPath); } } return parentNoteId; } function getNoteId(noteMeta, filePath) { if (noteMeta) { return getNewNoteId(noteMeta.noteId); } const filePathNoExt = utils.removeTextFileExtension(filePath); if (filePathNoExt in createdPaths) { return createdPaths[filePathNoExt]; } const noteId = utils.newEntityId(); createdPaths[filePathNoExt] = noteId; return noteId; } function detectFileTypeAndMime(taskContext, filePath) { const mime = mimeService.getMime(filePath) || "application/octet-stream"; const type = mimeService.getType(taskContext.data, mime); return { mime, type }; } function saveAttributes(note, noteMeta) { if (!noteMeta) { return; } for (const attr of noteMeta.attributes) { attr.noteId = note.noteId; if (attr.type === 'label-definition') { attr.type = 'label'; attr.name = 'label:' + attr.name; } else if (attr.type === 'relation-definition') { attr.type = 'label'; attr.name = 'relation:' + attr.name; } if (!attributeService.isAttributeType(attr.type)) { log.error("Unrecognized attribute type " + attr.type); continue; } if (attr.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(attr.name)) { // these relations are created automatically and as such don't need to be duplicated in the import continue; } if (attr.type === 'relation') { attr.value = getNewNoteId(attr.value); } if (taskContext.data.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) { attr.name = 'disabled:' + attr.name; } attributes.push(attr); } } function saveDirectory(filePath) { const { parentNoteMeta, noteMeta } = getMeta(filePath); const noteId = getNoteId(noteMeta, filePath); const noteTitle = utils.getNoteTitle(filePath, taskContext.data.replaceUnderscoresWithSpaces, noteMeta); const parentNoteId = getParentNoteId(filePath, parentNoteMeta); let note = repository.getNote(noteId); if (note) { return; } ({note} = noteService.createNewNote({ parentNoteId: parentNoteId, title: noteTitle, content: '', noteId: noteId, type: noteMeta ? noteMeta.type : 'text', mime: noteMeta ? noteMeta.mime : 'text/html', prefix: noteMeta ? noteMeta.prefix : '', isExpanded: noteMeta ? noteMeta.isExpanded : false, isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), })); createdNoteIds[note.noteId] = true; saveAttributes(note, noteMeta); if (!firstNote) { firstNote = note; } return noteId; } function getNoteIdFromRelativeUrl(url, filePath) { while (url.startsWith("./")) { url = url.substr(2); } let absUrl = path.dirname(filePath); while (url.startsWith("../")) { absUrl = path.dirname(absUrl); url = url.substr(3); } if (absUrl === '.') { absUrl = ''; } absUrl += (absUrl.length > 0 ? '/' : '') + url; const {noteMeta} = getMeta(absUrl); const targetNoteId = getNoteId(noteMeta, absUrl); return targetNoteId; } function saveNote(filePath, content) { const {parentNoteMeta, noteMeta} = getMeta(filePath); if (noteMeta && noteMeta.noImport) { return; } const noteId = getNoteId(noteMeta, filePath); const parentNoteId = getParentNoteId(filePath, parentNoteMeta); if (!parentNoteId) { throw new Error(`Cannot find parentNoteId for ${filePath}`); } if (noteMeta && noteMeta.isClone) { new Branch({ noteId, parentNoteId, isExpanded: noteMeta.isExpanded, prefix: noteMeta.prefix, notePosition: noteMeta.notePosition }).save(); return; } const {type, mime} = noteMeta ? noteMeta : detectFileTypeAndMime(taskContext, filePath); if (type !== 'file' && type !== 'image') { content = content.toString("UTF-8"); } if ((noteMeta && noteMeta.format === 'markdown') || (!noteMeta && taskContext.data.textImportedAsText && ['text/markdown', 'text/x-markdown'].includes(mime))) { const parsed = mdReader.parse(content); content = mdWriter.render(parsed); } const noteTitle = utils.getNoteTitle(filePath, taskContext.data.replaceUnderscoresWithSpaces, noteMeta); if (type === 'text') { function isUrlAbsolute(url) { return /^(?:[a-z]+:)?\/\//i.test(url); } content = content.replace(/