🦺(frontend) check content type pdf on PdfBlock
Pdfblock was quite permissive on the content type it was accepting. Now it checks that the content type is exactly 'application/pdf' before rendering the PDF viewer.
This commit is contained in:
@@ -15,6 +15,10 @@ and this project adheres to
|
|||||||
|
|
||||||
- 🥅(frontend) intercept 401 error on GET threads #1754
|
- 🥅(frontend) intercept 401 error on GET threads #1754
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- 🦺(frontend) check content type pdf on PdfBlock #1756
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- 🐛(frontend) fix tables deletion #1752
|
- 🐛(frontend) fix tables deletion #1752
|
||||||
|
|||||||
@@ -960,13 +960,35 @@ test.describe('Doc Editor', () => {
|
|||||||
test('it embeds PDF', async ({ page, browserName }) => {
|
test('it embeds PDF', async ({ page, browserName }) => {
|
||||||
await createDoc(page, 'doc-toolbar', browserName, 1);
|
await createDoc(page, 'doc-toolbar', browserName, 1);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await updateShareLink(page, 'Public', 'Reading');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Close the share modal' }).click();
|
||||||
|
|
||||||
await openSuggestionMenu({ page });
|
await openSuggestionMenu({ page });
|
||||||
await page.getByText('Embed a PDF file').click();
|
await page.getByText('Embed a PDF file').click();
|
||||||
|
|
||||||
const pdfBlock = page.locator('div[data-content-type="pdf"]').first();
|
const pdfBlock = page.locator('div[data-content-type="pdf"]').last();
|
||||||
|
|
||||||
await expect(pdfBlock).toBeVisible();
|
await expect(pdfBlock).toBeVisible();
|
||||||
|
|
||||||
|
// Try with invalid PDF first
|
||||||
|
await page.getByText(/Add (PDF|file)/).click();
|
||||||
|
|
||||||
|
await page.locator('[data-test="embed-tab"]').click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator('[data-test="embed-input"]')
|
||||||
|
.fill('https://example.test/test.test');
|
||||||
|
|
||||||
|
await page.locator('[data-test="embed-input-button"]').click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Invalid or missing PDF file')).toBeVisible();
|
||||||
|
|
||||||
|
await openSuggestionMenu({ page });
|
||||||
|
await page.getByText('Embed a PDF file').click();
|
||||||
|
|
||||||
|
// Now with a valid PDF
|
||||||
await page.getByText(/Add (PDF|file)/).click();
|
await page.getByText(/Add (PDF|file)/).click();
|
||||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
const downloadPromise = page.waitForEvent('download');
|
const downloadPromise = page.waitForEvent('download');
|
||||||
@@ -991,7 +1013,7 @@ test.describe('Doc Editor', () => {
|
|||||||
await expect(pdfEmbed).toHaveAttribute('role', 'presentation');
|
await expect(pdfEmbed).toHaveAttribute('role', 'presentation');
|
||||||
|
|
||||||
// Check download with original filename
|
// Check download with original filename
|
||||||
await page.locator('.bn-block-content[data-content-type="pdf"]').click();
|
await pdfBlock.click();
|
||||||
await page.locator('[data-test="downloadfile"]').click();
|
await page.locator('[data-test="downloadfile"]').click();
|
||||||
|
|
||||||
const download = await downloadPromise;
|
const download = await downloadPromise;
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ import {
|
|||||||
createReactBlockSpec,
|
createReactBlockSpec,
|
||||||
} from '@blocknote/react';
|
} from '@blocknote/react';
|
||||||
import { TFunction } from 'i18next';
|
import { TFunction } from 'i18next';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { createGlobalStyle } from 'styled-components';
|
import { createGlobalStyle, css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, Icon } from '@/components';
|
import { Box, Icon, Loading } from '@/components';
|
||||||
|
|
||||||
|
import { ANALYZE_URL } from '../../conf';
|
||||||
import { DocsBlockNoteEditor } from '../../types';
|
import { DocsBlockNoteEditor } from '../../types';
|
||||||
|
|
||||||
const PDFBlockStyle = createGlobalStyle`
|
const PDFBlockStyle = createGlobalStyle`
|
||||||
@@ -66,6 +67,9 @@ const PdfBlockComponent = ({
|
|||||||
const pdfUrl = block.props.url;
|
const pdfUrl = block.props.url;
|
||||||
const { i18n, t } = useTranslation();
|
const { i18n, t } = useTranslation();
|
||||||
const lang = i18n.resolvedLanguage;
|
const lang = i18n.resolvedLanguage;
|
||||||
|
const [isPDFContent, setIsPDFContent] = useState<boolean | null>(null);
|
||||||
|
const [isPDFContentLoading, setIsPDFContentLoading] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lang && locales[lang as keyof typeof locales]) {
|
if (lang && locales[lang as keyof typeof locales]) {
|
||||||
@@ -82,9 +86,55 @@ const PdfBlockComponent = ({
|
|||||||
}
|
}
|
||||||
}, [lang, t]);
|
}, [lang, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pdfUrl || pdfUrl.includes(ANALYZE_URL)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatePDFContent = async () => {
|
||||||
|
setIsPDFContentLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(pdfUrl, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
|
||||||
|
if (response.ok && contentType?.includes('application/pdf')) {
|
||||||
|
setIsPDFContent(true);
|
||||||
|
} else {
|
||||||
|
setIsPDFContent(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setIsPDFContent(false);
|
||||||
|
} finally {
|
||||||
|
setIsPDFContentLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void validatePDFContent();
|
||||||
|
}, [pdfUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box ref={contentRef} className="bn-file-block-content-wrapper">
|
<Box ref={contentRef} className="bn-file-block-content-wrapper">
|
||||||
<PDFBlockStyle />
|
<PDFBlockStyle />
|
||||||
|
{isPDFContentLoading && <Loading />}
|
||||||
|
{!isPDFContentLoading && isPDFContent !== null && !isPDFContent && (
|
||||||
|
<Box
|
||||||
|
$align="center"
|
||||||
|
$justify="center"
|
||||||
|
$color="#666"
|
||||||
|
$background="#f5f5f5"
|
||||||
|
$border="1px solid #ddd"
|
||||||
|
$height="300px"
|
||||||
|
$css={css`
|
||||||
|
text-align: center;
|
||||||
|
`}
|
||||||
|
contentEditable={false}
|
||||||
|
onClick={() => editor.setTextCursorPosition(block)}
|
||||||
|
>
|
||||||
|
{t('Invalid or missing PDF file.')}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<ResizableFileBlockWrapper
|
<ResizableFileBlockWrapper
|
||||||
buttonIcon={
|
buttonIcon={
|
||||||
<Icon iconName="upload" $size="24px" $css="line-height: normal;" />
|
<Icon iconName="upload" $size="24px" $css="line-height: normal;" />
|
||||||
@@ -92,18 +142,21 @@ const PdfBlockComponent = ({
|
|||||||
block={block as unknown as FileBlockBlock}
|
block={block as unknown as FileBlockBlock}
|
||||||
editor={editor as unknown as FileBlockEditor}
|
editor={editor as unknown as FileBlockEditor}
|
||||||
>
|
>
|
||||||
<Box
|
{!isPDFContentLoading && isPDFContent && (
|
||||||
className="bn-visual-media"
|
<Box
|
||||||
role="presentation"
|
as="embed"
|
||||||
as="embed"
|
className="bn-visual-media"
|
||||||
$width="100%"
|
role="presentation"
|
||||||
$height="450px"
|
$width="100%"
|
||||||
type="application/pdf"
|
$height="450px"
|
||||||
src={pdfUrl}
|
type="application/pdf"
|
||||||
contentEditable={false}
|
src={pdfUrl}
|
||||||
draggable={false}
|
aria-label={block.props.name || t('PDF document')}
|
||||||
onClick={() => editor.setTextCursorPosition(block)}
|
contentEditable={false}
|
||||||
/>
|
draggable={false}
|
||||||
|
onClick={() => editor.setTextCursorPosition(block)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ResizableFileBlockWrapper>
|
</ResizableFileBlockWrapper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const ANALYZE_URL = 'media-check';
|
||||||
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { backendUrl } from '@/api';
|
import { backendUrl } from '@/api';
|
||||||
|
|
||||||
import { useCreateDocAttachment } from '../api';
|
import { useCreateDocAttachment } from '../api';
|
||||||
|
import { ANALYZE_URL } from '../conf';
|
||||||
import { DocsBlockNoteEditor } from '../types';
|
import { DocsBlockNoteEditor } from '../types';
|
||||||
|
|
||||||
export const useUploadFile = (docId: string) => {
|
export const useUploadFile = (docId: string) => {
|
||||||
@@ -46,7 +47,6 @@ export const useUploadFile = (docId: string) => {
|
|||||||
* @param editor
|
* @param editor
|
||||||
*/
|
*/
|
||||||
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
||||||
const ANALYZE_URL = 'media-check';
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user