Skip to content

Commit b9783c6

Browse files
authored
feat: implement Dav for groups (#6799)
1 parent 4ed4536 commit b9783c6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1270
-135
lines changed

app/Domains/Contact/Dav/Exporter.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,12 @@ protected function escape(mixed $value): ?string
1010

1111
return ! empty($value) ? trim($value) : null;
1212
}
13+
14+
/**
15+
* Formats and returns a string for DAV Card/Cal.
16+
*/
17+
protected function formatValue(?string $value): ?string
18+
{
19+
return ! empty($value) ? str_replace('\;', ';', trim($value)) : null;
20+
}
1321
}

app/Domains/Contact/Dav/Services/ImportVCard.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,15 +178,15 @@ private function processEntry(VCard $entry, string $vcard): array
178178
];
179179
}
180180

181-
return $this->processEntryContact($entry, $vcard);
181+
return $this->processEntryCard($entry, $vcard);
182182
}
183183

184184
/**
185185
* Process entry importation.
186186
*
187187
* @return array<string,mixed>
188188
*/
189-
private function processEntryContact(VCard $entry, string $vcard): array
189+
private function processEntryCard(VCard $entry, string $vcard): array
190190
{
191191
$result = $this->importEntry($entry);
192192

app/Domains/Contact/ManageContact/Dav/ImportContact.php

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ public function import(VCard $vcard, ?VCardResource $result): ?VCardResource
6868
if ($this->context->external) {
6969
$contact->distant_etag = Arr::get($this->context->data, 'etag');
7070
$contact->distant_uri = $uri;
71-
}
7271

73-
$contact->save();
72+
$contact->save();
73+
}
7474

