Notes/src/becca/entities/battachment.ts

244 lines
8.1 KiB
TypeScript
Raw Normal View History

"use strict";
import utils = require('../../services/utils');
import dateUtils = require('../../services/date_utils');
import AbstractBeccaEntity = require('./abstract_becca_entity');
import sql = require('../../services/sql');
import protectedSessionService = require('../../services/protected_session');
import log = require('../../services/log');
import { AttachmentRow } from './rows';
2024-02-17 10:56:27 +02:00
import BNote = require('./bnote');
import BBranch = require('./bbranch');
2023-04-14 16:49:06 +02:00
const attachmentRoleToNoteTypeMapping = {
'image': 'image'
};
interface ContentOpts {
2024-02-17 10:56:27 +02:00
// FIXME: Found in bnote.ts, to check if it's actually used and not a typo.
forceSave?: boolean;
/** will also save this BAttachment entity */
2024-02-17 10:56:27 +02:00
forceFullSave?: boolean;
/** override frontend heuristics on when to reload, instruct to reload */
2024-02-17 10:56:27 +02:00
forceFrontendReload?: boolean;
}
/**
2023-03-16 12:17:55 +01:00
* Attachment represent data related/attached to the note. Conceptually similar to attributes, but intended for
* larger amounts of data and generally not accessible to the user.
*/
2024-02-17 10:56:27 +02:00
class BAttachment extends AbstractBeccaEntity<BAttachment> {
2023-03-16 12:17:55 +01:00
static get entityName() { return "attachments"; }
static get primaryKeyName() { return "attachmentId"; }
2023-09-28 00:24:53 +02:00
static get hashedProperties() { return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"]; }
noteId?: number;
attachmentId?: string;
/** either noteId or revisionId to which this attachment belongs */
ownerId: string;
role: string;
mime: string;
title: string;
type?: keyof typeof attachmentRoleToNoteTypeMapping;
position?: number;
2024-02-17 10:56:27 +02:00
blobId?: string;
isProtected?: boolean;
dateModified?: string;
utcDateScheduledForErasureSince?: string;
/** optionally added to the entity */
contentLength?: number;
isDecrypted?: boolean;
constructor(row: AttachmentRow) {
super();
if (!row.ownerId?.trim()) {
throw new Error("'ownerId' must be given to initialize a Attachment entity");
2023-03-16 12:11:00 +01:00
} else if (!row.role?.trim()) {
2023-03-16 12:17:55 +01:00
throw new Error("'role' must be given to initialize a Attachment entity");
2023-03-16 12:11:00 +01:00
} else if (!row.mime?.trim()) {
2023-03-16 12:17:55 +01:00
throw new Error("'mime' must be given to initialize a Attachment entity");
2023-03-16 12:11:00 +01:00
} else if (!row.title?.trim()) {
2023-03-16 12:17:55 +01:00
throw new Error("'title' must be given to initialize a Attachment entity");
}
2023-03-16 16:37:31 +01:00
this.attachmentId = row.attachmentId;
this.ownerId = row.ownerId;
2023-03-16 12:11:00 +01:00
this.role = row.role;
this.mime = row.mime;
2023-03-16 12:11:00 +01:00
this.title = row.title;
2023-04-11 22:55:50 +02:00
this.position = row.position;
2023-03-16 18:34:39 +01:00
this.blobId = row.blobId;
this.isProtected = !!row.isProtected;
2023-04-03 23:47:24 +02:00
this.dateModified = row.dateModified;
this.utcDateModified = row.utcDateModified;
2023-04-21 00:19:17 +02:00
this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince;
2023-05-21 18:14:17 +02:00
this.contentLength = row.contentLength;
2023-05-20 23:46:45 +02:00
this.decrypt();
}
copy(): BAttachment {
return new BAttachment({
ownerId: this.ownerId,
role: this.role,
mime: this.mime,
title: this.title,
blobId: this.blobId,
2023-04-25 00:01:58 +02:00
isProtected: this.isProtected
});
}
getNote(): BNote {
return this.becca.notes[this.ownerId];
}
/** @returns true if the note has string content (not binary) */
hasStringContent(): boolean {
2024-02-17 11:54:41 +02:00
return this.type !== undefined && utils.isStringNote(this.type, this.mime);
}
isContentAvailable() {
return !this.attachmentId // new attachment which was not encrypted yet
|| !this.isProtected
|| protectedSessionService.isProtectedSessionAvailable()
}
2023-05-20 23:46:45 +02:00
getTitleOrProtected() {
return this.isContentAvailable() ? this.title : '[protected]';
}
decrypt() {
2023-09-08 00:19:30 +02:00
if (!this.isProtected || !this.attachmentId) {
this.isDecrypted = true;
return;
}
if (!this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
2023-05-20 23:46:45 +02:00
try {
this.title = protectedSessionService.decryptString(this.title) || "";
2023-05-20 23:46:45 +02:00
this.isDecrypted = true;
}
catch (e: any) {
2023-05-20 23:46:45 +02:00
log.error(`Could not decrypt attachment ${this.attachmentId}: ${e.message} ${e.stack}`);
}
}
}
getContent(): string | Buffer {
return this._getContent();
}
setContent(content: any, opts: ContentOpts) {
2023-03-16 16:37:31 +01:00
this._setContent(content, opts);
}
convertToNote(): { note: BNote, branch: BBranch } {
2024-02-17 11:54:41 +02:00
// FIXME: can this ever be "search"?
if (this.type as string === 'search') {
2023-04-14 16:49:06 +02:00
throw new Error(`Note of type search cannot have child notes`);
}
if (!this.getNote()) {
throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " +
"Converting note revision's attachments to note is not (yet) supported.");
}
if (!(this.role in attachmentRoleToNoteTypeMapping)) {
throw new Error(`Mapping from attachment role '${this.role}' to note's type is not defined`);
}
2023-05-05 23:41:11 +02:00
if (!this.isContentAvailable()) { // isProtected is the same for attachment
2023-04-14 16:49:06 +02:00
throw new Error(`Cannot convert protected attachment outside of protected session`);
}
const noteService = require('../../services/notes');
2023-04-14 16:49:06 +02:00
const { note, branch } = noteService.createNewNote({
parentNoteId: this.ownerId,
2023-04-14 16:49:06 +02:00
title: this.title,
2024-02-17 11:54:41 +02:00
type: (attachmentRoleToNoteTypeMapping as any)[this.role],
2023-04-14 16:49:06 +02:00
mime: this.mime,
content: this.getContent(),
isProtected: this.isProtected
});
this.markAsDeleted();
const parentNote = this.getNote();
if (this.role === 'image' && parentNote.type === 'text') {
const origContent = parentNote.getContent();
const oldAttachmentUrl = `api/attachments/${this.attachmentId}/image/`;
2023-04-14 16:49:06 +02:00
const newNoteUrl = `api/images/${note.noteId}/`;
const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl);
if (fixedContent !== origContent) {
parentNote.setContent(fixedContent);
2023-04-14 16:49:06 +02:00
}
noteService.asyncPostProcessContent(note, fixedContent);
2023-04-14 16:49:06 +02:00
}
return { note, branch };
}
2023-05-03 10:23:20 +02:00
getFileName() {
const type = this.role === 'image' ? 'image' : 'file';
return utils.formatDownloadTitle(this.title, type, this.mime);
}
beforeSaving() {
super.beforeSaving();
2023-04-11 22:55:50 +02:00
if (this.position === undefined || this.position === null) {
this.position = 10 + sql.getValue<number>(`SELECT COALESCE(MAX(position), 0)
FROM attachments
WHERE ownerId = ?`, [this.noteId]);
2023-04-11 22:55:50 +02:00
}
2023-04-03 23:47:24 +02:00
this.dateModified = dateUtils.localNowDateTime();
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
return {
2023-03-16 12:17:55 +01:00
attachmentId: this.attachmentId,
ownerId: this.ownerId,
2023-03-16 17:43:37 +01:00
role: this.role,
mime: this.mime,
title: this.title || undefined,
2023-04-11 22:55:50 +02:00
position: this.position,
2023-03-16 18:34:39 +01:00
blobId: this.blobId,
isProtected: !!this.isProtected,
isDeleted: false,
2023-04-03 23:47:24 +02:00
dateModified: this.dateModified,
utcDateModified: this.utcDateModified,
2023-05-21 18:14:17 +02:00
utcDateScheduledForErasureSince: this.utcDateScheduledForErasureSince,
contentLength: this.contentLength
};
}
getPojoToSave() {
2023-05-20 23:46:45 +02:00
const pojo = this.getPojo();
2023-05-21 18:14:17 +02:00
delete pojo.contentLength;
2023-05-20 23:46:45 +02:00
if (pojo.isProtected) {
if (this.isDecrypted) {
pojo.title = protectedSessionService.encrypt(pojo.title || "") || undefined;
2023-05-20 23:46:45 +02:00
}
else {
// updating protected note outside of protected session means we will keep original ciphertexts
delete pojo.title;
}
}
return pojo;
}
}
export = BAttachment;