Skip to content

Commit 5e6509a

Browse files
pyphiliakim
andauthored
feat: export document item as html (#2063)
* feat: export document item as html * test: add tests for importing graasp documents --------- Co-authored-by: kim <kim.phanhoang@epfl.ch>
1 parent af96280 commit 5e6509a

6 files changed

Lines changed: 113 additions & 15 deletions

File tree

src/services/item/plugins/importExport/importExport.controller.test.ts

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,49 @@ describe('ZIP routes tests', () => {
432432
}
433433
}, 1000);
434434
});
435+
it('Import and sanitize graasp documents', async () => {
436+
const {
437+
actor,
438+
items: [parentItem],
439+
} = await seedFromJson({ items: [{ memberships: [{ account: 'actor' }] }] });
440+
assertIsDefined(actor);
441+
mockAuthenticate(actor);
442+
443+
const form = createFormData('documents.zip');
444+
const response = await app.inject({
445+
method: HttpMethod.Post,
446+
url: '/api/items/zip-import',
447+
payload: form,
448+
headers: form.getHeaders(),
449+
query: { parentId: parentItem.id },
450+
});
451+
452+
expect(response.statusCode).toBe(StatusCodes.ACCEPTED);
453+
454+
await waitForExpect(async () => {
455+
const documents = await db.query.itemsRawTable.findMany({
456+
where: and(
457+
isDescendantOrSelf(itemsRawTable.path, parentItem.path),
458+
ne(itemsRawTable.id, parentItem.id),
459+
eq(itemsRawTable.type, ItemType.DOCUMENT),
460+
),
461+
});
462+
expect(documents).toHaveLength(2);
463+
464+
for (const item of documents) {
465+
const content = item.extra[ItemType.DOCUMENT].content;
466+
467+
// the script with a console should not appear in the text
468+
expect(content).not.toContain('script');
469+
expect(content).not.toContain('console');
470+
expect(content).not.toContain('style');
471+
472+
// content
473+
expect(content).toContain('<h1>My First Heading</h1>');
474+
expect(content).toContain(`<p>My first paragraph.</p>`);
475+
}
476+
}, 1000);
477+
});
435478
it('Throws if signed out', async () => {
436479
const form = createFormData('archive.zip');
437480

@@ -536,7 +579,7 @@ describe('ZIP routes tests', () => {
536579
});
537580
});
538581