7575
return $contact;
7676
});
@@ -114,22 +114,17 @@ private function getExistingContact(VCard $vcard): ?Contact
114114
*/
115115
private function getContactData(?Contact $contact): array
116116
{
117-
$result = [
117+
return [
118118
'account_id' => $this->account()->id,
119119
'vault_id' => $this->vault()->id,
120120
'author_id' => $this->author()->id,
121-
'first_name' => $contact ? $contact->first_name : null,
122-
'last_name' => $contact ? $contact->last_name : null,
123-
'middle_name' => $contact ? $contact->middle_name : null,
124-
'nickname' => $contact ? $contact->nickname : null,
121+
'contact_id' => optional($contact)->id,
122+
'first_name' => optional($contact)->first_name,
123+
'last_name' => optional($contact)->last_name,
124+
'middle_name' => optional($contact)->middle_name,
125+
'nickname' => optional($contact)->nickname,
125126
'gender_id' => $contact ? $contact->gender_id : $this->getGender('O')->id,
126127
];
127-
128-
if ($contact) {
129-
$result['contact_id'] = $contact->id;
130-
}
131-
132-
return $result;
133128
}
134129

135130
/**
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace App\Domains\Contact\ManageGroups\Dav;
4+
5+
use App\Domains\Contact\Dav\Exporter;
6+
use App\Domains\Contact\Dav\ExportVCardResource;
7+
use App\Domains\Contact\Dav\Order;
8+
use App\Models\Group;
9+
use Sabre\VObject\Component\VCard;
10+
11+
/**
12+
* @implements ExportVCardResource<Group>
13+
*/
14+
#[Order(1)]
15+
class ExportKind extends Exporter implements ExportVCardResource
16+
{
17+
public function getType(): string
18+
{
19+
return Group::class;
20+
}
21+
22+
/**
23+
* @param Group $resource
24+
*/
25+
public function export($resource, VCard $vcard): void
26+
{
27+
$kind = collect($vcard->select('X-ADDRESSBOOKSERVER-KIND'))->first();
28+
29+
if ($kind) {
30+
$vcard->remove($kind);
31+
$vcard->add('X-ADDRESSBOOKSERVER-KIND', 'group');
32+
33+
return;
34+
}
35+
36+
$vcard->remove('KIND');
37+
$vcard->add('KIND', 'group');
38+
}
39+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace App\Domains\Contact\ManageGroups\Dav;
4+
5+
use App\Domains\Contact\Dav\Exporter;
6+
use App\Domains\Contact\Dav\ExportVCardResource;
7+
use App\Domains\Contact\Dav\Order;
8+
use App\Models\Contact;
9+
use App\Models\Group;
10+
use Sabre\VObject\Component\VCard;
11+
12+
/**
13+
* @implements ExportVCardResource<Group>
14+
*/
15+
#[Order(20)]
16+
class ExportMembers extends Exporter implements ExportVCardResource
17+
{
18+
public function getType(): string
19+
{
20+
return Group::class;
21+
}
22+
23+
/**
24+
* @param Group $resource
25+
*/
26+
public function export($resource, VCard $vcard): void
27+
{
28+
$kind = collect($vcard->select('X-ADDRESSBOOKSERVER-KIND'))->first();
29+
30+
$this->exportType($resource, $vcard, $kind ? 'X-ADDRESSBOOKSERVER-MEMBER' : 'MEMBER');
31+
}
32+
33+
private function exportType($resource, VCard $vcard, string $type): void
34+
{
35+
$contacts = $resource->contacts
36+
->map(fn (Contact $contact): string => $contact->distant_uuid ?? $contact->id)
37+
->sort();
38+
39+
$current = collect($vcard->select($type));
40+
$members = $current
41+
->map(fn ($member): string => $this->formatValue((string) $member));
42+
43+
// Add new members
44+
foreach ($contacts as $contact) {
45+
if (! $members->contains($contact)) {
46+
$vcard->add($type, $contact);
47+
}
48+
}
49+
50+
// Remove old members
51+
foreach ($current as $member) {
52+
if (! $contacts->contains($this->formatValue((string) $member))) {
53+
$vcard->remove($member);
54+
}
55+
}
56+
}
57+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace App\Domains\Contact\ManageGroups\Dav;
4+
5+
use App\Domains\Contact\Dav\Exporter;
6+
use App\Domains\Contact\Dav\ExportVCardResource;
7+
use App\Domains\Contact\Dav\Order;
8+
use App\Models\Group;
9+
use Sabre\VObject\Component\VCard;
10+
11+
/**
12+
* @implements ExportVCardResource<Group>
13+
*/
14+
#[Order(10)]
15+
class ExportNames extends Exporter implements ExportVCardResource
16+
{
17+
public function getType(): string
18+
{
19+
return Group::class;
20+
}
21+
22+
/**
23+
* @param Group $resource
24+
*/
25+
public function export($resource, VCard $vcard): void
26+
{
27+
$vcard->remove('FN');
28+
$vcard->remove('N');
29+
30+
$vcard->add('FN', $this->escape($resource->name));
31+
32+
$vcard->add('N', [
33+
$this->escape($resource->name),
34+
]);
35+
}
36+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\Domains\Contact\ManageGroups\Dav;
4+
5+
use App\Domains\Contact\Dav\ExportVCardResource;
6+
use App\Domains\Contact\Dav\Order;
7+
use App\Models\Group;
8+
use Sabre\VObject\Component\VCard;
9+
10+
/**
11+
* @implements ExportVCardResource<Group>
12+
*/
13+
#[Order(1000)]
14+
class ExportTimestamp implements ExportVCardResource
15+
{
16+
public function getType(): string
17+
{
18+
return Group::class;
19+
}
20+
21+
/**
22+
* @param Group $resource
23+
*/
24+
public function export($resource, VCard $vcard): void
25+
{
26+
$vcard->remove('REV');
27+
28+
$vcard->REV = $resource->updated_at->format('Ymd\\THis\\Z');
29+
}
30+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
namespace App\Domains\Contact\ManageGroups\Dav;
4+
5+
use App\Domains\Contact\Dav\Importer;
6+
use App\Domains\Contact\Dav\ImportVCardResource;
7+
use App\Domains\Contact\Dav\Order;
8+
use App\Domains\Contact\Dav\VCardResource;
9+
use App\Domains\Contact\ManageGroups\Services\CreateGroup;
10+
use App\Domains\Contact\ManageGroups\Services\UpdateGroup;
11+
use App\Models\Group;
12+
use Illuminate\Database\Eloquent\ModelNotFoundException;
13+
use Illuminate\Support\Arr;
14+
use Sabre\VObject\Component\VCard;
15+
16+
#[Order(10)]
17+
class ImportGroup extends Importer implements ImportVCardResource
18+
{
19+
/**
20+
* Can import Group.
21+
*/
22+
public function can(VCard $vcard): bool
23+
{
24+
$kind = (string) $vcard->KIND;
25+
if ($kind == null) {
26+
$kind = (string) collect($vcard->select('X-ADDRESSBOOKSERVER-KIND'))->first();
27+
}
28+
29+
return $kind === 'group';
30+
}
31+
32+
/**
33+
* Import group.
34+
*/
35+
public function import(VCard $vcard, ?VCardResource $result): ?VCardResource
36+
{
37+
$group = $this->getExistingGroup($vcard);
38+
39+
$data = $this->getGroupData($group);
40+
$original = $data;
41+
42+
$data = $this->importUid($data, $vcard);
43+
$data = $this->importNames($data, $vcard);
44+
45+
if ($group === null) {
46+
$group = app(CreateGroup::class)->execute($data);
47+
} elseif ($data !== $original) {
48+
$group = app(UpdateGroup::class)->execute($data);
49+
}
50+
51+
if ($this->context->external && $group->distant_uuid === null) {
52+
$group->distant_uuid = $this->getUid($vcard);
53+
$group->save();
54+
}
55+
56+
return Group::withoutTimestamps(function () use ($group): Group {
57+
$uri = Arr::get($this->context->data, 'uri');
58+
if ($this->context->external) {
59+
$group->distant_etag = Arr::get($this->context->data, 'etag');
60+
$group->distant_uri = $uri;
61+
62+
$group->save();
63+
}
64+
65+
return $group;
66+
});
67+
}
68+
69+
/**
70+
* Get existing group.
71+
*/
72+
protected function getExistingGroup(VCard $vcard): ?Group
73+
{
74+
$group = null;
75+
76+
if (($uri = Arr::get($this->context->data, 'uri')) !== null) {
77+
$group = Group::firstWhere([
78+
'vault_id' => $this->vault()->id,
79+
'distant_uri' => $uri,
80+
]);
81+
}
82+
83+
if ($group === null && ($groupId = $this->getUid($vcard)) !== null) {
84+
$group = Group::firstWhere([
85+
'vault_id' => $this->vault()->id,
86+
'distant_uuid' => $groupId,
87+
]);
88+
}
89+
90+
if ($group !== null && $group->vault_id !== $this->vault()->id) {
91+
throw new ModelNotFoundException();
92+
}
93+
94+
return $group;
95+
}
96+
97+
/**
98+
* Get group data.
99+
*/
100+
private function getGroupData(?Group $group): array
101+
{
102+
return [
103+
'account_id' => $this->account()->id,
104+
'vault_id' => $this->vault()->id,
105+
'author_id' => $this->author()->id,
106+
'group_id' => optional($group)->id,
107+
'name' => optional($group)->name,
108+
];
109+
}
110+
111+
/**
112+
* Import names of the group.
113+
*/
114+
public function importNames(array $contactData, VCard $entry): array
115+
{
116+
if ($this->hasNameInN($entry)) {
117+
$contactData = $this->importNameFromN($contactData, $entry);
118+
} elseif ($this->hasFN($entry)) {
119+
$contactData = $this->importNameFromFN($contactData, $entry);
120+
} else {
121+
throw new \LogicException('Check if you can import entry!');
122+
}
123+
124+
return $contactData;
125+
}
126+
127+
private function hasNameInN(VCard $entry): bool
128+
{
129+
return $entry->N !== null && ! empty(Arr::get($entry->N->getParts(), '0'));
130+
}
131+
132+
private function hasFN(VCard $entry): bool
133+
{
134+
return ! empty((string) $entry->FN);
135+
}
136+
137+
private function importNameFromN(array $contactData, VCard $entry): array
138+
{
139+
$contactData['name'] = $this->formatValue(Arr::get($entry->N->getParts(), '0'));
140+
141+
return $contactData;
142+
}
143+
144+
private function importNameFromFN(array $contactData, VCard $entry): array
145+
{
146+
$contactData['name'] = (string) $entry->FN;
147+
148+
return $contactData;
149+
}
150+
}

0 commit comments

Comments
 (0)