2017-11-05 10:41:54 -05:00
const sql = require ( './sql' ) ;
2019-01-05 21:49:40 +01:00
const sqlInit = require ( './sql_init' ) ;
2018-04-01 21:27:46 -04:00
const optionService = require ( './options' ) ;
2018-04-02 20:46:46 -04:00
const dateUtils = require ( './date_utils' ) ;
2018-04-01 21:27:46 -04:00
const syncTableService = require ( './sync_table' ) ;
2018-08-01 09:26:02 +02:00
const eventService = require ( './events' ) ;
2018-03-31 10:51:37 -04:00
const repository = require ( './repository' ) ;
2018-11-06 14:23:49 +01:00
const cls = require ( '../services/cls' ) ;
2018-03-31 22:23:40 -04:00
const Note = require ( '../entities/note' ) ;
2018-03-31 22:15:06 -04:00
const NoteRevision = require ( '../entities/note_revision' ) ;
2018-03-31 22:23:40 -04:00
const Branch = require ( '../entities/branch' ) ;
2018-08-21 13:49:45 +02:00
const Attribute = require ( '../entities/attribute' ) ;
2019-04-16 21:40:04 +02:00
const hoistedNoteService = require ( '../services/hoisted_note' ) ;
2019-09-01 22:09:55 +02:00
const protectedSessionService = require ( '../services/protected_session' ) ;
const log = require ( '../services/log' ) ;
2019-11-09 11:58:52 +01:00
const noteRevisionService = require ( '../services/note_revisions' ) ;
2020-03-25 11:28:44 +01:00
const attributeService = require ( '../services/attributes' ) ;
const request = require ( './request' ) ;
const path = require ( 'path' ) ;
const url = require ( 'url' ) ;
2017-11-05 10:41:54 -05:00
2019-11-14 23:10:56 +01:00
async function getNewNotePosition ( parentNoteId ) {
const maxNotePos = await sql . getValue ( `
SELECT MAX ( notePosition )
FROM branches
WHERE parentNoteId = ?
AND isDeleted = 0 ` , [parentNoteId]);
return maxNotePos === null ? 0 : maxNotePos + 10 ;
2018-03-31 22:23:40 -04:00
}
2018-08-15 22:06:49 +02:00
async function triggerChildNoteCreated ( childNote , parentNote ) {
await eventService . emit ( eventService . CHILD _NOTE _CREATED , { childNote , parentNote } ) ;
}
2018-08-01 09:26:02 +02:00
async function triggerNoteTitleChanged ( note ) {
await eventService . emit ( eventService . NOTE _TITLE _CHANGED , note ) ;
}
2019-11-16 11:09:52 +01:00
function deriveMime ( type , mime ) {
if ( ! type ) {
throw new Error ( ` Note type is a required param ` ) ;
2018-10-31 12:29:01 +01:00
}
2019-11-16 11:09:52 +01:00
if ( mime ) {
return mime ;
}
if ( type === 'text' ) {
mime = 'text/html' ;
} else if ( type === 'code' ) {
mime = 'text/plain' ;
} else if ( [ 'relation-map' , 'search' ] . includes ( type ) ) {
mime = 'application/json' ;
2020-01-04 19:34:01 +01:00
} else if ( [ 'render' , 'book' ] . includes ( type ) ) {
2019-11-27 19:42:10 +01:00
mime = '' ;
2019-03-29 23:24:41 +01:00
}
2019-11-16 11:09:52 +01:00
return mime ;
2019-11-14 23:10:56 +01:00
}
2018-01-27 17:18:19 -05:00
2019-11-14 23:10:56 +01:00
async function copyChildAttributes ( parentNote , childNote ) {
2019-12-01 10:32:30 +01:00
for ( const attr of await parentNote . getOwnedAttributes ( ) ) {
2019-11-14 23:10:56 +01:00
if ( attr . name . startsWith ( "child:" ) ) {
await new Attribute ( {
noteId : childNote . noteId ,
type : attr . type ,
name : attr . name . substr ( 6 ) ,
value : attr . value ,
position : attr . position ,
isInheritable : attr . isInheritable
} ) . save ( ) ;
2018-01-26 21:31:52 -05:00
2019-11-14 23:10:56 +01:00
childNote . invalidateAttributeCache ( ) ;
}
2019-03-17 12:19:23 +01:00
}
2019-11-14 23:10:56 +01:00
}
2019-03-17 12:19:23 +01:00
2019-11-14 23:10:56 +01:00
/ * *
* Following object properties are mandatory :
* - { string } parentNoteId
* - { string } title
* - { * } content
2019-12-03 15:49:27 -03:00
* - { string } type - text , code , file , image , search , book , relation - map , render
2019-11-14 23:10:56 +01:00
*
* Following are optional ( have defaults )
* - { string } mime - value is derived from default mimes for type
* - { boolean } isProtected - default is false
* - { boolean } isExpanded - default is false
* - { string } prefix - default is empty string
* - { integer } notePosition - default is last existing notePosition in a parent + 10
*
* @ param params
2019-11-16 11:09:52 +01:00
* @ return { Promise < { note : Note , branch : Branch } > }
2019-11-14 23:10:56 +01:00
* /
async function createNewNote ( params ) {
const parentNote = await repository . getNote ( params . parentNoteId ) ;
if ( ! parentNote ) {
throw new Error ( ` Parent note ${ params . parentNoteId } not found. ` ) ;
}
if ( ! params . title || params . title . trim ( ) . length === 0 ) {
throw new Error ( ` Note title must not be empty ` ) ;
}
const note = await new Note ( {
noteId : params . noteId , // optionally can force specific noteId
title : params . title ,
isProtected : ! ! params . isProtected ,
type : params . type ,
2019-11-16 11:09:52 +01:00
mime : deriveMime ( params . type , params . mime )
2019-11-14 23:10:56 +01:00
} ) . save ( ) ;
await note . setContent ( params . content ) ;
2019-02-06 21:29:23 +01:00
2018-04-02 22:53:01 -04:00
const branch = await new Branch ( {
2018-03-31 22:23:40 -04:00
noteId : note . noteId ,
2019-11-14 23:10:56 +01:00
parentNoteId : params . parentNoteId ,
notePosition : params . notePosition !== undefined ? params . notePosition : await getNewNotePosition ( params . parentNoteId ) ,
prefix : params . prefix ,
isExpanded : ! ! params . isExpanded
2018-04-02 22:53:01 -04:00
} ) . save ( ) ;
2018-01-27 17:18:19 -05:00
2020-03-25 21:01:42 +01:00
await scanForLinks ( note ) ;
2020-04-05 12:28:16 +02:00
await copyChildAttributes ( parentNote , note ) ;
2018-10-08 11:09:45 +02:00
await triggerNoteTitleChanged ( note ) ;
await triggerChildNoteCreated ( note , parentNote ) ;
2017-11-18 17:05:50 -05:00
return {
2018-04-01 11:05:09 -04:00
note ,
branch
2017-11-18 17:05:50 -05:00
} ;
2017-11-05 10:41:54 -05:00
}
2019-11-16 12:28:47 +01:00
async function createNewNoteWithTarget ( target , targetBranchId , params ) {
2019-11-16 17:00:22 +01:00
if ( ! params . type ) {
const parentNote = await repository . getNote ( params . parentNoteId ) ;
// code note type can be inherited, otherwise text is default
params . type = parentNote . type === 'code' ? 'code' : 'text' ;
params . mime = parentNote . type === 'code' ? parentNote . mime : 'text/html' ;
}
2019-11-16 12:28:47 +01:00
if ( target === 'into' ) {
return await createNewNote ( params ) ;
}
else if ( target === 'after' ) {
2019-11-24 22:15:33 +01:00
const afterNote = await sql . getRow ( 'SELECT notePosition FROM branches WHERE branchId = ?' , [ targetBranchId ] ) ;
2019-11-16 12:28:47 +01:00
// not updating utcDateModified to avoig having to sync whole rows
await sql . execute ( 'UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0' ,
[ params . parentNoteId , afterNote . notePosition ] ) ;
params . notePosition = afterNote . notePosition + 10 ;
2019-11-24 22:15:33 +01:00
const retObject = await createNewNote ( params ) ;
2019-11-16 12:28:47 +01:00
await syncTableService . addNoteReorderingSync ( params . parentNoteId ) ;
2019-11-24 22:15:33 +01:00
return retObject ;
2019-11-16 12:28:47 +01:00
}
else {
throw new Error ( ` Unknown target ${ target } ` ) ;
}
}
2020-02-26 16:37:17 +01:00
async function protectNoteRecursively ( note , protect , includingSubTree , taskContext ) {
2018-03-31 08:53:52 -04:00
await protectNote ( note , protect ) ;
2017-11-15 00:04:26 -05:00
2019-10-19 09:58:18 +02:00
taskContext . increaseProgressCount ( ) ;
2020-02-26 16:37:17 +01:00
if ( includingSubTree ) {
for ( const child of await note . getChildNotes ( ) ) {
2020-03-15 22:09:48 +01:00
await protectNoteRecursively ( child , protect , includingSubTree , taskContext ) ;
2020-02-26 16:37:17 +01:00
}
2017-11-15 00:04:26 -05:00
}
}
2018-03-31 08:53:52 -04:00
async function protectNote ( note , protect ) {
2018-03-31 22:15:06 -04:00
if ( protect !== note . isProtected ) {
2019-05-04 14:46:17 +02:00
const content = await note . getContent ( ) ;
2018-03-31 22:15:06 -04:00
note . isProtected = protect ;
2017-11-15 00:04:26 -05:00
2019-05-04 14:46:17 +02:00
// this will force de/encryption
await note . setContent ( content ) ;
2018-04-01 17:38:24 -04:00
await note . save ( ) ;
2017-11-15 00:04:26 -05:00
}
2019-11-09 11:58:52 +01:00
await noteRevisionService . protectNoteRevisions ( note ) ;
2017-11-15 00:04:26 -05:00
}
2018-11-08 20:17:56 +01:00
function findImageLinks ( content , foundLinks ) {
2019-08-29 23:11:59 +02:00
const re = /src="[^"]*api\/images\/([a-zA-Z0-9]+)\//g ;
2018-11-08 20:17:56 +01:00
let match ;
while ( match = re . exec ( content ) ) {
foundLinks . push ( {
2019-11-23 20:54:49 +01:00
name : 'imageLink' ,
2019-08-19 20:12:00 +02:00
value : match [ 1 ]
2018-11-08 20:17:56 +01:00
} ) ;
}
2018-11-19 15:00:49 +01:00
// removing absolute references to server to keep it working between instances
2019-02-22 23:03:20 +01:00
// we also omit / at the beginning to keep the paths relative
return content . replace ( /src="[^"]*\/api\/images\//g , 'src="api/images/' ) ;
2018-11-08 20:17:56 +01:00
}
2019-08-19 20:12:00 +02:00
function findInternalLinks ( content , foundLinks ) {
2018-11-19 15:00:49 +01:00
const re = /href="[^"]*#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g ;
2018-11-08 20:17:56 +01:00
let match ;
while ( match = re . exec ( content ) ) {
foundLinks . push ( {
2019-11-23 20:54:49 +01:00
name : 'internalLink' ,
2019-08-19 20:12:00 +02:00
value : match [ 1 ]
2018-11-08 20:17:56 +01:00
} ) ;
}
2018-11-19 15:00:49 +01:00
// removing absolute references to server to keep it working between instances
return content . replace ( /href="[^"]*#root/g , 'href="#root' ) ;
2018-11-08 20:17:56 +01:00
}
2020-01-10 21:41:00 +01:00
function findIncludeNoteLinks ( content , foundLinks ) {
2020-04-13 18:12:41 +02:00
const re = /<section class="include-note[^>]+data-note-id="([a-zA-Z0-9]+)"[^>]*>/g ;
2020-01-10 21:41:00 +01:00
let match ;
while ( match = re . exec ( content ) ) {
foundLinks . push ( {
name : 'includeNoteLink' ,
value : match [ 1 ]
} ) ;
}
return content ;
}
2018-11-16 23:27:57 +01:00
function findRelationMapLinks ( content , foundLinks ) {
const obj = JSON . parse ( content ) ;
for ( const note of obj . notes ) {
foundLinks . push ( {
2019-11-23 20:54:49 +01:00
name : 'relationMapLink' ,
2019-08-19 20:12:00 +02:00
value : note . noteId
2019-10-02 23:22:58 +02:00
} ) ;
2018-11-16 23:27:57 +01:00
}
}
2020-03-25 11:28:44 +01:00
const imageUrlToNoteIdMapping = { } ;
async function downloadImage ( noteId , imageUrl ) {
2020-03-25 21:01:42 +01:00
try {
const imageBuffer = await request . getImage ( imageUrl ) ;
const parsedUrl = url . parse ( imageUrl ) ;
const title = path . basename ( parsedUrl . pathname ) ;
const imageService = require ( '../services/image' ) ;
const { note } = await imageService . saveImage ( noteId , imageBuffer , title , true ) ;
2020-03-25 11:28:44 +01:00
2020-03-25 21:01:42 +01:00
await note . addLabel ( 'imageUrl' , imageUrl ) ;
2020-03-25 11:28:44 +01:00
2020-03-25 21:01:42 +01:00
imageUrlToNoteIdMapping [ imageUrl ] = note . noteId ;
2020-03-25 11:28:44 +01:00
2020-03-25 21:01:42 +01:00
log . info ( ` Download of ${ imageUrl } succeeded and was saved as image note ${ note . noteId } ` ) ;
}
catch ( e ) {
2020-04-02 22:55:11 +02:00
log . error ( ` Download of ${ imageUrl } for note ${ noteId } failed with error: ${ e . message } ${ e . stack } ` ) ;
2020-03-25 21:01:42 +01:00
}
2020-03-25 11:28:44 +01:00
}
2020-03-25 21:01:42 +01:00
/** url => download promise */
2020-03-25 11:28:44 +01:00
const downloadImagePromises = { } ;
function replaceUrl ( content , url , imageNote ) {
2020-05-03 14:33:59 +02:00
const quoted = url . replace ( /[.*+\-?^${}()|[\]\\]/g , '\\$&' ) ;
2020-03-25 18:21:55 +01:00
2020-05-03 14:33:59 +02:00
return content . replace ( new RegExp ( ` \\ s+src=[ \" '] ${ quoted } [ \" '] ` , "g" ) , ` src="api/images/ ${ imageNote . noteId } / ${ imageNote . title } " ` ) ;
2020-03-25 11:28:44 +01:00
}
async function downloadImages ( noteId , content ) {
2020-04-02 22:55:11 +02:00
const re = /<img[^>]*?\ssrc=['"]([^'">]+)['"]/ig ;
2020-03-25 11:28:44 +01:00
let match ;
2020-04-02 22:55:11 +02:00
const origContent = content ;
while ( match = re . exec ( origContent ) ) {
2020-05-03 14:33:59 +02:00
const url = match [ 1 ] ;
2020-03-25 11:28:44 +01:00
2020-05-04 10:03:54 +02:00
if ( ! url . includes ( 'api/images/' )
2020-04-02 22:55:11 +02:00
// this is and exception for the web clipper's "imageId"
2020-05-03 14:33:59 +02:00
&& ( url . length !== 20 || url . toLowerCase ( ) . startsWith ( 'http' ) ) ) {
2020-03-25 11:28:44 +01:00
if ( url in imageUrlToNoteIdMapping ) {
const imageNote = await repository . getNote ( imageUrlToNoteIdMapping [ url ] ) ;
2020-05-06 23:11:34 +02:00
if ( ! imageNote || imageNote . isDeleted ) {
2020-03-25 11:28:44 +01:00
delete imageUrlToNoteIdMapping [ url ] ;
}
else {
content = replaceUrl ( content , url , imageNote ) ;
continue ;
}
}
const existingImage = ( await attributeService . getNotesWithLabel ( 'imageUrl' , url ) )
. find ( note => note . type === 'image' ) ;
if ( existingImage ) {
imageUrlToNoteIdMapping [ url ] = existingImage . noteId ;
content = replaceUrl ( content , url , existingImage ) ;
continue ;
}
2020-05-06 23:11:34 +02:00
if ( url in downloadImagePromises ) {
// download is already in progress
continue ;
}
2020-03-25 21:01:42 +01:00
// this is done asynchronously, it would be too slow to wait for the download
// given that save can be triggered very often
2020-03-25 11:28:44 +01:00
downloadImagePromises [ url ] = downloadImage ( noteId , url ) ;
}
}
2020-03-25 18:21:55 +01:00
Promise . all ( Object . values ( downloadImagePromises ) ) . then ( ( ) => {
2020-03-25 11:28:44 +01:00
setTimeout ( async ( ) => {
2020-03-25 21:01:42 +01:00
// the normal expected flow of the offline image saving is that users will paste the image(s)
// which will get asynchronously downloaded, during that time they keep editing the note
// once the download is finished, the image note representing downloaded image will be used
// to replace the IMG link.
// However there's another flow where user pastes the image and leaves the note before the images
// are downloaded and the IMG references are not updated. For this occassion we have this code
// which upon the download of all the images will update the note if the links have not been fixed before
2020-05-06 23:11:34 +02:00
await sql . transactional ( async ( ) => {
const imageNotes = await repository . getNotes ( Object . values ( imageUrlToNoteIdMapping ) ) ;
2020-03-25 11:28:44 +01:00
2020-05-06 23:11:34 +02:00
const origNote = await repository . getNote ( noteId ) ;
const origContent = await origNote . getContent ( ) ;
let updatedContent = origContent ;
2020-03-25 11:28:44 +01:00
2020-05-06 23:11:34 +02:00
for ( const url in imageUrlToNoteIdMapping ) {
const imageNote = imageNotes . find ( note => note . noteId === imageUrlToNoteIdMapping [ url ] ) ;
2020-03-25 11:28:44 +01:00
2020-05-06 23:11:34 +02:00
if ( imageNote && ! imageNote . isDeleted ) {
updatedContent = replaceUrl ( updatedContent , url , imageNote ) ;
}
2020-03-25 11:28:44 +01:00
}
2020-05-06 23:11:34 +02:00
// update only if the links have not been already fixed.
if ( updatedContent !== origContent ) {
await origNote . setContent ( updatedContent ) ;
2020-03-25 21:01:42 +01:00
2020-05-06 23:11:34 +02:00
await scanForLinks ( origNote ) ;
2020-05-03 14:33:59 +02:00
2020-05-06 23:11:34 +02:00
console . log ( ` Fixed the image links for note ${ noteId } to the offline saved. ` ) ;
}
} ) ;
2020-03-25 11:28:44 +01:00
} , 5000 ) ;
} ) ;
2020-03-25 18:21:55 +01:00
return content ;
2020-03-25 11:28:44 +01:00
}
2018-11-19 15:00:49 +01:00
async function saveLinks ( note , content ) {
2018-11-16 23:27:57 +01:00
if ( note . type !== 'text' && note . type !== 'relation-map' ) {
2018-11-19 23:11:36 +01:00
return content ;
2018-02-23 22:58:24 -05:00
}
2019-09-01 22:09:55 +02:00
if ( note . isProtected && ! protectedSessionService . isProtectedSessionAvailable ( ) ) {
return content ;
}
2018-11-08 20:17:56 +01:00
const foundLinks = [ ] ;
2020-01-11 09:50:05 +01:00
2018-11-16 23:27:57 +01:00
if ( note . type === 'text' ) {
2020-05-03 14:33:59 +02:00
content = await downloadImages ( note . noteId , content ) ;
2018-11-19 15:00:49 +01:00
content = findImageLinks ( content , foundLinks ) ;
2019-08-19 20:12:00 +02:00
content = findInternalLinks ( content , foundLinks ) ;
2020-01-10 21:41:00 +01:00
content = findIncludeNoteLinks ( content , foundLinks ) ;
2018-11-16 23:27:57 +01:00
}
else if ( note . type === 'relation-map' ) {
2018-11-19 15:00:49 +01:00
findRelationMapLinks ( content , foundLinks ) ;
2018-11-16 23:27:57 +01:00
}
else {
throw new Error ( "Unrecognized type " + note . type ) ;
}
2018-11-08 20:17:56 +01:00
2018-11-08 11:08:16 +01:00
const existingLinks = await note . getLinks ( ) ;
2018-01-06 22:38:53 -05:00
2018-11-08 20:17:56 +01:00
for ( const foundLink of foundLinks ) {
2020-04-05 16:06:13 +02:00
const targetNote = await repository . getNote ( foundLink . value ) ;
if ( ! targetNote || targetNote . isDeleted ) {
continue ;
2019-09-01 20:35:55 +02:00
}
2018-11-08 20:17:56 +01:00
const existingLink = existingLinks . find ( existingLink =>
2019-08-19 20:12:00 +02:00
existingLink . value === foundLink . value
&& existingLink . name === foundLink . name ) ;
2018-01-06 22:38:53 -05:00
2018-11-08 11:08:16 +01:00
if ( ! existingLink ) {
2020-04-05 15:35:01 +02:00
const newLink = await new Attribute ( {
2018-03-31 22:15:06 -04:00
noteId : note . noteId ,
2020-04-05 16:06:13 +02:00
type : 'relation' ,
2019-08-19 20:12:00 +02:00
name : foundLink . name ,
2019-08-27 22:47:10 +02:00
value : foundLink . value ,
2018-04-02 22:53:01 -04:00
} ) . save ( ) ;
2020-04-05 15:35:01 +02:00
existingLinks . push ( newLink ) ;
2018-01-06 22:38:53 -05:00
}
2018-11-08 11:08:16 +01:00
else if ( existingLink . isDeleted ) {
existingLink . isDeleted = false ;
await existingLink . save ( ) ;
}
// else the link exists so we don't need to do anything
2018-01-06 22:38:53 -05:00
}
2018-11-08 11:08:16 +01:00
// marking links as deleted if they are not present on the page anymore
2018-11-08 20:17:56 +01:00
const unusedLinks = existingLinks . filter ( existingLink => ! foundLinks . some ( foundLink =>
2019-08-19 20:12:00 +02:00
existingLink . value === foundLink . value
&& existingLink . name === foundLink . name ) ) ;
2018-01-06 22:38:53 -05:00
2018-11-08 11:08:16 +01:00
for ( const unusedLink of unusedLinks ) {
unusedLink . isDeleted = true ;
await unusedLink . save ( ) ;
2018-01-06 22:38:53 -05:00
}
2018-11-19 15:00:49 +01:00
return content ;
2018-01-06 22:38:53 -05:00
}
2018-03-31 22:15:06 -04:00
async function saveNoteRevision ( note ) {
2019-12-02 20:21:52 +01:00
// files and images are versioned separately
2019-02-27 22:15:52 +01:00
if ( note . type === 'file' || note . type === 'image' || await note . hasLabel ( 'disableVersioning' ) ) {
2019-02-06 21:29:23 +01:00
return ;
}
2017-12-10 12:56:59 -05:00
const now = new Date ( ) ;
2018-04-02 21:47:46 -04:00
const noteRevisionSnapshotTimeInterval = parseInt ( await optionService . getOption ( 'noteRevisionSnapshotTimeInterval' ) ) ;
2017-11-05 10:41:54 -05:00
2019-03-31 12:49:42 +02:00
const revisionCutoff = dateUtils . utcDateStr ( new Date ( now . getTime ( ) - noteRevisionSnapshotTimeInterval * 1000 ) ) ;
2017-11-05 10:41:54 -05:00
2018-08-31 18:22:53 +02:00
const existingNoteRevisionId = await sql . getValue (
2019-11-01 19:21:48 +01:00
"SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND utcDateCreated >= ?" , [ note . noteId , revisionCutoff ] ) ;
2018-03-31 22:15:06 -04:00
2019-03-31 12:49:42 +02:00
const msSinceDateCreated = now . getTime ( ) - dateUtils . parseDateTime ( note . utcDateCreated ) . getTime ( ) ;
2018-03-31 22:15:06 -04:00
2019-02-06 21:29:23 +01:00
if ( ! existingNoteRevisionId && msSinceDateCreated >= noteRevisionSnapshotTimeInterval * 1000 ) {
2019-11-01 19:21:48 +01:00
const noteRevision = await new NoteRevision ( {
2018-03-31 22:15:06 -04:00
noteId : note . noteId ,
// title and text should be decrypted now
title : note . title ,
2019-11-01 19:21:48 +01:00
contentLength : - 1 , // will be updated in .setContent()
2018-04-08 11:57:14 -04:00
type : note . type ,
mime : note . mime ,
2018-08-07 11:38:00 +02:00
isProtected : false , // will be fixed in the protectNoteRevisions() call
2019-11-01 19:21:48 +01:00
utcDateLastEdited : note . utcDateModified ,
utcDateCreated : dateUtils . utcNowDateTime ( ) ,
utcDateModified : dateUtils . utcNowDateTime ( ) ,
dateLastEdited : note . dateModified ,
dateCreated : dateUtils . localNowDateTime ( )
2018-04-02 22:53:01 -04:00
} ) . save ( ) ;
2019-11-01 19:21:48 +01:00
await noteRevision . setContent ( await note . getContent ( ) ) ;
2018-03-31 22:15:06 -04:00
}
}
2017-11-05 10:41:54 -05:00
2018-03-31 22:15:06 -04:00
async function updateNote ( noteId , noteUpdates ) {
const note = await repository . getNote ( noteId ) ;
2017-12-10 12:56:59 -05:00
2018-10-30 22:18:20 +01:00
if ( ! note . isContentAvailable ) {
throw new Error ( ` Note ${ noteId } is not available for change! ` ) ;
}
2018-03-31 22:15:06 -04:00
await saveNoteRevision ( note ) ;
2017-11-05 10:41:54 -05:00
2019-05-04 16:05:28 +02:00
// if protected status changed, then we need to encrypt/decrypt the content anyway
if ( [ 'file' , 'image' ] . includes ( note . type ) && note . isProtected !== noteUpdates . isProtected ) {
noteUpdates . content = await note . getContent ( ) ;
}
2018-08-01 09:26:02 +02:00
const noteTitleChanged = note . title !== noteUpdates . title ;
2018-03-31 22:15:06 -04:00
note . title = noteUpdates . title ;
note . isProtected = noteUpdates . isProtected ;
2018-04-01 17:38:24 -04:00
await note . save ( ) ;
2017-11-14 22:21:56 -05:00
2019-11-08 22:34:30 +01:00
if ( noteUpdates . content !== undefined && noteUpdates . content !== null ) {
2019-03-31 12:49:42 +02:00
noteUpdates . content = await saveLinks ( note , noteUpdates . content ) ;
2019-03-08 22:25:12 +01:00
2019-03-31 12:49:42 +02:00
await note . setContent ( noteUpdates . content ) ;
2019-02-06 21:29:23 +01:00
}
2018-08-01 09:26:02 +02:00
if ( noteTitleChanged ) {
await triggerNoteTitleChanged ( note ) ;
}
2019-11-09 11:58:52 +01:00
await noteRevisionService . protectNoteRevisions ( note ) ;
2019-08-06 22:39:27 +02:00
return {
dateModified : note . dateModified ,
utcDateModified : note . utcDateModified
} ;
2017-11-05 10:41:54 -05:00
}
2020-01-03 10:48:36 +01:00
/ * *
* @ param { Branch } branch
* @ param { string } deleteId
* @ param { TaskContext } taskContext
*
* @ return { boolean } - true if note has been deleted , false otherwise
* /
async function deleteBranch ( branch , deleteId , taskContext ) {
2019-10-18 22:27:38 +02:00
taskContext . increaseProgressCount ( ) ;
2018-08-07 11:38:00 +02:00
if ( ! branch || branch . isDeleted ) {
2019-05-20 21:50:01 +02:00
return false ;
2018-01-14 21:39:21 -05:00
}
2019-04-16 21:40:04 +02:00
if ( branch . branchId === 'root'
|| branch . noteId === 'root'
|| branch . noteId === await hoistedNoteService . getHoistedNoteId ( ) ) {
2018-08-30 22:38:34 +02:00
throw new Error ( "Can't delete root branch/note" ) ;
}
2018-03-31 23:08:22 -04:00
branch . isDeleted = true ;
2020-01-03 10:48:36 +01:00
branch . deleteId = deleteId ;
2018-03-31 23:08:22 -04:00
await branch . save ( ) ;
2017-11-19 23:12:39 -05:00
2018-03-31 23:08:22 -04:00
const note = await branch . getNote ( ) ;
const notDeletedBranches = await note . getBranches ( ) ;
2017-11-05 10:41:54 -05:00
2018-03-31 23:08:22 -04:00
if ( notDeletedBranches . length === 0 ) {
for ( const childBranch of await note . getChildBranches ( ) ) {
2020-01-03 10:48:36 +01:00
await deleteBranch ( childBranch , deleteId , taskContext ) ;
2017-11-19 23:12:39 -05:00
}
2018-08-15 15:27:22 +02:00
2020-01-03 13:41:44 +01:00
// first delete children and then parent - this will show up better in recent changes
note . isDeleted = true ;
note . deleteId = deleteId ;
await note . save ( ) ;
2020-04-06 22:08:54 +02:00
log . info ( "Deleting note " + note . noteId ) ;
2020-04-06 20:59:04 +02:00
2018-08-15 15:27:22 +02:00
for ( const attribute of await note . getOwnedAttributes ( ) ) {
attribute . isDeleted = true ;
2020-01-03 10:48:36 +01:00
attribute . deleteId = deleteId ;
2018-08-15 15:27:22 +02:00
await attribute . save ( ) ;
}
2018-08-22 14:40:49 +02:00
2018-11-15 13:58:14 +01:00
for ( const relation of await note . getTargetRelations ( ) ) {
relation . isDeleted = true ;
2020-01-03 10:48:36 +01:00
relation . deleteId = deleteId ;
2018-11-15 13:58:14 +01:00
await relation . save ( ) ;
}
2019-05-20 21:50:01 +02:00
return true ;
}
else {
return false ;
2017-11-19 23:12:39 -05:00
}
2017-11-05 10:41:54 -05:00
}
2020-01-03 13:14:43 +01:00
/ * *
* @ param { Note } note
* @ param { string } deleteId
* @ param { TaskContext } taskContext
* /
async function undeleteNote ( note , deleteId , taskContext ) {
const undeletedParentBranches = await getUndeletedParentBranches ( note . noteId , deleteId ) ;
if ( undeletedParentBranches . length === 0 ) {
// cannot undelete if there's no undeleted parent
return ;
}
for ( const parentBranch of undeletedParentBranches ) {
await undeleteBranch ( parentBranch , deleteId , taskContext ) ;
}
}
/ * *
* @ param { Branch } branch
* @ param { string } deleteId
* @ param { TaskContext } taskContext
* /
async function undeleteBranch ( branch , deleteId , taskContext ) {
if ( ! branch . isDeleted ) {
return ;
}
const note = await branch . getNote ( ) ;
if ( note . isDeleted && note . deleteId !== deleteId ) {
return ;
}
branch . isDeleted = false ;
await branch . save ( ) ;
taskContext . increaseProgressCount ( ) ;
if ( note . isDeleted && note . deleteId === deleteId ) {
note . isDeleted = false ;
await note . save ( ) ;
const attrs = await repository . getEntities ( `
SELECT * FROM attributes
WHERE isDeleted = 1
AND deleteId = ?
AND ( noteId = ?
OR ( type = 'relation' AND value = ? ) ) ` , [deleteId, note.noteId, note.noteId]);
for ( const attr of attrs ) {
attr . isDeleted = false ;
await attr . save ( ) ;
}
const childBranches = await repository . getEntities ( `
SELECT branches . *
FROM branches
WHERE branches . isDeleted = 1
AND branches . deleteId = ?
AND branches . parentNoteId = ? ` , [deleteId, note.noteId]);
for ( const childBranch of childBranches ) {
2020-01-03 13:41:44 +01:00
await undeleteBranch ( childBranch , deleteId , taskContext ) ;
2020-01-03 13:14:43 +01:00
}
}
}
/ * *
* @ return return deleted branches of an undeleted parent note
* /
async function getUndeletedParentBranches ( noteId , deleteId ) {
return await repository . getEntities ( `
SELECT branches . *
FROM branches
JOIN notes AS parentNote ON parentNote . noteId = branches . parentNoteId
WHERE branches . noteId = ?
AND branches . isDeleted = 1
AND branches . deleteId = ?
AND parentNote . isDeleted = 0 ` , [noteId, deleteId]);
}
2020-03-25 21:01:42 +01:00
async function scanForLinks ( note ) {
2019-09-01 20:35:55 +02:00
if ( ! note || ! [ 'text' , 'relation-map' ] . includes ( note . type ) ) {
return ;
}
2019-09-01 22:09:55 +02:00
try {
const content = await note . getContent ( ) ;
const newContent = await saveLinks ( note , content ) ;
2019-09-01 20:35:55 +02:00
2019-09-01 22:09:55 +02:00
await note . setContent ( newContent ) ;
}
catch ( e ) {
2020-03-26 16:59:40 +01:00
log . error ( ` Could not scan for links note ${ note . noteId } : ${ e . message } ${ e . stack } ` ) ;
2019-09-01 22:09:55 +02:00
}
2019-09-01 20:35:55 +02:00
}
2019-11-01 22:09:51 +01:00
async function eraseDeletedNotes ( ) {
2020-01-03 22:32:49 +01:00
const eraseNotesAfterTimeInSeconds = await optionService . getOptionInt ( 'eraseNotesAfterTimeInSeconds' ) ;
const cutoffDate = new Date ( Date . now ( ) - eraseNotesAfterTimeInSeconds * 1000 ) ;
2018-11-01 10:23:21 +01:00
2019-11-01 22:09:51 +01:00
const noteIdsToErase = await sql . getColumn ( "SELECT noteId FROM notes WHERE isDeleted = 1 AND isErased = 0 AND notes.utcDateModified <= ?" , [ dateUtils . utcDateStr ( cutoffDate ) ] ) ;
2019-11-12 22:26:25 +01:00
if ( noteIdsToErase . length === 0 ) {
return ;
}
2019-12-16 22:00:44 +01:00
// it's better to not use repository for this because:
// - it would complain about saving protected notes out of protected session
// - we don't want these changes to be synced (since they are done on all instances anyway)
// - we don't want change the hash since this erasing happens on each instance separately
// and changing the hash would fire up the sync errors temporarily
2018-11-01 10:23:21 +01:00
2019-11-01 22:09:51 +01:00
await sql . executeMany ( `
UPDATE notes
2020-01-03 22:32:49 +01:00
SET title = '[deleted]' ,
contentLength = 0 ,
2020-02-19 19:51:36 +01:00
isProtected = 0 ,
2020-01-03 22:32:49 +01:00
isErased = 1
2019-11-01 22:09:51 +01:00
WHERE noteId IN ( ? ? ? ) ` , noteIdsToErase);
await sql . executeMany ( `
UPDATE note _contents
2019-12-03 19:10:40 +01:00
SET content = NULL
2019-11-01 22:09:51 +01:00
WHERE noteId IN ( ? ? ? ) ` , noteIdsToErase);
2019-11-09 15:21:14 +01:00
// deleting first contents since the WHERE relies on isErased = 0
2019-11-01 22:09:51 +01:00
await sql . executeMany ( `
2019-11-09 15:21:14 +01:00
UPDATE note _revision _contents
2019-12-03 19:10:40 +01:00
SET content = NULL
2019-11-09 15:21:14 +01:00
WHERE noteRevisionId IN
2019-11-30 11:36:36 +01:00
( SELECT noteRevisionId FROM note _revisions WHERE isErased = 0 AND noteId IN ( ? ? ? ) ) ` , noteIdsToErase);
2019-11-09 15:21:14 +01:00
await sql . executeMany ( `
UPDATE note _revisions
SET isErased = 1 ,
2020-01-03 22:32:49 +01:00
title = NULL ,
contentLength = 0
2019-11-09 15:21:14 +01:00
WHERE isErased = 0 AND noteId IN ( ? ? ? ) ` , noteIdsToErase);
2020-01-03 22:32:49 +01:00
await sql . executeMany ( `
UPDATE attributes
SET name = 'deleted' ,
value = ''
WHERE noteId IN ( ? ? ? ) ` , noteIdsToErase);
2020-01-07 22:29:54 +01:00
2020-01-07 20:53:41 +01:00
log . info ( ` Erased notes: ${ JSON . stringify ( noteIdsToErase ) } ` ) ;
2018-11-01 10:23:21 +01:00
}
2019-10-19 12:36:16 +02:00
async function duplicateNote ( noteId , parentNoteId ) {
const origNote = await repository . getNote ( noteId ) ;
if ( origNote . isProtected && ! protectedSessionService . isProtectedSessionAvailable ( ) ) {
throw new Error ( ` Cannot duplicate note= ${ origNote . noteId } because it is protected and protected session is not available ` ) ;
}
// might be null if orig note is not in the target parentNoteId
const origBranch = ( await origNote . getBranches ( ) ) . find ( branch => branch . parentNoteId === parentNoteId ) ;
const newNote = new Note ( origNote ) ;
newNote . noteId = undefined ; // force creation of new note
newNote . title += " (dup)" ;
await newNote . save ( ) ;
await newNote . setContent ( await origNote . getContent ( ) ) ;
const newBranch = await new Branch ( {
noteId : newNote . noteId ,
parentNoteId : parentNoteId ,
// here increasing just by 1 to make sure it's directly after original
notePosition : origBranch ? origBranch . notePosition + 1 : null
} ) . save ( ) ;
2019-12-01 10:28:05 +01:00
for ( const attribute of await origNote . getOwnedAttributes ( ) ) {
2019-10-19 12:36:16 +02:00
const attr = new Attribute ( attribute ) ;
attr . attributeId = undefined ; // force creation of new attribute
attr . noteId = newNote . noteId ;
await attr . save ( ) ;
}
return {
note : newNote ,
branch : newBranch
} ;
}
2019-01-05 21:49:40 +01:00
sqlInit . dbReady . then ( ( ) => {
// first cleanup kickoff 5 minutes after startup
2019-11-01 22:09:51 +01:00
setTimeout ( cls . wrap ( eraseDeletedNotes ) , 5 * 60 * 1000 ) ;
2018-11-01 10:23:21 +01:00
2019-11-01 22:09:51 +01:00
setInterval ( cls . wrap ( eraseDeletedNotes ) , 4 * 3600 * 1000 ) ;
2019-01-05 21:49:40 +01:00
} ) ;
2018-11-01 10:23:21 +01:00
2017-11-05 10:41:54 -05:00
module . exports = {
createNewNote ,
2019-11-16 12:28:47 +01:00
createNewNoteWithTarget ,
2017-11-05 10:41:54 -05:00
updateNote ,
2019-09-01 20:57:25 +02:00
deleteBranch ,
2020-01-03 13:14:43 +01:00
undeleteNote ,
2019-09-01 20:35:55 +02:00
protectNoteRecursively ,
2019-10-19 12:36:16 +02:00
scanForLinks ,
2020-01-03 13:14:43 +01:00
duplicateNote ,
getUndeletedParentBranches
2019-12-03 15:49:27 -03:00
} ;