Skip to content

Commit 5067285

Browse files
authored
Merge pull request #6998 from nextcloud/feat/federated-calendar-sharing
feat: federated calendar sharing
2 parents 9c8fd72 + d08a9ba commit 5067285

6 files changed

Lines changed: 149 additions & 11 deletions

File tree

src/components/AppNavigation/EditCalendarModal/ShareItem.vue

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
</p>
1717
</div>
1818

19-
<input :id="`${id}-can-edit`"
20-
:disabled="updatingSharee"
21-
:checked="sharee.writeable"
22-
type="checkbox"
23-
class="checkbox"
24-
@change="updatePermission">
25-
<label :for="`${id}-can-edit`">{{ $t('calendar', 'can edit') }}</label>
19+
<template v-if="canBeSharedWritable">
20+
<input :id="`${id}-can-edit`"
21+
:disabled="updatingSharee"
22+
:checked="sharee.writeable"
23+
type="checkbox"
24+
class="checkbox"
25+
@change="updatePermission">
26+
<label :for="`${id}-can-edit`">{{ $t('calendar', 'can edit') }}</label>
27+
</template>
2628

2729
<NcActions>
2830
<NcActionButton :disabled="updatingSharee"
@@ -93,6 +95,13 @@ export default {
9395
9496
return this.sharee.displayName
9597
},
98+
/**
99+
* @return {boolean}
100+
*/
101+
canBeSharedWritable() {
102+
// TODO: read-write sharing is not implemented for federated calendars yet
103+
return !this.sharee.isRemoteUser
104+
}
96105
},
97106
mounted() {
98107
this.updateShareeEmail()

src/components/AppNavigation/EditCalendarModal/SharingSearch.vue

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ export default {
7777
},
7878
computed: {
7979
...mapStores(usePrincipalsStore, useCalendarsStore),
80+
81+
/**
82+
* True, if the Nextcloud server supports read-only federated calendar shares.
83+
*
84+
* @return {boolean}
85+
*/
86+
supportsFederatedCalendars() {
87+
const nextcloudMajorVersion = parseInt(window.OC.config.version.split('.')[0])
88+
return nextcloudMajorVersion >= 32
89+
},
8090
},
8191
methods: {
8292
/**
@@ -88,15 +98,17 @@ export default {
8898
* @param {string} data.uri the sharing principalScheme uri
8999
* @param {boolean} data.isGroup is this a group ?
90100
* @param {boolean} data.isCircle is this a circle-group ?
101+
* @param {boolean} data.isRemoteUser is this a remote user (on a federated instance)?
91102
*/
92-
shareCalendar({ user, displayName, uri, isGroup, isCircle }) {
103+
shareCalendar({ user, displayName, uri, isGroup, isCircle, isRemoteUser }) {
93104
this.calendarsStore.shareCalendar({
94105
calendar: this.calendar,
95106
user,
96107
displayName,
97108
uri,
98109
isGroup,
99110
isCircle,
111+
isRemoteUser,
100112
})
101113
},
102114
/**
@@ -133,11 +145,13 @@ export default {
133145
if (query.length > 0) {
134146
const davPromise = this.findShareesFromDav(query, hiddenPrincipalSchemes, hiddenUrls)
135147
const ocsPromise = this.findShareesFromCircles(query, hiddenPrincipalSchemes, hiddenUrls)
148+
const remotePromise = this.findRemoteSharees(query)
136149
137-
const [davResults, ocsResults] = await Promise.all([davPromise, ocsPromise])
150+
const [davResults, ocsResults, remoteResults] = await Promise.all([davPromise, ocsPromise, remotePromise])
138151
this.usersOrGroups = [
139152
...davResults,
140153
...ocsResults,
154+
...remoteResults,
141155
]
142156
143157
this.isLoading = false
@@ -192,6 +206,7 @@ export default {
192206
uri: decodedPrincipalScheme,
193207
isGroup,
194208
isCircle: false,
209+
isRemoteUser: false,
195210
isNoUser: isGroup,
196211
search: query,
197212
email: result.email,
@@ -246,10 +261,60 @@ export default {
246261
uri: 'principal:principals/circles/' + circle.value.shareWith,
247262
isGroup: false,
248263
isCircle: true,
264+
isRemoteUser: false,
249265
isNoUser: true,
250266
search: query,
251267
}))
252268
},
269+
/**
270+
*
271+
* @param {string} query The search query
272+
* @return {Promise<object[]>}
273+
*/
274+
async findRemoteSharees(query) {
275+
if (!this.supportsFederatedCalendars) {
276+
return []
277+
}
278+
279+
let results
280+
try {
281+
results = await HttpClient.get(generateOcsUrl('apps/files_sharing/api/v1/') + 'sharees', {
282+
params: {
283+
format: 'json',
284+
search: query,
285+
perPage: 200,
286+
itemType: 'calendar',
287+
shareType: [4, 6, 9],
288+
lookup: false,
289+
},
290+
})
291+
} catch (error) {
292+
return []
293+
}
294+
295+
if (results.data.ocs.meta.status === 'failure') {
296+
return []
297+
}
298+
299+
const remoteUsers = []
300+
if (Array.isArray(results.data.ocs.data.remotes)) {
301+
remoteUsers.push(...results.data.ocs.data.remotes)
302+
}
303+
if (Array.isArray(results.data.ocs.data.exact.remotes)) {
304+
remoteUsers.push(...results.data.ocs.data.exact.remotes)
305+
}
306+
return remoteUsers.map((user) => ({
307+
user: user.uuid,
308+
displayName: `${user.name}@${user.value.server}`,
309+
icon: 'icon-circle',
310+
uri: `principal:principals/remote-users/${btoa(user.value.shareWith)}`,
311+
isGroup: false,
312+
isCircle: false,
313+
isRemoteUser: true,
314+
isNoUser: false,
315+
search: query,
316+
}))
317+
},
253318
},
254319
}
255320
</script>

src/models/calendarShare.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import {
66
PRINCIPAL_PREFIX_CIRCLE,
77
PRINCIPAL_PREFIX_GROUP,
8+
PRINCIPAL_PREFIX_REMOTE_USER,
89
PRINCIPAL_PREFIX_USER,
910
} from './consts.js'
1011

@@ -27,6 +28,8 @@ const getDefaultCalendarShareObject = (props = {}) => Object.assign({}, {
2728
isGroup: false,
2829
// Whether or not sharee is a user-defined group
2930
isCircle: false,
31+
// Whether or not sharee is a remote user (on a federated instance)
32+
isRemoteUser: false,
3033
// Uri necessary for deleting / updating share
3134
uri: null,
3235
}, props)
@@ -48,6 +51,8 @@ const mapDavShareeToCalendarShareObject = (sharee) => {
4851
displayName = decodeURIComponent(sharee.href.slice(28))
4952
} else if (sharee.href.startsWith(PRINCIPAL_PREFIX_USER)) {
5053
displayName = decodeURIComponent(sharee.href.slice(27))
54+
} else if (sharee.href.startsWith(PRINCIPAL_PREFIX_REMOTE_USER)) {
55+
displayName = atob(sharee.href.slice(34))
5156
} else {
5257
displayName = sharee.href
5358
}
@@ -56,6 +61,7 @@ const mapDavShareeToCalendarShareObject = (sharee) => {
5661
const isUser = sharee.href.startsWith(PRINCIPAL_PREFIX_USER)
5762
const isGroup = sharee.href.startsWith(PRINCIPAL_PREFIX_GROUP)
5863
const isCircle = sharee.href.startsWith(PRINCIPAL_PREFIX_CIRCLE)
64+
const isRemoteUser = sharee.href.startsWith(PRINCIPAL_PREFIX_REMOTE_USER)
5965
const uri = sharee.href
6066

6167
return getDefaultCalendarShareObject({
@@ -65,6 +71,7 @@ const mapDavShareeToCalendarShareObject = (sharee) => {
6571
isUser,
6672
isGroup,
6773
isCircle,
74+
isRemoteUser,
6875
uri,
6976
})
7077
}

src/models/consts.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const PRINCIPAL_PREFIX_GROUP = 'principal:principals/groups/'
2020
const PRINCIPAL_PREFIX_CIRCLE = 'principal:principals/circles/'
2121
const PRINCIPAL_PREFIX_CALENDAR_RESOURCE = 'principal:principals/calendar-resources/'
2222
const PRINCIPAL_PREFIX_CALENDAR_ROOM = 'principal:principals/calendar-rooms/'
23+
const PRINCIPAL_PREFIX_REMOTE_USER = 'principal:principals/remote-users/'
2324

2425
const CALDAV_BIRTHDAY_CALENDAR = 'contact_birthdays'
2526
const CALDAV_PERSONAL_CALENDAR = 'personal'
@@ -46,6 +47,7 @@ export {
4647
PRINCIPAL_PREFIX_CIRCLE,
4748
PRINCIPAL_PREFIX_CALENDAR_RESOURCE,
4849
PRINCIPAL_PREFIX_CALENDAR_ROOM,
50+
PRINCIPAL_PREFIX_REMOTE_USER,
4951
CALDAV_BIRTHDAY_CALENDAR,
5052
CALDAV_PERSONAL_CALENDAR,
5153
IMPORT_STAGE_DEFAULT,

src/store/calendars.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,8 +599,9 @@ export default defineStore('calendars', {
599599
* @param {string} data.uri the sharing principalScheme uri
600600
* @param {boolean} data.isGroup is this a group?
601601
* @param {boolean} data.isCircle is this a circle?
602+
* @param {boolean} data.isRemoteUser is this a remote user (on a federated instance)?
602603
*/
603-
async shareCalendar({ calendar, user, displayName, uri, isGroup, isCircle }) {
604+
async shareCalendar({ calendar, user, displayName, uri, isGroup, isCircle, isRemoteUser }) {
604605
// Share calendar with entered group or user
605606
await calendar.dav.share(uri)
606607
const newSharee = {
@@ -609,6 +610,7 @@ export default defineStore('calendars', {
609610
writeable: false,
610611
isGroup,
611612
isCircle,
613+
isRemoteUser,
612614
uri,
613615
}
614616
this.calendarsById[calendar.id].shares.push(newSharee)

tests/javascript/unit/models/calendarShare.test.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe('Test suite: Calendar share model (models/calendarShare.js)', () => {
1818
isUser: false,
1919
isGroup: false,
2020
isCircle: false,
21+
isRemoteUser: false,
2122
uri: null,
2223
})
2324
})
@@ -33,6 +34,7 @@ describe('Test suite: Calendar share model (models/calendarShare.js)', () => {
3334
isUser: false,
3435
isGroup: false,
3536
isCircle: false,
37+
isRemoteUser: false,
3638
uri: null,
3739
otherProp: 'foo',
3840
})
@@ -55,6 +57,7 @@ describe('Test suite: Calendar share model (models/calendarShare.js)', () => {
5557
isUser: true,
5658
isGroup: false,
5759
isCircle: false,
60+
isRemoteUser: false,
5861
uri: 'principal:principals/users/user4',
5962
})
6063
})
@@ -76,6 +79,7 @@ describe('Test suite: Calendar share model (models/calendarShare.js)', () => {
7679
isUser: true,
7780
isGroup: false,
7881
isCircle: false,
82+
isRemoteUser: false,
7983
uri: 'principal:principals/users/user4',
8084
})
8185
})
@@ -97,6 +101,7 @@ describe('Test suite: Calendar share model (models/calendarShare.js)', () => {
97101
isUser: false,
98102
isGroup: true,
99103
isCircle: false,
104+
isRemoteUser: false,
100105
uri: 'principal:principals/groups/admin',
101106
})
102107
})
@@ -118,6 +123,7 @@ describe('Test suite: Calendar share model (models/calendarShare.js)', () => {
118123
isUser: false,
119124
isGroup: false,
120125
isCircle: true,
126+
isRemoteUser: false,
121127
uri: 'principal:principals/circles/c479c14bd82415',
122128
})
123129
})
@@ -141,10 +147,57 @@ describe('Test suite: Calendar share model (models/calendarShare.js)', () => {
141147
isUser: false,
142148
isGroup: false,
143149
isCircle: true,
150+
isRemoteUser: false,
144151
uri: 'principal:principals/circles/c479c14bd82415',
145152
})
146153
})
147154

