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' ) ;
2021-06-29 22:15:57 +02:00
const entityChangesService = require ( './entity_changes' ) ;
2018-08-01 09:26:02 +02:00
const eventService = require ( './events' ) ;
2018-11-06 14:23:49 +01:00
const cls = require ( '../services/cls' ) ;
2019-09-01 22:09:55 +02:00
const protectedSessionService = require ( '../services/protected_session' ) ;
const log = require ( '../services/log' ) ;
2020-05-12 10:28:31 +02:00
const utils = require ( '../services/utils' ) ;
2023-06-04 23:01:40 +02:00
const revisionService = require ( './revisions.js' ) ;
2020-03-25 11:28:44 +01:00
const request = require ( './request' ) ;
const path = require ( 'path' ) ;
const url = require ( 'url' ) ;
2021-06-29 22:15:57 +02:00
const becca = require ( '../becca/becca' ) ;
2023-01-03 13:52:37 +01:00
const BBranch = require ( '../becca/entities/bbranch' ) ;
const BNote = require ( '../becca/entities/bnote' ) ;
const BAttribute = require ( '../becca/entities/battribute' ) ;
2022-05-15 15:21:35 +02:00
const dayjs = require ( "dayjs" ) ;
2022-12-09 16:13:22 +01:00
const htmlSanitizer = require ( "./html_sanitizer" ) ;
const ValidationError = require ( "../errors/validation_error" ) ;
2022-12-16 16:00:49 +01:00
const noteTypesService = require ( "./note_types" ) ;
2023-04-16 23:11:18 +02:00
const fs = require ( "fs" ) ;
2023-05-29 13:02:25 +02:00
const ws = require ( "./ws.js" ) ;
2017-11-05 10:41:54 -05:00
2023-04-14 16:49:06 +02:00
/** @param {BNote} parentNote */
2023-03-19 22:23:58 +01:00
function getNewNotePosition ( parentNote ) {
2023-04-03 21:08:32 +02:00
if ( parentNote . isLabelTruthy ( 'newNotesOnTop' ) ) {
2023-03-19 22:23:58 +01:00
const minNotePos = parentNote . getChildBranches ( )
. reduce ( ( min , note ) => Math . min ( min , note . notePosition ) , 0 ) ;
2019-11-14 23:10:56 +01:00
2023-03-19 22:23:58 +01:00
return minNotePos - 10 ;
} else {
const maxNotePos = parentNote . getChildBranches ( )
. reduce ( ( max , note ) => Math . max ( max , note . notePosition ) , 0 ) ;
2021-04-17 20:52:46 +02:00
2023-03-19 22:23:58 +01:00
return maxNotePos + 10 ;
}
2018-03-31 22:23:40 -04:00
}
2023-04-14 16:49:06 +02:00
/** @param {BNote} note */
2020-06-20 12:31:38 +02:00
function triggerNoteTitleChanged ( note ) {
eventService . emit ( eventService . NOTE _TITLE _CHANGED , note ) ;
2018-08-01 09:26:02 +02:00
}
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 ;
}
2022-12-16 16:00:49 +01:00
return noteTypesService . getDefaultMimeForNoteType ( type ) ;
2019-11-14 23:10:56 +01:00
}
2018-01-27 17:18:19 -05:00
2023-04-14 16:49:06 +02:00
/ * *
* @ param { BNote } parentNote
* @ param { BNote } childNote
* /
2020-06-20 12:31:38 +02:00
function copyChildAttributes ( parentNote , childNote ) {
2020-09-14 21:08:11 +02:00
for ( const attr of parentNote . getAttributes ( ) ) {
2019-11-14 23:10:56 +01:00
if ( attr . name . startsWith ( "child:" ) ) {
2023-02-27 21:07:32 +01:00
const name = attr . name . substr ( 6 ) ;
2023-06-01 00:07:57 +02:00
const hasAlreadyTemplate = childNote . hasRelation ( 'template' ) ;
2023-02-27 21:07:32 +01:00
if ( hasAlreadyTemplate && attr . type === 'relation' && name === 'template' ) {
// if the note already has a template, it means the template was chosen by the user explicitly
// in the menu. In that case we should override the default templates defined in the child: attrs
continue ;
}
2023-01-03 13:52:37 +01:00
new BAttribute ( {
2019-11-14 23:10:56 +01:00
noteId : childNote . noteId ,
type : attr . type ,
2023-02-27 21:07:32 +01:00
name : name ,
2019-11-14 23:10:56 +01:00
value : attr . value ,
position : attr . position ,
isInheritable : attr . isInheritable
} ) . save ( ) ;
}
2019-03-17 12:19:23 +01:00
}
2019-11-14 23:10:56 +01:00
}
2019-03-17 12:19:23 +01:00
2023-04-14 16:49:06 +02:00
/** @param {BNote} parentNote */
2022-05-15 15:21:35 +02:00
function getNewNoteTitle ( parentNote ) {
let title = "new note" ;
const titleTemplate = parentNote . getLabelValue ( 'titleTemplate' ) ;
if ( titleTemplate !== null ) {
try {
const now = dayjs ( cls . getLocalNowDateTime ( ) || new Date ( ) ) ;
// "officially" injected values:
// - now
// - parentNote
2022-12-21 15:19:05 +01:00
title = eval ( ` \` ${ titleTemplate } \` ` ) ;
2022-05-15 15:21:35 +02:00
} catch ( e ) {
log . error ( ` Title template of note ' ${ parentNote . noteId } ' failed with: ${ e . message } ` ) ;
}
}
2022-07-06 23:09:16 +02:00
// this isn't in theory a good place to sanitize title, but this will catch a lot of XSS attempts
// title is supposed to contain text only (not HTML) and be printed text only, but given the number of usages
// it's difficult to guarantee correct handling in all cases
title = htmlSanitizer . sanitize ( title ) ;
2022-05-15 15:21:35 +02:00
return title ;
}
2022-11-27 23:43:25 +01:00
function getAndValidateParent ( params ) {
const parentNote = becca . notes [ params . parentNoteId ] ;
if ( ! parentNote ) {
2023-05-04 22:16:18 +02:00
throw new ValidationError ( ` Parent note ' ${ params . parentNoteId } ' was not found. ` ) ;
2022-11-27 23:43:25 +01:00
}
2022-12-24 12:26:32 +01:00
if ( parentNote . type === 'launcher' && parentNote . noteId !== '_lbBookmarks' ) {
2022-12-09 16:48:00 +01:00
throw new ValidationError ( ` Creating child notes into launcher notes is not allowed. ` ) ;
}
2022-12-21 16:11:00 +01:00
if ( [ '_lbAvailableLaunchers' , '_lbVisibleLaunchers' ] . includes ( params . parentNoteId ) && params . type !== 'launcher' ) {
2022-12-18 20:12:43 +01:00
throw new ValidationError ( ` Only 'launcher' notes can be created in parent ' ${ params . parentNoteId } ' ` ) ;
}
2023-01-13 11:34:35 +01:00
if ( ! params . ignoreForbiddenParents ) {
if ( [ '_lbRoot' , '_hidden' ] . includes ( parentNote . noteId )
|| parentNote . noteId . startsWith ( "_lbTpl" )
|| parentNote . isOptions ( ) ) {
throw new ValidationError ( ` Creating child notes into ' ${ parentNote . noteId } ' is not allowed. ` ) ;
}
2022-11-27 23:43:25 +01:00
}
return parentNote ;
}
2019-11-14 23:10:56 +01:00
/ * *
* Following object properties are mandatory :
* - { string } parentNoteId
* - { string } title
* - { * } content
2022-12-06 23:01:42 +01:00
* - { string } type - text , code , file , image , search , book , relationMap , canvas , 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
2023-01-05 23:38:41 +01:00
* @ returns { { note : BNote , branch : BBranch } }
2019-11-14 23:10:56 +01:00
* /
2020-06-20 12:31:38 +02:00
function createNewNote ( params ) {
2022-11-27 23:43:25 +01:00
const parentNote = getAndValidateParent ( params ) ;
2019-11-14 23:10:56 +01:00
2022-01-08 19:33:56 +01:00
if ( params . title === null || params . title === undefined ) {
2022-05-15 15:21:35 +02:00
params . title = getNewNoteTitle ( parentNote ) ;
2019-11-14 23:10:56 +01:00
}
2022-01-07 23:06:04 +01:00
2022-01-07 19:33:59 +01:00
if ( params . content === null || params . content === undefined ) {
throw new Error ( ` Note content must be set ` ) ;
}
2019-11-14 23:10:56 +01:00
2020-08-18 22:20:47 +02:00
return sql . transactional ( ( ) => {
2022-10-22 21:34:38 +02:00
let note , branch , isEntityEventsDisabled ;
try {
isEntityEventsDisabled = cls . isEntityEventsDisabled ( ) ;
if ( ! isEntityEventsDisabled ) {
// it doesn't make sense to run note creation events on a partially constructed note, so
// defer them until note creation is completed
cls . disableEntityEvents ( ) ;
}
2022-12-27 14:44:28 +01:00
// TODO: think about what can happen if the note already exists with the forced ID
// I guess on DB it's going to be fine, but becca references between entities
2023-06-01 00:07:57 +02:00
// might get messed up (two note instances for the same ID existing in the references)
2023-01-03 13:52:37 +01:00
note = new BNote ( {
2022-10-22 21:34:38 +02:00
noteId : params . noteId , // optionally can force specific noteId
title : params . title ,
isProtected : ! ! params . isProtected ,
type : params . type ,
mime : deriveMime ( params . type , params . mime )
} ) . save ( ) ;
note . setContent ( params . content ) ;
2023-01-03 13:52:37 +01:00
branch = new BBranch ( {
2022-10-22 21:34:38 +02:00
noteId : note . noteId ,
parentNoteId : params . parentNoteId ,
2023-03-19 22:23:58 +01:00
notePosition : params . notePosition !== undefined ? params . notePosition : getNewNotePosition ( parentNote ) ,
2022-10-22 21:34:38 +02:00
prefix : params . prefix ,
isExpanded : ! ! params . isExpanded
} ) . save ( ) ;
}
finally {
if ( ! isEntityEventsDisabled ) {
2023-06-01 00:07:57 +02:00
// re-enable entity events only if they were previously enabled
2022-10-22 21:34:38 +02:00
// (they can be disabled in case of import)
cls . enableEntityEvents ( ) ;
}
}
2020-08-18 21:32:45 +02:00
2023-01-26 20:32:27 +01:00
asyncPostProcessContent ( note , params . content ) ;
2020-08-18 21:32:45 +02:00
2022-05-31 23:27:45 +02:00
if ( params . templateNoteId ) {
if ( ! becca . getNote ( params . templateNoteId ) ) {
throw new Error ( ` Template note ' ${ params . templateNoteId } ' does not exist. ` ) ;
}
2023-02-27 21:07:32 +01:00
note . addRelation ( 'template' , params . templateNoteId ) ;
2023-01-06 20:31:55 +01:00
// no special handling for ~inherit since it doesn't matter if it's assigned with the note creation or later
2022-05-31 23:27:45 +02:00
}
2023-02-27 21:07:32 +01:00
copyChildAttributes ( parentNote , note ) ;
2023-06-01 00:07:57 +02:00
eventService . emit ( eventService . ENTITY _CREATED , { entityName : 'notes' , entity : note } ) ;
eventService . emit ( eventService . ENTITY _CHANGED , { entityName : 'notes' , entity : note } ) ;
2020-08-18 21:32:45 +02:00
triggerNoteTitleChanged ( note ) ;
2023-06-05 00:09:55 +02:00
// blobs doesn't use "created" event
eventService . emit ( eventService . ENTITY _CHANGED , { entityName : 'blobs' , entity : note } ) ;
2023-06-01 00:07:57 +02:00
eventService . emit ( eventService . ENTITY _CREATED , { entityName : 'branches' , entity : branch } ) ;
eventService . emit ( eventService . ENTITY _CHANGED , { entityName : 'branches' , entity : branch } ) ;
eventService . emit ( eventService . CHILD _NOTE _CREATED , { childNote : note , parentNote : parentNote } ) ;
2020-08-18 21:32:45 +02:00
2022-04-19 23:36:21 +02:00
log . info ( ` Created new note ' ${ note . noteId } ', branch ' ${ branch . branchId } ' of type ' ${ note . type } ', mime ' ${ note . mime } ' ` ) ;
2022-01-31 21:25:18 +01:00
2020-08-18 21:32:45 +02:00
return {
note ,
branch
} ;
} ) ;
2017-11-05 10:41:54 -05:00
}
2020-06-20 12:31:38 +02:00
function createNewNoteWithTarget ( target , targetBranchId , params ) {
2019-11-16 17:00:22 +01:00
if ( ! params . type ) {
2021-04-25 21:24:32 +02:00
const parentNote = becca . notes [ params . parentNoteId ] ;
2019-11-16 17:00:22 +01:00
// 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' ) {
2020-06-20 12:31:38 +02:00
return createNewNote ( params ) ;
2019-11-16 12:28:47 +01:00
}
else if ( target === 'after' ) {
2021-04-25 21:24:32 +02:00
const afterBranch = becca . branches [ targetBranchId ] ;
2019-11-16 12:28:47 +01:00
2021-04-25 21:24:32 +02:00
// not updating utcDateModified to avoid having to sync whole rows
2020-06-20 12:31:38 +02:00
sql . execute ( 'UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0' ,
2021-04-25 21:24:32 +02:00
[ params . parentNoteId , afterBranch . notePosition ] ) ;
2019-11-16 12:28:47 +01:00
2021-04-25 21:24:32 +02:00
params . notePosition = afterBranch . notePosition + 10 ;
2019-11-16 12:28:47 +01:00
2020-06-20 12:31:38 +02:00
const retObject = createNewNote ( params ) ;
2019-11-16 12:28:47 +01:00
2020-08-02 23:43:39 +02:00
entityChangesService . addNoteReorderingEntityChange ( params . parentNoteId ) ;
2019-11-24 22:15:33 +01:00
return retObject ;
2019-11-16 12:28:47 +01:00
}
else {
2023-05-04 22:16:18 +02:00
throw new Error ( ` Unknown target ' ${ target } ' ` ) ;
2019-11-16 12:28:47 +01:00
}
}
2023-04-14 16:49:06 +02:00
/ * *
* @ param { BNote } note
* @ param { boolean } protect
* @ param { boolean } includingSubTree
* @ param { TaskContext } taskContext
* /
2020-06-20 12:31:38 +02:00
function protectNoteRecursively ( note , protect , includingSubTree , taskContext ) {
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 ) {
2020-06-20 12:31:38 +02:00
for ( const child of note . getChildNotes ( ) ) {
protectNoteRecursively ( child , protect , includingSubTree , taskContext ) ;
2020-02-26 16:37:17 +01:00
}
2017-11-15 00:04:26 -05:00
}
}
2023-04-14 16:49:06 +02:00
/ * *
* @ param { BNote } note
* @ param { boolean } protect
* /
2020-06-20 12:31:38 +02:00
function protectNote ( note , protect ) {
2023-04-14 16:49:06 +02:00
if ( ! protectedSessionService . isProtectedSessionAvailable ( ) ) {
throw new Error ( ` Cannot (un)protect note ' ${ note . noteId } ' with protect flag ' ${ protect } ' without active protected session ` ) ;
}
2020-12-09 22:49:55 +01:00
try {
if ( protect !== note . isProtected ) {
const content = note . getContent ( ) ;
2019-05-04 14:46:17 +02:00
2020-12-09 22:49:55 +01:00
note . isProtected = protect ;
2023-05-20 23:46:45 +02:00
note . setContent ( content , { forceSave : true } ) ;
2020-12-09 22:49:55 +01:00
}
2023-06-04 23:01:40 +02:00
revisionService . protectRevisions ( note ) ;
2023-05-20 23:46:45 +02:00
for ( const attachment of note . getAttachments ( ) ) {
if ( protect !== attachment . isProtected ) {
const content = attachment . getContent ( ) ;
attachment . isProtected = protect ;
attachment . setContent ( content , { forceSave : true } ) ;
}
}
2017-11-15 00:04:26 -05:00
}
2020-12-09 22:49:55 +01:00
catch ( e ) {
2023-04-14 16:49:06 +02:00
log . error ( ` Could not un/protect note ' ${ note . noteId } ' ` ) ;
2017-11-15 00:04:26 -05:00
2020-12-09 22:49:55 +01:00
throw e ;
}
2017-11-15 00:04:26 -05:00
}
2023-04-20 00:11:09 +02:00
function checkImageAttachments ( note , content ) {
const foundAttachmentIds = new Set ( ) ;
let match ;
2023-05-29 13:02:25 +02:00
const imgRegExp = /src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image/g ;
while ( match = imgRegExp . exec ( content ) ) {
2023-04-21 00:19:17 +02:00
foundAttachmentIds . add ( match [ 1 ] ) ;
2023-04-20 00:11:09 +02:00
}
2023-05-29 13:02:25 +02:00
const linkRegExp = /href="[^"]+attachmentId=([a-zA-Z0-9_]+)/g ;
while ( match = linkRegExp . exec ( content ) ) {
foundAttachmentIds . add ( match [ 1 ] ) ;
}
2023-04-25 00:01:58 +02:00
2023-05-29 13:02:25 +02:00
const attachments = note . getAttachments ( ) ;
2023-04-20 00:11:09 +02:00
2023-05-29 13:02:25 +02:00
for ( const attachment of attachments ) {
const attachmentInContent = foundAttachmentIds . has ( attachment . attachmentId ) ;
if ( attachment . utcDateScheduledForErasureSince && attachmentInContent ) {
2023-04-21 00:19:17 +02:00
attachment . utcDateScheduledForErasureSince = null ;
2023-04-20 00:11:09 +02:00
attachment . save ( ) ;
2023-05-29 13:02:25 +02:00
} else if ( ! attachment . utcDateScheduledForErasureSince && ! attachmentInContent ) {
2023-04-21 00:19:17 +02:00
attachment . utcDateScheduledForErasureSince = dateUtils . utcNowDateTime ( ) ;
2023-04-20 00:11:09 +02:00
attachment . save ( ) ;
}
}
2023-04-25 00:01:58 +02:00
2023-05-29 13:02:25 +02:00
const existingAttachmentIds = new Set ( attachments . map ( att => att . attachmentId ) ) ;
2023-04-25 00:01:58 +02:00
const unknownAttachmentIds = Array . from ( foundAttachmentIds ) . filter ( foundAttId => ! existingAttachmentIds . has ( foundAttId ) ) ;
2023-05-03 10:23:20 +02:00
const unknownAttachments = becca . getAttachments ( unknownAttachmentIds ) ;
2023-04-25 00:01:58 +02:00
2023-05-03 10:23:20 +02:00
for ( const unknownAttachment of unknownAttachments ) {
2023-05-29 22:37:19 +02:00
// the attachment belongs to a different note (was copy pasted). Attachments can be linked only from the note
// which owns it, so either find an existing attachment having the same content or make a copy.
let localAttachment = note . getAttachments ( ) . find ( att => att . role === unknownAttachment . role && att . blobId === unknownAttachment . blobId ) ;
if ( localAttachment ) {
if ( localAttachment . utcDateScheduledForErasureSince ) {
// the attachment is for sure linked now, so reset the scheduled deletion
localAttachment . utcDateScheduledForErasureSince = null ;
localAttachment . save ( ) ;
}
2023-04-25 00:01:58 +02:00
2023-05-29 22:37:19 +02:00
log . info ( ` Found equivalent attachment ' ${ localAttachment . attachmentId } ' of note ' ${ note . noteId } ' for the linked foreign attachment ' ${ unknownAttachment . attachmentId } ' of note ' ${ unknownAttachment . parentId } ' ` ) ;
} else {
localAttachment = unknownAttachment . copy ( ) ;
localAttachment . parentId = note . noteId ;
localAttachment . setContent ( unknownAttachment . getContent ( ) , { forceSave : true } ) ;
2023-05-29 13:02:25 +02:00
2023-05-29 22:37:19 +02:00
ws . sendMessageToAllClients ( { type : 'toast' , message : ` Attachment ' ${ localAttachment . title } ' has been copied to note ' ${ note . title } '. ` } ) ;
log . info ( ` Copied attachment ' ${ unknownAttachment . attachmentId } ' of note ' ${ unknownAttachment . parentId } ' to new ' ${ localAttachment . attachmentId } ' of note ' ${ note . noteId } ' ` ) ;
}
2023-05-02 22:46:39 +02:00
2023-05-29 22:37:19 +02:00
// replace image links
content = content . replace ( ` api/attachments/ ${ unknownAttachment . attachmentId } /image ` , ` api/attachments/ ${ localAttachment . attachmentId } /image ` ) ;
// replace reference links
content = content . replace ( new RegExp ( ` href="[^"]+attachmentId= ${ unknownAttachment . attachmentId } [^"]*" ` , "g" ) ,
` href="#root/ ${ localAttachment . parentId } ?viewMode=attachments&attachmentId= ${ localAttachment . attachmentId } " ` ) ;
2023-04-25 00:01:58 +02:00
}
2023-05-03 10:23:20 +02:00
return {
forceFrontendReload : unknownAttachments . length > 0 ,
content
} ;
2023-04-20 00:11:09 +02:00
}
2018-11-08 20:17:56 +01:00
function findImageLinks ( content , foundLinks ) {
2022-11-28 23:39:23 +01: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 ) {
2022-11-28 23:39:23 +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 ) {
2022-11-28 23:39:23 +01: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
}
}
2023-03-16 20:13:34 +01:00
const imageUrlToAttachmentIdMapping = { } ;
2020-03-25 11:28:44 +01:00
async function downloadImage ( noteId , imageUrl ) {
2020-03-25 21:01:42 +01:00
try {
2023-04-16 23:11:18 +02:00
let imageBuffer ;
if ( imageUrl . toLowerCase ( ) . startsWith ( "file://" ) ) {
imageBuffer = await new Promise ( ( res , rej ) => {
const localFilePath = imageUrl . substr ( "file://" . length ) ;
return fs . readFile ( localFilePath , ( err , data ) => {
if ( err ) {
rej ( err ) ;
} else {
res ( data ) ;
}
} ) ;
} ) ;
} else {
imageBuffer = await request . getImage ( imageUrl ) ;
}
2020-03-25 21:01:42 +01:00
const parsedUrl = url . parse ( imageUrl ) ;
const title = path . basename ( parsedUrl . pathname ) ;
const imageService = require ( '../services/image' ) ;
2023-03-16 20:13:34 +01:00
const { attachment } = imageService . saveImageToAttachment ( noteId , imageBuffer , title , true , true ) ;
2020-03-25 11:28:44 +01:00
2023-03-16 20:13:34 +01:00
imageUrlToAttachmentIdMapping [ imageUrl ] = attachment . attachmentId ;
2020-03-25 11:28:44 +01:00
2023-03-16 20:13:34 +01:00
log . info ( ` Download of ' ${ imageUrl } ' succeeded and was saved as image attachment ' ${ attachment . attachmentId } ' of note ' ${ noteId } ' ` ) ;
2020-03-25 21:01:42 +01:00
}
catch ( e ) {
2022-04-19 23:36:21 +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 = { } ;
2023-03-16 20:13:34 +01:00
function replaceUrl ( content , url , attachment ) {
2020-07-26 23:47:06 +02:00
const quotedUrl = utils . quoteRegex ( url ) ;
2020-03-25 18:21:55 +01:00
2023-04-17 22:40:53 +02:00
return content . replace ( new RegExp ( ` \\ s+src=[ \" '] ${ quotedUrl } [ \" '] ` , "ig" ) , ` src="api/attachments/ ${ encodeURIComponent ( attachment . title ) } /image" ` ) ;
2020-03-25 11:28:44 +01:00
}
2020-06-20 12:31:38 +02:00
function downloadImages ( noteId , content ) {
2022-05-21 14:00:53 +02:00
if ( ! optionService . getOptionBool ( "downloadImagesAutomatically" ) ) {
return content ;
}
2020-08-03 23:33:44 +02:00
const imageRe = /<img[^>]*?\ssrc=['"]([^'">]+)['"]/ig ;
let imageMatch ;
2020-03-25 11:28:44 +01:00
2020-08-03 23:33:44 +02:00
while ( imageMatch = imageRe . exec ( content ) ) {
const url = imageMatch [ 1 ] ;
2020-07-26 22:58:22 +02:00
const inlineImageMatch = /^data:image\/[a-z]+;base64,/ . exec ( url ) ;
if ( inlineImageMatch ) {
const imageBase64 = url . substr ( inlineImageMatch [ 0 ] . length ) ;
const imageBuffer = Buffer . from ( imageBase64 , 'base64' ) ;
const imageService = require ( '../services/image' ) ;
2021-11-04 21:48:46 +01:00
const { note } = imageService . saveImage ( noteId , imageBuffer , "inline image" , true , true ) ;
2020-07-26 22:58:22 +02:00
2022-07-05 22:40:41 +02:00
const sanitizedTitle = note . title . replace ( /[^a-z0-9-.]/gi , "" ) ;
2022-12-21 15:19:05 +01:00
content = ` ${ content . substr ( 0 , imageMatch . index ) } <img src="api/images/ ${ note . noteId } / ${ sanitizedTitle } " ${ content . substr ( imageMatch . index + imageMatch [ 0 ] . length ) } ` ;
2020-07-26 22:58:22 +02:00
}
2023-04-17 22:40:53 +02:00
else if ( ! url . includes ( 'api/images/' ) && ! /api\/attachments\/.+\/image\/?.*/ . test ( url )
2020-07-26 22:58:22 +02:00
// this is an 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
2023-03-16 20:13:34 +01:00
if ( url in imageUrlToAttachmentIdMapping ) {
const attachment = becca . getAttachment ( imageUrlToAttachmentIdMapping [ url ] ) ;
2020-03-25 11:28:44 +01:00
2023-03-16 20:13:34 +01:00
if ( ! attachment ) {
delete imageUrlToAttachmentIdMapping [ url ] ;
2020-03-25 11:28:44 +01:00
}
else {
2023-03-16 20:13:34 +01:00
content = replaceUrl ( content , url , attachment ) ;
2020-03-25 11:28:44 +01:00
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-06-20 12:31:38 +02:00
setTimeout ( ( ) => {
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.
2022-12-18 23:53:47 +01:00
// However, there's another flow where user pastes the image and leaves the note before the images
2020-03-25 21:01:42 +01:00
// 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-06-20 12:31:38 +02:00
sql . transactional ( ( ) => {
2023-03-16 20:13:34 +01:00
const imageNotes = becca . getNotes ( Object . values ( imageUrlToAttachmentIdMapping ) , true ) ;
2020-03-25 11:28:44 +01:00
2021-05-02 11:23:58 +02:00
const origNote = becca . getNote ( noteId ) ;
2021-07-21 20:18:03 +02:00
if ( ! origNote ) {
2022-04-19 23:36:21 +02:00
log . error ( ` Cannot find note ' ${ noteId } ' to replace image link. ` ) ;
2021-07-21 20:18:03 +02:00
return ;
}
2020-06-20 12:31:38 +02:00
const origContent = origNote . getContent ( ) ;
2020-05-06 23:11:34 +02:00
let updatedContent = origContent ;
2020-03-25 11:28:44 +01:00
2023-03-16 20:13:34 +01:00
for ( const url in imageUrlToAttachmentIdMapping ) {
const imageNote = imageNotes . find ( note => note . noteId === imageUrlToAttachmentIdMapping [ url ] ) ;
2020-03-25 11:28:44 +01:00
2023-06-05 09:23:42 +02:00
if ( imageNote ) {
2020-05-06 23:11:34 +02:00
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 ) {
2020-06-20 12:31:38 +02:00
origNote . setContent ( updatedContent ) ;
2020-03-25 21:01:42 +01:00
2023-01-26 20:32:27 +01:00
asyncPostProcessContent ( origNote , updatedContent ) ;
2020-05-03 14:33:59 +02:00
2022-04-19 23:36:21 +02:00
console . log ( ` Fixed the image links for note ' ${ noteId } ' to the offline saved. ` ) ;
2020-05-06 23:11:34 +02:00
}
} ) ;
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
}
2020-06-20 12:31:38 +02:00
function saveLinks ( note , content ) {
2023-05-07 11:20:51 +02:00
if ( ( note . type !== 'text' && note . type !== 'relationMap' )
|| ( note . isProtected && ! protectedSessionService . isProtectedSessionAvailable ( ) ) ) {
return {
forceFrontendReload : false ,
content
} ;
2019-09-01 22:09:55 +02:00
}
2018-11-08 20:17:56 +01:00
const foundLinks = [ ] ;
2023-05-03 10:23:20 +02:00
let forceFrontendReload = false ;
2020-01-11 09:50:05 +01:00
2018-11-16 23:27:57 +01:00
if ( note . type === 'text' ) {
2020-06-20 12:31:38 +02:00
content = downloadImages ( note . noteId , content ) ;
2020-05-03 14:33:59 +02:00
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 ) ;
2023-04-20 00:11:09 +02:00
2023-05-03 10:23:20 +02:00
( { forceFrontendReload , content } = checkImageAttachments ( note , content ) ) ;
2018-11-16 23:27:57 +01:00
}
2022-12-06 23:01:42 +01:00
else if ( note . type === 'relationMap' ) {
2018-11-19 15:00:49 +01:00
findRelationMapLinks ( content , foundLinks ) ;
2018-11-16 23:27:57 +01:00
}
else {
2023-05-07 11:20:51 +02:00
throw new Error ( ` Unrecognized type ' ${ note . type } ' ` ) ;
2018-11-16 23:27:57 +01:00
}
2018-11-08 20:17:56 +01:00
2021-04-25 21:19:18 +02:00
const existingLinks = note . getRelations ( ) . filter ( rel =>
[ 'internalLink' , 'imageLink' , 'relationMapLink' , 'includeNoteLink' ] . includes ( rel . name ) ) ;
2018-01-06 22:38:53 -05:00
2018-11-08 20:17:56 +01:00
for ( const foundLink of foundLinks ) {
2021-04-25 21:19:18 +02:00
const targetNote = becca . notes [ foundLink . value ] ;
if ( ! targetNote ) {
2020-04-05 16:06:13 +02:00
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 ) {
2023-01-03 13:52:37 +01:00
const newLink = new BAttribute ( {
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
}
2023-01-15 21:04:17 +01:00
// 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 ) {
2021-04-25 21:19:18 +02:00
unusedLink . markAsDeleted ( ) ;
2018-01-06 22:38:53 -05:00
}
2018-11-19 15:00:49 +01:00
2023-05-03 10:23:20 +02:00
return { forceFrontendReload , content } ;
2018-01-06 22:38:53 -05:00
}
2023-04-14 16:49:06 +02:00
/** @param {BNote} note */
2023-06-04 23:01:40 +02:00
function saveRevisionIfNeeded ( note ) {
2019-12-02 20:21:52 +01:00
// files and images are versioned separately
2020-06-20 12:31:38 +02:00
if ( note . type === 'file' || note . type === 'image' || note . hasLabel ( 'disableVersioning' ) ) {
2019-02-06 21:29:23 +01:00
return ;
}
2017-12-10 12:56:59 -05:00
const now = new Date ( ) ;
2023-06-04 23:01:40 +02:00
const revisionSnapshotTimeInterval = parseInt ( optionService . getOption ( 'revisionSnapshotTimeInterval' ) ) ;
2017-11-05 10:41:54 -05:00
2023-06-04 23:01:40 +02:00
const revisionCutoff = dateUtils . utcDateTimeStr ( new Date ( now . getTime ( ) - revisionSnapshotTimeInterval * 1000 ) ) ;
2017-11-05 10:41:54 -05:00
2023-06-04 23:01:40 +02:00
const existingRevisionId = sql . getValue (
"SELECT revisionId FROM 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
2023-06-04 23:01:40 +02:00
if ( ! existingRevisionId && msSinceDateCreated >= revisionSnapshotTimeInterval * 1000 ) {
note . saveRevision ( ) ;
2018-03-31 22:15:06 -04:00
}
}
2017-11-05 10:41:54 -05:00
2023-02-17 14:49:45 +01:00
function updateNoteData ( noteId , content ) {
2021-05-02 11:23:58 +02:00
const note = becca . getNote ( noteId ) ;
2017-12-10 12:56:59 -05:00
2021-05-17 22:35:36 +02:00
if ( ! note . isContentAvailable ( ) ) {
2022-04-19 23:36:21 +02:00
throw new Error ( ` Note ' ${ noteId } ' is not available for change! ` ) ;
2018-10-30 22:18:20 +01:00
}
2023-06-04 23:01:40 +02:00
saveRevisionIfNeeded ( note ) ;
2017-11-05 10:41:54 -05:00
2023-05-03 10:23:20 +02:00
const { forceFrontendReload , content : newContent } = saveLinks ( note , content ) ;
2019-08-06 22:39:27 +02:00
2023-05-03 10:23:20 +02:00
note . setContent ( newContent , { forceFrontendReload } ) ;
2017-11-05 10:41:54 -05:00
}
2020-01-03 13:14:43 +01:00
/ * *
2021-05-09 11:12:53 +02:00
* @ param { string } noteId
2020-01-03 13:14:43 +01:00
* @ param { TaskContext } taskContext
* /
2021-05-09 11:12:53 +02:00
function undeleteNote ( noteId , taskContext ) {
2023-06-05 09:23:42 +02:00
const noteRow = sql . getRow ( "SELECT * FROM notes WHERE noteId = ?" , [ noteId ] ) ;
2021-05-09 11:12:53 +02:00
2023-06-05 09:23:42 +02:00
if ( ! noteRow . isDeleted ) {
2022-04-19 23:36:21 +02:00
log . error ( ` Note ' ${ noteId } ' is not deleted and thus cannot be undeleted. ` ) ;
2021-05-09 11:12:53 +02:00
return ;
}
2023-06-05 09:23:42 +02:00
const undeletedParentBranchIds = getUndeletedParentBranchIds ( noteId , noteRow . deleteId ) ;
2020-01-03 13:14:43 +01:00
2021-05-08 22:01:14 +02:00
if ( undeletedParentBranchIds . length === 0 ) {
2020-01-03 13:14:43 +01:00
// cannot undelete if there's no undeleted parent
return ;
}
2021-05-08 22:01:14 +02:00
for ( const parentBranchId of undeletedParentBranchIds ) {
2023-06-05 09:23:42 +02:00
undeleteBranch ( parentBranchId , noteRow . deleteId , taskContext ) ;
2020-01-03 13:14:43 +01:00
}
}
/ * *
2021-05-08 22:01:14 +02:00
* @ param { string } branchId
2020-01-03 13:14:43 +01:00
* @ param { string } deleteId
* @ param { TaskContext } taskContext
* /
2021-05-08 22:01:14 +02:00
function undeleteBranch ( branchId , deleteId , taskContext ) {
2023-06-05 09:23:42 +02:00
const branchRow = sql . getRow ( "SELECT * FROM branches WHERE branchId = ?" , [ branchId ] )
2021-05-08 22:01:14 +02:00
2023-06-05 09:23:42 +02:00
if ( ! branchRow . isDeleted ) {
2020-01-03 13:14:43 +01:00
return ;
}
2023-06-05 09:23:42 +02:00
const noteRow = sql . getRow ( "SELECT * FROM notes WHERE noteId = ?" , [ branchRow . noteId ] ) ;
2020-01-03 13:14:43 +01:00
2023-06-05 09:23:42 +02:00
if ( noteRow . isDeleted && noteRow . deleteId !== deleteId ) {
2020-01-03 13:14:43 +01:00
return ;
}
2023-06-05 09:23:42 +02:00
new BBranch ( branchRow ) . save ( ) ;
2020-01-03 13:14:43 +01:00
taskContext . increaseProgressCount ( ) ;
2023-06-05 09:23:42 +02:00
if ( noteRow . isDeleted && noteRow . deleteId === deleteId ) {
2022-02-08 23:38:54 +01:00
// becca entity was already created as skeleton in "new Branch()" above
2023-06-05 09:23:42 +02:00
const noteEntity = becca . getNote ( noteRow . noteId ) ;
noteEntity . updateFromRow ( noteRow ) ;
2022-02-08 23:38:54 +01:00
noteEntity . save ( ) ;
2020-01-03 13:14:43 +01:00
2023-06-05 09:23:42 +02:00
const attributeRows = sql . getRows ( `
2021-05-08 22:01:14 +02:00
SELECT * FROM attributes
2020-01-03 13:14:43 +01:00
WHERE isDeleted = 1
AND deleteId = ?
AND ( noteId = ?
2023-06-05 09:23:42 +02:00
OR ( type = 'relation' AND value = ? ) ) ` , [deleteId, noteRow.noteId, noteRow.noteId]);
2020-01-03 13:14:43 +01:00
2023-06-05 09:23:42 +02:00
for ( const attributeRow of attributeRows ) {
2023-01-24 09:43:10 +01:00
// relation might point to a note which hasn't been undeleted yet and would thus throw up
2023-06-05 09:23:42 +02:00
new BAttribute ( attributeRow ) . save ( { skipValidation : true } ) ;
2020-01-03 13:14:43 +01:00
}
2021-05-08 22:01:14 +02:00
const childBranchIds = sql . getColumn ( `
2021-05-09 11:12:53 +02:00
SELECT branches . branchId
2020-01-03 13:14:43 +01:00
FROM branches
WHERE branches . isDeleted = 1
AND branches . deleteId = ?
2023-06-05 09:23:42 +02:00
AND branches . parentNoteId = ? ` , [deleteId, noteRow.noteId]);
2020-01-03 13:14:43 +01:00
2021-05-08 22:01:14 +02:00
for ( const childBranchId of childBranchIds ) {
undeleteBranch ( childBranchId , deleteId , taskContext ) ;
2020-01-03 13:14:43 +01:00
}
}
}
/ * *
2023-01-05 23:38:41 +01:00
* @ returns return deleted branchIds of an undeleted parent note
2020-01-03 13:14:43 +01:00
* /
2021-05-08 22:01:14 +02:00
function getUndeletedParentBranchIds ( noteId , deleteId ) {
return sql . getColumn ( `
2021-05-02 19:59:16 +02:00
SELECT branches . branchId
2020-01-03 13:14:43 +01:00
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]);
}
2023-01-26 20:32:27 +01:00
function scanForLinks ( note , content ) {
2022-12-06 23:01:42 +01:00
if ( ! note || ! [ 'text' , 'relationMap' ] . includes ( note . type ) ) {
2019-09-01 20:35:55 +02:00
return ;
}
2019-09-01 22:09:55 +02:00
try {
2023-04-20 00:11:09 +02:00
sql . transactional ( ( ) => {
2023-05-03 10:23:20 +02:00
const { forceFrontendReload , content : newContent } = saveLinks ( note , content ) ;
2019-09-01 20:35:55 +02:00
2023-04-20 00:11:09 +02:00
if ( content !== newContent ) {
2023-05-03 10:23:20 +02:00
note . setContent ( newContent , { forceFrontendReload } ) ;
2023-04-20 00:11:09 +02:00
}
} ) ;
2019-09-01 22:09:55 +02:00
}
catch ( e ) {
2023-05-03 10:23:20 +02: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
}
2023-01-26 20:32:27 +01:00
/ * *
2023-04-14 16:49:06 +02:00
* @ param { BNote } note
* @ param { string } content
2023-01-26 20:32:27 +01:00
* Things which have to be executed after updating content , but asynchronously ( separate transaction )
* /
async function asyncPostProcessContent ( note , content ) {
scanForLinks ( note , content ) ;
}
2020-12-14 13:47:33 +01:00
function eraseNotes ( noteIdsToErase ) {
if ( noteIdsToErase . length === 0 ) {
return ;
2020-12-06 22:11:49 +01:00
}
2020-01-03 22:32:49 +01:00
2020-12-14 13:47:33 +01:00
sql . executeMany ( ` DELETE FROM notes WHERE noteId IN (???) ` , noteIdsToErase ) ;
2021-11-12 21:19:23 +01:00
setEntityChangesAsErased ( sql . getManyRows ( ` SELECT * FROM entity_changes WHERE entityName = 'notes' AND entityId IN (???) ` , noteIdsToErase ) ) ;
2018-11-01 10:23:21 +01:00
2020-12-14 13:47:33 +01:00
// we also need to erase all "dependent" entities of the erased notes
const branchIdsToErase = sql . getManyRows ( ` SELECT branchId FROM branches WHERE noteId IN (???) ` , noteIdsToErase )
. map ( row => row . branchId ) ;
eraseBranches ( branchIdsToErase ) ;
const attributeIdsToErase = sql . getManyRows ( ` SELECT attributeId FROM attributes WHERE noteId IN (???) ` , noteIdsToErase )
. map ( row => row . attributeId ) ;
eraseAttributes ( attributeIdsToErase ) ;
2023-06-04 23:01:40 +02:00
const revisionIdsToErase = sql . getManyRows ( ` SELECT revisionId FROM revisions WHERE noteId IN (???) ` , noteIdsToErase )
. map ( row => row . revisionId ) ;
2020-12-14 13:47:33 +01:00
2023-06-04 23:01:40 +02:00
revisionService . eraseRevisions ( revisionIdsToErase ) ;
2020-12-14 13:47:33 +01:00
log . info ( ` Erased notes: ${ JSON . stringify ( noteIdsToErase ) } ` ) ;
}
2021-11-12 21:19:23 +01:00
function setEntityChangesAsErased ( entityChanges ) {
for ( const ec of entityChanges ) {
ec . isErased = true ;
entityChangesService . addEntityChange ( ec ) ;
}
}
2020-12-14 13:47:33 +01:00
function eraseBranches ( branchIdsToErase ) {
if ( branchIdsToErase . length === 0 ) {
2019-11-12 22:26:25 +01:00
return ;
}
2020-12-14 13:47:33 +01:00
sql . executeMany ( ` DELETE FROM branches WHERE branchId IN (???) ` , branchIdsToErase ) ;
2020-01-07 22:29:54 +01:00
2021-11-12 21:19:23 +01:00
setEntityChangesAsErased ( sql . getManyRows ( ` SELECT * FROM entity_changes WHERE entityName = 'branches' AND entityId IN (???) ` , branchIdsToErase ) ) ;
2022-02-01 22:25:38 +01:00
log . info ( ` Erased branches: ${ JSON . stringify ( branchIdsToErase ) } ` ) ;
2020-12-14 13:47:33 +01:00
}
function eraseAttributes ( attributeIdsToErase ) {
if ( attributeIdsToErase . length === 0 ) {
return ;
}
sql . executeMany ( ` DELETE FROM attributes WHERE attributeId IN (???) ` , attributeIdsToErase ) ;
2021-11-12 21:19:23 +01:00
setEntityChangesAsErased ( sql . getManyRows ( ` SELECT * FROM entity_changes WHERE entityName = 'attributes' AND entityId IN (???) ` , attributeIdsToErase ) ) ;
2022-02-01 22:25:38 +01:00
log . info ( ` Erased attributes: ${ JSON . stringify ( attributeIdsToErase ) } ` ) ;
2020-12-14 13:47:33 +01:00
}
2023-04-21 00:19:17 +02:00
function eraseAttachments ( attachmentIdsToErase ) {
if ( attachmentIdsToErase . length === 0 ) {
return ;
}
sql . executeMany ( ` DELETE FROM attachments WHERE attachmentId IN (???) ` , attachmentIdsToErase ) ;
setEntityChangesAsErased ( sql . getManyRows ( ` SELECT * FROM entity_changes WHERE entityName = 'attachments' AND entityId IN (???) ` , attachmentIdsToErase ) ) ;
log . info ( ` Erased attachments: ${ JSON . stringify ( attachmentIdsToErase ) } ` ) ;
}
function eraseUnusedBlobs ( ) {
2023-06-04 22:50:07 +02:00
// this method is rather defense in depth - in normal operation, the unused blobs should be erased immediately
// after getting unused (handled in entity._setContent())
2023-04-21 00:19:17 +02:00
const unusedBlobIds = sql . getColumn ( `
2023-05-02 23:04:41 +02:00
SELECT blobs . blobId
2023-04-21 00:19:17 +02:00
FROM blobs
LEFT JOIN notes ON notes . blobId = blobs . blobId
LEFT JOIN attachments ON attachments . blobId = blobs . blobId
2023-06-04 23:01:40 +02:00
LEFT JOIN revisions ON revisions . blobId = blobs . blobId
2023-05-04 21:01:25 +02:00
WHERE notes . noteId IS NULL
AND attachments . attachmentId IS NULL
2023-06-04 23:01:40 +02:00
AND revisions . revisionId IS NULL ` );
2023-04-21 00:19:17 +02:00
2023-05-04 22:16:18 +02:00
if ( unusedBlobIds . length === 0 ) {
return ;
}
2023-04-21 00:19:17 +02:00
sql . executeMany ( ` DELETE FROM blobs WHERE blobId IN (???) ` , unusedBlobIds ) ;
setEntityChangesAsErased ( sql . getManyRows ( ` SELECT * FROM entity_changes WHERE entityName = 'blobs' AND entityId IN (???) ` , unusedBlobIds ) ) ;
log . info ( ` Erased unused blobs: ${ JSON . stringify ( unusedBlobIds ) } ` ) ;
}
2020-12-14 13:47:33 +01:00
function eraseDeletedEntities ( eraseEntitiesAfterTimeInSeconds = null ) {
2021-12-21 11:04:41 +01:00
// this is important also so that the erased entity changes are sent to the connected clients
sql . transactional ( ( ) => {
if ( eraseEntitiesAfterTimeInSeconds === null ) {
eraseEntitiesAfterTimeInSeconds = optionService . getOptionInt ( 'eraseEntitiesAfterTimeInSeconds' ) ;
}
2020-12-14 13:47:33 +01:00
2021-12-21 11:04:41 +01:00
const cutoffDate = new Date ( Date . now ( ) - eraseEntitiesAfterTimeInSeconds * 1000 ) ;
2020-12-14 13:47:33 +01:00
2021-12-21 11:04:41 +01:00
const noteIdsToErase = sql . getColumn ( "SELECT noteId FROM notes WHERE isDeleted = 1 AND utcDateModified <= ?" , [ dateUtils . utcDateTimeStr ( cutoffDate ) ] ) ;
2020-12-14 13:47:33 +01:00
2021-12-21 11:04:41 +01:00
eraseNotes ( noteIdsToErase ) ;
2020-12-14 13:47:33 +01:00
2021-12-21 11:04:41 +01:00
const branchIdsToErase = sql . getColumn ( "SELECT branchId FROM branches WHERE isDeleted = 1 AND utcDateModified <= ?" , [ dateUtils . utcDateTimeStr ( cutoffDate ) ] ) ;
2020-12-14 13:47:33 +01:00
2021-12-21 11:04:41 +01:00
eraseBranches ( branchIdsToErase ) ;
2020-12-14 13:47:33 +01:00
2021-12-21 11:04:41 +01:00
const attributeIdsToErase = sql . getColumn ( "SELECT attributeId FROM attributes WHERE isDeleted = 1 AND utcDateModified <= ?" , [ dateUtils . utcDateTimeStr ( cutoffDate ) ] ) ;
2020-12-14 13:47:33 +01:00
2021-12-21 11:04:41 +01:00
eraseAttributes ( attributeIdsToErase ) ;
2023-04-21 00:19:17 +02:00
const attachmentIdsToErase = sql . getColumn ( "SELECT attachmentId FROM attachments WHERE isDeleted = 1 AND utcDateModified <= ?" , [ dateUtils . utcDateTimeStr ( cutoffDate ) ] ) ;
eraseAttachments ( attachmentIdsToErase ) ;
eraseUnusedBlobs ( ) ;
2021-12-21 11:04:41 +01:00
} ) ;
2018-11-01 10:23:21 +01:00
}
2021-09-16 14:38:09 +02:00
function eraseNotesWithDeleteId ( deleteId ) {
2023-04-24 21:22:34 +02:00
const noteIdsToErase = sql . getColumn ( "SELECT noteId FROM notes WHERE isDeleted = 1 AND deleteId = ?" , [ deleteId ] ) ;
2021-09-16 14:38:09 +02:00
eraseNotes ( noteIdsToErase ) ;
2023-04-24 21:22:34 +02:00
const branchIdsToErase = sql . getColumn ( "SELECT branchId FROM branches WHERE isDeleted = 1 AND deleteId = ?" , [ deleteId ] ) ;
2021-09-16 14:38:09 +02:00
eraseBranches ( branchIdsToErase ) ;
2023-04-24 21:22:34 +02:00
const attributeIdsToErase = sql . getColumn ( "SELECT attributeId FROM attributes WHERE isDeleted = 1 AND deleteId = ?" , [ deleteId ] ) ;
2021-09-16 14:38:09 +02:00
eraseAttributes ( attributeIdsToErase ) ;
2023-04-24 21:22:34 +02:00
const attachmentIdsToErase = sql . getColumn ( "SELECT attachmentId FROM attachments WHERE isDeleted = 1 AND deleteId = ?" , [ deleteId ] ) ;
eraseAttachments ( attachmentIdsToErase ) ;
eraseUnusedBlobs ( ) ;
2021-09-16 14:38:09 +02:00
}
2020-12-06 22:11:49 +01:00
function eraseDeletedNotesNow ( ) {
2020-12-14 13:47:33 +01:00
eraseDeletedEntities ( 0 ) ;
2020-12-06 22:11:49 +01:00
}
2023-04-24 21:22:34 +02:00
function eraseUnusedAttachmentsNow ( ) {
eraseScheduledAttachments ( 0 ) ;
}
2020-11-19 14:06:32 +01:00
// do a replace in str - all keys should be replaced by the corresponding values
function replaceByMap ( str , mapObj ) {
const re = new RegExp ( Object . keys ( mapObj ) . join ( "|" ) , "g" ) ;
return str . replace ( re , matched => mapObj [ matched ] ) ;
}
2020-11-19 14:29:26 +01:00
function duplicateSubtree ( origNoteId , newParentNoteId ) {
if ( origNoteId === 'root' ) {
2020-11-19 14:06:32 +01:00
throw new Error ( 'Duplicating root is not possible' ) ;
}
2023-04-09 22:45:31 +02:00
log . info ( ` Duplicating ' ${ origNoteId } ' subtree into ' ${ newParentNoteId } ' ` ) ;
2020-12-18 22:35:40 +01:00
2021-04-25 22:02:32 +02:00
const origNote = becca . notes [ origNoteId ] ;
2020-11-19 14:06:32 +01:00
// might be null if orig note is not in the target newParentNoteId
2021-12-20 17:30:47 +01:00
const origBranch = origNote . getParentBranches ( ) . find ( branch => branch . parentNoteId === newParentNoteId ) ;
2020-11-19 14:06:32 +01:00
2020-11-19 14:29:26 +01:00
const noteIdMapping = getNoteIdMapping ( origNote ) ;
2020-11-19 14:06:32 +01:00
const res = duplicateSubtreeInner ( origNote , origBranch , newParentNoteId , noteIdMapping ) ;
2020-11-23 19:44:49 +01:00
if ( ! res . note . title . endsWith ( '(dup)' ) ) {
res . note . title += " (dup)" ;
}
2020-11-19 14:06:32 +01:00
res . note . save ( ) ;
2019-10-19 12:36:16 +02:00
2020-11-19 14:06:32 +01:00
return res ;
}
2020-11-19 14:29:26 +01:00
function duplicateSubtreeWithoutRoot ( origNoteId , newNoteId ) {
if ( origNoteId === 'root' ) {
throw new Error ( 'Duplicating root is not possible' ) ;
}
2021-05-02 11:23:58 +02:00
const origNote = becca . getNote ( origNoteId ) ;
2020-11-19 14:29:26 +01:00
const noteIdMapping = getNoteIdMapping ( origNote ) ;
for ( const childBranch of origNote . getChildBranches ( ) ) {
duplicateSubtreeInner ( childBranch . getNote ( ) , childBranch , newNoteId , noteIdMapping ) ;
}
}
2020-11-19 14:06:32 +01:00
function duplicateSubtreeInner ( origNote , origBranch , newParentNoteId , noteIdMapping ) {
2019-10-19 12:36:16 +02:00
if ( origNote . isProtected && ! protectedSessionService . isProtectedSessionAvailable ( ) ) {
2023-04-24 21:22:34 +02:00
throw new Error ( ` Cannot duplicate note ' ${ origNote . noteId } ' because it is protected and protected session is not available. Enter protected session and try again. ` ) ;
2019-10-19 12:36:16 +02:00
}
2021-02-14 12:14:33 +01:00
const newNoteId = noteIdMapping [ origNote . noteId ] ;
2021-10-02 23:26:18 +02:00
function createDuplicatedBranch ( ) {
2023-01-03 13:52:37 +01:00
return new BBranch ( {
2021-10-02 23:26:18 +02:00
noteId : newNoteId ,
parentNoteId : newParentNoteId ,
// here increasing just by 1 to make sure it's directly after original
notePosition : origBranch ? origBranch . notePosition + 1 : null
} ) . save ( ) ;
2021-02-14 12:14:33 +01:00
}
2021-10-02 23:26:18 +02:00
function createDuplicatedNote ( ) {
2023-01-03 13:52:37 +01:00
const newNote = new BNote ( {
2021-10-02 23:26:18 +02:00
... origNote ,
noteId : newNoteId ,
dateCreated : dateUtils . localNowDateTime ( ) ,
utcDateCreated : dateUtils . utcNowDateTime ( )
} ) . save ( ) ;
2020-08-18 22:20:47 +02:00
2021-10-02 23:26:18 +02:00
let content = origNote . getContent ( ) ;
2020-11-19 14:06:32 +01:00
2022-12-06 23:01:42 +01:00
if ( [ 'text' , 'relationMap' , 'search' ] . includes ( origNote . type ) ) {
2021-10-02 23:26:18 +02:00
// fix links in the content
content = replaceByMap ( content , noteIdMapping ) ;
}
2020-11-19 14:06:32 +01:00
2021-10-02 23:26:18 +02:00
newNote . setContent ( content ) ;
2019-10-19 12:36:16 +02:00
2021-10-02 23:26:18 +02:00
for ( const attribute of origNote . getOwnedAttributes ( ) ) {
2023-01-03 13:52:37 +01:00
const attr = new BAttribute ( {
2021-10-02 23:26:18 +02:00
... attribute ,
attributeId : undefined ,
noteId : newNote . noteId
} ) ;
// if relation points to within the duplicated tree then replace the target to the duplicated note
// if it points outside of duplicated tree then keep the original target
if ( attr . type === 'relation' && attr . value in noteIdMapping ) {
attr . value = noteIdMapping [ attr . value ] ;
}
2019-10-19 12:36:16 +02:00
2023-04-09 22:45:31 +02:00
// the relation targets may not be created yet, the mapping is pre-generated
attr . save ( { skipValidation : true } ) ;
2020-11-19 14:06:32 +01:00
}
2021-10-02 23:26:18 +02:00
for ( const childBranch of origNote . getChildBranches ( ) ) {
duplicateSubtreeInner ( childBranch . getNote ( ) , childBranch , newNote . noteId , noteIdMapping ) ;
}
2023-01-22 23:36:05 +01:00
2021-10-02 23:26:18 +02:00
return newNote ;
2019-10-19 12:36:16 +02:00
}
2021-10-02 23:26:18 +02:00
const existingNote = becca . notes [ newNoteId ] ;
2020-11-19 14:06:32 +01:00
2021-10-02 23:26:18 +02:00
if ( existingNote && existingNote . title !== undefined ) { // checking that it's not just note's skeleton created because of Branch above
// note has multiple clones and was already created from another placement in the tree
// so a branch is all we need for this clone
return {
note : existingNote ,
branch : createDuplicatedBranch ( )
}
}
else {
return {
// order here is important, note needs to be created first to not mess up the becca
note : createDuplicatedNote ( ) ,
branch : createDuplicatedBranch ( )
}
}
2019-10-19 12:36:16 +02:00
}
2020-11-19 14:29:26 +01:00
function getNoteIdMapping ( origNote ) {
const noteIdMapping = { } ;
// pregenerate new noteIds since we'll need to fix relation references even for not yet created notes
for ( const origNoteId of origNote . getDescendantNoteIds ( ) ) {
noteIdMapping [ origNoteId ] = utils . newEntityId ( ) ;
}
2021-02-14 12:14:33 +01:00
2020-11-19 14:29:26 +01:00
return noteIdMapping ;
}
2023-05-29 13:02:25 +02:00
function eraseScheduledAttachments ( eraseUnusedAttachmentsAfterSeconds = null ) {
if ( eraseUnusedAttachmentsAfterSeconds === null ) {
eraseUnusedAttachmentsAfterSeconds = optionService . getOptionInt ( 'eraseUnusedAttachmentsAfterSeconds' ) ;
2023-04-24 21:22:34 +02:00
}
2023-05-29 13:02:25 +02:00
const cutOffDate = dateUtils . utcDateTimeStr ( new Date ( Date . now ( ) - ( eraseUnusedAttachmentsAfterSeconds * 1000 ) ) ) ;
2023-04-21 00:19:17 +02:00
const attachmentIdsToErase = sql . getColumn ( 'SELECT attachmentId FROM attachments WHERE utcDateScheduledForErasureSince < ?' , [ cutOffDate ] ) ;
eraseAttachments ( attachmentIdsToErase ) ;
}
2020-06-20 21:42:41 +02:00
sqlInit . dbReady . then ( ( ) => {
// first cleanup kickoff 5 minutes after startup
2020-12-14 13:47:33 +01:00
setTimeout ( cls . wrap ( ( ) => eraseDeletedEntities ( ) ) , 5 * 60 * 1000 ) ;
2023-04-21 00:19:17 +02:00
setTimeout ( cls . wrap ( ( ) => eraseScheduledAttachments ( ) ) , 6 * 60 * 1000 ) ;
2018-11-01 10:23:21 +01:00
2020-12-14 13:47:33 +01:00
setInterval ( cls . wrap ( ( ) => eraseDeletedEntities ( ) ) , 4 * 3600 * 1000 ) ;
2023-04-21 00:19:17 +02:00
setInterval ( cls . wrap ( ( ) => eraseScheduledAttachments ( ) ) , 3600 * 1000 ) ;
2020-06-20 21:42:41 +02: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 ,
2023-01-24 09:19:49 +01:00
updateNoteData ,
2020-01-03 13:14:43 +01:00
undeleteNote ,
2019-09-01 20:35:55 +02:00
protectNoteRecursively ,
2020-11-19 14:06:32 +01:00
duplicateSubtree ,
2020-11-19 14:29:26 +01:00
duplicateSubtreeWithoutRoot ,
2021-05-08 23:31:20 +02:00
getUndeletedParentBranchIds ,
2020-12-06 22:11:49 +01:00
triggerNoteTitleChanged ,
2021-09-16 14:38:09 +02:00
eraseDeletedNotesNow ,
2023-04-24 21:22:34 +02:00
eraseUnusedAttachmentsNow ,
2021-12-08 21:04:22 +01:00
eraseNotesWithDeleteId ,
2023-06-04 23:01:40 +02:00
saveRevisionIfNeeded ,
2023-01-26 20:32:27 +01:00
downloadImages ,
asyncPostProcessContent
2019-12-03 15:49:27 -03:00
} ;