539-
describe('POST /api/download-file', () => {
582+
describe('GET /api/download-file', () => {
540583
it('Export successfully if has access', async () => {
541584
const {
542585
actor,
@@ -555,7 +598,7 @@ describe('ZIP routes tests', () => {
555598
mockAuthenticate(actor);
556599

557600
const response = await app.inject({
558-
method: HttpMethod.Post,
601+
method: HttpMethod.Get,
559602
url: `/api/items/${item.id}/download-file`,
560603
});
561604

@@ -580,7 +623,7 @@ describe('ZIP routes tests', () => {
580623
mockAuthenticate(guest);
581624

582625
const response = await app.inject({
583-
method: HttpMethod.Post,
626+
method: HttpMethod.Get,
584627
url: `/api/items/${item.id}/download-file`,
585628
});
586629

@@ -603,7 +646,7 @@ describe('ZIP routes tests', () => {
603646
});
604647

605648
const response = await app.inject({
606-
method: HttpMethod.Post,
649+
method: HttpMethod.Get,
607650
url: `/api/items/${item.id}/download-file`,
608651
});
609652

@@ -641,7 +684,7 @@ describe('ZIP routes tests', () => {
641684
const { id: h5pId, name: h5pName } = h5pUploadResponse.json();
642685

643686
const response = await app.inject({
644-
method: HttpMethod.Post,
687+
method: HttpMethod.Get,
645688
url: `/api/items/${h5pId}/download-file`,
646689
});
647690

@@ -651,6 +694,65 @@ describe('ZIP routes tests', () => {
651694
expect(response.headers['content-disposition']).toContain('.h5p');
652695
expect(response.headers['content-disposition']).not.toContain('.zip');
653696
});
697+
698+
it('Export successfully document item', async () => {
699+
const {
700+
actor,
701+
items: [doc],
702+
} = await seedFromJson({
703+
items: [
704+
{
705+
name: 'doc',
706+
type: ItemType.DOCUMENT,
707+
extra: { document: { content: 'my content in the document', isRaw: true } },
708+
memberships: [{ account: 'actor' }],
709+
},
710+
],
711+
});
712+
assertIsDefined(actor);
713+
mockAuthenticate(actor);
714+
715+
const response = await app.inject({
716+
method: HttpMethod.Get,
717+
url: `/api/items/${doc.id}/download-file`,
718+
});
719+
720+
expect(response.statusCode).toBe(StatusCodes.OK);
721+
expect(response.payload.length).toBeGreaterThan(10);
722+
expect(response.headers['content-disposition']).toContain(doc.name);
723+
expect(response.headers['content-disposition']).toContain('.html');
724+
expect(response.headers['content-type']).toContain('text/html');
725+
});
726+
727+
it('Export successfully html document item', async () => {
728+
const {
729+
actor,
730+
items: [doc],
731+
} = await seedFromJson({
732+
items: [
733+
{
734+
name: 'doc',
735+
type: ItemType.DOCUMENT,
736+
extra: { document: { content: 'my html in the document', isRaw: false } },
737+
memberships: [{ account: 'actor' }],
738+
},
739+
],
740+
});
741+
assertIsDefined(actor);
742+
mockAuthenticate(actor);
743+
744+
const response = await app.inject({
745+
method: HttpMethod.Get,
746+
url: `/api/items/${doc.id}/download-file`,
747+
});
748+
749+
expect(response.statusCode).toBe(StatusCodes.OK);
750+
expect(response.payload.length).toBeGreaterThan(10);
751+
expect(response.headers['content-disposition']).toContain(doc.name);
752+
expect(response.headers['content-disposition']).toContain('.html');
753+
expect(response.headers['content-type']).toContain('text/html');
754+
});
755+
654756
it('Throw for folder', async () => {
655757
const {
656758
actor,
@@ -662,7 +764,7 @@ describe('ZIP routes tests', () => {
662764
mockAuthenticate(actor);
663765

664766
const response = await app.inject({
665-
method: HttpMethod.Post,
767+
method: HttpMethod.Get,
666768
url: `/api/items/${item.id}/download-file`,
667769
});
668770
expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST);

src/services/item/plugins/importExport/importExport.controller.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
9898
);
9999

100100
// export non-folder item as raw file
101-
fastify.post(
101+
fastify.get(
102102
'/:itemId/download-file',
103103
{
104104
schema: downloadFile,
@@ -132,7 +132,6 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
132132

133133
// return single file
134134
const { stream, mimetype, name } = await itemExportService.fetchItemData(db, maybeUser, item);
135-
136135
// allow browser to access content disposition
137136
reply.header('Access-Control-Expose-Headers', 'Content-Disposition');
138137
reply.raw.setHeader('Content-Disposition', `attachment; filename="${encodeFilename(name)}"`);

src/services/item/plugins/importExport/itemExport.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class ItemExportService {
6060
return {
6161
stream: Readable.from([item.extra.document?.content]),
6262
name: getFilenameFromItem(item),
63-
mimetype: item.extra.document.isRaw ? 'text/html' : 'text/plain',
63+
mimetype: 'text/html',
6464
};
6565
}
6666
case isItemType(item, ItemType.LINK): {
Binary file not shown.

src/services/item/plugins/importExport/utils.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,11 @@ describe('File name', () => {
162162
const item = DocumentItemFactory({
163163
name: 'mydoc',
164164
}) as unknown as ItemRaw;
165-
expect(getFilenameFromItem(item)).toEqual('mydoc.graasp');
165+
expect(getFilenameFromItem(item)).toEqual('mydoc.html');
166166
const item1 = DocumentItemFactory({
167167
name: 'mydoc.graasp',
168168
}) as unknown as ItemRaw;
169-
expect(getFilenameFromItem(item1)).toEqual('mydoc.graasp');
169+
expect(getFilenameFromItem(item1)).toEqual('mydoc.graasp.html');
170170
});
171171
it('get file name from raw document item', () => {
172172
const item = DocumentItemFactory({

src/services/item/plugins/importExport/utils.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ export const getFilenameFromItem = (item: ItemRaw): string => {
7474
return extractFileName(item.name, 'app');
7575
}
7676
case isItemType(item, ItemType.DOCUMENT): {
77-
if (item.extra.document.isRaw) {
78-
return extractFileName(item.name, 'html');
79-
}
80-
return extractFileName(item.name, 'graasp');
77+
return extractFileName(item.name, 'html');
8178
}
8279
case isItemType(item, ItemType.FILE): {
8380
const mimetype = getMimetype(item.extra);

0 commit comments

Comments
 (0)