diff --git a/CHANGELOG.md b/CHANGELOG.md index f64608c303..cc5a0730af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to - ✨(frontend) Can print a doc #1832 - ✨(backend) manage reconciliation requests for user accounts #1878 - 👷(CI) add GHCR workflow for forked repo testing #1851 +- ⚡️(backend) remove content from Document serializer when asked #1910 - ✨(backend) allow the duplication of subpages #1893 - ✨(backend) Onboarding docs for new users #1891 - 🩺(trivy) add trivyignore file and add minimatch CVE #1915 diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 57ac5bae03..a7f3d40178 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -225,8 +225,16 @@ def get_fields(self): fields = super().get_fields() request = self.context.get("request") - if request and request.method == "POST": - fields["id"].read_only = False + if request: + if request.method == "POST": + fields["id"].read_only = False + if ( + serializers.BooleanField().to_internal_value( + request.query_params.get("without_content", False) + ) + is True + ): + del fields["content"] return fields diff --git a/src/backend/core/tests/documents/test_api_documents_retrieve.py b/src/backend/core/tests/documents/test_api_documents_retrieve.py index 59c5e0298c..f4fae711c2 100644 --- a/src/backend/core/tests/documents/test_api_documents_retrieve.py +++ b/src/backend/core/tests/documents/test_api_documents_retrieve.py @@ -1057,3 +1057,48 @@ def test_api_documents_retrieve_permanently_deleted_related(role, depth): assert response.status_code == 404 assert response.json() == {"detail": "Not found."} + + +def test_api_documents_retrieve_without_content(): + """ + Test retrieve using without_content query string should remove the content in the response + """ + + user = factories.UserFactory() + + document = factories.DocumentFactory(creator=user, users=[(user, "owner")]) + + client = APIClient() + client.force_login(user) + + with mock.patch("core.models.Document.content") as mock_document_content: + response = client.get( + f"/api/v1.0/documents/{document.id!s}/?without_content=true" + ) + + assert response.status_code == 200 + + payload = response.json() + assert "content" not in payload + mock_document_content.assert_not_called() + + +def test_api_documents_retrieve_without_content_invalid_value(): + """ + Test retrieve using without_content query string but an invalid value + should return a 400 + """ + + user = factories.UserFactory() + + document = factories.DocumentFactory(creator=user, users=[(user, "owner")]) + + client = APIClient() + client.force_login(user) + + response = client.get( + f"/api/v1.0/documents/{document.id!s}/?without_content=invalid-value" + ) + assert response.status_code == 400 + + assert response.json() == ["Must be a valid boolean."] diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx index be5145dda2..f07ac5fac1 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx @@ -95,7 +95,7 @@ export const LinkSelected = ({ isEditable, updateInlineContent, }: LinkSelectedProps) => { - const { data: doc } = useDoc({ id: docId }); + const { data: doc } = useDoc({ id: docId, withoutContent: true }); /** * Update the content title if the referenced doc title changes diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDoc.tsx index 4fd6e07ffc..5441634c95 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDoc.tsx @@ -6,10 +6,15 @@ import { Doc } from '../types'; export type DocParams = { id: string; + withoutContent?: boolean; }; -export const getDoc = async ({ id }: DocParams): Promise => { - const response = await fetchAPI(`documents/${id}/`); +export const getDoc = async ({ + id, + withoutContent, +}: DocParams): Promise => { + const params = withoutContent ? '?without_content=true' : ''; + const response = await fetchAPI(`documents/${id}/${params}`); if (!response.ok) { throw new APIError('Failed to get the doc', await errorCauses(response)); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx index 64640f9ec1..75f1981c90 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx @@ -53,7 +53,7 @@ export interface Doc { title?: string; children?: Doc[]; childrenCount?: number; - content: Base64; + content?: Base64; created_at: string; creator: string; deleted_at: string | null; diff --git a/src/frontend/servers/y-provider/__tests__/collaborationBackend.test.ts b/src/frontend/servers/y-provider/__tests__/collaborationBackend.test.ts index d5b7678f37..561ece8767 100644 --- a/src/frontend/servers/y-provider/__tests__/collaborationBackend.test.ts +++ b/src/frontend/servers/y-provider/__tests__/collaborationBackend.test.ts @@ -19,10 +19,13 @@ describe('CollaborationBackend', () => { const { fetchDocument } = await import('@/api/collaborationBackend'); const documentId = 'test-document-123'; - await fetchDocument(documentId, { cookie: 'test-cookie' }); + await fetchDocument( + { name: documentId, withoutContent: true }, + { cookie: 'test-cookie' }, + ); expect(axiosGetSpy).toHaveBeenCalledWith( - `http://app-dev:8000/api/v1.0/documents/${documentId}/`, + `http://app-dev:8000/api/v1.0/documents/${documentId}/?without_content=true`, expect.objectContaining({ headers: expect.objectContaining({ 'X-Y-Provider-Key': 'test-yprovider-key', diff --git a/src/frontend/servers/y-provider/__tests__/hocuspocusWS.test.ts b/src/frontend/servers/y-provider/__tests__/hocuspocusWS.test.ts index eaaa785e5f..3a23f90bdb 100644 --- a/src/frontend/servers/y-provider/__tests__/hocuspocusWS.test.ts +++ b/src/frontend/servers/y-provider/__tests__/hocuspocusWS.test.ts @@ -228,7 +228,7 @@ describe('Server Tests', () => { wsHocus.stopConnectionAttempt(); expect(data.reason).toBe('permission-denied'); expect(fetchDocumentMock).toHaveBeenCalledExactlyOnceWith( - room, + { name: room, withoutContent: true }, expect.any(Object), ); wsHocus.webSocket?.close(); @@ -273,7 +273,7 @@ describe('Server Tests', () => { wsHocus.stopConnectionAttempt(); expect(data.reason).toBe('permission-denied'); expect(fetchDocumentMock).toHaveBeenCalledExactlyOnceWith( - room, + { name: room, withoutContent: true }, expect.any(Object), ); wsHocus.webSocket?.close(); @@ -322,7 +322,7 @@ describe('Server Tests', () => { wsHocus.destroy(); expect(fetchDocumentMock).toHaveBeenCalledWith( - room, + { name: room, withoutContent: true }, expect.any(Object), ); @@ -371,7 +371,7 @@ describe('Server Tests', () => { wsHocus.destroy(); expect(fetchDocumentMock).toHaveBeenCalledWith( - room, + { name: room, withoutContent: true }, expect.any(Object), ); diff --git a/src/frontend/servers/y-provider/src/api/collaborationBackend.ts b/src/frontend/servers/y-provider/src/api/collaborationBackend.ts index 6fca2b84f3..f0ec850aac 100644 --- a/src/frontend/servers/y-provider/src/api/collaborationBackend.ts +++ b/src/frontend/servers/y-provider/src/api/collaborationBackend.ts @@ -17,7 +17,7 @@ type Base64 = string; interface Doc { id: string; title?: string; - content: Base64; + content?: Base64; creator: string; is_favorite: boolean; link_reach: 'restricted' | 'public' | 'authenticated'; @@ -74,10 +74,11 @@ async function fetch( } export function fetchDocument( - name: string, + { name, withoutContent }: { name: string; withoutContent?: boolean }, requestHeaders: IncomingHttpHeaders, ): Promise { - return fetch(`/api/v1.0/documents/${name}/`, requestHeaders); + const params = withoutContent ? '?without_content=true' : ''; + return fetch(`/api/v1.0/documents/${name}/${params}`, requestHeaders); } export function fetchCurrentUser( diff --git a/src/frontend/servers/y-provider/src/servers/hocuspocusServer.ts b/src/frontend/servers/y-provider/src/servers/hocuspocusServer.ts index 5b2fde0c05..1686960b94 100644 --- a/src/frontend/servers/y-provider/src/servers/hocuspocusServer.ts +++ b/src/frontend/servers/y-provider/src/servers/hocuspocusServer.ts @@ -39,7 +39,10 @@ export const hocuspocusServer = new Server({ let canEdit; try { - const document = await fetchDocument(documentName, requestHeaders); + const document = await fetchDocument( + { name: documentName, withoutContent: true }, + requestHeaders, + ); if (!document.abilities.retrieve) { logger(