mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Server: Add support for recursively publishing a note
This commit is contained in:
parent
af665f247c
commit
29a1cc022c
@ -32,13 +32,13 @@ function docReady(fn) {
|
||||
docReady(() => {
|
||||
addPluginAssets(joplinNoteViewer.appBaseUrl, joplinNoteViewer.pluginAssets);
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
const element = event.target;
|
||||
// document.addEventListener('click', event => {
|
||||
// const element = event.target;
|
||||
|
||||
// Detects if it's a note link and, if so, display a message
|
||||
if (element && element.getAttribute('href') === '#' && element.getAttribute('data-resource-id')) {
|
||||
event.preventDefault();
|
||||
alert('This note has not been shared');
|
||||
}
|
||||
});
|
||||
// // Detects if it's a note link and, if so, display a message
|
||||
// if (element && element.getAttribute('href') === '#' && element.getAttribute('data-resource-id')) {
|
||||
// event.preventDefault();
|
||||
// alert('This note has not been shared');
|
||||
// }
|
||||
// });
|
||||
});
|
||||
|
Binary file not shown.
@ -0,0 +1,14 @@
|
||||
import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.alterTable('shares', (table: Knex.CreateTableBuilder) => {
|
||||
table.specificType('recursive', 'smallint').defaultTo(0).nullable();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(db: DbConnection): Promise<any> {
|
||||
await db.schema.alterTable('shares', (table: Knex.CreateTableBuilder) => {
|
||||
table.dropColumn('recursive');
|
||||
});
|
||||
}
|
99
packages/server/src/models/ItemResourceModel.test.ts
Normal file
99
packages/server/src/models/ItemResourceModel.test.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, models, createUserAndSession, createNote, createResource } from '../utils/testing/testUtils';
|
||||
|
||||
describe('ItemResourceModel', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('ItemResourceModel');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should get an item tree', async () => {
|
||||
const { session } = await createUserAndSession();
|
||||
|
||||
const linkedNote1 = await createNote(session.id, {
|
||||
id: '000000000000000000000000000000C1',
|
||||
});
|
||||
|
||||
const resource = await createResource(session.id, {
|
||||
id: '000000000000000000000000000000E1',
|
||||
}, 'test');
|
||||
|
||||
const linkedNote2 = await createNote(session.id, {
|
||||
id: '000000000000000000000000000000C2',
|
||||
body: `![](:/${resource.jop_id})`,
|
||||
});
|
||||
|
||||
const rootNote = await createNote(session.id, {
|
||||
id: '00000000000000000000000000000001',
|
||||
body: `[](:/${linkedNote1.jop_id}) [](:/${linkedNote2.jop_id})`,
|
||||
});
|
||||
|
||||
const tree = await models().itemResource().itemTree(rootNote.id, rootNote.jop_id);
|
||||
|
||||
expect(tree).toEqual({
|
||||
item_id: rootNote.id,
|
||||
resource_id: '00000000000000000000000000000001',
|
||||
children: [
|
||||
{
|
||||
item_id: linkedNote1.id,
|
||||
resource_id: '000000000000000000000000000000C1',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
item_id: linkedNote2.id,
|
||||
resource_id: '000000000000000000000000000000C2',
|
||||
children: [
|
||||
{
|
||||
item_id: resource.id,
|
||||
resource_id: '000000000000000000000000000000E1',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('should not go into infinite loop when a note links to itself', async () => {
|
||||
const { session } = await createUserAndSession();
|
||||
|
||||
const rootNote = await createNote(session.id, {
|
||||
id: '00000000000000000000000000000001',
|
||||
body: '![](:/00000000000000000000000000000002)',
|
||||
});
|
||||
|
||||
const linkedNote = await createNote(session.id, {
|
||||
id: '00000000000000000000000000000002',
|
||||
title: 'Linked note 2',
|
||||
body: '![](:/00000000000000000000000000000001)',
|
||||
});
|
||||
|
||||
const tree = await models().itemResource().itemTree(rootNote.id, rootNote.jop_id);
|
||||
|
||||
expect(tree).toEqual({
|
||||
item_id: rootNote.id,
|
||||
resource_id: '00000000000000000000000000000001',
|
||||
children: [
|
||||
{
|
||||
item_id: linkedNote.id,
|
||||
resource_id: '00000000000000000000000000000002',
|
||||
children: [
|
||||
{
|
||||
item_id: rootNote.id,
|
||||
resource_id: '00000000000000000000000000000001',
|
||||
children: [], // Empty to prevent an infinite loop
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -2,6 +2,12 @@ import { resourceBlobPath } from '../utils/joplinUtils';
|
||||
import { Item, ItemResource, Uuid } from '../services/database/types';
|
||||
import BaseModel from './BaseModel';
|
||||
|
||||
export interface TreeItem {
|
||||
item_id: Uuid;
|
||||
resource_id: string;
|
||||
children: TreeItem[];
|
||||
}
|
||||
|
||||
export default class ItemResourceModel extends BaseModel<ItemResource> {
|
||||
|
||||
public get tableName(): string {
|
||||
@ -52,9 +58,53 @@ export default class ItemResourceModel extends BaseModel<ItemResource> {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async itemIdsByResourceId(resourceId: string): Promise<string[]> {
|
||||
const rows: ItemResource[] = await this.db(this.tableName).select('item_id').where('resource_id', '=', resourceId);
|
||||
return rows.map(r => r.item_id);
|
||||
}
|
||||
|
||||
public async blobItemsByResourceIds(userIds: Uuid[], resourceIds: string[]): Promise<Item[]> {
|
||||
const resourceBlobNames = resourceIds.map(id => resourceBlobPath(id));
|
||||
return this.models().item().loadByNames(userIds, resourceBlobNames);
|
||||
}
|
||||
|
||||
public async itemTree(rootItemId: Uuid, rootJopId: string, currentItemIds: string[] = []): Promise<TreeItem> {
|
||||
interface Row {
|
||||
id: Uuid;
|
||||
jop_id: string;
|
||||
}
|
||||
|
||||
const rows: Row[] = await this
|
||||
.db('item_resources')
|
||||
.leftJoin('items', 'item_resources.resource_id', 'items.jop_id')
|
||||
.select('items.id', 'items.jop_id')
|
||||
.where('item_resources.item_id', '=', rootItemId);
|
||||
|
||||
const output: TreeItem[] = [];
|
||||
|
||||
// Only process the children if the parent ID is not already in the
|
||||
// tree. This is to prevent an infinite loop if one of the leaves links
|
||||
// to a descendant note.
|
||||
|
||||
if (!currentItemIds.includes(rootJopId)) {
|
||||
currentItemIds.push(rootJopId);
|
||||
|
||||
for (const row of rows) {
|
||||
const subTree = await this.itemTree(row.id, row.jop_id, currentItemIds);
|
||||
|
||||
output.push({
|
||||
item_id: row.id,
|
||||
resource_id: row.jop_id,
|
||||
children: subTree.children,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
item_id: rootItemId,
|
||||
resource_id: rootJopId,
|
||||
children: output,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -378,7 +378,7 @@ export default class ShareModel extends BaseModel<Share> {
|
||||
return super.save(shareToSave);
|
||||
}
|
||||
|
||||
public async shareNote(owner: User, noteId: string, masterKeyId: string): Promise<Share> {
|
||||
public async shareNote(owner: User, noteId: string, masterKeyId: string, recursive: boolean): Promise<Share> {
|
||||
const noteItem = await this.models().item().loadByJopId(owner.id, noteId);
|
||||
if (!noteItem) throw new ErrorNotFound(`No such note: ${noteId}`);
|
||||
|
||||
@ -391,6 +391,7 @@ export default class ShareModel extends BaseModel<Share> {
|
||||
owner_id: owner.id,
|
||||
note_id: noteId,
|
||||
master_key_id: masterKeyId,
|
||||
recursive: recursive ? 1 : 0,
|
||||
};
|
||||
|
||||
await this.checkIfAllowed(owner, AclAction.Create, shareToSave);
|
||||
|
@ -10,6 +10,7 @@ import { AclAction } from '../../models/BaseModel';
|
||||
interface ShareApiInput extends Share {
|
||||
folder_id?: string;
|
||||
note_id?: string;
|
||||
recursive?: number;
|
||||
}
|
||||
|
||||
const router = new Router(RouteType.Api);
|
||||
@ -23,6 +24,7 @@ router.post('api/shares', async (_path: SubPath, ctx: AppContext) => {
|
||||
folder_id?: string;
|
||||
note_id?: string;
|
||||
master_key_id?: string;
|
||||
recursive?: number;
|
||||
}
|
||||
|
||||
const shareModel = ctx.joplin.models.share();
|
||||
@ -40,7 +42,7 @@ router.post('api/shares', async (_path: SubPath, ctx: AppContext) => {
|
||||
if (shareInput.folder_id) {
|
||||
return ctx.joplin.models.share().shareFolder(ctx.joplin.owner, shareInput.folder_id, masterKeyId);
|
||||
} else if (shareInput.note_id) {
|
||||
return ctx.joplin.models.share().shareNote(ctx.joplin.owner, shareInput.note_id, masterKeyId);
|
||||
return ctx.joplin.models.share().shareNote(ctx.joplin.owner, shareInput.note_id, masterKeyId, fields.recursive === 1);
|
||||
} else {
|
||||
throw new ErrorBadRequest('Either folder_id or note_id must be provided');
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Share, ShareType } from '../../services/database/types';
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { ErrorForbidden } from '../../utils/errors';
|
||||
import { ErrorForbidden, ErrorNotFound } from '../../utils/errors';
|
||||
import { postApi } from '../../utils/testing/apiUtils';
|
||||
import { testImageBuffer } from '../../utils/testing/fileApiUtils';
|
||||
import { beforeAllDb, afterAllTests, parseHtml, beforeEachDb, createUserAndSession, koaAppContext, checkContextError, expectNotThrow, createNote, createItem, models, expectHttpError } from '../../utils/testing/testUtils';
|
||||
import { beforeAllDb, afterAllTests, parseHtml, beforeEachDb, createUserAndSession, koaAppContext, checkContextError, expectNotThrow, createNote, createItem, models, expectHttpError, createResource } from '../../utils/testing/testUtils';
|
||||
|
||||
const resourceSize = 2720;
|
||||
|
||||
@ -129,6 +129,89 @@ describe('shares.link', function() {
|
||||
expect(resourceContent.byteLength).toBe(resourceSize);
|
||||
});
|
||||
|
||||
test('should share a linked note', async function() {
|
||||
const { session } = await createUserAndSession();
|
||||
|
||||
const linkedNote1 = await createNote(session.id, {
|
||||
id: '000000000000000000000000000000C1',
|
||||
});
|
||||
|
||||
const resource = await createResource(session.id, {
|
||||
id: '000000000000000000000000000000E1',
|
||||
}, 'test');
|
||||
|
||||
const linkedNote2 = await createNote(session.id, {
|
||||
id: '000000000000000000000000000000C2',
|
||||
body: `[](:/${resource.jop_id})`,
|
||||
});
|
||||
|
||||
const rootNote = await createNote(session.id, {
|
||||
id: '00000000000000000000000000000001',
|
||||
body: `[](:/${linkedNote1.jop_id}) [](:/${linkedNote2.jop_id})`,
|
||||
});
|
||||
|
||||
const share = await postApi<Share>(session.id, 'shares', {
|
||||
type: ShareType.Note,
|
||||
note_id: rootNote.jop_id,
|
||||
recursive: 1,
|
||||
});
|
||||
|
||||
const bodyHtml = await getShareContent(share.id, { note_id: '000000000000000000000000000000C2' }) as string;
|
||||
const doc = parseHtml(bodyHtml);
|
||||
const image = doc.querySelector('a[data-resource-id="000000000000000000000000000000E1"]');
|
||||
expect(image.getAttribute('href')).toBe(`http://localhost:22300/shares/${share.id}?resource_id=000000000000000000000000000000E1&t=1602758278090`);
|
||||
|
||||
const resourceContent = await getShareContent(share.id, { resource_id: '000000000000000000000000000000E1' });
|
||||
expect(resourceContent.toString()).toBe('test');
|
||||
});
|
||||
|
||||
test('should not share items that are not linked to a shared note', async function() {
|
||||
const { session } = await createUserAndSession();
|
||||
|
||||
const notSharedResource = await createResource(session.id, {
|
||||
id: '000000000000000000000000000000E2',
|
||||
}, 'test2');
|
||||
|
||||
await createNote(session.id, {
|
||||
id: '000000000000000000000000000000C5',
|
||||
body: `[](:/${notSharedResource.jop_id})`,
|
||||
});
|
||||
|
||||
const rootNote = await createNote(session.id, {
|
||||
id: '00000000000000000000000000000001',
|
||||
});
|
||||
|
||||
const share = await postApi<Share>(session.id, 'shares', {
|
||||
type: ShareType.Note,
|
||||
note_id: rootNote.jop_id,
|
||||
recursive: 1,
|
||||
});
|
||||
|
||||
await expectNotThrow(async () => getShareContent(share.id, { note_id: '00000000000000000000000000000001' }));
|
||||
await expectHttpError(async () => getShareContent(share.id, { note_id: '000000000000000000000000000000C5' }), ErrorNotFound.httpCode);
|
||||
await expectHttpError(async () => getShareContent(share.id, { note_id: '000000000000000000000000000000E2' }), ErrorNotFound.httpCode);
|
||||
});
|
||||
|
||||
test('should not share linked notes if the "recursive" field is not set', async function() {
|
||||
const { session } = await createUserAndSession();
|
||||
|
||||
const linkedNote1 = await createNote(session.id, {
|
||||
id: '000000000000000000000000000000C1',
|
||||
});
|
||||
|
||||
const rootNote = await createNote(session.id, {
|
||||
id: '00000000000000000000000000000001',
|
||||
body: `[](:/${linkedNote1.jop_id})`,
|
||||
});
|
||||
|
||||
const share = await postApi<Share>(session.id, 'shares', {
|
||||
type: ShareType.Note,
|
||||
note_id: rootNote.jop_id,
|
||||
});
|
||||
|
||||
await expectHttpError(async () => getShareContent(share.id, { note_id: '000000000000000000000000000000C1' }), ErrorForbidden.httpCode);
|
||||
});
|
||||
|
||||
test('should not throw an error if the note contains links to non-existing items', async function() {
|
||||
const { session } = await createUserAndSession();
|
||||
|
||||
@ -161,7 +244,6 @@ describe('shares.link', function() {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('should throw an error if owner of share is disabled', async function() {
|
||||
const { user, session } = await createUserAndSession();
|
||||
|
||||
|
@ -182,6 +182,7 @@ export interface Share extends WithDates, WithUuid {
|
||||
folder_id?: Uuid;
|
||||
note_id?: Uuid;
|
||||
master_key_id?: Uuid;
|
||||
recursive?: number;
|
||||
}
|
||||
|
||||
export interface Change extends WithDates, WithUuid {
|
||||
@ -378,6 +379,7 @@ export const databaseSchema: DatabaseTables = {
|
||||
folder_id: { type: 'string' },
|
||||
note_id: { type: 'string' },
|
||||
master_key_id: { type: 'string' },
|
||||
recursive: { type: 'number' },
|
||||
},
|
||||
changes: {
|
||||
counter: { type: 'number' },
|
||||
|
@ -14,7 +14,7 @@ import { Item, Share, Uuid } from '../services/database/types';
|
||||
import ItemModel from '../models/ItemModel';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { formatDateTime } from './time';
|
||||
import { ErrorNotFound } from './errors';
|
||||
import { ErrorBadRequest, ErrorForbidden, ErrorNotFound } from './errors';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
import { OptionsResourceModel } from '@joplin/renderer/MarkupToHtml';
|
||||
import { isValidHeaderIdentifier } from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
@ -25,6 +25,7 @@ import { Models } from '../models/factory';
|
||||
import MustacheService from '../services/MustacheService';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import config from '../config';
|
||||
import { TreeItem } from '../models/ItemResourceModel';
|
||||
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
|
||||
|
||||
const logger = Logger.create('JoplinUtils');
|
||||
@ -136,8 +137,8 @@ async function getResourceInfos(linkedItemInfos: LinkedItemInfos): Promise<Resou
|
||||
return output;
|
||||
}
|
||||
|
||||
async function noteLinkedItemInfos(userId: Uuid, itemModel: ItemModel, note: NoteEntity): Promise<LinkedItemInfos> {
|
||||
const jopIds = await Note.linkedItemIds(note.body);
|
||||
async function noteLinkedItemInfos(userId: Uuid, itemModel: ItemModel, noteBody: string): Promise<LinkedItemInfos> {
|
||||
const jopIds = await Note.linkedItemIds(noteBody);
|
||||
const output: LinkedItemInfos = {};
|
||||
|
||||
for (const jopId of jopIds) {
|
||||
@ -190,7 +191,7 @@ async function renderNote(share: Share, note: NoteEntity, resourceInfos: Resourc
|
||||
if (!item) throw new Error(`No such item in this note: ${itemId}`);
|
||||
|
||||
if (item.type_ === ModelType.Note) {
|
||||
return '#';
|
||||
return `${models_.share().shareUrl(share.owner_id, share.id)}?note_id=${item.id}&t=${item.updated_time}`;
|
||||
} else if (item.type_ === ModelType.Resource) {
|
||||
return `${models_.share().shareUrl(share.owner_id, share.id)}?resource_id=${item.id}&t=${item.updated_time}`;
|
||||
} else {
|
||||
@ -255,35 +256,120 @@ export function itemIsEncrypted(item: Item): boolean {
|
||||
return isValidHeaderIdentifier(header);
|
||||
}
|
||||
|
||||
export async function renderItem(userId: Uuid, item: Item, share: Share, query: Record<string, any>): Promise<FileViewerResponse> {
|
||||
const rootNote: NoteEntity = models_.item().itemToJoplinItem(item); // await this.unserializeItem(content);
|
||||
const linkedItemInfos: LinkedItemInfos = await noteLinkedItemInfos(userId, models_.item(), rootNote);
|
||||
const resourceInfos = await getResourceInfos(linkedItemInfos);
|
||||
const findParentNote = async (itemTree: TreeItem, resourceId: string) => {
|
||||
const find_ = (parentItem: TreeItem, currentTreeItems: TreeItem[], resourceId: string): TreeItem => {
|
||||
for (const it of currentTreeItems) {
|
||||
if (it.resource_id === resourceId) return parentItem;
|
||||
const child = find_(it, it.children, resourceId);
|
||||
if (child) return it;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const result = find_(itemTree, itemTree.children, resourceId);
|
||||
if (!result) throw new ErrorBadRequest(`Cannot find parent of ${resourceId}`);
|
||||
|
||||
const item = await models_.item().loadWithContent(result.item_id);
|
||||
if (!item) throw new ErrorNotFound(`Cannot load item with ID ${result.item_id}`);
|
||||
|
||||
return models_.item().itemToJoplinItem(item);
|
||||
};
|
||||
|
||||
const isInTree = (itemTree: TreeItem, jopId: string) => {
|
||||
if (itemTree.resource_id === jopId) return true;
|
||||
for (const child of itemTree.children) {
|
||||
if (child.resource_id === jopId) return true;
|
||||
const found = isInTree(child, jopId);
|
||||
if (found) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
interface RenderItemQuery {
|
||||
resource_id?: string;
|
||||
note_id?: string;
|
||||
}
|
||||
|
||||
// "item" is always the item associated with the share (the "root item"). It may
|
||||
// be different from the item that will eventually get rendered - for example
|
||||
// for resources or linked notes.
|
||||
export async function renderItem(userId: Uuid, item: Item, share: Share, query: RenderItemQuery): Promise<FileViewerResponse> {
|
||||
interface FileToRender {
|
||||
item: Item;
|
||||
content: any;
|
||||
jopItemId: string;
|
||||
}
|
||||
|
||||
const fileToRender: FileToRender = {
|
||||
item: item,
|
||||
content: null as any,
|
||||
jopItemId: rootNote.id,
|
||||
};
|
||||
const rootNote: NoteEntity = models_.item().itemToJoplinItem(item);
|
||||
const itemTree = await models_.itemResource().itemTree(item.id, rootNote.id);
|
||||
|
||||
let linkedItemInfos: LinkedItemInfos = {};
|
||||
let resourceInfos: ResourceInfos = {};
|
||||
let fileToRender: FileToRender;
|
||||
let itemToRender: any = null;
|
||||
|
||||
if (query.resource_id) {
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Render a resource that is attached to a note
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
const resourceItem = await models_.item().loadByName(userId, resourceBlobPath(query.resource_id), { fields: ['*'], withContent: true });
|
||||
fileToRender.item = resourceItem;
|
||||
fileToRender.content = resourceItem.content;
|
||||
fileToRender.jopItemId = query.resource_id;
|
||||
if (!resourceItem) throw new ErrorNotFound(`No such resource: ${query.resource_id}`);
|
||||
|
||||
fileToRender = {
|
||||
item: resourceItem,
|
||||
content: resourceItem.content,
|
||||
jopItemId: query.resource_id,
|
||||
};
|
||||
|
||||
const parentNote = await findParentNote(itemTree, fileToRender.jopItemId);
|
||||
linkedItemInfos = await noteLinkedItemInfos(userId, models_.item(), parentNote.body);
|
||||
itemToRender = linkedItemInfos[fileToRender.jopItemId].item;
|
||||
} else if (query.note_id) {
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Render a linked note
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
if (!share.recursive) throw new ErrorForbidden('Linked notes are not published');
|
||||
|
||||
const noteItem = await models_.item().loadByName(userId, `${query.note_id}.md`, { fields: ['*'], withContent: true });
|
||||
if (!noteItem) throw new ErrorNotFound(`No such note: ${query.note_id}`);
|
||||
|
||||
fileToRender = {
|
||||
item: noteItem,
|
||||
content: noteItem.content,
|
||||
jopItemId: query.note_id,
|
||||
};
|
||||
|
||||
linkedItemInfos = await noteLinkedItemInfos(userId, models_.item(), noteItem.content.toString());
|
||||
resourceInfos = await getResourceInfos(linkedItemInfos);
|
||||
itemToRender = models_.item().itemToJoplinItem(noteItem);
|
||||
} else {
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Render the root note
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
fileToRender = {
|
||||
item: item,
|
||||
content: null as any,
|
||||
jopItemId: rootNote.id,
|
||||
};
|
||||
|
||||
linkedItemInfos = await noteLinkedItemInfos(userId, models_.item(), rootNote.body);
|
||||
resourceInfos = await getResourceInfos(linkedItemInfos);
|
||||
itemToRender = rootNote;
|
||||
}
|
||||
|
||||
if (fileToRender.item !== item && !linkedItemInfos[fileToRender.jopItemId]) {
|
||||
throw new ErrorNotFound(`Item "${fileToRender.jopItemId}" does not belong to this note`);
|
||||
if (!itemToRender) throw new ErrorNotFound(`Cannot render item: ${item.id}: ${JSON.stringify(query)}`);
|
||||
|
||||
// Verify that the item we're going to render is indeed part of the item
|
||||
// tree (i.e. it is either the root note, or one of the ancestor is the root
|
||||
// note). This is for security reason - otherwise it would be possible to
|
||||
// display any note by setting note_id to an arbitrary ID.
|
||||
if (!isInTree(itemTree, fileToRender.jopItemId)) {
|
||||
throw new ErrorNotFound(`Item "${fileToRender.jopItemId}" does not belong to this share`);
|
||||
}
|
||||
|
||||
const itemToRender = fileToRender.item === item ? rootNote : linkedItemInfos[fileToRender.jopItemId].item;
|
||||
const itemType: ModelType = itemToRender.type_;
|
||||
|
||||
if (itemType === ModelType.Resource) {
|
||||
|
Loading…
Reference in New Issue
Block a user