From 5b62a853c152c85c1b8d07b9475419c2bbba57a0 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 15 Oct 2026 08:08:31 +0200 Subject: [PATCH] fix(attachments): normalize attachment names to Unicode NFC Files uploaded from macOS in Chrome or Firefox have NFD names, while the filecache, and therefore the attachment list, stores names in NFC. The upload response returned the raw NFD name, so the editor inserted an NFD `src`. AttachmentResolver compared names exactly, found no matching attachment and fell back to loading the file as an image, which failed. Normalize uploaded names to NFC before saving them, and compare names in NFC in AttachmentResolver so that documents that already contain NFD srcs resolve as well. Fixes: #7873 Refs: nextcloud/server#65514 Refs: nextcloud/collectives#2327 Signed-off-by: Jonas Assisted-by: ClaudeCode:claude-opus-5-5 --- lib/Service/AttachmentService.php | 5 +++++ src/services/AttachmentResolver.js | 6 +++++- src/tests/services/AttachmentResolver.spec.js | 21 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/Service/AttachmentService.php b/lib/Service/AttachmentService.php index 8ecc9f6a612..94a74020ea3 100755 --- a/lib/Service/AttachmentService.php +++ b/lib/Service/AttachmentService.php @@ -284,6 +284,11 @@ public function uploadAttachment(int $documentId, string $newFileName, $newFileR throw new NotPermittedException('No write permissions'); } $saveDir = $this->getAttachmentDirectoryForFile($textFile, true); + // The filecache stores names in Unicode NFC, while uploads from macOS may use NFD + $normalizedFileName = \Normalizer::normalize($newFileName, \Normalizer::FORM_C); + if ($normalizedFileName !== false) { + $newFileName = $normalizedFileName; + } $fileName = self::getUniqueFileName($saveDir, $newFileName); $this->filenameValidator->validateFilename($fileName); $savedFile = $saveDir->newFile($fileName, $newFileResource); diff --git a/src/services/AttachmentResolver.js b/src/services/AttachmentResolver.js index 48f9ce921b0..06b7a2fbe6d 100644 --- a/src/services/AttachmentResolver.js +++ b/src/services/AttachmentResolver.js @@ -36,7 +36,11 @@ export default class AttachmentResolver { } #findAttachment(fileName) { - return this.#attachmentList.find((a) => a.name === fileName) + // Attachment srcs may use NFD (e.g. uploads from macOS), the attachment list uses NFC + const normalizedName = fileName.normalize('NFC') + return this.#attachmentList.find( + (a) => a.name.normalize('NFC') === normalizedName, + ) } /* diff --git a/src/tests/services/AttachmentResolver.spec.js b/src/tests/services/AttachmentResolver.spec.js index f1593201a39..b665bf7cc25 100644 --- a/src/tests/services/AttachmentResolver.spec.js +++ b/src/tests/services/AttachmentResolver.spec.js @@ -100,4 +100,25 @@ describe('Image resolver', () => { 'http://localhost:3000/remote.php/dav/files/user-uid/parentDir/path/to/some%20image.png', ) }) + + it('matches attachment names regardless of Unicode normalization', async () => { + const nfcName = 'B\u00FCcher.pdf' + const nfdName = 'Bu\u0308cher.pdf' + vi.spyOn(axios, 'post').mockReturnValue({ + data: [ + { + fileId: 1237, + name: nfcName, + mimetype: 'application/pdf', + isImage: false, + }, + ], + }) + const resolver = new AttachmentResolver({ session }) + const attachment = await resolver.resolve( + `.attachments.${session.documentId}/${encodeURIComponent(nfdName)}`, + ) + expect(attachment.isImage).toBe(false) + expect(attachment.name).toBe(nfcName) + }) })