🦺(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
|
||||
|
||||
## Changed
|
||||
|
||||
- 🦺(frontend) check content type pdf on PdfBlock #1756
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix tables deletion #1752
|
||||
|
||||
@@ -960,13 +960,35 @@ test.describe('Doc Editor', () => {
|
||||
test('it embeds PDF', async ({ page, browserName }) => {
|
||||
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 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();
|
||||
|
||||
// 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();
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
@@ -991,7 +1013,7 @@ test.describe('Doc Editor', () => {
|
||||
await expect(pdfEmbed).toHaveAttribute('role', 'presentation');
|
||||
|
||||
// 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();
|
||||
|
||||
const download = await downloadPromise;
|
||||
|
||||
@@ -13,12 +13,13 @@ import {
|
||||
createReactBlockSpec,
|
||||
} from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
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';
|
||||
|
||||
const PDFBlockStyle = createGlobalStyle`
|
||||
@@ -66,6 +67,9 @@ const PdfBlockComponent = ({
|
||||
const pdfUrl = block.props.url;
|
||||
const { i18n, t } = useTranslation();
|
||||
const lang = i18n.resolvedLanguage;
|
||||
const [isPDFContent, setIsPDFContent] = useState<boolean | null>(null);
|
||||
const [isPDFContentLoading, setIsPDFContentLoading] =
|
||||
useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (lang && locales[lang as keyof typeof locales]) {
|
||||
@@ -82,9 +86,55 @@ const PdfBlockComponent = ({
|
||||
}
|
||||
}, [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 (
|
||||
<Box ref={contentRef} className="bn-file-block-content-wrapper">
|
||||
<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
|
||||
buttonIcon={
|
||||
<Icon iconName="upload" $size="24px" $css="line-height: normal;" />
|
||||
@@ -92,18 +142,21 @@ const PdfBlockComponent = ({
|
||||
block={block as unknown as FileBlockBlock}
|
||||
editor={editor as unknown as FileBlockEditor}
|
||||
>
|
||||
<Box
|
||||
className="bn-visual-media"
|
||||
role="presentation"
|
||||
as="embed"
|
||||
$width="100%"
|
||||
$height="450px"
|
||||
type="application/pdf"
|
||||
src={pdfUrl}
|
||||
contentEditable={false}
|
||||
draggable={false}
|
||||
onClick={() => editor.setTextCursorPosition(block)}
|
||||
/>
|
||||
{!isPDFContentLoading && isPDFContent && (
|
||||
<Box
|
||||
as="embed"
|
||||
className="bn-visual-media"
|
||||
role="presentation"
|
||||
$width="100%"
|
||||
$height="450px"
|
||||
type="application/pdf"
|
||||
src={pdfUrl}
|
||||
aria-label={block.props.name || t('PDF document')}
|
||||
contentEditable={false}
|
||||
draggable={false}
|
||||
onClick={() => editor.setTextCursorPosition(block)}
|
||||
/>
|
||||
)}
|
||||
</ResizableFileBlockWrapper>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const ANALYZE_URL = 'media-check';
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { backendUrl } from '@/api';
|
||||
|
||||
import { useCreateDocAttachment } from '../api';
|
||||
import { ANALYZE_URL } from '../conf';
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
export const useUploadFile = (docId: string) => {
|
||||
@@ -46,7 +47,6 @@ export const useUploadFile = (docId: string) => {
|
||||
* @param editor
|
||||
*/
|
||||
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
||||
const ANALYZE_URL = 'media-check';
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user