Skip to content

Commit 59278b3

Browse files
feat: restrict calendar invitation users
Signed-off-by: SebastianKrupinski <[email protected]>
1 parent fbcc51b commit 59278b3

3 files changed

Lines changed: 144 additions & 68 deletions

File tree

lib/Controller/ContactController.php

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use OCP\AppFramework\Http\JSONResponse;
1919
use OCP\AppFramework\QueryException;
2020
use OCP\Contacts\IManager;
21+
use OCP\IAppConfig;
2122
use OCP\IRequest;
2223
use OCP\IUserManager;
2324
use Psr\Log\LoggerInterface;
@@ -41,6 +42,7 @@ public function __construct(
4142
private IAppManager $appManager,
4243
private IUserManager $userManager,
4344
private ContactsService $contactsService,
45+
private IAppConfig $appConfig,
4446
private LoggerInterface $logger,
4547
) {
4648
parent::__construct($appName, $request);
@@ -100,11 +102,16 @@ public function searchAttendee(string $search):JSONResponse {
100102
return new JSONResponse();
101103
}
102104

103-
$result = $this->contactsManager->search($search, ['FN', 'EMAIL'], ['enumeration' => false]);
105+
$externalAttendeesDisabled = $this->appConfig->getValueBool('dav', 'caldav_external_attendees_disabled', false);
106+
$result = $this->contactsManager->search($search, ['FN', 'EMAIL'], ['enumeration' => true]);
104107

105108
$contacts = [];
106109
foreach ($result as $r) {
107-
if ($this->contactsService->isSystemBook($r) || !$this->contactsService->hasEmail($r)) {
110+
if (!$this->contactsService->hasEmail($r)) {
111+
continue;
112+
}
113+
// When external attendees are disabled, only include system book contacts
114+
if ($externalAttendeesDisabled && !$this->contactsService->isSystemBook($r)) {
108115
continue;
109116
}
110117
$name = $this->contactsService->getNameFromContact($r);
@@ -122,24 +129,27 @@ public function searchAttendee(string $search):JSONResponse {
122129
];
123130
}
124131

125-
$groups = $this->contactsManager->search($search, ['CATEGORIES']);
126-
$groups = array_filter($groups, function ($group) {
127-
return $this->contactsService->hasEmail($group);
128-
});
129-
$filtered = $this->contactsService->filterGroupsWithCount($groups, $search);
130-
foreach ($filtered as $groupName => $count) {
131-
if ($count === 0) {
132-
continue;
132+
// Skip contact groups when external attendees are disabled
133+
if (!$externalAttendeesDisabled) {
134+
$groups = $this->contactsManager->search($search, ['CATEGORIES']);
135+
$groups = array_filter($groups, function ($group) {
136+
return $this->contactsService->hasEmail($group);
137+
});
138+
$filtered = $this->contactsService->filterGroupsWithCount($groups, $search);
139+
foreach ($filtered as $groupName => $count) {
140+
if ($count === 0) {
141+
continue;
142+
}
143+
$contacts[] = [
144+
'name' => $groupName,
145+
'emails' => ['mailto:' . urlencode($groupName) . '@group'],
146+
'lang' => '',
147+
'tzid' => '',
148+
'photo' => '',
149+
'type' => 'contactsgroup',
150+
'members' => $count,
151+
];
133152
}
134-
$contacts[] = [
135-
'name' => $groupName,
136-
'emails' => ['mailto:' . urlencode($groupName) . '@group'],
137-
'lang' => '',
138-
'tzid' => '',
139-
'photo' => '',
140-
'type' => 'contactsgroup',
141-
'members' => $count,
142-
];
143153
}
144154

145155
return new JSONResponse($contacts);

src/components/Editor/Invitees/InviteesListSearch.vue

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ import {
6262
} from '@nextcloud/vue'
6363
import debounce from 'debounce'
6464
import GoogleCirclesCommunitiesIcon from 'vue-material-design-icons/GoogleCirclesCommunities.vue'
65-
import { principalPropertySearchByDisplaynameOrEmail } from '../../../services/caldavService.js'
6665
import {
6766
circleGetMembers,
6867
circleSearchByName,
@@ -118,15 +117,14 @@ export default {
118117
if (query.length > 0) {
119118
const promises = [
120119
this.findAttendeesFromContactsAPI(query),
121-
this.findAttendeesFromDAV(query),
122120
]
123121
if (isCirclesEnabled) {
124122
promises.push(this.findAttendeesFromCircles(query))
125123
}
126124
127-
const [contactsResults, davResults, circleResults] = await Promise.all(promises)
125+
const results = await Promise.all(promises)
126+
const [contactsResults, circleResults] = results
128127
matches.push(...contactsResults)
129-
matches.push(...davResults)
130128
if (isCirclesEnabled) {
131129
matches.push(...circleResults)
132130
}
@@ -290,44 +288,6 @@ export default {
290288
}, [])
291289
},
292290
293-
async findAttendeesFromDAV(query) {
294-
let results
295-
try {
296-
results = await principalPropertySearchByDisplaynameOrEmail(query)
297-
} catch (error) {
298-
console.debug(error)
299-
return []
300-
}
301-
302-
return results.filter((principal) => {
303-
if (!principal.email) {
304-
return false
305-
}
306-
307-
if (this.alreadyInvitedEmails.includes(principal.email)) {
308-
return false
309-
}
310-
311-
// Do not include resources and rooms
312-
if (['ROOM', 'RESOURCE'].includes(principal.calendarUserType)) {
313-
return false
314-
}
315-
316-
return true
317-
}).map((principal) => {
318-
return {
319-
commonName: principal.displayname,
320-
calendarUserType: principal.calendarUserType,
321-
email: principal.email,
322-
language: principal.language,
323-
isUser: principal.calendarUserType === 'INDIVIDUAL',
324-
avatar: decodeURIComponent(principal.userId),
325-
hasMultipleEMails: false,
326-
dropdownName: principal.displayname ? [principal.displayname, principal.email].join(' ') : principal.email,
327-
}
328-
})
329-
},
330-
331291
async findAttendeesFromCircles(query) {
332292
let results
333293
try {

tests/php/unit/Controller/ContactControllerTest.php

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OCP\App\IAppManager;
1313
use OCP\AppFramework\Http\JSONResponse;
1414
use OCP\Contacts\IManager;
15+
use OCP\IAppConfig;
1516
use OCP\IRequest;
1617
use OCP\IUserManager;
1718
use PHPUnit\Framework\MockObject\MockObject;
@@ -34,6 +35,7 @@ class ContactControllerTest extends TestCase {
3435
/** @var IUserManager|MockObject */
3536
private $userManager;
3637
private ContactsService|MockObject $service;
38+
private IAppConfig|MockObject $appConfig;
3739

3840
/** @var ContactController */
3941
protected $controller;
@@ -49,13 +51,15 @@ protected function setUp():void {
4951
$this->appManager = $this->createMock(IAppManager::class);
5052
$this->userManager = $this->createMock(IUserManager::class);
5153
$this->service = $this->createMock(ContactsService::class);
54+
$this->appConfig = $this->createMock(IAppConfig::class);
5255
$this->logger = $this->createMock(NullLogger::class);
5356
$this->controller = new ContactController($this->appName,
5457
$this->request,
5558
$this->manager,
5659
$this->appManager,
5760
$this->userManager,
5861
$this->service,
62+
$this->appConfig,
5963
$this->logger,
6064
);
6165
}
@@ -338,6 +342,10 @@ public function testSearchAttendee():void {
338342
$this->manager->expects(self::once())
339343
->method('isEnabled')
340344
->willReturn(true);
345+
$this->appConfig->expects(self::once())
346+
->method('getValueBool')
347+
->with('dav', 'caldav.external_attendees_disabled', false)
348+
->willReturn(false);
341349
$this->service
342350
->method('hasEmail')
343351
->willReturnMap([
@@ -360,20 +368,23 @@ public function testSearchAttendee():void {
360368
[$user1, 'Person 1'],
361369
[$user2, 'Person 2'],
362370
[$user3, ''],
371+
[$user4, 'Person 3'],
363372
]);
364-
$this->service->expects(self::exactly(2))
373+
$this->service->expects(self::exactly(3))
365374
->method('getLanguageId')
366375
->willReturnMap([
367376
[$user1, 'de'],
368-
[$user3, 'en_us'],
377+
[$user2, null],
378+
[$user4, 'en_us'],
369379
]);
370-
$this->service->expects(self::exactly(2))
380+
$this->service->expects(self::exactly(3))
371381
->method('getTimezoneId')
372382
->willReturnMap([
373383
[$user1, 'Europe/Berlin'],
374-
[$user3, 'Australia/Adelaide'],
384+
[$user2, null],
385+
[$user4, 'Australia/Adelaide'],
375386
]);
376-
$this->service->expects(self::exactly(2))
387+
$this->service->expects(self::exactly(3))
377388
->method('getEmail')
378389
->willReturnMap([
379390
[$user1, [
@@ -382,7 +393,7 @@ public function testSearchAttendee():void {
382393
]
383394
],
384395
[$user2, ['[email protected]']],
385-
[$user3, ['foo5@example.com']],
396+
[$user4, ['foo4@example.com']],
386397
]);
387398
$this->service->method('getPhotoUri')
388399
->willReturnMap([
@@ -394,7 +405,7 @@ public function testSearchAttendee():void {
394405
$this->manager->expects(self::exactly(2))
395406
->method('search')
396407
->willReturnMap([
397-
['search 123', ['FN', 'EMAIL'], ['enumeration' => false], [$user1, $user2, $user3, $user4]],
408+
['search 123', ['FN', 'EMAIL'], ['enumeration' => true], [$user1, $user2, $user3, $user4]],
398409
['search 123', ['CATEGORIES'], [], [$user4]]
399410
]);
400411

@@ -421,6 +432,101 @@ public function testSearchAttendee():void {
421432
'tzid' => null,
422433
'photo' => null,
423434
'type' => 'individual'
435+
], [
436+
'name' => 'Person 3',
437+
'emails' => [
438+
439+
],
440+
'lang' => 'en_us',
441+
'tzid' => 'Australia/Adelaide',
442+
'photo' => null,
443+
'type' => 'individual'
444+
]
445+
], $response->getData());
446+
$this->assertEquals(200, $response->getStatus());
447+
}
448+
449+
public function testSearchAttendeeExternalAttendeesDisabled():void {
450+
$user1 = [
451+
'FN' => 'Person 1',
452+
'EMAIL' => [
453+
454+
455+
],
456+
'LANG' => 'de',
457+
'TZ' => 'Europe/Berlin',
458+
'PHOTO' => 'VALUE=uri:http://foo.bar'
459+
];
460+
$user2 = [
461+
'FN' => 'Person 2',
462+
'EMAIL' => '[email protected]',
463+
];
464+
$user3 = [
465+
'isLocalSystemBook' => true,
466+
'FN' => 'System User',
467+
'EMAIL' => '[email protected]',
468+
'LANG' => 'en',
469+
'TZ' => 'UTC',
470+
];
471+
472+
$this->manager->expects(self::once())
473+
->method('isEnabled')
474+
->willReturn(true);
475+
$this->appConfig->expects(self::once())
476+
->method('getValueBool')
477+
->with('dav', 'caldav.external_attendees_disabled', false)
478+
->willReturn(true);
479+
$this->service
480+
->method('hasEmail')
481+
->willReturnMap([
482+
[$user1, true],
483+
[$user2, true],
484+
[$user3, true],
485+
]);
486+
$this->service
487+
->method('isSystemBook')
488+
->willReturnMap([
489+
[$user1, false],
490+
[$user2, false],
491+
[$user3, true],
492+
]);
493+
$this->service
494+
->method('getNameFromContact')
495+
->willReturnMap([
496+
[$user3, 'System User'],
497+
]);
498+
$this->service->expects(self::once())
499+
->method('getLanguageId')
500+
->with($user3)
501+
->willReturn('en');
502+
$this->service->expects(self::once())
503+
->method('getTimezoneId')
504+
->with($user3)
505+
->willReturn('UTC');
506+
$this->service->expects(self::once())
507+
->method('getEmail')
508+
->with($user3)
509+
->willReturn(['[email protected]']);
510+
$this->service->expects(self::once())
511+
->method('getPhotoUri')
512+
->with($user3)
513+
->willReturn(null);
514+
$this->manager->expects(self::once())
515+
->method('search')
516+
->with('search 123', ['FN', 'EMAIL'], ['enumeration' => true])
517+
->willReturn([$user1, $user2, $user3]);
518+
519+
$response = $this->controller->searchAttendee('search 123');
520+
521+
$this->assertInstanceOf(JSONResponse::class, $response);
522+
$this->assertEquals([
523+
[
524+
'name' => 'System User',
525+
'emails' => ['[email protected]'],
526+
'lang' => 'en',
527+
'tzid' => 'UTC',
528+
'photo' => null,
529+
'type' => 'individual'
424530
]
425531
], $response->getData());
426532
$this->assertEquals(200, $response->getStatus());

0 commit comments

Comments
 (0)