"use strict"; import sql from "../../sql.js"; import utils from "../../../services/utils.js"; import AbstractShacaEntity from "./abstract_shaca_entity.js"; import escape from "escape-html"; import { Blob } from '../../../services/blob-interface.js'; import SAttachment from "./sattachment.js"; import SAttribute from "./sattribute.js"; import SBranch from "./sbranch.js"; import { SNoteRow } from './rows.js'; const LABEL = 'label'; const RELATION = 'relation'; const CREDENTIALS = 'shareCredentials'; const isCredentials = (attr: SAttribute) => attr.type === 'label' && attr.name === CREDENTIALS; class SNote extends AbstractShacaEntity { noteId: string; title: string; type: string; mime: string; private blobId: string; utcDateModified: string; isProtected: boolean; parentBranches: SBranch[]; parents: SNote[]; children: SNote[]; ownedAttributes: SAttribute[]; private __attributeCache: SAttribute[] | null; private __inheritableAttributeCache: SAttribute[] | null; targetRelations: SAttribute[]; attachments: SAttachment[]; constructor([noteId, title, type, mime, blobId, utcDateModified, isProtected]: SNoteRow) { super(); this.noteId = noteId; this.title = isProtected ? "[protected]" : title; this.type = type; this.mime = mime; this.blobId = blobId; this.utcDateModified = utcDateModified; // used for caching of images this.isProtected = isProtected; this.parentBranches = []; this.parents = []; this.children = []; this.ownedAttributes = []; this.__attributeCache = null; this.__inheritableAttributeCache = null; this.targetRelations = []; this.attachments = []; this.shaca.notes[this.noteId] = this; } getParentBranches() { return this.parentBranches; } getBranches() { return this.parentBranches; } getChildBranches(): SBranch[] { return this.children.map(childNote => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId)); } getVisibleChildBranches() { return this.getChildBranches() .filter(branch => !branch.isHidden && !branch.getNote().isLabelTruthy('shareHiddenFromTree')); } getParentNotes() { return this.parents; } getChildNotes() { return this.children; } getVisibleChildNotes() { return this.getVisibleChildBranches() .map(branch => branch.getNote()); } hasChildren() { return this.children && this.children.length > 0; } hasVisibleChildren() { return this.getVisibleChildNotes().length > 0; } getContent(silentNotFoundError = false) { const row = sql.getRow>(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]); if (!row) { if (silentNotFoundError) { return undefined; } else { throw new Error(`Cannot find note content for note '${this.noteId}', blob '${this.blobId}'`); } } let content = row.content; if (this.hasStringContent()) { return content === null ? "" : content.toString("utf-8"); } else { return content; } } /** @returns true if the note has string content (not binary) */ hasStringContent() { return utils.isStringNote(this.type, this.mime); } /** * @param type - (optional) attribute type to filter * @param name - (optional) attribute name to filter * @returns all note's attributes, including inherited ones */ getAttributes(type?: string, name?: string) { let attributeCache = this.__attributeCache; if (!attributeCache) { attributeCache = this.__getAttributes([]); } if (type && name) { return attributeCache.filter(attr => attr.type === type && attr.name === name && !isCredentials(attr)); } else if (type) { return attributeCache.filter(attr => attr.type === type && !isCredentials(attr)); } else if (name) { return attributeCache.filter(attr => attr.name === name && !isCredentials(attr)); } else { return attributeCache.filter(attr => !isCredentials(attr)); } } getCredentials() { return this.__getAttributes([]).filter(isCredentials); } __getAttributes(path: string[]) { if (path.includes(this.noteId)) { return []; } if (!this.__attributeCache) { const parentAttributes = this.ownedAttributes.slice(); const newPath = [...path, this.noteId]; if (this.noteId !== 'root') { for (const parentNote of this.parents) { parentAttributes.push(...parentNote.__getInheritableAttributes(newPath)); } } const templateAttributes: SAttribute[] = []; for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates if (ownedAttr.type === 'relation' && ['template', 'inherit'].includes(ownedAttr.name)) { const templateNote = this.shaca.notes[ownedAttr.value]; if (templateNote) { templateAttributes.push(...templateNote.__getAttributes(newPath)); } } } this.__attributeCache = []; const addedAttributeIds = new Set(); for (const attr of parentAttributes.concat(templateAttributes)) { if (!addedAttributeIds.has(attr.attributeId)) { addedAttributeIds.add(attr.attributeId); this.__attributeCache.push(attr); } } this.__inheritableAttributeCache = []; for (const attr of this.__attributeCache) { if (attr.isInheritable) { this.__inheritableAttributeCache.push(attr); } } } return this.__attributeCache; } __getInheritableAttributes(path: string[]) { if (path.includes(this.noteId)) { return []; } if (!this.__inheritableAttributeCache) { this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache } return this.__inheritableAttributeCache || []; } /** * @throws Error in case of invalid JSON */ getJsonContent(): any | null { const content = this.getContent(); if (typeof content !== "string" || !content || !content.trim()) { return null; } return JSON.parse(content); } /** @returns valid object or null if the content cannot be parsed as JSON */ getJsonContentSafely() { try { return this.getJsonContent(); } catch (e) { return null; } } hasAttribute(type: string, name: string) { return !!this.getAttributes().find(attr => attr.type === type && attr.name === name); } getRelationTarget(name: string) { const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name); return relation ? relation.targetNote : null; } /** * @param name - label name * @returns true if label exists (including inherited) */ hasLabel(name: string) { return this.hasAttribute(LABEL, name); } /** * @param name - label name * @returns true if label exists (including inherited) and does not have "false" value. */ isLabelTruthy(name: string) { const label = this.getLabel(name); if (!label) { return false; } return !!label && label.value !== 'false'; } /** * @param name - label name * @returns true if label exists (excluding inherited) */ hasOwnedLabel(name: string) { return this.hasOwnedAttribute(LABEL, name); } /** * @param name - relation name * @returns true if relation exists (including inherited) */ hasRelation(name: string) { return this.hasAttribute(RELATION, name); } /** * @param name - relation name * @returns true if relation exists (excluding inherited) */ hasOwnedRelation(name: string) { return this.hasOwnedAttribute(RELATION, name); } /** * @param name - label name * @returns label if it exists, null otherwise */ getLabel(name: string) { return this.getAttribute(LABEL, name); } /** * @param name - label name * @returns label if it exists, null otherwise */ getOwnedLabel(name: string) { return this.getOwnedAttribute(LABEL, name); } /** * @param name - relation name * @returns relation if it exists, null otherwise */ getRelation(name: string) { return this.getAttribute(RELATION, name); } /** * @param name - relation name * @returns relation if it exists, null otherwise */ getOwnedRelation(name: string) { return this.getOwnedAttribute(RELATION, name); } /** * @param name - label name * @returns label value if label exists, null otherwise */ getLabelValue(name: string) { return this.getAttributeValue(LABEL, name); } /** * @param name - label name * @returns label value if label exists, null otherwise */ getOwnedLabelValue(name: string) { return this.getOwnedAttributeValue(LABEL, name); } /** * @param name - relation name * @returns relation value if relation exists, null otherwise */ getRelationValue(name: string) { return this.getAttributeValue(RELATION, name); } /** * @param name - relation name * @returns relation value if relation exists, null otherwise */ getOwnedRelationValue(name: string) { return this.getOwnedAttributeValue(RELATION, name); } /** * @param type - attribute type (label, relation, etc.) * @param name - attribute name * @returns true if note has an attribute with given type and name (excluding inherited) */ hasOwnedAttribute(type: string, name: string) { return !!this.getOwnedAttribute(type, name); } /** * @param type - attribute type (label, relation, etc.) * @param name - attribute name * @returns attribute of the given type and name. If there are more such attributes, first is returned. * Returns null if there's no such attribute belonging to this note. */ getAttribute(type: string, name: string) { const attributes = this.getAttributes(); return attributes.find(attr => attr.type === type && attr.name === name); } /** * @param type - attribute type (label, relation, etc.) * @param name - attribute name * @returns attribute value of the given type and name or null if no such attribute exists. */ getAttributeValue(type: string, name: string) { const attr = this.getAttribute(type, name); return attr ? attr.value : null; } /** * @param type - attribute type (label, relation, etc.) * @param name - attribute name * @returns attribute value of the given type and name or null if no such attribute exists. */ getOwnedAttributeValue(type: string, name: string) { const attr = this.getOwnedAttribute(type, name); return attr ? attr.value as string : null; // FIXME } /** * @param name - label name to filter * @returns all note's labels (attributes with type label), including inherited ones */ getLabels(name: string) { return this.getAttributes(LABEL, name); } /** * @param name - label name to filter * @returns all note's label values, including inherited ones */ getLabelValues(name: string) { return this.getLabels(name).map(l => l.value) as string[]; // FIXME } /** * @param name - label name to filter * @returns all note's labels (attributes with type label), excluding inherited ones */ getOwnedLabels(name: string) { return this.getOwnedAttributes(LABEL, name); } /** * @param name - label name to filter * @returns all note's label values, excluding inherited ones */ getOwnedLabelValues(name: string) { return this.getOwnedAttributes(LABEL, name).map(l => l.value); } /** * @param name - relation name to filter * @returns all note's relations (attributes with type relation), including inherited ones */ getRelations(name: string) { return this.getAttributes(RELATION, name); } /** * @param name - relation name to filter * @returns all note's relations (attributes with type relation), excluding inherited ones */ getOwnedRelations(name: string) { return this.getOwnedAttributes(RELATION, name); } /** * @param type - (optional) attribute type to filter * @param name - (optional) attribute name to filter * @returns note's "owned" attributes - excluding inherited ones */ getOwnedAttributes(type: string, name: string) { // it's a common mistake to include # or ~ into attribute name if (name && ["#", "~"].includes(name[0])) { name = name.substr(1); } if (type && name) { return this.ownedAttributes.filter(attr => attr.type === type && attr.name === name); } else if (type) { return this.ownedAttributes.filter(attr => attr.type === type); } else if (name) { return this.ownedAttributes.filter(attr => attr.name === name); } else { return this.ownedAttributes.slice(); } } /** * @returns attribute belonging to this specific note (excludes inherited attributes) * * This method can be significantly faster than the getAttribute() */ getOwnedAttribute(type: string, name: string) { const attrs = this.getOwnedAttributes(type, name); return attrs.length > 0 ? attrs[0] : null; } get isArchived() { return this.hasAttribute('label', 'archived'); } isInherited() { return !!this.targetRelations.find(rel => rel.name === 'template' || rel.name === 'inherit'); } getTargetRelations() { return this.targetRelations; } getAttachments() { return this.attachments; } getAttachmentByTitle(title: string) { return this.attachments.find(attachment => attachment.title === title); } get shareId() { if (this.hasOwnedLabel('shareRoot')) { return ""; } const sharedAlias = this.getOwnedLabelValue("shareAlias"); return sharedAlias || this.noteId; } get escapedTitle() { return escape(this.title); } get encodedTitle() { return encodeURIComponent(this.title); } getPojo() { return { noteId: this.noteId, title: this.title, type: this.type, mime: this.mime, utcDateModified: this.utcDateModified, attributes: this.getAttributes() // relations could link across shared subtrees which might leak them // individual relations might be whitelisted based on needs #3434 .filter(attr => attr.type === 'label') .map(attr => attr.getPojo()), attachments: this.getAttachments() .map(attachment => attachment.getPojo()), parentNoteIds: this.parents.map(parentNote => parentNote.noteId), childNoteIds: this.children.map(child => child.noteId) }; } } export default SNote;