let db = require('@config/database') let Note = module.exports = {} let Tags = require('@models/Tag') let Attachment = require('@models/Attachment') let ShareNote = require('@models/ShareNote') let ProcessText = require('@helpers/ProcessText') const DiffMatchPatch = require('@helpers/DiffMatchPatch') const crypto = require('crypto') const cs = require('@helpers/CryptoString') const rp = require('request-promise'); const fs = require('fs') const gm = require('gm') Note.test = (userId, masterKey, printResults) => { return new Promise((resolve, reject) => { let testNoteId = 0 let testNoteId2 = 0 let sharedNoteId = 0 //ID of note shared with user const shareUserId = 61 Note.create(userId, 'Random Note','With Random Text dogs', masterKey) .then( newNoteId => { if(printResults) console.log('Test: Created Note -> ', newNoteId) return Note.create(userId, 'Yo, note','a second test note cheese mate', masterKey) }) .then( newNoteId => { if(printResults) console.log('Test: Created Note -> ', newNoteId) testNoteId2 = newNoteId //Create a blank note to test updating note and reindexing it return Note.create(userId, '','', masterKey) }) .then(newNoteId => { if(printResults) console.log('Test: Created Note -> ', newNoteId) testNoteId = newNoteId return Note.update (userId, testNoteId, 'Note text', 'Test Note beans Title', 0, 0, 0, 'hash', masterKey) }) .then(() => { if(printResults) console.log('Test: Update Note '+testNoteId+' - Pass') return Note.get(userId, testNoteId, masterKey) }) .then(() => { if(printResults) console.log('Test: Open Updated Note - Pass') return Note.reindex(userId, masterKey) }) .then(() => { if(printResults) console.log('Test: Reindex normal note - Pass') return Note.encryptedIndexSearch(userId, 'beans', null, masterKey) }) .then(textSearchResults => { if(textSearchResults['ids'] && textSearchResults['ids'].length >= 1){ if(printResults) console.log('Test: Normal Note Search Index - Pass') } else { console.log('Test: Search Index - Fail') } return ShareNote.addUserToSharedNote(userId, testNoteId, shareUserId, masterKey) }) .then(({success, shareUserId, sharedUserNoteId}) => { if(printResults) console.log('Test: Set Note To Shared - Pass') sharedNoteId = sharedUserNoteId return Note.get(userId, testNoteId, masterKey) }) .then(() => { if(printResults) console.log('Test: Open Shared Note - Pass') return Note.update (userId, testNoteId, 'Shared Update', 'Test Note yarnsmarf Title', 0, 0, 0, 'hash', masterKey) }) .then(() => { if(printResults) console.log('Test: Update Shared Note - Pass') return Note.reindex(userId, masterKey) }) .then( reindexResults => { if(printResults) console.log(`Test: Reindex Notes - ${reindexResults?'Pass':'Fail'}`) return Note.encryptedIndexSearch(userId, 'yarnsmarf', null, masterKey) }) .then(textSearchResults => { if(textSearchResults['ids'] && textSearchResults['ids'].length >= 1){ if(printResults) console.log('Test: Search Index - Pass') } else { console.log('Test: Search Index - Fail') } return Note.delete(userId, testNoteId, masterKey) }) .then(results => { if(printResults) console.log('Test: Delete Note - Pass') return Note.encryptedIndexSearch(userId, 'yarnsmarf', null, masterKey) }) .then(textSearchResults => { if(textSearchResults['ids'] && textSearchResults['ids'].length == 0){ if(printResults) console.log('Test: Search Deleted Note Text - Pass') } else { console.log('Test: Search Deleted Note Text - Fail') } return Note.delete(shareUserId, sharedNoteId) }) .then(() => { if(printResults) console.log('Test: Delete Shared Note - Pass') return ShareNote.addUserToSharedNote(userId, testNoteId2, shareUserId, masterKey) }) .then(({success, shareUserId, sharedUserNoteId}) => { if(printResults) console.log('Test: Created Another New Shared Note - pass') return ShareNote.removeUserFromSharedNote(userId, testNoteId2, sharedUserNoteId, masterKey) }) .then(() => { if(printResults) console.log('Test: Unshared New Shared Note -> convert back to normal note - pass') return Note.get(userId, testNoteId2, masterKey) }) .then(() => { if(printResults) console.log('Test: Decrypt unshared note - pass') let User = require('@models/User') return User.deleteUser(userId, '1') }) .then(results => { if(printResults) console.log('Test: Delete User Account - Pass') return resolve('Test: Complete ---') }) }) } //Returns insertedId of new note Note.create = (userId, noteTitle = '', noteText = '', masterKey) => { return new Promise((resolve, reject) => { if(userId == null || userId < 10){ reject('User Id required to create note') } const created = Math.round((+new Date)/1000) const salt = cs.createSmallSalt() const snippetSalt = cs.createSmallSalt() const snippetObj = JSON.stringify([noteTitle, noteText.substring(0, 500)]) const snippet = cs.encrypt(masterKey, snippetSalt, snippetObj) const textObject = JSON.stringify([noteTitle, noteText]) const encryptedText = cs.encrypt(masterKey, salt, textObject) db.promise() .query(`INSERT INTO note_raw_text (text, salt, updated) VALUE (?, ?, ?)`, [encryptedText, salt, created]) .then( (rows, fields) => { const rawTextId = rows[0].insertId return db.promise() .query('INSERT INTO note (user_id, note_raw_text_id, created, quick_note, snippet, snippet_salt, indexed) VALUES (?,?,?,?,?,?,0)', [userId, rawTextId, created, 0, snippet, snippetSalt]) }) .then((rows, fields) => { if(SocketIo){ SocketIo.to(userId).emit('new_note_created', rows[0].insertId) } // Indexing is done on save resolve(rows[0].insertId) //Only return the new note ID when creating a new note }) .catch(console.log) }) } // Called when a note is close // Will attempt to reindex all notes that are flagged in database as not indexed // Limit to 100 notes per batch // removeId = array of Ids to remove, [122,223] Note.reindex = (userId, masterKey, removeId = null) => { return new Promise((resolve, reject) => { if(!masterKey || masterKey.length == 0){ return reject('Master key needed for reindex') } let notIndexedNoteIds = [] let searchIndex = null let searchIndexSalt = null let foundNotes = null let userPrivateKey = null let User = require('@models/User') User.generateKeypair(userId, masterKey) .then(({publicKey, privateKey}) => { userPrivateKey = privateKey //First check if we have any notes to index return db.promise().query(` SELECT note.id, text, salt, encrypted_share_password_key FROM note JOIN note_raw_text ON note.note_raw_text_id = note_raw_text.id WHERE indexed = 0 AND salt IS NOT NULL AND trashed = 0 AND user_id = ? LIMIT 100`, [userId]) }) .then((rows, fields) => { //Halt execution if there are no new notes foundNotes = rows[0] //Remove ID from index but don't reindex text if(removeId != null){ removeId.forEach(removeId => { foundNotes.push({ id:removeId, text:'', salt: null, }) }) } if(foundNotes.length == 0){ throw new Error('No new notes to index') } //Select search index, if it doesn't exist, create it return db.promise().query(`SELECT * FROM user_encrypted_search_index WHERE user_id = ? LIMIT 1`, [userId]) }) .then((rows, fields) => { if(rows[0].length == 0){ //Create search index entry, return an object searchIndexSalt = cs.createSmallSalt() //Select all user notes to recreate index return db.promise().query(` SELECT note.id, text, salt, encrypted_share_password_key FROM note JOIN note_raw_text ON note.note_raw_text_id = note_raw_text.id WHERE user_id = ?`, [userId]) .then((rows, fields) => { foundNotes = rows[0] return db.promise().query("INSERT INTO user_encrypted_search_index (`user_id`, `salt`) VALUES (?,?)", [userId, searchIndexSalt]) }) .then((rows, fields) => { //return a fresh search index return new Promise((resolve, reject) => { resolve('{}') }) }) } else { const row = rows[0][0] searchIndexSalt = row.salt //Decrypt search index and continue. let decipheredSearchIndex = '{}' if(row.index && row.index.length > 0){ //Decrypt json, do not parse json yet, we want raw text decipheredSearchIndex = cs.decrypt(masterKey, searchIndexSalt, row.index) } return new Promise((resolve, reject) => { resolve( decipheredSearchIndex ) }) } }) .then(rawSearchIndex => { if(rawSearchIndex == null){ throw new Error('Search Index Not Found/Decrypted') } searchIndex = rawSearchIndex //Remove all instances of IDs from text foundNotes.forEach(note => { notIndexedNoteIds.push(note.id) //Remove every instance of note id const removeId = new RegExp(note.id,"gm") const removeDoubles = new RegExp(',,',"g") // const removeTrail = new RegExp(',]',"g") searchIndex = searchIndex .replace(removeId, '') .replace(removeDoubles, ',') .replace(/,]/g, ']') .replace(/\[\,/g, '[') //search [, }) searchIndex = JSON.parse(searchIndex) //Remove unused words, this may not be needed and it increases overhead Object.keys(searchIndex).forEach(word => { if(searchIndex[word].length == 0){ delete searchIndex[word] } }) //Process text of each note and add it to the index let reindexQueue = [] let reindexTimer = 0 foundNotes.forEach(note => { reindexTimer += 50 let reindexPromise = new Promise((resolve, reject) => { setTimeout(() => { if(masterKey == null || note.salt == null){ console.log('Error indexing note', note.id) return resolve(true) } let currentNoteKey = masterKey //Decrypt shared key if it exists const encryptedShareKey = note.encrypted_share_password_key if(encryptedShareKey != null){ currentNoteKey = crypto.privateDecrypt(userPrivateKey, Buffer.from(encryptedShareKey, 'base64') ) } //Decrypt text with proper key const noteHtml = cs.decrypt(currentNoteKey, note.salt, note.text) const rawText = ProcessText.removeHtml(noteHtml) //Remove HTML .toLowerCase() .replace(/style=".*?"/g,'') //Remove inline styles .replace (/&#{0,1}[a-z0-9]+;/ig, '') //remove HTML entities .replace(/[^A-Za-z0-9]/g, ' ') //Convert all to a-z only .replace(/ +(?= )/g,'') //Remove double spaces rawText.split(' ').forEach(word => { //Skip small words if(word.length <= 2){ return } if(Array.isArray( searchIndex[word] )){ if(searchIndex[word].indexOf( note.id ) == -1){ searchIndex[word].push( note.id ) } } else { searchIndex[word] = [ note.id ] } }) return resolve(true) }, reindexTimer) }) reindexQueue.push(reindexPromise) }) return Promise.all(reindexQueue) }) .then(rawSearchIndex => { const created = Math.round((+new Date)/1000) const jsonSearchIndex = JSON.stringify(searchIndex) const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex) return db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1", [encryptedJsonIndex, created, userId]) .then((rows, fields) => { return db.promise().query('UPDATE note SET `indexed` = 1 WHERE (`id` IN (?))', [notIndexedNoteIds]) }) .then((rows, fields) => { // console.log('Indexd Note Count: ' + rows[0]['affectedRows']) resolve(true) }) }).catch(error => { console.log('Reindex Error:') console.log(error) }) }) } // Returns updated note text Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, hash, masterKey) => { return new Promise((resolve, reject) => { // const now = Math.round((+new Date)/1000) const now = +new Date let noteSnippet = '' let User = require('@models/User') let userPrivateKey = null User.getPrivateKey(userId, masterKey) .then(privateKey => { userPrivateKey = privateKey return db.promise() .query(` SELECT note_raw_text_id, salt, snippet_salt, encrypted_share_password_key FROM note JOIN note_raw_text ON note_raw_text_id = note_raw_text.id WHERE note.id = ? AND user_id = ?`, [noteId, userId]) }) .then((rows, fields) => { const textId = rows[0][0]['note_raw_text_id'] let salt = rows[0][0]['salt'] let snippetSalt = rows[0][0]['snippet_salt'] //Shared notes use encrypted key - decrypt key then decrypt note const encryptedShareKey = rows[0][0].encrypted_share_password_key if(encryptedShareKey != null){ masterKey = crypto.privateDecrypt(userPrivateKey, Buffer.from(encryptedShareKey, 'base64') ) } let encryptedNoteText = '' //Create encrypted snippet if its a long note let snippet = '' if(noteText.length > 500){ snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)]) noteSnippet = cs.encrypt(masterKey, snippetSalt, snippet) } //Encrypt note text const textObject = JSON.stringify([noteTitle, noteText]) encryptedNoteText = cs.encrypt(masterKey, salt, textObject) // // @TODO - this needs some kind of rate limiting // A note shared with a lot of users could do a ton of updates every save // //Update text snippet for all other shared users db.promise().query('SELECT * FROM note WHERE note_raw_text_id = ? AND id != ?', [textId, noteId]) .then((rows, fields) => { for (var i = 0; i < rows[0].length; i++) { const otherNote = rows[0][i] //Re-encrypt for other user const updatedSnippet = cs.encrypt(masterKey, otherNote.snippet_salt, snippet) db.promise().query('UPDATE note SET snippet = ? WHERE id = ?', [updatedSnippet, otherNote.id]) SocketIo.to(otherNote['user_id']).emit('new_note_text_saved', {'noteId':otherNote.id, hash}) } }) //Update Note text return db.promise() .query('UPDATE note_raw_text SET text = ?, updated = ? WHERE id = ?', [encryptedNoteText, now, textId]) }) .then( (rows, fields) => { //Update other note attributes return db.promise() .query('UPDATE note SET pinned = ?, archived = ?, color = ?, snippet = ?, indexed = 0 WHERE id = ? AND user_id = ? LIMIT 1', [pinned, archived, color, noteSnippet, noteId, userId]) }) .then((rows, fields) => { if(SocketIo){ SocketIo.to(userId).emit('new_note_text_saved', {noteId, hash}) } //Async attachment reindex Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText) //Send back updated response resolve(rows[0]) }) .catch(console.log) }) } Note.setPinned = (userId, noteId, pinnedBoolean) => { return new Promise((resolve, reject) => { const pinned = pinnedBoolean ? 1:0 const now = Math.round((+new Date)/1000) //Update other note attributes return db.promise() .query('UPDATE note JOIN note_raw_text ON note_raw_text_id = note_raw_text.id SET pinned = ?, updated = ? WHERE note.id = ? AND user_id = ?', [pinned, now, noteId, userId]) .then((rows, fields) => { SocketIo.to(userId).emit('note_attribute_modified', noteId) resolve(true) }) }) } Note.setArchived = (userId, noteId, archivedBoolead) => { return new Promise((resolve, reject) => { const archived = archivedBoolead ? 1:0 const now = Math.round((+new Date)/1000) //Update other note attributes return db.promise() .query('UPDATE note JOIN note_raw_text ON note_raw_text_id = note_raw_text.id SET archived = ?, updated = ? WHERE note.id = ? AND user_id = ?', [archived, now, noteId, userId]) .then((rows, fields) => { SocketIo.to(userId).emit('note_attribute_modified', noteId) resolve(true) }) }) } Note.setTrashed = (userId, noteId, trashedBoolean, masterKey) => { return new Promise((resolve, reject) => { const trashed = trashedBoolean ? 1:0 const now = Math.round((+new Date)/1000) //Update other note attributes return db.promise() .query('UPDATE note JOIN note_raw_text ON note_raw_text_id = note_raw_text.id SET trashed = ?, updated = ?, indexed = 0 WHERE note.id = ? AND user_id = ?', [trashed, now, noteId, userId]) .then((rows, fields) => { const removeFromIndex = [] if(trashed){ //Remove note from index removeFromIndex.push(noteId) } Note.reindex(userId, masterKey, removeFromIndex) SocketIo.to(userId).emit('note_attribute_modified', noteId) resolve(true) }) }) } // // Delete a note and all its remaining parts // Note.delete = (userId, noteId, masterKey = null) => { return new Promise((resolve, reject) => { // // Delete, note, text, search index and associated tags // Leave the attachments, they can be deleted on their own // Leave Tags, their text is shared let rawTextId = null let noteTextCount = 0 // Lookup the note text ID, we need this to count usages db.promise() .query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId]) .then((rows, fields) => { //Save the raw text ID rawTextId = rows[0][0]['note_raw_text_id'] return db.promise() .query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId]) }) .then((rows, fields) => { //Save the number of times the note is used noteTextCount = rows[0][0]['count'] //Don't delete text if its shared if(noteTextCount == 1){ //If text is only used on one note, delete it (its not shared) return db.promise() .query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId]) } else { return new Promise((resolve, reject) => { resolve(true) }) } }) .then( results => { // Delete Note entry for this user. return db.promise() .query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId, userId]) }) .then( results => { // Delete hidden attachments for this note (files for attachment are already gone) return db.promise() .query('DELETE FROM attachment WHERE visible = 0 AND note_id = ? AND user_id = ?', [noteId, userId]) }) .then((rows, fields) => { // delete tags return db.promise() .query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId]) }) .then((rows, fields) => { //IF there are notes with a matching raw text id, we want to update their share status db.promise().query('SELECT id FROM note WHERE note_raw_text_id = ?',[rawTextId]) .then((rows, fields) => { if(rows[0].length == 1){ db.promise().query('UPDATE note SET shared = 0 WHERE id = ?', [rows[0][0]['id']]) } }) SocketIo.to(userId).emit('update_counts') if(masterKey){ //Remove note ID from index Note.reindex(userId, masterKey, [noteId]) .then(results => { return resolve(true) }) } else { return resolve(true) } }) }) } //text is the current text for the note that will be compared to the text in the database Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => { return new Promise((resolve, reject) => { Note.get(userId, noteId) .then(noteObject => { if(!noteObject.text || !usersCurrentText || noteObject.encrypted == 1){ return resolve(null) } let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"") let newText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"") if(noteObject.updated == lastUpdated){ // console.log('No note diff') return resolve(null) } if(noteObject.updated > lastUpdated){ newText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"") oldText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"") } const dmp = new DiffMatchPatch.diff_match_patch() const diff = dmp.diff_main(oldText, newText) dmp.diff_cleanupSemantic(diff) const patch_list = dmp.patch_make(oldText, newText, diff); const patch_text = dmp.patch_toText(patch_list); //Patch text - shows a list of changes var patches = dmp.patch_fromText(patch_text); // console.log(patch_text) //results[1] - contains diagnostic data for patch apply, its possible it can fail var results = dmp.patch_apply(patches, oldText); //Compile return data for front end const returnData = { updatedText: results[0], diffs: results[1].length, //Only use length for now updated: Math.max(noteObject.updated,lastUpdated) //Return most recently updated date } //Final change in notes // console.log(returnData) resolve(returnData) }) }) } Note.get = (userId, noteId, masterKey) => { return new Promise((resolve, reject) => { let User = require('@models/User') if(!masterKey || masterKey.length == 0){ return reject('Get note called without master key') } let userPrivateKey = null; User.getPrivateKey(userId, masterKey) .then(privateKey => { //Grab users private key userPrivateKey = privateKey return db.promise() .query(` SELECT note_raw_text.text, note_raw_text.salt, note_raw_text.updated as updated, note.id, note.user_id, note.created, note.pinned, note.archived, note.trashed, note.color, note.shared, note.encrypted_share_password_key, count(distinct attachment.id) as attachment_count, note.note_raw_text_id as rawTextId, shareUser.username as shareUsername FROM note JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) LEFT JOIN attachment ON (note.id = attachment.note_id) LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id) WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, noteId]) }) .then((rows, fields) => { const nowTime = Math.round((+new Date)/1000) let noteLockedOut = false let noteData = rows[0][0] // const rawTextId = noteData['rawTextId'] //Block access to notes if invalid or user doesn't have access if(!noteData || !noteData['user_id'] || noteData['user_id'] != userId || noteData['id'] != noteId){ return resolve(false) } //Shared notes use encrypted key - decrypt key then decrypt note if(noteData.encrypted_share_password_key != null){ masterKey = crypto.privateDecrypt(userPrivateKey, Buffer.from(noteData.encrypted_share_password_key, 'base64') ) } //Normal Encrypted note const decipheredText = cs.decrypt(masterKey, noteData.salt, noteData.text) if(decipheredText == null){ throw new Error('Unable to decropt note text') } //Parse title and text from encrypted data and update object const textObject = JSON.parse(decipheredText) noteData.title = textObject[0] noteData.text = textObject[1] db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId]) //Return note data // delete noteData.salt //remove salt from return data // delete noteData.encrypted_share_password_key noteData.lockedOut = noteLockedOut resolve(noteData) }) .catch(error => { console.log(error) resolve(false) }) }) } //Public note share action -> may not be used Note.getShared = (noteId, sharedKey) => { return new Promise((resolve, reject) => { db.promise() .query(` SELECT note_raw_text.text, note_raw_text.salt, note_raw_text.updated as updated, note.id, note.user_id, note.created, note.pinned, note.archived, note.trashed, note.color, note.shared, note.encrypted_share_password_key, note.note_raw_text_id as rawTextId FROM note JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) WHERE note.id = ? LIMIT 1`, [noteId]) .then((rows, fields) => { let noteData = rows[0][0] const decipheredText = cs.decrypt(sharedKey, noteData.salt, noteData.text) if(decipheredText == null){ throw new Error('Unable to decropt note text') } const success = true const noteObject = JSON.parse(decipheredText) const title = noteObject[0] const text = noteObject[1] const styleObject = JSON.parse(noteData.color) return resolve({success, title, text, styleObject}) }) .catch(error => { resolve({'success':false}) }) }) } // Searches text index, returns nothing if there is no search query Note.encryptedIndexSearch = (userId, searchQuery, searchTags, masterKey) => { return new Promise((resolve, reject) => { if(searchQuery.length == 0){ resolve(null) } else { if(!masterKey || masterKey == null){ console.log('Attempting to search wiouth key') return resolve(null) } //Search the search index db.promise().query(`SELECT * FROM user_encrypted_search_index WHERE user_id = ? LIMIT 1`, [userId]) .then((rows, fields) => { if(rows[0].length == 1){ //Lookup, decrypt and parse search index const row = rows[0][0] const decipheredSearchIndex = cs.decrypt(masterKey, row.salt, row.index) const searchIndex = JSON.parse(decipheredSearchIndex) //Clean up search word const word = searchQuery.toLowerCase().replace(/[^a-z0-9]/g, '') let noteIds = [] let partials = [] Object.keys(searchIndex).forEach(wordIndex => { if( wordIndex.indexOf(word) != -1 && wordIndex != word){ partials.push(wordIndex) noteIds.push(...searchIndex[wordIndex]) } }) const exactArray = searchIndex[word] ? searchIndex[word] : [] let searchData = { 'word':word, 'exact': exactArray, 'partials': partials, 'partial': [...new Set(noteIds) ], } //Remove exact matches from partials set if there is overlap if(searchData['exact'].length > 0 && searchData['partial'].length > 0){ searchData['partial'] = searchData['partial'] .filter( ( el ) => !searchData['exact'].includes( el ) ) } searchData['ids'] = searchData['exact'].concat(searchData['partial']) searchData['total'] = searchData['ids'].length // console.log(searchData['total']) return resolve({ 'ids':searchData['ids'] }) } else { return resolve(null) } }) } }) } Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => { return new Promise((resolve, reject) => { //Define return data objects let returnData = { 'notes':[], 'total':0, } let userPrivateKey = null let User = require('@models/User') User.generateKeypair(userId, masterKey) .then(({publicKey, privateKey}) => { userPrivateKey = privateKey return Note.encryptedIndexSearch(userId, searchQuery, searchTags, masterKey) }) .then( (textSearchResults) => { //Pull out search results from previous query let textSearchIds = [] let returnTagResults = false let searchAllNotes = false if(textSearchResults != null){ textSearchIds = textSearchResults['ids'] returnData['total'] = textSearchIds.length // highlights = textSearchResults['snippets'] } //No results, return empty data if(textSearchIds.length == 0 && searchQuery.length > 0){ return resolve(returnData) } // Base of the query, modified with fastFilters // Add to query for character counts -> CHAR_LENGTH(note.text) as chars //SUBSTRING(note_raw_text.text, 1, 500) as text, let searchParams = [userId] let noteSearchQuery = ` SELECT note.id, note.snippet as snippetText, note.snippet_salt as snippetSalt, note_raw_text.text as noteText, note_raw_text.salt as noteSalt, note_raw_text.updated as updated, opened, color, count(distinct note_tag.id) as tag_count, count(distinct attachment.id) as attachment_count, note.pinned, note.archived, note.trashed, GROUP_CONCAT(DISTINCT tag.text) as tags, GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs, shareUser.username as shareUsername, note.shared, note.encrypted_share_password_key FROM note JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) LEFT JOIN note_tag ON (note.id = note_tag.note_id) LEFT JOIN tag ON (tag.id = note_tag.tag_id) LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.visible = 1) LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id) WHERE note.user_id = ? ` //If text search returned results, limit search to those ids if(textSearchIds.length > 0){ searchParams.push(textSearchIds) noteSearchQuery += ' AND note.id IN (?)' searchAllNotes = true } //If Specific ID's are being searched, search ALL notes if(fastFilters.noteIdSet && fastFilters.noteIdSet.length > 0){ searchParams.push(fastFilters.noteIdSet) noteSearchQuery += ' AND note.id IN (?)' searchAllNotes = true } //If tags are passed, use those tags in search if(searchTags.length > 0){ searchParams.push(searchTags) noteSearchQuery += ' AND note_tag.tag_id IN (?) AND note.trashed = 0' searchAllNotes = true } //Show archived notes, only if fast filter is set, default to not archived if(searchAllNotes == false){ //Default set of filters for all notes if(fastFilters.notesHome == 1){ noteSearchQuery += ' AND note.archived = 0 AND note.trashed = 0 AND note.share_user_id IS NULL' } if(fastFilters.onlyShowSharedNotes == 1){ //share_user_id means your shared them, a note with a shared user id filled in means it was shared noteSearchQuery += ` AND share_user_id IS NOT NULL OR (note.shared = 2 AND note.user_id = ?) AND note.trashed = 0` searchParams.push(userId) //Show notes shared with you } if(fastFilters.onlyArchived == 1){ noteSearchQuery += ' AND note.archived = 1 AND note.trashed = 0' //Show Archived } if(fastFilters.onlyShowTrashed == 1){ noteSearchQuery += ' AND note.trashed = 1' //Show Exclude } } //Finish up note query noteSearchQuery += ' GROUP BY note.id' //Only show notes with Tags if(fastFilters.withTags == 1){ noteSearchQuery += ' HAVING tag_count > 0' } //Only show notes with links if(fastFilters.withLinks == 1){ noteSearchQuery += ' HAVING attachment_count > 0' } //Only show archived notes // if(fastFilters.onlyArchived == 1){ // noteSearchQuery += ' HAVING note.archived = 1' // } // //Only show trashed notes // if(fastFilters.onlyShowTrashed == 1){ // noteSearchQuery += ' HAVING note.trashed = 1' // } // // Always prioritize pinned notes in searches. //Default Sort, order by last updated let defaultOrderBy = ' ORDER BY note.pinned DESC, updated DESC, note.created DESC, note.opened DESC, id DESC' //Order by Last Created Date if(fastFilters.lastCreated == 1){ defaultOrderBy = ' ORDER BY note.pinned DESC, note.created DESC, updated DESC, note.opened DESC, id DESC' } //Order by last Opened Date if(fastFilters.lastOpened == 1){ defaultOrderBy = ' ORDER BY note.pinned DESC, opened DESC, updated DESC, note.created DESC, id DESC' } //Append Order by to query noteSearchQuery += defaultOrderBy //Manage limit params if set if(fastFilters.limitSize > 0 || fastFilters.limitOffset > 0){ const limitSize = parseInt(fastFilters.limitSize, 10) || 10 //Use int or default to 10 const limitOffset = parseInt(fastFilters.limitOffset, 10) || 0 //Either parse int, or use zero noteSearchQuery += ` LIMIT ${limitOffset}, ${limitSize}` } // console.log('------------- Final Query --------------') // console.log(noteSearchQuery) // console.log('------------- ----------- --------------') db.promise() .query(noteSearchQuery, searchParams) .then((noteRows, noteFields) => { //Push all notes returnData['notes'] = noteRows[0] //pull out all note ids so we can fetch all tags for those notes returnData['notes'].forEach(note => { //Current note key may change, default to master key let currentNoteKey = masterKey //Shared notes use encrypted key - decrypt key then decrypt note const encryptedShareKey = note.encrypted_share_password_key if(encryptedShareKey != null){ currentNoteKey = crypto.privateDecrypt(userPrivateKey, Buffer.from(encryptedShareKey, 'base64') ) } //Only long notes have snippets, decipher it if present let displayTitle = '' let displayText = '' let encryptedText = note.noteText let relatedSalt = note.noteSalt //Default to note text, use snippet if set if(note.snippetSalt && note.snippetText && note.snippetSalt.length > 0 && note.snippetText.length > 0){ encryptedText = note.snippetText relatedSalt = note.snippetSalt } try { const decipheredText = cs.decrypt(currentNoteKey, relatedSalt, encryptedText) const textObject = JSON.parse(decipheredText) if(textObject != null && textObject.length == 2){ if(textObject[0] && textObject[0] != null && textObject[0].length > 0){ displayTitle = textObject[0] } if(textObject[1] && textObject[1] != null && textObject[1].length > 0){ displayText = textObject[1] } } } catch(err) { console.log('Error opening note id -> ', note.id) console.log(err) } note.title = displayTitle note.subtext = ProcessText.stripDoubleBlankLines(displayText) //Limit number of attachment thumbs to 4 if(note.thumbs){ //Convert comma delimited string to array let thumbArray = note.thumbs.split(',').reverse() //Limit array to 4 or size of array thumbArray.length = Math.min(thumbArray.length, 4) note.thumbs = thumbArray } //Clear out note.text before sending it to front end, its being used in title and subtext delete note.snippetText delete note.snippetSalt delete note.noteText delete note.noteSalt delete note.encrypted_share_password_key delete note.text //Passed back as title and subtext }) return resolve(returnData) }) .catch(console.log) }) .catch(console.log) }) }