🦺(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:
Anthony LC
2025-12-22 17:30:50 +01:00
parent b0bd6e2c01
commit 78c7ab247b
5 changed files with 98 additions and 18 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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>
); );

View File

@@ -0,0 +1 @@
export const ANALYZE_URL = 'media-check';

View File

@@ -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();
/** /**