From d983113999c81794563645c62da01158bf39f3cc 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 2373dfb0a47..a7b213c2e78 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) + }) })