From 3fea8a85c884e0d1520096648da6ee7e8b6c598e Mon Sep 17 00:00:00 2001 From: Louis Mazin Date: Wed, 14 Oct 2026 19:17:56 +0200 Subject: [PATCH] perfect --- backend/drive/document_formats.py | 101 ++++++++++++++++++ backend/drive/templates/blank.odp | Bin 0 -> 1546 bytes backend/drive/templates/blank.ods | Bin 0 -> 1557 bytes backend/drive/templates/blank.odt | Bin 0 -> 1407 bytes backend/drive/test_create_documents.py | 63 +++++++++++ backend/drive/urls.py | 1 + backend/drive/views.py | 2 +- backend/drive/views_content.py | 7 +- backend/drive/views_items.py | 55 +++++++++- backend/drive/views_shares.py | 5 +- frontend/src/api/client.js | 1 + .../src/components/drive/DriveSidebar.jsx | 40 ++++++- .../drive/dialogs/ItemFormDialog.jsx | 2 +- .../drive/dialogs/PreviewDialog.jsx | 58 +++------- frontend/src/config/creatable-files.js | 34 ++++++ frontend/src/hooks/useDriveController.js | 4 +- frontend/src/index.css | 19 +--- frontend/src/pages/PublicSharePage.jsx | 45 +++----- 18 files changed, 333 insertions(+), 104 deletions(-) create mode 100644 backend/drive/document_formats.py create mode 100644 backend/drive/templates/blank.odp create mode 100644 backend/drive/templates/blank.ods create mode 100644 backend/drive/templates/blank.odt create mode 100644 backend/drive/test_create_documents.py create mode 100644 frontend/src/config/creatable-files.js diff --git a/backend/drive/document_formats.py b/backend/drive/document_formats.py new file mode 100644 index 0000000..81e1706 --- /dev/null +++ b/backend/drive/document_formats.py @@ -0,0 +1,101 @@ +"""Small valid, editable blank ODF/DOCX documents and inert previews. + +No cloud APIs, external requests, macros or document execution. +""" +from html import escape +from io import BytesIO +from pathlib import Path +from zipfile import ZIP_DEFLATED, ZipFile, BadZipFile +import xml.etree.ElementTree as ET + +ODF_TYPES = { + 'odt': ('application/vnd.oasis.opendocument.text', 'text'), + 'ods': ('application/vnd.oasis.opendocument.spreadsheet', 'spreadsheet'), + 'odp': ('application/vnd.oasis.opendocument.presentation', 'presentation'), +} +BLANK_TYPES = {'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + **{ext: mime for ext, (mime, _) in ODF_TYPES.items()}, + **{ext: 'text/plain; charset=utf-8' for ext in ('txt', 'md', 'py', 'js', 'jsx', 'ts', 'tsx', 'css', 'html', 'json', 'xml', 'csv')}} +ODF_NS = 'urn:oasis:names:tc:opendocument:xmlns:' +OFFICE = '{' + ODF_NS + 'office:1.0}' +TEXT = '{' + ODF_NS + 'text:1.0}' +TABLE = '{' + ODF_NS + 'table:1.0}' +DRAW = '{' + ODF_NS + 'drawing:1.0}' +MAX_DOC_BYTES = 35 * 1024 * 1024 +MAX_CONTENT_BYTES = 12 * 1024 * 1024 + + +def blank_file(extension): + if extension not in BLANK_TYPES: + raise ValueError('Format non autorisé.') + if extension in ODF_TYPES: + # Valid empty templates tested with LibreOffice, no server dependency. + return (Path(__file__).with_name('templates') / f'blank.{extension}').read_bytes() + if extension != 'docx': + return b'' if extension != 'json' else b'{}\n' + out = BytesIO() + with ZipFile(out, 'w', ZIP_DEFLATED) as archive: + archive.writestr('[Content_Types].xml', '''''') + archive.writestr('_rels/.rels', '''''') + archive.writestr('word/document.xml', '''''') + return out.getvalue() + + +def render_odf_preview(path): + """Render plain text/cells/slides only; no untrusted HTML or external resources.""" + path = Path(path) + ext = path.suffix.lower().lstrip('.') + if ext not in ODF_TYPES or path.stat().st_size > MAX_DOC_BYTES: + raise ValueError('Format ou taille de document incompatible.') + try: + with ZipFile(path) as archive: + info = archive.getinfo('content.xml') + if info.file_size > MAX_CONTENT_BYTES: + raise ValueError('Document trop volumineux.') + with archive.open(info) as stream: + raw = stream.read(MAX_CONTENT_BYTES + 1) + if len(raw) > MAX_CONTENT_BYTES or b' 2500: break + tag = 'h2' if el.tag == TEXT + 'h' else 'p' + sections.append(f'<{tag}>' + (t(el) or ' ') + f'') + elif ext == 'ods': + for table in root.iter(TABLE + 'table'): + if len(sections) >= 12: break + sections.append('

' + escape(table.attrib.get(TABLE + 'name', 'Feuille')) + '

') + for row in table.iter(TABLE + 'table-row'): + count += 1 + if count > 1500: break + cells = [] + for cell in row: + if cell.tag in (TABLE + 'table-cell', TABLE + 'covered-table-cell'): + cell_text = t(cell) + repeat = min(int(cell.attrib.get(TABLE + 'number-columns-repeated', '1') or '1'), 100) + cells.extend([''] * min(repeat, max(0, 60 - len(cells)))) + if len(cells) >= 60: break + if cells: sections.append('' + ''.join(cells) + '') + sections.append('
' + cell_text + '
') + else: + for page in root.iter(DRAW + 'page'): + count += 1 + if count > 120: break + name = escape(page.attrib.get(DRAW + 'name', f'Diapositive {count}')) + paras = [t(p) for p in page.iter() if p.tag in (TEXT + 'p', TEXT + 'h')][:100] + sections.append('

' + name + '

' + ''.join('

' + x + '

' for x in paras) + '
') + content = ''.join(sections) or '

Document vide

' + return ('''
''' + + content + '
') diff --git a/backend/drive/templates/blank.odp b/backend/drive/templates/blank.odp new file mode 100644 index 0000000000000000000000000000000000000000..887ab36a942407eacdcab434518835260fb964d8 GIT binary patch literal 1546 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 literal 0 HcmV?d00001 diff --git a/backend/drive/templates/blank.odt b/backend/drive/templates/blank.odt new file mode 100644 index 0000000000000000000000000000000000000000..7b1bff8d7b83d599cbbdf7d7fa2ba19ceab8ef27 GIT binary patch literal 1407 zcmWIWW@Zs#0D-xkez9@KjCj<6Y!K!E;@r&K)RM}A)Wm{x`ETFgoaw#i?!hWkr*sJ6A7z zP)=wCgPTY0?JO(Ruz873_I|l@caz+WH>uCxiDzHf>aaWi_`glbf0cI$yOs%Abw&Mo zGXExz%$~&b@Zw!({;Sz<`YMg;pIzOoyLSTpaRlfeZXlhUpH~9%Imkm3ZTXrFc-+3v zV@geA@>;I-DE&3(E#@T$LncZ|3kDy2%iXFVp=IdWdB6MX&B&X#%Re+;lRRYO^SD@o zA!`-SzE2+t_q$%-pyMZbb?Ze1?Mri6pLhs=s$g5(>v!&IOo^^n&^e3FQ%#fCsG2t1 zn-m?@xA({+<1aN==G|DE-FP*yJgI!0Q*g8Oo!g&3%X{J% z)?5lnO|?5Mq_Q&Gb-9-H8}rMl+k}MI&35xl**a~z*h$~$k_jbRk>4s;U7D@9?4Z_Z zj;JkXJeRHaXg=gLtF-;n`u87RCgq-QTle2>&Fh;JxtB<7xwxI(?#zUAgNux|GPkxi zy03Y2bKT^C{ZdhPAgsSUD9rAvr6FKqqlPZZrnHI`RskVW?olf_CEcC zzpwduWE{v;-uC+JyND@2mGmn=zV8)R-uz>0HA*u#gcr-SBITtt(|bK=Pq~kJ1LVx=PuRs-Ti%gW&PW}%a40<-x7!vdi@RwkQ8SGzgy3HdzZ(!Gj9+E*`mMsJeh%(}Ux zHX%PZ z|MbFT%WId5t=!Nrwcqsj-Bls)8_p$LZ~49TSfrEx?C#D7b0#U38^@eEQ)H#>92qsU zY(@X6v#es-e^reo`Ck6yUv@~!>*$~L*|%PJoql-y{oH+w0p5&EBFwmp5?~O5!IDN0 z1uIw(y0DiS5IqbGOB#KFOhgHSt`9wLBJ{}u;}9vQqH9D?@(7Lpff|9OA2q1W) c2wmL3 File and LibreOffice safe previews.""" +import tempfile +from pathlib import Path +from zipfile import ZipFile +from django.contrib.auth import get_user_model +from django.test import SimpleTestCase, TestCase, override_settings +from rest_framework.test import APIClient +from .document_formats import ODF_TYPES, blank_file, render_odf_preview +from .models import DriveItem + + +class DocumentFormatsTests(SimpleTestCase): + def test_valid_blank_office_files_have_expected_structure(self): + for extension in ('docx', 'odt', 'ods', 'odp'): + with self.subTest(ext=extension): + content = blank_file(extension) + with tempfile.TemporaryDirectory() as temp: + p = Path(temp) / f'blank.{extension}' + p.write_bytes(content) + with ZipFile(p) as archive: + self.assertIsNone(archive.testzip()) + self.assertIn('word/document.xml' if extension == 'docx' else 'content.xml', archive.namelist()) + if extension in ODF_TYPES: + html = render_odf_preview(p) + self.assertIn(']>&x;') + with self.assertRaises(ValueError): + render_odf_preview(p) + + +class NewFileEndpointTests(TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + override = override_settings(DRIVE_STORAGE_ROOT=self.root, FILE_UPLOAD_TEMP_DIR=str(self.root / '.tmp')) + override.enable() + self.addCleanup(override.disable) + self.user = get_user_model().objects.create_superuser('newfilesadmin', 'new@example.com', 'secret') + self.client = APIClient() + self.client.force_authenticate(user=self.user) + self.space = self.client.get('/api/auth/me/').data['space_id'] + + def test_create_lo_file_and_reject_duplicate(self): + payload = {'name': 'Feuille.ods', 'type': 'ods', 'space': self.space} + response = self.client.post('/api/drive/files/create/', payload, format='json') + self.assertEqual(response.status_code, 201, response.data) + item = DriveItem.objects.get(pk=response.data['id']) + self.assertEqual(item.size, (self.root / 'Feuille.ods').stat().st_size) + self.assertTrue(item.storage_location.endswith('Feuille.ods')) + self.assertEqual(self.client.post('/api/drive/files/create/', payload, format='json').status_code, 409) + self.assertEqual(DriveItem.objects.filter(name='Feuille.ods').count(), 1) + + def test_forbidden_extensions_and_empty_extensions(self): + for payload in [{'name':'x.exe','type':'exe','space':self.space}, + {'name':'x.txt','type':'ods','space':self.space}]: + self.assertEqual(self.client.post('/api/drive/files/create/', payload, format='json').status_code, 400) diff --git a/backend/drive/urls.py b/backend/drive/urls.py index 2d3bd8a..33e4c9c 100644 --- a/backend/drive/urls.py +++ b/backend/drive/urls.py @@ -29,6 +29,7 @@ urlpatterns=[ path('trash/',views.empty_trash), path('stats/',views.stats), path('folders/',views.create_folder), + path('files/create/',views.create_file), path('uploads/',views.upload), path('uploads/start/',views.upload_start), path('uploads/pending/',views.upload_pending), diff --git a/backend/drive/views.py b/backend/drive/views.py index 292930b..6d8238f 100644 --- a/backend/drive/views.py +++ b/backend/drive/views.py @@ -4,7 +4,7 @@ Implementations are grouped by responsibility without changing route names. """ -from .views_items import (me, listing, stats, create_folder, update_item, +from .views_items import (me, listing, stats, create_folder, create_file, update_item, trash_item, restore_item, delete_item, empty_trash) from .views_transfers import (upload, upload_start, upload_status, upload_chunk, upload_finish, upload_cancel, upload_pending) diff --git a/backend/drive/views_content.py b/backend/drive/views_content.py index 49fae46..78397d0 100644 --- a/backend/drive/views_content.py +++ b/backend/drive/views_content.py @@ -12,6 +12,7 @@ from .services import error, get_item, readable from .spaces import storage_path from .permissions import item_rights from .word_preview import render_word_preview +from .document_formats import ODF_TYPES, render_odf_preview IMAGE_TYPES = {'image/jpeg','image/png','image/gif','image/webp','image/avif','image/bmp'} VIDEO_TYPES = {'video/mp4','video/webm','video/ogg','video/quicktime','video/x-m4v'} @@ -22,7 +23,7 @@ TEXT_TYPES = {'text/plain','text/csv','text/markdown','text/x-python','text/java def safe_preview_type(item): mime = (mimetypes.guess_type(item.name)[0] or item.mime_type or '').lower() - if item.name.lower().endswith('.docx'): + if item.name.lower().endswith(('.docx', '.odt', '.ods', '.odp')): return 'text/html; charset=utf-8' if mime in IMAGE_TYPES | VIDEO_TYPES | AUDIO_TYPES | {'application/pdf'}: return mime @@ -75,9 +76,9 @@ def content(request,item_id): if not path.is_file():raise Http404 size=path.stat().st_size content_type=media if inline=='1' else 'application/octet-stream' - if inline=='1' and item.name.lower().endswith('.docx'): + if inline=='1' and item.name.lower().endswith(('.docx', '.odt', '.ods', '.odp')): try: - html = render_word_preview(path) + html = render_word_preview(path) if item.name.lower().endswith('.docx') else render_odf_preview(path) except ValueError: raise Http404 response = HttpResponse(html if request.method=='GET' else '', content_type='text/html; charset=utf-8') diff --git a/backend/drive/views_items.py b/backend/drive/views_items.py index bc0ebb5..4153055 100644 --- a/backend/drive/views_items.py +++ b/backend/drive/views_items.py @@ -1,4 +1,6 @@ """Space-bound navigation, folder management and trash endpoints.""" +import mimetypes +import uuid from django.core.exceptions import ValidationError from django.db import transaction from django.db.models import Sum, Count @@ -8,10 +10,11 @@ from rest_framework.response import Response from .models import DriveItem, DriveSpace, ItemGrant from .spaces import ensure_space, ensure_private_space, is_admin, resolve_space, can_write_space, can_read_space from .permissions import has_space_right, item_rights -from .quotas import quota_state +from .quotas import quota_state, locked_quota_tree, assert_space_capacity, QuotaExceeded from .services import error, item_info, get_item, chain, readable, writable, valid_name, target_parent, writable_destination -from .storage_layout import assert_available, prepare_parent, relocate_item, move_to_trash, cleanup_empty_trash_container, NameConflict +from .storage_layout import assert_available, prepare_parent, relocate_item, move_to_trash, cleanup_empty_trash_container, file_location, NameConflict from .trash_cleanup import delete_trashed_item, has_active_uploads_in_tree +from .document_formats import BLANK_TYPES, blank_file @api_view(['GET']) @@ -128,6 +131,54 @@ def create_folder(request): return Response(item_info(folder,request.user),status=201) +@api_view(['POST']) +def create_file(request): + """Create a real, valid blank local file; never accept arbitrary file contents.""" + try: + extension = str(request.data.get('type', '')).lower().strip().lstrip('.') + if extension not in BLANK_TYPES: + return error('Type de fichier non autorisé.') + name = valid_name(request.data.get('name')) + if not name.lower().endswith('.' + extension): + return error('L’extension ne correspond pas au type demandé.') + space, parent = writable_destination(request.user, request.data.get('space'), request.data.get('parent')) + content = blank_file(extension) + except ValueError as exc: + return error(str(exc)) + except PermissionError as exc: + return error(str(exc), 403) + destination = None + created = False + committed = False + try: + with transaction.atomic(): + with locked_quota_tree(space): + assert_space_capacity(space, len(content)) + destination = assert_available(space, parent, name) + prepare_parent(space, parent) + with destination.open('xb') as outfile: + created = True + outfile.write(content) + node = DriveItem(owner=request.user, space=space, parent=parent, name=name, + is_folder=False, storage_key=str(uuid.uuid4()), + storage_location=file_location(space, parent, name), + size=len(content), mime_type=BLANK_TYPES[extension]) + node.full_clean() + node.save() + committed = True + return Response(item_info(node, request.user), status=201) + except (NameConflict, FileExistsError) as exc: + return error(str(exc), 409) + except QuotaExceeded as exc: + return error(str(exc), 413) + except (OSError, ValidationError) as exc: + return error(f'Création impossible : {exc}', 409) + finally: + # Do not leave an orphan file if a DB transaction rolls back. + if created and not committed and destination is not None: + destination.unlink(missing_ok=True) + + @api_view(['PATCH']) def update_item(request,item_id): item=get_item(item_id) diff --git a/backend/drive/views_shares.py b/backend/drive/views_shares.py index 4dc3a97..5ce7a34 100644 --- a/backend/drive/views_shares.py +++ b/backend/drive/views_shares.py @@ -18,6 +18,7 @@ from .models import DriveItem, ItemGrant, PublicItemLink from .services import error, get_item, readable, writable, chain, item_info from .spaces import storage_path from .word_preview import render_word_preview +from .document_formats import render_odf_preview from .permissions import item_rights, assert_item_delegation, available_item_share_rights, PRESETS from .views_content import safe_preview_type, read_bytes @@ -177,9 +178,9 @@ def public_file(request,token,item_id): if inline and not preview:return error('Aperçu indisponible pour ce format.',415) content_type=preview if inline else 'application/octet-stream' size=path.stat().st_size - if inline and item.name.lower().endswith('.docx'): + if inline and item.name.lower().endswith(('.docx', '.odt', '.ods', '.odp')): try: - html = render_word_preview(path) + html = render_word_preview(path) if item.name.lower().endswith('.docx') else render_odf_preview(path) except ValueError: raise Http404 response = HttpResponse(html if request.method=='GET' else '', content_type='text/html; charset=utf-8') diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 37c6515..b573aa6 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -80,6 +80,7 @@ export const drive={ updateUserAccess:(id,kind,spaceId,permissions)=>api.patch(`/drive/admin/users/${id}/access/${kind}/${spaceId}/`,{permissions}).then(r=>r.data), stats:params=>api.get('/drive/stats/',{params}).then(r=>r.data), folder:(name,parent,space)=>api.post('/drive/folders/',{name,parent,space}).then(r=>r.data), + createFile:(name,type,parent,space)=>api.post('/drive/files/create/',{name,type,parent,space}).then(r=>r.data), patch:(id,updates)=>api.patch(`/drive/items/${id}/`,updates).then(r=>r.data), trash:id=>api.post(`/drive/items/${id}/trash/`), emptyTrash:()=>api.delete('/drive/trash/').then(r=>r.data), diff --git a/frontend/src/components/drive/DriveSidebar.jsx b/frontend/src/components/drive/DriveSidebar.jsx index 70ff9b9..cc4b669 100644 --- a/frontend/src/components/drive/DriveSidebar.jsx +++ b/frontend/src/components/drive/DriveSidebar.jsx @@ -2,12 +2,14 @@ import { useCallback, useEffect, useState } from 'react' import { Button } from '@/components/ui/button' import ContextActions, {spaceOptions, mobileDots} from './ContextActions' import { ChevronDown, ChevronRight, MoreHorizontal, Plus } from 'lucide-react' -import { Database, FolderPlus, LogOut, FolderLock, FolderOpen, X, Users, Share2 } from 'lucide-react' +import { Database, FolderPlus, FilePlus2, FileText, ExternalLink, LogOut, FolderLock, FolderOpen, X, Users, Share2 } from 'lucide-react' +import { localFileGroups, googleFileTypes } from '@/config/creatable-files' +import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuLabel } from '@/components/ui/dropdown-menu' import { sections } from '@/config/navigation' import { fmtSize } from '@/lib/drive-format' export default function DriveSidebar({ controller, user, onLogout }) { - const { sidebar, view, search, setValue, setModal, setSidebar, section, stats, spaces, activeSpaceId, activeSpace, switchSpace, canWrite, + const { sidebar, view, search, setValue, setModal, setSidebar, section, stats, spaces, activeSpaceId, activeSpace, switchSpace, canWrite, setNewFileType, pendingUploads, cancelPendingUpload, uploads, setSpaceDialog, setManagedSpaceId } = controller const [spaceMenu,setSpaceMenu] = useState(null) @@ -55,6 +57,12 @@ export default function DriveSidebar({ controller, user, onLogout }) { setSidebar(false) } + const newFile = type => { + setNewFileType(type.extension) + setValue(`${type.suggestedName}.${type.extension}`) + setModal('new-file') + setSidebar(false) + } return (