From de55df7763b348723f4874042e13e618cbf7a1f4 Mon Sep 17 00:00:00 2001 From: erseco Date: Sun, 31 May 2026 20:34:44 +0100 Subject: [PATCH] fix: store imported files under a clean aiscan folder Replace the confusing 'aiscan_tmp' MyFiles subfolder with 'aiscan' so imported attachments no longer look temporary in the file library (#36). - Add StoragePathHelper to centralize the storage path and sanitize file names with basename() (defense against path traversal). - Route every aiscan_tmp reference (controller, AttachmentService, ExtractionService) through the helper. - Stabilize the MIME fallback controller test with deterministic content. - Complete the LGPL headers on the new files to match the project standard. --- Controller/AiScanInvoice.php | 7 ++-- Lib/AttachmentService.php | 6 ++-- Lib/ExtractionService.php | 2 +- Lib/StoragePathHelper.php | 41 +++++++++++++++++++++++ Test/main/AiScanInvoiceControllerTest.php | 6 ++-- Test/main/StoragePathHelperTest.php | 41 +++++++++++++++++++++++ 6 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 Lib/StoragePathHelper.php create mode 100644 Test/main/StoragePathHelperTest.php diff --git a/Controller/AiScanInvoice.php b/Controller/AiScanInvoice.php index 2863833..2a2d227 100644 --- a/Controller/AiScanInvoice.php +++ b/Controller/AiScanInvoice.php @@ -27,6 +27,7 @@ use FacturaScripts\Plugins\AiScan\Lib\ExtractionService; use FacturaScripts\Plugins\AiScan\Lib\HistoricalContextService; use FacturaScripts\Plugins\AiScan\Lib\InvoiceMapper; +use FacturaScripts\Plugins\AiScan\Lib\StoragePathHelper; use FacturaScripts\Plugins\AiScan\Lib\SupplierMatcher; use FacturaScripts\Plugins\AiScan\Model\AiScanImportBatch; use FacturaScripts\Plugins\AiScan\Model\AiScanImportDocument; @@ -259,7 +260,7 @@ private function storeUploadedFile(array $file, int $clientIndex): array ); } - $tmpDir = FS_FOLDER . '/MyFiles/aiscan_tmp'; + $tmpDir = StoragePathHelper::absoluteDirectory(); if (!is_dir($tmpDir)) { mkdir($tmpDir, 0700, true); } @@ -341,7 +342,7 @@ private function handleAnalyze(): void echo json_encode(['error' => Tools::lang()->trans('aiscan-invalid-file-name')]); return; } - $tmpPath = FS_FOLDER . '/MyFiles/aiscan_tmp/' . $tmpFile; + $tmpPath = StoragePathHelper::absoluteFile($tmpFile); if (!file_exists($tmpPath)) { http_response_code(404); @@ -631,7 +632,7 @@ private function handleGetText(): void echo json_encode(['error' => Tools::lang()->trans('aiscan-invalid-file-name')]); return; } - $tmpPath = FS_FOLDER . '/MyFiles/aiscan_tmp/' . $tmpFile; + $tmpPath = StoragePathHelper::absoluteFile($tmpFile); if (!file_exists($tmpPath)) { http_response_code(404); diff --git a/Lib/AttachmentService.php b/Lib/AttachmentService.php index fa974ce..eb9365a 100644 --- a/Lib/AttachmentService.php +++ b/Lib/AttachmentService.php @@ -35,8 +35,8 @@ public function attachTemporaryFile(FacturaProveedor $invoice, array $uploadData return; } - $tmpDir = realpath(FS_FOLDER . '/MyFiles/aiscan_tmp'); - $tmpPath = realpath(FS_FOLDER . '/MyFiles/aiscan_tmp/' . $tmpFile); + $tmpDir = realpath(StoragePathHelper::absoluteDirectory()); + $tmpPath = realpath(StoragePathHelper::absoluteFile($tmpFile)); $prefix = false === $tmpDir ? '' : rtrim($tmpDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; if ( false === $tmpDir @@ -58,7 +58,7 @@ public function attachTemporaryFile(FacturaProveedor $invoice, array $uploadData } $attachedFile = new AttachedFile(); - $attachedFile->path = 'aiscan_tmp/' . $tmpFile; + $attachedFile->path = StoragePathHelper::relativeFile($tmpFile); if (false === $attachedFile->save()) { return; } diff --git a/Lib/ExtractionService.php b/Lib/ExtractionService.php index a5f3252..62003b5 100644 --- a/Lib/ExtractionService.php +++ b/Lib/ExtractionService.php @@ -638,7 +638,7 @@ public static function extractPdfText(string $filePath): string } $realPath = realpath($filePath); - $expectedDir = realpath(FS_FOLDER . '/MyFiles/aiscan_tmp'); + $expectedDir = realpath(StoragePathHelper::absoluteDirectory()); if ($realPath === false || $expectedDir === false || strpos($realPath, $expectedDir) !== 0) { return ''; } diff --git a/Lib/StoragePathHelper.php b/Lib/StoragePathHelper.php new file mode 100644 index 0000000..3f5fb8c --- /dev/null +++ b/Lib/StoragePathHelper.php @@ -0,0 +1,41 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +namespace FacturaScripts\Plugins\AiScan\Lib; + +final class StoragePathHelper +{ + private const DIRECTORY = 'aiscan'; + + public static function absoluteDirectory(): string + { + return FS_FOLDER . '/MyFiles/' . self::DIRECTORY; + } + + public static function absoluteFile(string $filename): string + { + return self::absoluteDirectory() . '/' . basename($filename); + } + + public static function relativeFile(string $filename): string + { + return self::DIRECTORY . '/' . basename($filename); + } +} diff --git a/Test/main/AiScanInvoiceControllerTest.php b/Test/main/AiScanInvoiceControllerTest.php index 2823597..77f54d7 100644 --- a/Test/main/AiScanInvoiceControllerTest.php +++ b/Test/main/AiScanInvoiceControllerTest.php @@ -50,15 +50,15 @@ public function testNormalizeUploadedFilesExpandsMultipleUploadShape(): void $this->assertSame('image/png', $result[1]['type']); } - public function testResolveMimeTypeFallsBackToExtensionForOctetStream(): void + public function testResolveMimeTypeFallsBackToExtensionForGenericMimeTypes(): void { $controller = $this->buildController(); - $tmpFile = tempnam(sys_get_temp_dir(), 'aiscan-octet-'); + $tmpFile = tempnam(sys_get_temp_dir(), 'aiscan-generic-'); if (false === $tmpFile) { self::fail('Failed to create temporary file for MIME fallback test.'); } - file_put_contents($tmpFile, random_bytes(32)); + file_put_contents($tmpFile, 'This is plain text content without PDF structure.'); try { $result = $this->callResolveMimeType($controller, $tmpFile, 'pdf'); diff --git a/Test/main/StoragePathHelperTest.php b/Test/main/StoragePathHelperTest.php new file mode 100644 index 0000000..bb16212 --- /dev/null +++ b/Test/main/StoragePathHelperTest.php @@ -0,0 +1,41 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +namespace FacturaScripts\Test\Plugins; + +use FacturaScripts\Plugins\AiScan\Lib\StoragePathHelper; +use PHPUnit\Framework\TestCase; + +final class StoragePathHelperTest extends TestCase +{ + public function testAbsoluteDirectoryUsesCleanAiScanFolder(): void + { + $this->assertSame(FS_FOLDER . '/MyFiles/aiscan', StoragePathHelper::absoluteDirectory()); + } + + public function testAbsoluteAndRelativeFileSanitizeNestedInput(): void + { + $this->assertSame( + FS_FOLDER . '/MyFiles/aiscan/invoice.pdf', + StoragePathHelper::absoluteFile('../nested/invoice.pdf') + ); + $this->assertSame('aiscan/invoice.pdf', StoragePathHelper::relativeFile('../nested/invoice.pdf')); + } +}