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')
}