Skip to content

Commit f769188

Browse files
committed
fix(files_sharing): prevent double-escaping of display names in sharing UI
Signed-off-by: Nicolai Ehrhardt <245527909+predictor2718@users.noreply.github.com>
1 parent afe0d44 commit f769188

4 files changed

Lines changed: 118 additions & 9 deletions

File tree

apps/files_sharing/src/components/SharingEntry.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export default {
9393
if (!this.isShareOwner && this.share.ownerDisplayName) {
9494
title += ' ' + t('files_sharing', 'by {initiator}', {
9595
initiator: this.share.ownerDisplayName,
96-
})
96+
}, undefined, { escape: false })
9797
}
9898
return title
9999
},
@@ -107,12 +107,12 @@ export default {
107107
owner: this.share.ownerDisplayName,
108108
}
109109
if (this.share.type === ShareType.Group) {
110-
return t('files_sharing', 'Shared with the group {user} by {owner}', data)
110+
return t('files_sharing', 'Shared with the group {user} by {owner}', data, undefined, { escape: false })
111111
} else if (this.share.type === ShareType.Room) {
112-
return t('files_sharing', 'Shared with the conversation {user} by {owner}', data)
112+
return t('files_sharing', 'Shared with the conversation {user} by {owner}', data, undefined, { escape: false })
113113
}
114114
115-
return t('files_sharing', 'Shared with {user} by {owner}', data)
115+
return t('files_sharing', 'Shared with {user} by {owner}', data, undefined, { escape: false })
116116
}
117117
return null
118118
},

apps/files_sharing/src/components/SharingEntryInherited.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
class="sharing-entry__avatar" />
1616
</template>
1717
<NcActionText icon="icon-user">
18-
{{ t('files_sharing', 'Added by {initiator}', { initiator: share.ownerDisplayName }) }}
18+
{{ t('files_sharing', 'Added by {initiator}', { initiator: share.ownerDisplayName }, undefined, { escape: false }) }}
1919
</NcActionText>
2020
<NcActionLink
2121
v-if="share.viaPath && share.viaFileid"
2222
icon="icon-folder"
2323
:href="viaFileTargetUrl">
24-
{{ t('files_sharing', 'Via “{folder}”', { folder: viaFolderName }) }}
24+
{{ t('files_sharing', 'Via “{folder}”', { folder: viaFolderName }, undefined, { escape: false }) }}
2525
</NcActionLink>
2626
<NcActionButton
2727
v-if="share.canDelete"
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { IFolder, IView } from '@nextcloud/files'
7+
8+
import { File, Permission } from '@nextcloud/files'
9+
import { ShareType } from '@nextcloud/sharing'
10+
import { beforeAll, describe, expect, test, vi } from 'vitest'
11+
import { action } from './sharingStatusAction.ts'
12+
13+
vi.mock('@nextcloud/auth', () => ({
14+
getCurrentUser: vi.fn(() => ({ uid: 'admin' })),
15+
}))
16+
17+
vi.mock('@nextcloud/sharing/public', () => ({
18+
isPublicShare: vi.fn(() => false),
19+
}))
20+
21+
const view = {
22+
id: 'files',
23+
name: 'Files',
24+
} as IView
25+
26+
beforeAll(() => {
27+
(window as any)._oc_webroot = ''
28+
})
29+
30+
describe('Sharing status action title tests', () => {
31+
test('Title does not double-escape special characters in owner display name', () => {
32+
const file = new File({
33+
id: 1,
34+
source: 'https://cloud.domain.com/remote.php/dav/files/admin/SharedFolder',
35+
owner: 'testuser',
36+
mime: 'httpd/unix-directory',
37+
permissions: Permission.ALL,
38+
root: '/files/admin',
39+
attributes: {
40+
'owner-display-name': 'bits & trees',
41+
'share-types': [ShareType.User],
42+
},
43+
})
44+
45+
const title = action.title!({
46+
nodes: [file],
47+
view,
48+
folder: {} as IFolder,
49+
contents: [],
50+
})
51+
52+
expect(title).toBe('Shared by bits & trees')
53+
expect(title).not.toContain('&amp;')
54+
})
55+
56+
test('Title does not double-escape special characters in sharee display name', () => {
57+
const file = new File({
58+
id: 1,
59+
source: 'https://cloud.domain.com/remote.php/dav/files/admin/SharedFolder',
60+
owner: 'admin',
61+
mime: 'httpd/unix-directory',
62+
permissions: Permission.ALL | Permission.SHARE,
63+
root: '/files/admin',
64+
attributes: {
65+
'share-types': [ShareType.User],
66+
sharees: {
67+
sharee: [{ id: 'bob', 'display-name': 'Bob & Alice', type: ShareType.User }],
68+
},
69+
},
70+
})
71+
72+
const title = action.title!({
73+
nodes: [file],
74+
view,
75+
folder: {} as IFolder,
76+
contents: [],
77+
})
78+
79+
expect(title).toBe('Shared with Bob & Alice')
80+
expect(title).not.toContain('&amp;')
81+
})
82+
83+
test('Title does not double-escape special characters in group display name', () => {
84+
const file = new File({
85+
id: 1,
86+
source: 'https://cloud.domain.com/remote.php/dav/files/admin/SharedFolder',
87+
owner: 'admin',
88+
mime: 'httpd/unix-directory',
89+
permissions: Permission.ALL | Permission.SHARE,
90+
root: '/files/admin',
91+
attributes: {
92+
'share-types': [ShareType.Group],
93+
sharees: {
94+
sharee: [{ id: 'dev-group', 'display-name': 'Dev & Ops', type: ShareType.Group }],
95+
},
96+
},
97+
})
98+
99+
const title = action.title!({
100+
nodes: [file],
101+
view,
102+
folder: {} as IFolder,
103+
contents: [],
104+
})
105+
106+
expect(title).toBe('Shared with group Dev & Ops')
107+
expect(title).not.toContain('&amp;')
108+
})
109+
})

apps/files_sharing/src/files_actions/sharingStatusAction.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const action: IFileAction = {
4747
const node = nodes[0]!
4848
if (node.owner && (node.owner !== getCurrentUser()?.uid || isExternal(node))) {
4949
const ownerDisplayName = node?.attributes?.['owner-display-name']
50-
return t('files_sharing', 'Shared by {ownerDisplayName}', { ownerDisplayName })
50+
return t('files_sharing', 'Shared by {ownerDisplayName}', { ownerDisplayName }, undefined, { escape: false })
5151
}
5252

5353
const shareTypes = Object.values(node?.attributes?.['share-types'] || {}).flat() as number[]
@@ -64,9 +64,9 @@ export const action: IFileAction = {
6464
const sharee = [sharees].flat()[0] // the property is sometimes weirdly normalized, so we need to compensate
6565
switch (sharee?.type) {
6666
case ShareType.User:
67-
return t('files_sharing', 'Shared with {user}', { user: sharee['display-name'] })
67+
return t('files_sharing', 'Shared with {user}', { user: sharee['display-name'] }, undefined, { escape: false })
6868
case ShareType.Group:
69-
return t('files_sharing', 'Shared with group {group}', { group: sharee['display-name'] ?? sharee.id })
69+
return t('files_sharing', 'Shared with group {group}', { group: sharee['display-name'] ?? sharee.id }, undefined, { escape: false })
7070
default:
7171
return t('files_sharing', 'Shared with others')
7272
}

0 commit comments

Comments
 (0)