155+
it('should map a dav sharee to a calendar share object - remote user', () => {
156+
const davSharee = {
157+
// Decoded cloud id: marcus@federated.cloud.com
158+
'href': 'principal:principals/remote-users/bWFyY3VzQGZlZGVyYXRlZC5jbG91ZC5jb20=',
159+
'common-name': 'Marcus Beehler@federated.cloud.com',
160+
'invite-accepted': true,
161+
'access': [
162+
'{http://owncloud.org/ns}read'
163+
]
164+
}
165+
166+
expect(mapDavShareeToCalendarShareObject(davSharee)).toEqual({
167+
id: 'cHJpbmNpcGFsOnByaW5jaXBhbHMvcmVtb3RlLXVzZXJzL2JXRnlZM1Z6UUdabFpHVnlZWFJsWkM1amJHOTFaQzVqYjIwPQ==',
168+
displayName: 'Marcus Beehler@federated.cloud.com',
169+
writeable: false,
170+
isUser: false,
171+
isGroup: false,
172+
isCircle: false,
173+
isRemoteUser: true,
174+
uri: 'principal:principals/remote-users/bWFyY3VzQGZlZGVyYXRlZC5jbG91ZC5jb20=',
175+
})
176+
})
177+
178+
it('should map a dav sharee to a calendar share object - remote user without displayname', () => {
179+
const davSharee = {
180+
// Decoded cloud id: marcus@federated.cloud.com
181+
'href': 'principal:principals/remote-users/bWFyY3VzQGZlZGVyYXRlZC5jbG91ZC5jb20=',
182+
'common-name': '',
183+
'invite-accepted': true,
184+
'access': [
185+
'{http://owncloud.org/ns}read'
186+
]
187+
}
188+
189+
expect(mapDavShareeToCalendarShareObject(davSharee)).toEqual({
190+
id: 'cHJpbmNpcGFsOnByaW5jaXBhbHMvcmVtb3RlLXVzZXJzL2JXRnlZM1Z6UUdabFpHVnlZWFJsWkM1amJHOTFaQzVqYjIwPQ==',
191+
displayName: 'marcus@federated.cloud.com',
192+
writeable: false,
193+
isUser: false,
194+
isGroup: false,
195+
isCircle: false,
196+
isRemoteUser: true,
197+
uri: 'principal:principals/remote-users/bWFyY3VzQGZlZGVyYXRlZC5jbG91ZC5jb20=',
198+
})
199+
})
200+
148201
it('should properly handle sharee URIs with non-ascii characters', () => {
149202
const davSharee = {
150203
'href': 'principal:principals/groups/מַזָּל טוֹב',
@@ -162,8 +215,8 @@ describe('Test suite: Calendar share model (models/calendarShare.js)', () => {
162215
isUser: false,
163216
isGroup: true,
164217
isCircle: false,
218+
isRemoteUser: false,
165219
uri: 'principal:principals/groups/מַזָּל טוֹב',
166220
})
167221
})
168-
169222
})

0 commit comments

Comments
 (0)