From 267ac93415008f1e37558a63efedb234905e2771 Mon Sep 17 00:00:00 2001 From: Nicolai Ehrhardt <245527909+predictor2718@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:38:36 +0200 Subject: [PATCH] fix(files_sharing): prevent double-escaping of display names in sharing UI Signed-off-by: Nicolai Ehrhardt <245527909+predictor2718@users.noreply.github.com> --- .../src/components/SharingEntry.vue | 8 +- .../src/components/SharingEntryInherited.vue | 4 +- .../files_actions/sharingStatusAction.spec.ts | 109 ++++++++++++++++++ .../src/files_actions/sharingStatusAction.ts | 6 +- 4 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 apps/files_sharing/src/files_actions/sharingStatusAction.spec.ts diff --git a/apps/files_sharing/src/components/SharingEntry.vue b/apps/files_sharing/src/components/SharingEntry.vue index 24b7b3f9da6e5..65383d6bd42ff 100644 --- a/apps/files_sharing/src/components/SharingEntry.vue +++ b/apps/files_sharing/src/components/SharingEntry.vue @@ -93,7 +93,7 @@ export default { if (!this.isShareOwner && this.share.ownerDisplayName) { title += ' ' + t('files_sharing', 'by {initiator}', { initiator: this.share.ownerDisplayName, - }) + }, undefined, { escape: false }) } return title }, @@ -107,12 +107,12 @@ export default { owner: this.share.ownerDisplayName, } if (this.share.type === ShareType.Group) { - return t('files_sharing', 'Shared with the group {user} by {owner}', data) + return t('files_sharing', 'Shared with the group {user} by {owner}', data, undefined, { escape: false }) } else if (this.share.type === ShareType.Room) { - return t('files_sharing', 'Shared with the conversation {user} by {owner}', data) + return t('files_sharing', 'Shared with the conversation {user} by {owner}', data, undefined, { escape: false }) } - return t('files_sharing', 'Shared with {user} by {owner}', data) + return t('files_sharing', 'Shared with {user} by {owner}', data, undefined, { escape: false }) } return null }, diff --git a/apps/files_sharing/src/components/SharingEntryInherited.vue b/apps/files_sharing/src/components/SharingEntryInherited.vue index d58a229031016..a3e0b2c28fdec 100644 --- a/apps/files_sharing/src/components/SharingEntryInherited.vue +++ b/apps/files_sharing/src/components/SharingEntryInherited.vue @@ -15,13 +15,13 @@ class="sharing-entry__avatar" /> - {{ t('files_sharing', 'Added by {initiator}', { initiator: share.ownerDisplayName }) }} + {{ t('files_sharing', 'Added by {initiator}', { initiator: share.ownerDisplayName }, undefined, { escape: false }) }} - {{ t('files_sharing', 'Via “{folder}”', { folder: viaFolderName }) }} + {{ t('files_sharing', 'Via “{folder}”', { folder: viaFolderName }, undefined, { escape: false }) }} ({ + getCurrentUser: vi.fn(() => ({ uid: 'admin' })), +})) + +vi.mock('@nextcloud/sharing/public', () => ({ + isPublicShare: vi.fn(() => false), +})) + +const view = { + id: 'files', + name: 'Files', +} as IView + +beforeAll(() => { + (window as any)._oc_webroot = '' +}) + +describe('Sharing status action title tests', () => { + test('Title does not double-escape special characters in owner display name', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/SharedFolder', + owner: 'testuser', + mime: 'httpd/unix-directory', + permissions: Permission.ALL, + root: '/files/admin', + attributes: { + 'owner-display-name': 'bits & trees', + 'share-types': [ShareType.User], + }, + }) + + const title = action.title!({ + nodes: [file], + view, + folder: {} as IFolder, + contents: [], + }) + + expect(title).toBe('Shared by bits & trees') + expect(title).not.toContain('&') + }) + + test('Title does not double-escape special characters in sharee display name', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/SharedFolder', + owner: 'admin', + mime: 'httpd/unix-directory', + permissions: Permission.ALL | Permission.SHARE, + root: '/files/admin', + attributes: { + 'share-types': [ShareType.User], + sharees: { + sharee: [{ id: 'bob', 'display-name': 'Bob & Alice', type: ShareType.User }], + }, + }, + }) + + const title = action.title!({ + nodes: [file], + view, + folder: {} as IFolder, + contents: [], + }) + + expect(title).toBe('Shared with Bob & Alice') + expect(title).not.toContain('&') + }) + + test('Title does not double-escape special characters in group display name', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/SharedFolder', + owner: 'admin', + mime: 'httpd/unix-directory', + permissions: Permission.ALL | Permission.SHARE, + root: '/files/admin', + attributes: { + 'share-types': [ShareType.Group], + sharees: { + sharee: [{ id: 'dev-group', 'display-name': 'Dev & Ops', type: ShareType.Group }], + }, + }, + }) + + const title = action.title!({ + nodes: [file], + view, + folder: {} as IFolder, + contents: [], + }) + + expect(title).toBe('Shared with group Dev & Ops') + expect(title).not.toContain('&') + }) +}) diff --git a/apps/files_sharing/src/files_actions/sharingStatusAction.ts b/apps/files_sharing/src/files_actions/sharingStatusAction.ts index 086d507ad4953..75b71ecb8759f 100644 --- a/apps/files_sharing/src/files_actions/sharingStatusAction.ts +++ b/apps/files_sharing/src/files_actions/sharingStatusAction.ts @@ -47,7 +47,7 @@ export const action: IFileAction = { const node = nodes[0]! if (node.owner && (node.owner !== getCurrentUser()?.uid || isExternal(node))) { const ownerDisplayName = node?.attributes?.['owner-display-name'] - return t('files_sharing', 'Shared by {ownerDisplayName}', { ownerDisplayName }) + return t('files_sharing', 'Shared by {ownerDisplayName}', { ownerDisplayName }, undefined, { escape: false }) } const shareTypes = Object.values(node?.attributes?.['share-types'] || {}).flat() as number[] @@ -64,9 +64,9 @@ export const action: IFileAction = { const sharee = [sharees].flat()[0] // the property is sometimes weirdly normalized, so we need to compensate switch (sharee?.type) { case ShareType.User: - return t('files_sharing', 'Shared with {user}', { user: sharee['display-name'] }) + return t('files_sharing', 'Shared with {user}', { user: sharee['display-name'] }, undefined, { escape: false }) case ShareType.Group: - return t('files_sharing', 'Shared with group {group}', { group: sharee['display-name'] ?? sharee.id }) + return t('files_sharing', 'Shared with group {group}', { group: sharee['display-name'] ?? sharee.id }, undefined, { escape: false }) default: return t('files_sharing', 'Shared with others') }