refactor: replace custom CRM with Monica fork
Some checks failed
Build & Push Monica Image to Gitea Registry / build-and-push (push) Failing after 9s
Some checks failed
Build & Push Monica Image to Gitea Registry / build-and-push (push) Failing after 9s
This commit is contained in:
91
app/Services/DavClient/CreateAddressBookSubscription.php
Normal file
91
app/Services/DavClient/CreateAddressBookSubscription.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Services\BaseService;
|
||||
use function Safe\preg_replace;
|
||||
use App\Models\Account\AddressBook;
|
||||
use App\Models\Account\AddressBookSubscription;
|
||||
use App\Services\DavClient\Utils\Dav\DavClient;
|
||||
use App\Services\DavClient\Utils\AddressBookGetter;
|
||||
use App\Services\DavClient\Utils\Dav\DavClientException;
|
||||
|
||||
class CreateAddressBookSubscription extends BaseService
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the service.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'account_id' => 'required|integer|exists:accounts,id',
|
||||
'user_id' => 'required|integer|exists:users,id',
|
||||
'base_uri' => 'required|string|url',
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new Adress Book.
|
||||
*
|
||||
* @param array $data
|
||||
* @return AddressBookSubscription|null
|
||||
*/
|
||||
public function execute(array $data): ?AddressBookSubscription
|
||||
{
|
||||
$this->validate($data);
|
||||
|
||||
$addressBookData = $this->getAddressBookData($data);
|
||||
if (! $addressBookData) {
|
||||
throw new DavClientException(__('Could not get address book data.'));
|
||||
}
|
||||
|
||||
$lastAddressBook = AddressBook::where('account_id', $data['account_id'])
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
$lastId = 0;
|
||||
if ($lastAddressBook) {
|
||||
$lastId = intval(preg_replace('/\w+(\d+)/i', '$1', $lastAddressBook->name));
|
||||
}
|
||||
$nextAddressBookName = 'contacts'.($lastId + 1);
|
||||
|
||||
$addressBook = AddressBook::create([
|
||||
'account_id' => $data['account_id'],
|
||||
'user_id' => $data['user_id'],
|
||||
'name' => $nextAddressBookName,
|
||||
'description' => $addressBookData['name'],
|
||||
]);
|
||||
$subscription = AddressBookSubscription::create([
|
||||
'account_id' => $data['account_id'],
|
||||
'user_id' => $data['user_id'],
|
||||
'username' => $data['username'],
|
||||
'address_book_id' => $addressBook->id,
|
||||
'uri' => $addressBookData['uri'],
|
||||
'capabilities' => $addressBookData['capabilities'],
|
||||
]);
|
||||
$subscription->password = $data['password'];
|
||||
$subscription->save();
|
||||
|
||||
return $subscription;
|
||||
}
|
||||
|
||||
private function getAddressBookData(array $data): ?array
|
||||
{
|
||||
$client = $this->getClient($data);
|
||||
|
||||
return app(AddressBookGetter::class)
|
||||
->execute($client);
|
||||
}
|
||||
|
||||
private function getClient(array $data): DavClient
|
||||
{
|
||||
return app(DavClient::class)
|
||||
->setBaseUri(Arr::get($data, 'base_uri'))
|
||||
->setCredentials(Arr::get($data, 'username'), Arr::get($data, 'password'));
|
||||
}
|
||||
}
|
||||
76
app/Services/DavClient/SynchronizeAddressBook.php
Normal file
76
app/Services/DavClient/SynchronizeAddressBook.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Services\BaseService;
|
||||
use App\Helpers\AccountHelper;
|
||||
use App\Models\Account\Account;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use App\Models\Account\AddressBookSubscription;
|
||||
use App\Services\DavClient\Utils\Dav\DavClient;
|
||||
use App\Services\DavClient\Utils\Model\SyncDto;
|
||||
use App\Services\DavClient\Utils\AddressBookSynchronizer;
|
||||
|
||||
class SynchronizeAddressBook extends BaseService
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the service.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'account_id' => 'required|integer|exists:accounts,id',
|
||||
'addressbook_subscription_id' => 'required|integer|exists:addressbook_subscriptions,id',
|
||||
'force' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function execute(array $data)
|
||||
{
|
||||
$this->validate($data);
|
||||
|
||||
$account = Account::find($data['account_id']);
|
||||
if (AccountHelper::hasReachedContactLimit($account)
|
||||
&& AccountHelper::hasLimitations($account)
|
||||
&& ! $account->legacy_free_plan_unlimited_contacts) {
|
||||
abort(402);
|
||||
}
|
||||
|
||||
$subscription = AddressBookSubscription::where('account_id', $data['account_id'])
|
||||
->findOrFail($data['addressbook_subscription_id']);
|
||||
|
||||
try {
|
||||
$this->sync($data, $subscription);
|
||||
} catch (ClientException $e) {
|
||||
Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [
|
||||
'body' => $e->hasResponse() ? $e->getResponse()->getBody() : null,
|
||||
$e,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function sync(array $data, AddressBookSubscription $subscription)
|
||||
{
|
||||
$client = $this->getDavClient($subscription);
|
||||
$sync = new SyncDto($subscription, $client);
|
||||
$force = Arr::get($data, 'force', false);
|
||||
|
||||
app(AddressBookSynchronizer::class)
|
||||
->execute($sync, $force);
|
||||
}
|
||||
|
||||
private function getDavClient(AddressBookSubscription $subscription): DavClient
|
||||
{
|
||||
return app(DavClient::class)
|
||||
->setBaseUri($subscription->uri)
|
||||
->setCredentials($subscription->username, $subscription->password);
|
||||
}
|
||||
}
|
||||
55
app/Services/DavClient/UpdateSubscriptionLocalSyncToken.php
Normal file
55
app/Services/DavClient/UpdateSubscriptionLocalSyncToken.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient;
|
||||
|
||||
use App\Services\BaseService;
|
||||
use App\Models\Account\AddressBookSubscription;
|
||||
use App\Http\Controllers\DAV\Backend\CardDAV\CardDAVBackend;
|
||||
|
||||
class UpdateSubscriptionLocalSyncToken extends BaseService
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the service.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'account_id' => 'required|integer|exists:accounts,id',
|
||||
'addressbook_subscription_id' => 'required|integer|exists:addressbook_subscriptions,id',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function execute(array $data): void
|
||||
{
|
||||
$this->validate($data);
|
||||
|
||||
$subscription = AddressBookSubscription::where('account_id', $data['account_id'])
|
||||
->findOrFail($data['addressbook_subscription_id']);
|
||||
|
||||
$this->updateSyncToken($subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the synctoken.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function updateSyncToken(AddressBookSubscription $subscription): void
|
||||
{
|
||||
$backend = app(CardDAVBackend::class)
|
||||
->init($subscription->user);
|
||||
|
||||
$token = $backend->getCurrentSyncToken($subscription->addressbook->name);
|
||||
|
||||
if ($token !== null) {
|
||||
$subscription->localSyncToken = $token->id;
|
||||
$subscription->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
118
app/Services/DavClient/Utils/AddressBookContactsPush.php
Normal file
118
app/Services/DavClient/Utils/AddressBookContactsPush.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils;
|
||||
|
||||
use App\Jobs\Dav\PushVCard;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Jobs\Dav\DeleteVCard;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Services\DavClient\Utils\Model\SyncDto;
|
||||
use App\Services\DavClient\Utils\Model\ContactDto;
|
||||
use App\Services\DavClient\Utils\Traits\WithSyncDto;
|
||||
use App\Services\DavClient\Utils\Model\ContactPushDto;
|
||||
|
||||
class AddressBookContactsPush
|
||||
{
|
||||
use WithSyncDto;
|
||||
|
||||
/**
|
||||
* Push contacts to the distant server.
|
||||
*
|
||||
* @param SyncDto $sync
|
||||
* @param Collection<array-key, ContactDto> $changes
|
||||
* @param array<array-key, string>|null $localChanges
|
||||
* @return Collection
|
||||
*/
|
||||
public function execute(SyncDto $sync, Collection $changes, ?array $localChanges): Collection
|
||||
{
|
||||
$this->sync = $sync;
|
||||
|
||||
$changes = $this->preparePushChangedContacts($changes, Arr::get($localChanges, 'modified', []));
|
||||
$added = $this->preparePushAddedContacts(Arr::get($localChanges, 'added', []));
|
||||
$deleted = $this->prepareDeletedContacts(Arr::get($localChanges, 'deleted', []));
|
||||
|
||||
return $changes
|
||||
->union($added)
|
||||
->union($deleted)
|
||||
->filter(function ($c) {
|
||||
return $c !== null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of requests to push new contacts.
|
||||
*
|
||||
* @param array $contacts
|
||||
* @return Collection
|
||||
*/
|
||||
private function preparePushAddedContacts(array $contacts): Collection
|
||||
{
|
||||
// All added contact must be pushed
|
||||
return collect($contacts)
|
||||
->map(function (string $uri): ?PushVCard {
|
||||
$card = $this->backend()->getCard($this->sync->addressBookName(), $uri);
|
||||
|
||||
return $card === false ? null
|
||||
: new PushVCard($this->sync->subscription,
|
||||
new ContactPushDto(
|
||||
$uri,
|
||||
$card['distant_etag'],
|
||||
$card['carddata'],
|
||||
$card['contact_id']
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of requests to delete contacts.
|
||||
*
|
||||
* @param array $contacts
|
||||
* @return Collection
|
||||
*/
|
||||
private function prepareDeletedContacts(array $contacts): Collection
|
||||
{
|
||||
// All removed contact must be deleted
|
||||
return collect($contacts)
|
||||
->map(function (string $uri): DeleteVCard {
|
||||
return new DeleteVCard($this->sync->subscription, $uri);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of requests to push modified contacts.
|
||||
*
|
||||
* @param Collection<array-key, ContactDto> $changes
|
||||
* @param array $contacts
|
||||
* @return Collection
|
||||
*/
|
||||
private function preparePushChangedContacts(Collection $changes, array $contacts): Collection
|
||||
{
|
||||
$backend = $this->backend();
|
||||
|
||||
$refreshIds = $changes->map(function (ContactDto $contact) use ($backend) {
|
||||
return $backend->getUuid($contact->uri);
|
||||
});
|
||||
|
||||
// We don't push contact that have just been pulled
|
||||
return collect($contacts)
|
||||
->reject(function (string $uri) use ($refreshIds, $backend): bool {
|
||||
$uuid = $backend->getUuid($uri);
|
||||
|
||||
return $refreshIds->contains($uuid);
|
||||
})->map(function (string $uri) use ($backend): ?PushVCard {
|
||||
$card = $backend->getCard($this->sync->addressBookName(), $uri);
|
||||
|
||||
return $card === false ? null
|
||||
: new PushVCard($this->sync->subscription,
|
||||
new ContactPushDto(
|
||||
$uri,
|
||||
$card['distant_etag'],
|
||||
$card['carddata'],
|
||||
$card['contact_id'],
|
||||
$card['distant_etag'] !== null ? ContactPushDto::MODE_MATCH_ETAG : ContactPushDto::MODE_MATCH_ANY
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils;
|
||||
|
||||
use App\Jobs\Dav\PushVCard;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Models\Contact\Contact;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Services\DavClient\Utils\Model\SyncDto;
|
||||
use App\Services\DavClient\Utils\Model\ContactDto;
|
||||
use App\Services\DavClient\Utils\Traits\WithSyncDto;
|
||||
use App\Services\DavClient\Utils\Model\ContactPushDto;
|
||||
|
||||
class AddressBookContactsPushMissed
|
||||
{
|
||||
use WithSyncDto;
|
||||
|
||||
/**
|
||||
* Push contacts to the distant server.
|
||||
*
|
||||
* @param SyncDto $sync
|
||||
* @param array<array-key, string>|null $localChanges
|
||||
* @param Collection<array-key, ContactDto> $distContacts
|
||||
* @param Collection<array-key, Contact> $localContacts
|
||||
* @return Collection
|
||||
*/
|
||||
public function execute(SyncDto $sync, ?array $localChanges, Collection $distContacts, Collection $localContacts): Collection
|
||||
{
|
||||
$this->sync = $sync;
|
||||
|
||||
$missings = $this->preparePushMissedContacts(Arr::get($localChanges, 'added', []), $distContacts, $localContacts);
|
||||
|
||||
return app(AddressBookContactsPush::class)
|
||||
->execute($sync, collect(), $localChanges)
|
||||
->union($missings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of requests of missed contacts.
|
||||
*
|
||||
* @param array<array-key, string> $added
|
||||
* @param Collection<array-key, ContactDto> $distContacts
|
||||
* @param Collection<array-key, Contact> $localContacts
|
||||
* @return Collection
|
||||
*/
|
||||
private function preparePushMissedContacts(array $added, Collection $distContacts, Collection $localContacts): Collection
|
||||
{
|
||||
$backend = $this->backend();
|
||||
|
||||
$distUuids = $distContacts->map(function (ContactDto $contact) use ($backend): string {
|
||||
return $backend->getUuid($contact->uri);
|
||||
});
|
||||
$addedUuids = collect($added)->map(function (string $uri) use ($backend): string {
|
||||
return $backend->getUuid($uri);
|
||||
});
|
||||
|
||||
return collect($localContacts)
|
||||
->filter(function (Contact $contact) use ($distUuids, $addedUuids) {
|
||||
return ! $distUuids->contains($contact->uuid)
|
||||
&& ! $addedUuids->contains($contact->uuid);
|
||||
})->map(function (Contact $contact) use ($backend): PushVCard {
|
||||
$card = $backend->prepareCard($contact);
|
||||
|
||||
return new PushVCard($this->sync->subscription,
|
||||
new ContactPushDto(
|
||||
$card['uri'],
|
||||
$contact->distant_etag,
|
||||
$card['carddata'],
|
||||
$contact->id,
|
||||
ContactPushDto::MODE_MATCH_ANY
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
79
app/Services/DavClient/Utils/AddressBookContactsUpdater.php
Normal file
79
app/Services/DavClient/Utils/AddressBookContactsUpdater.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils;
|
||||
|
||||
use App\Jobs\Dav\GetVCard;
|
||||
use App\Jobs\Dav\DeleteVCard;
|
||||
use App\Jobs\Dav\GetMultipleVCard;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Jobs\Dav\DeleteMultipleVCard;
|
||||
use App\Services\DavClient\Utils\Model\SyncDto;
|
||||
use App\Services\DavClient\Utils\Model\ContactDto;
|
||||
use App\Services\DavClient\Utils\Traits\WithSyncDto;
|
||||
use App\Services\DavClient\Utils\Traits\HasCapability;
|
||||
use App\Services\DavClient\Utils\Model\ContactDeleteDto;
|
||||
|
||||
class AddressBookContactsUpdater
|
||||
{
|
||||
use HasCapability, WithSyncDto;
|
||||
|
||||
/**
|
||||
* Update local contacts.
|
||||
*
|
||||
* @param SyncDto $sync
|
||||
* @param Collection<array-key, \App\Services\DavClient\Utils\Model\ContactDto> $refresh
|
||||
* @return Collection
|
||||
*/
|
||||
public function execute(SyncDto $sync, Collection $refresh): Collection
|
||||
{
|
||||
$this->sync = $sync;
|
||||
|
||||
return $this->hasCapability('addressbookMultiget')
|
||||
? $this->refreshMultigetContacts($refresh)
|
||||
: $this->refreshSimpleGetContacts($refresh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contacts data with addressbook-multiget request.
|
||||
*
|
||||
* @param Collection<array-key, \App\Services\DavClient\Utils\Model\ContactDto> $refresh
|
||||
* @return Collection
|
||||
*/
|
||||
private function refreshMultigetContacts(Collection $refresh): Collection
|
||||
{
|
||||
$updated = $refresh
|
||||
->filter(function ($item): bool {
|
||||
return ! ($item instanceof ContactDeleteDto);
|
||||
})
|
||||
->pluck('uri')->toArray();
|
||||
|
||||
$deleted = $refresh
|
||||
->filter(function ($item): bool {
|
||||
return $item instanceof ContactDeleteDto;
|
||||
})
|
||||
->pluck('uri')->toArray();
|
||||
|
||||
return collect([
|
||||
new GetMultipleVCard($this->sync->subscription, $updated),
|
||||
new DeleteMultipleVCard($this->sync->subscription, $deleted),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contacts data with request.
|
||||
*
|
||||
* @param Collection<array-key, \App\Services\DavClient\Utils\Model\ContactDto> $refresh
|
||||
* @return Collection
|
||||
*/
|
||||
private function refreshSimpleGetContacts(Collection $refresh): Collection
|
||||
{
|
||||
return $refresh
|
||||
->map(function (ContactDto $contact) {
|
||||
if ($contact instanceof ContactDeleteDto) {
|
||||
return new DeleteVCard($this->sync->subscription, $contact->uri);
|
||||
} else {
|
||||
return new GetVCard($this->sync->subscription, $contact);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Services\DavClient\Utils\Model\SyncDto;
|
||||
use App\Services\DavClient\Utils\Model\ContactDto;
|
||||
use App\Services\DavClient\Utils\Traits\WithSyncDto;
|
||||
|
||||
class AddressBookContactsUpdaterMissed
|
||||
{
|
||||
use WithSyncDto;
|
||||
|
||||
/**
|
||||
* Update local missed contacts.
|
||||
*
|
||||
* @param SyncDto $sync
|
||||
* @param Collection<array-key, \App\Models\Contact\Contact> $localContacts
|
||||
* @param Collection<array-key, \App\Services\DavClient\Utils\Model\ContactDto> $distContacts
|
||||
* @return Collection
|
||||
*/
|
||||
public function execute(SyncDto $sync, Collection $localContacts, Collection $distContacts): Collection
|
||||
{
|
||||
$this->sync = $sync;
|
||||
|
||||
$uuids = $localContacts->pluck('uuid');
|
||||
|
||||
$missed = $distContacts->reject(function (ContactDto $contact) use ($uuids): bool {
|
||||
return $uuids->contains($this->backend()->getUuid($contact->uri));
|
||||
});
|
||||
|
||||
return app(AddressBookContactsUpdater::class)
|
||||
->execute($this->sync, $missed);
|
||||
}
|
||||
}
|
||||
272
app/Services/DavClient/Utils/AddressBookGetter.php
Normal file
272
app/Services/DavClient/Utils/AddressBookGetter.php
Normal file
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Sabre\CardDAV\Plugin as CardDAVPlugin;
|
||||
use App\Services\DavClient\Utils\Dav\DavClient;
|
||||
use App\Services\DavClient\Utils\Dav\DavClientException;
|
||||
use App\Services\DavClient\Utils\Dav\DavServerNotCompliantException;
|
||||
|
||||
class AddressBookGetter
|
||||
{
|
||||
/**
|
||||
* @var DavClient
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* Get address book data: uri, capabilities, and name.
|
||||
*
|
||||
* @param DavClient $client
|
||||
* @return array|null
|
||||
*/
|
||||
public function execute(DavClient $client): ?array
|
||||
{
|
||||
$this->client = $client;
|
||||
|
||||
try {
|
||||
return $this->getAddressBookData();
|
||||
} catch (ClientException $e) {
|
||||
Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [$e]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get address book data: uri, capabilities, and name.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getAddressBookData(): array
|
||||
{
|
||||
$uri = $this->getAddressBookBaseUri();
|
||||
|
||||
$this->client->setBaseUri($uri);
|
||||
|
||||
if (Str::startsWith($uri, 'https://www.googleapis.com')) {
|
||||
// Google API sucks
|
||||
$capabilities = [
|
||||
'addressbookMultiget' => true,
|
||||
'addressbookQuery' => true,
|
||||
'syncCollection' => true,
|
||||
'addressData' => [
|
||||
'content-type' => 'text/vcard',
|
||||
'version' => '3.0',
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$capabilities = $this->getCapabilities();
|
||||
}
|
||||
|
||||
$name = $this->client->getProperty('{DAV:}displayname');
|
||||
|
||||
return [
|
||||
'uri' => $uri,
|
||||
'capabilities' => $capabilities,
|
||||
'name' => $name,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate address book base uri.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getAddressBookBaseUri(): string
|
||||
{
|
||||
try {
|
||||
// Get the principal of this account
|
||||
$principal = $this->getCurrentUserPrincipal();
|
||||
$baseUri = $this->client->path($principal);
|
||||
} catch (\Exception $e) {
|
||||
$baseUri = $this->client->getServiceUrl();
|
||||
}
|
||||
|
||||
if ($baseUri) {
|
||||
$this->client->setBaseUri($baseUri);
|
||||
}
|
||||
|
||||
if (! Str::contains($baseUri, 'https://www.googleapis.com')) {
|
||||
// Google API does not follow rfc2518 section-15 !
|
||||
|
||||
// Check the OPTIONS of the server
|
||||
$this->checkOptions();
|
||||
}
|
||||
|
||||
// Get the principal of this account
|
||||
$principal = $this->getCurrentUserPrincipal();
|
||||
|
||||
// Get the AddressBook of this principal
|
||||
$addressBook = $this->getAddressBookUrl($principal);
|
||||
$addressBookUrl = $this->client->path($addressBook);
|
||||
|
||||
if (! Str::contains($addressBookUrl, 'https://www.googleapis.com')) {
|
||||
// Check the OPTIONS of the server
|
||||
$this->checkOptions(true, $addressBookUrl);
|
||||
}
|
||||
|
||||
if ($addressBook === null) {
|
||||
throw new DavClientException('No address book found');
|
||||
}
|
||||
|
||||
return $addressBookUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check options of the server.
|
||||
*
|
||||
* @param bool $addressbook
|
||||
* @param string $url
|
||||
* @return void
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc2518#section-15
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6352#section-6.1
|
||||
*
|
||||
* @throws DavServerNotCompliantException
|
||||
*/
|
||||
private function checkOptions(bool $addressbook = false, string $url = '')
|
||||
{
|
||||
$options = $this->client->options($url);
|
||||
|
||||
if (! in_array('1', $options) || ! in_array('3', $options) || ($addressbook && ! in_array('addressbook', $options))) {
|
||||
throw new DavServerNotCompliantException('server is not compliant with rfc2518 section 15.1, or rfc6352 section 6.1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get principal name.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc5397#section-3
|
||||
*
|
||||
* @throws DavServerNotCompliantException
|
||||
*/
|
||||
private function getCurrentUserPrincipal(): string
|
||||
{
|
||||
$prop = $this->client->getProperty('{DAV:}current-user-principal');
|
||||
|
||||
if (is_null($prop) || empty($prop)) {
|
||||
throw new DavServerNotCompliantException('Server does not support rfc 5397 section 3 (DAV:current-user-principal)');
|
||||
} elseif (is_string($prop)) {
|
||||
return $prop;
|
||||
}
|
||||
|
||||
return $prop[0]['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get addressbook url.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6352#section-7.1.1
|
||||
*
|
||||
* @throws DavServerNotCompliantException
|
||||
*/
|
||||
private function getAddressBookHome(string $principal): string
|
||||
{
|
||||
$prop = $this->client->getProperty('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-home-set', $principal);
|
||||
|
||||
if (is_null($prop) || empty($prop)) {
|
||||
throw new DavServerNotCompliantException('Server does not support rfc 6352 section 7.1.1 (CARD:addressbook-home-set)');
|
||||
} elseif (is_string($prop)) {
|
||||
return $prop;
|
||||
}
|
||||
|
||||
return $prop[0]['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Url fro address book.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function getAddressBookUrl(string $principal): ?string
|
||||
{
|
||||
$home = $this->getAddressBookHome($principal);
|
||||
|
||||
$books = $this->client->propfind('{DAV:}resourcetype', 1, [], $home);
|
||||
|
||||
foreach ($books as $book => $properties) {
|
||||
if ($book == $home) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($resources = Arr::get($properties, '{DAV:}resourcetype', null)) &&
|
||||
$resources->is('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook')) {
|
||||
return $book;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get capabilities properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getCapabilities()
|
||||
{
|
||||
return $this->getSupportedReportSet()
|
||||
+
|
||||
$this->getSupportedAddressData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported-report-set property.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getSupportedReportSet(): array
|
||||
{
|
||||
$supportedReportSet = $this->client->getSupportedReportSet();
|
||||
|
||||
$addressbookMultiget = in_array('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-multiget', $supportedReportSet);
|
||||
$addressbookQuery = in_array('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-query', $supportedReportSet);
|
||||
$syncCollection = in_array('{DAV:}sync-collection', $supportedReportSet);
|
||||
|
||||
return [
|
||||
'addressbookMultiget' => $addressbookMultiget,
|
||||
'addressbookQuery' => $addressbookQuery,
|
||||
'syncCollection' => $syncCollection,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported-address-data property.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getSupportedAddressData(): array
|
||||
{
|
||||
// get the supported card format
|
||||
$addressData = collect($this->client->getProperty('{'.CardDAVPlugin::NS_CARDDAV.'}supported-address-data'));
|
||||
$datas = $addressData->firstWhere('attributes.version', '4.0');
|
||||
if (! $datas) {
|
||||
$datas = $addressData->firstWhere('attributes.version', '3.0');
|
||||
}
|
||||
|
||||
if (! $datas) {
|
||||
// It should not happen !
|
||||
$datas = [
|
||||
'attributes' => [
|
||||
'content-type' => 'text/vcard',
|
||||
'version' => '4.0',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'addressData' => [
|
||||
'content-type' => Arr::get($datas, 'attributes.content-type'),
|
||||
'version' => Arr::get($datas, 'attributes.version'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
245
app/Services/DavClient/Utils/AddressBookSynchronizer.php
Normal file
245
app/Services/DavClient/Utils/AddressBookSynchronizer.php
Normal file
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils;
|
||||
|
||||
use Illuminate\Bus\Batch;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use App\Services\DavClient\Utils\Model\SyncDto;
|
||||
use App\Services\DavClient\Utils\Model\ContactDto;
|
||||
use App\Services\DavClient\Utils\Traits\WithSyncDto;
|
||||
use App\Services\DavClient\Utils\Traits\HasCapability;
|
||||
use App\Services\DavClient\Utils\Model\ContactDeleteDto;
|
||||
use App\Services\DavClient\UpdateSubscriptionLocalSyncToken;
|
||||
|
||||
class AddressBookSynchronizer
|
||||
{
|
||||
use HasCapability, WithSyncDto;
|
||||
|
||||
/**
|
||||
* Sync the address book.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function execute(SyncDto $sync, bool $force = false)
|
||||
{
|
||||
$this->sync = $sync;
|
||||
|
||||
$force
|
||||
? $this->forcesync()
|
||||
: $this->sync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the address book.
|
||||
*/
|
||||
private function sync()
|
||||
{
|
||||
// Get changes to sync
|
||||
$localChanges = $this->backend()->getChangesForAddressBook($this->sync->addressBookName(), (string) $this->sync->subscription->localSyncToken, 1);
|
||||
|
||||
// Get distant changes to sync
|
||||
$changes = $this->getDistantChanges();
|
||||
|
||||
// Get distant contacts
|
||||
$batch = app(AddressBookContactsUpdater::class)
|
||||
->execute($this->sync, $changes);
|
||||
|
||||
if (! $this->sync->subscription->readonly) {
|
||||
$batch->union(
|
||||
app(AddressBookContactsPush::class)
|
||||
->execute($this->sync, $changes, $localChanges)
|
||||
);
|
||||
}
|
||||
|
||||
$accountId = $this->sync->subscription->account_id;
|
||||
$subscriptionId = $this->sync->subscription->id;
|
||||
Bus::batch($batch)
|
||||
->then(function (Batch $batch) use ($accountId, $subscriptionId) {
|
||||
app(UpdateSubscriptionLocalSyncToken::class)->execute([
|
||||
'account_id' => $accountId,
|
||||
'addressbook_subscription_id' => $subscriptionId,
|
||||
]);
|
||||
})
|
||||
->allowFailures()
|
||||
->dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the address book.
|
||||
*/
|
||||
private function forcesync()
|
||||
{
|
||||
$backend = $this->backend();
|
||||
|
||||
// Get changes to sync
|
||||
$localChanges = $backend->getChangesForAddressBook($this->sync->addressBookName(), (string) $this->sync->subscription->localSyncToken, 1);
|
||||
|
||||
// Get current list of contacts
|
||||
$localContacts = $backend->getObjects($this->sync->addressBookName());
|
||||
|
||||
// Get distant changes to sync
|
||||
$distContacts = $this->getAllContactsEtag();
|
||||
|
||||
// Get missed contacts
|
||||
$batch = app(AddressBookContactsUpdaterMissed::class)
|
||||
->execute($this->sync, $localContacts, $distContacts);
|
||||
|
||||
if (! $this->sync->subscription->readonly) {
|
||||
$batch->union(
|
||||
app(AddressBookContactsPushMissed::class)
|
||||
->execute($this->sync, $localChanges, $distContacts, $localContacts)
|
||||
);
|
||||
}
|
||||
|
||||
$accountId = $this->sync->subscription->account_id;
|
||||
$subscriptionId = $this->sync->subscription->id;
|
||||
Bus::batch($batch)
|
||||
->then(function (Batch $batch) use ($accountId, $subscriptionId) {
|
||||
app(UpdateSubscriptionLocalSyncToken::class)->execute([
|
||||
'account_id' => $accountId,
|
||||
'addressbook_subscription_id' => $subscriptionId,
|
||||
]);
|
||||
})
|
||||
->allowFailures()
|
||||
->dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get distant changes to sync.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
private function getDistantChanges(): Collection
|
||||
{
|
||||
$etags = collect($this->getDistantEtags());
|
||||
$contacts = $etags->filter(function ($contact, $href): bool {
|
||||
return $this->filterDistantContacts($contact, $href);
|
||||
})
|
||||
->map(function (array $contact, string $href): ContactDto {
|
||||
return new ContactDto($href, Arr::get($contact, 'properties.200.{DAV:}getetag'));
|
||||
});
|
||||
|
||||
$deleted = $etags->filter(function ($contact): bool {
|
||||
return is_array($contact) && $contact['status'] === '404';
|
||||
})
|
||||
->map(function (array $contact, string $href): ContactDto {
|
||||
return new ContactDeleteDto($href);
|
||||
});
|
||||
|
||||
return $contacts->union($deleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter contacts to only return vcards type and new contacts or contacts with matching etags.
|
||||
*
|
||||
* @param mixed $contact
|
||||
* @param string $href
|
||||
* @return bool
|
||||
*/
|
||||
private function filterDistantContacts($contact, $href): bool
|
||||
{
|
||||
// only return vcards
|
||||
if (! is_array($contact) || ! Str::contains(Arr::get($contact, 'properties.200.{DAV:}getcontenttype'), 'text/vcard')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only new contact or contact with etag that match
|
||||
$card = $this->backend()->getCard($this->sync->addressBookName(), $href);
|
||||
|
||||
return $card === false || $card['etag'] !== Arr::get($contact, 'properties.200.{DAV:}getetag');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get refreshed etags.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getDistantEtags(): array
|
||||
{
|
||||
if ($this->hasCapability('syncCollection')) {
|
||||
// With sync-collection
|
||||
return $this->callSyncCollectionWhenNeeded();
|
||||
} else {
|
||||
// With PROPFIND
|
||||
return $this->sync->propFind([
|
||||
'{DAV:}getcontenttype',
|
||||
'{DAV:}getetag',
|
||||
], 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sync-collection request if sync-token has changed.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function callSyncCollectionWhenNeeded(): array
|
||||
{
|
||||
// get the current distant syncToken
|
||||
$distantSyncToken = $this->sync->getProperty('{DAV:}sync-token');
|
||||
|
||||
if (($this->sync->subscription->syncToken ?? '') === $distantSyncToken) {
|
||||
// no change at all
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->callSyncCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sync-collection request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function callSyncCollection(): array
|
||||
{
|
||||
$syncToken = $this->sync->subscription->syncToken ?? '';
|
||||
|
||||
// get sync
|
||||
$collection = $this->sync->syncCollection([
|
||||
'{DAV:}getcontenttype',
|
||||
'{DAV:}getetag',
|
||||
], $syncToken);
|
||||
|
||||
// save the new syncToken as current one
|
||||
if ($newSyncToken = Arr::get($collection, 'synctoken')) {
|
||||
$this->sync->subscription->syncToken = $newSyncToken;
|
||||
$this->sync->subscription->save();
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all contacts etag.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
private function getAllContactsEtag(): Collection
|
||||
{
|
||||
if (! $this->hasCapability('addressbookQuery')) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$data = $this->sync->addressbookQuery('{DAV:}getetag');
|
||||
$data = collect($data);
|
||||
|
||||
$updated = $data->filter(function ($contact): bool {
|
||||
return is_array($contact) && $contact['status'] === '200';
|
||||
})
|
||||
->map(function (array $contact, string $href): ContactDto {
|
||||
return new ContactDto($href, Arr::get($contact, 'properties.200.{DAV:}getetag'));
|
||||
});
|
||||
$deleted = $data->filter(function ($contact): bool {
|
||||
return is_array($contact) && $contact['status'] === '404';
|
||||
})
|
||||
->map(function (array $contact, string $href): ContactDto {
|
||||
return new ContactDeleteDto($href);
|
||||
});
|
||||
|
||||
return $updated->union($deleted);
|
||||
}
|
||||
}
|
||||
602
app/Services/DavClient/Utils/Dav/DavClient.php
Normal file
602
app/Services/DavClient/Utils/Dav/DavClient.php
Normal file
@@ -0,0 +1,602 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Dav;
|
||||
|
||||
use Sabre\DAV\Xml\Service;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Sabre\DAV\Xml\Request\PropPatch;
|
||||
use GuzzleHttp\Psr7\Utils as GuzzleUtils;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Sabre\CardDAV\Plugin as CardDAVPlugin;
|
||||
|
||||
class DavClient
|
||||
{
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $baseUri;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $username;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $password;
|
||||
|
||||
/**
|
||||
* Set the base uri of client.
|
||||
*
|
||||
* @param string $uri
|
||||
* @return self
|
||||
*/
|
||||
public function setBaseUri(string $uri): self
|
||||
{
|
||||
$this->baseUri = $uri;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set credentials.
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return self
|
||||
*/
|
||||
public function setCredentials(string $username, string $password): self
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current uri.
|
||||
*
|
||||
* @param string|null $path
|
||||
* @return string
|
||||
*/
|
||||
public function path(?string $path = null): string
|
||||
{
|
||||
$uri = GuzzleUtils::uriFor($this->baseUri);
|
||||
|
||||
return (string) (is_null($path) || empty($path) ? $uri : $uri->withPath((string) Str::of($path)->start('/')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a PendingRequest.
|
||||
*
|
||||
* @return PendingRequest
|
||||
*/
|
||||
public function getRequest(): PendingRequest
|
||||
{
|
||||
$request = Http::withUserAgent('Monica DavClient '.config('monica.app_version').'/Guzzle');
|
||||
|
||||
if (! is_null($this->username) && ! is_null($this->password)) {
|
||||
$request = $request->withBasicAuth($this->username, $this->password);
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow rfc6764 to get carddav service url.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6764
|
||||
*/
|
||||
public function getServiceUrl()
|
||||
{
|
||||
// first attempt on relative url
|
||||
$target = $this->standardServiceUrl('.well-known/carddav');
|
||||
|
||||
if (! $target) {
|
||||
// second attempt on absolute root url
|
||||
$target = $this->standardServiceUrl('/.well-known/carddav');
|
||||
}
|
||||
|
||||
if (! $target) {
|
||||
// third attempt for non standard server, like Google API
|
||||
$target = $this->nonStandardServiceUrl('/.well-known/carddav');
|
||||
}
|
||||
|
||||
if (! $target) {
|
||||
// Get service name register (section 9.2)
|
||||
$target = app(ServiceUrlQuery::class)->execute('_carddavs._tcp', true, $this->path(), $this);
|
||||
if (is_null($target)) {
|
||||
$target = app(ServiceUrlQuery::class)->execute('_carddav._tcp', false, $this->path(), $this);
|
||||
}
|
||||
}
|
||||
|
||||
return $target;
|
||||
}
|
||||
|
||||
private function standardServiceUrl(string $url): ?string
|
||||
{
|
||||
// Get well-known register (section 9.1)
|
||||
$response = $this->getRequest()
|
||||
->withoutRedirecting()
|
||||
->get($this->path($url));
|
||||
|
||||
$code = $response->status();
|
||||
if ($code === 301 || $code === 302) {
|
||||
return $response->header('Location');
|
||||
}
|
||||
|
||||
if ($response->serverError()) {
|
||||
$response->throw();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function nonStandardServiceUrl($url): ?string
|
||||
{
|
||||
$response = $this->getRequest()
|
||||
->withoutRedirecting()
|
||||
->send('PROPFIND', $this->path($url));
|
||||
|
||||
$code = $response->status();
|
||||
if ($code === 301 || $code === 302) {
|
||||
return $this->path($response->header('Location'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a PROPFIND request.
|
||||
*
|
||||
* The list of requested properties must be specified as an array, in clark
|
||||
* notation.
|
||||
*
|
||||
* The returned array will contain a list of filenames as keys, and
|
||||
* properties as values.
|
||||
*
|
||||
* The properties array will contain the list of properties. Only properties
|
||||
* that are actually returned from the server (without error) will be
|
||||
* returned, anything else is discarded.
|
||||
*
|
||||
* Depth should be either 0 or 1. A depth of 1 will cause a request to be
|
||||
* made to the server to also return all child resources.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array|string $properties
|
||||
* @param int $depth
|
||||
* @return array
|
||||
*/
|
||||
public function propFind($properties, int $depth = 0, array $options = [], string $url = ''): array
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$root = self::addElementNS($dom, 'DAV:', 'd:propfind');
|
||||
$prop = self::addElement($dom, $root, 'd:prop');
|
||||
|
||||
$namespaces = ['DAV:' => 'd'];
|
||||
|
||||
self::fetchProperties($dom, $prop, $properties, $namespaces);
|
||||
|
||||
$body = $dom->saveXML();
|
||||
|
||||
$response = $this->request('PROPFIND', $url, $body, ['Depth' => $depth], $options);
|
||||
|
||||
$result = self::parseMultiStatus($response->body());
|
||||
|
||||
// If depth was 0, we only return the top item value
|
||||
if ($depth === 0) {
|
||||
reset($result);
|
||||
$result = current($result);
|
||||
|
||||
return Arr::get($result, 'properties.200', []);
|
||||
}
|
||||
|
||||
return array_map(function ($statusList) {
|
||||
return Arr::get($statusList, 'properties.200', []);
|
||||
}, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a REPORT {DAV:}sync-collection.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array|string $properties
|
||||
* @param string $syncToken
|
||||
* @return array
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6578
|
||||
*/
|
||||
public function syncCollection($properties, string $syncToken, array $options = [], string $url = ''): array
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$root = self::addElementNS($dom, 'DAV:', 'd:sync-collection');
|
||||
|
||||
self::addElement($dom, $root, 'd:sync-token', $syncToken);
|
||||
self::addElement($dom, $root, 'd:sync-level', '1');
|
||||
|
||||
$prop = self::addElement($dom, $root, 'd:prop');
|
||||
|
||||
$namespaces = ['DAV:' => 'd'];
|
||||
|
||||
self::fetchProperties($dom, $prop, $properties, $namespaces);
|
||||
|
||||
$body = $dom->saveXML();
|
||||
|
||||
$response = $this->request('REPORT', $url, $body, ['Depth' => '0'], $options);
|
||||
|
||||
return self::parseMultiStatus($response->body());
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a REPORT card:addressbook-multiget.
|
||||
*
|
||||
* @param array|string $properties
|
||||
* @param iterable $contacts
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @return array
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6352#section-8.7
|
||||
*/
|
||||
public function addressbookMultiget($properties, iterable $contacts, array $options = [], string $url = ''): array
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$root = self::addElementNS($dom, CardDAVPlugin::NS_CARDDAV, 'card:addressbook-multiget');
|
||||
$dom->createAttributeNS('DAV:', 'd:e');
|
||||
|
||||
$prop = self::addElement($dom, $root, 'd:prop');
|
||||
|
||||
$namespaces = [
|
||||
'DAV:' => 'd',
|
||||
CardDAVPlugin::NS_CARDDAV => 'card',
|
||||
];
|
||||
|
||||
self::fetchProperties($dom, $prop, $properties, $namespaces);
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
self::addElement($dom, $root, 'd:href', $contact);
|
||||
}
|
||||
|
||||
$body = $dom->saveXML();
|
||||
|
||||
$response = $this->request('REPORT', $url, $body, ['Depth' => '1'], $options);
|
||||
|
||||
return self::parseMultiStatus($response->body());
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a REPORT card:addressbook-query.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array|string $properties
|
||||
* @return array
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6352#section-8.6
|
||||
*/
|
||||
public function addressbookQuery($properties, array $options = [], string $url = ''): array
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$root = self::addElementNS($dom, CardDAVPlugin::NS_CARDDAV, 'card:addressbook-query');
|
||||
$dom->createAttributeNS('DAV:', 'd:e');
|
||||
|
||||
$prop = self::addElement($dom, $root, 'd:prop');
|
||||
|
||||
$namespaces = [
|
||||
'DAV:' => 'd',
|
||||
CardDAVPlugin::NS_CARDDAV => 'card',
|
||||
];
|
||||
|
||||
self::fetchProperties($dom, $prop, $properties, $namespaces);
|
||||
|
||||
$body = $dom->saveXML();
|
||||
|
||||
$response = $this->request('REPORT', $url, $body, ['Depth' => '1'], $options);
|
||||
|
||||
return self::parseMultiStatus($response->body());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add properties to the prop object.
|
||||
*
|
||||
* Properties must follow:
|
||||
* - for a simple value
|
||||
* [
|
||||
* '{namespace}value',
|
||||
* ]
|
||||
*
|
||||
* - for a more complex value element
|
||||
* [
|
||||
* [
|
||||
* 'name' => '{namespace}value',
|
||||
* 'value' => 'content element',
|
||||
* 'attributes' => ['name' => 'value', ...],
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* @param \DOMDocument $dom
|
||||
* @param \DOMNode $prop
|
||||
* @param array|string $properties
|
||||
* @param array $namespaces
|
||||
* @return void
|
||||
*/
|
||||
private static function fetchProperties(\DOMDocument $dom, \DOMNode $prop, $properties, array $namespaces)
|
||||
{
|
||||
if (is_string($properties)) {
|
||||
$properties = [$properties];
|
||||
}
|
||||
|
||||
foreach ($properties as $property) {
|
||||
if (is_array($property)) {
|
||||
$propertyExt = $property;
|
||||
$property = $propertyExt['name'];
|
||||
}
|
||||
[$namespace, $elementName] = Service::parseClarkNotation($property);
|
||||
|
||||
$ns = Arr::get($namespaces, $namespace);
|
||||
$element = $ns !== null
|
||||
? $dom->createElement("$ns:$elementName")
|
||||
: $dom->createElementNS($namespace, "x:$elementName");
|
||||
|
||||
$child = $prop->appendChild($element);
|
||||
|
||||
if (isset($propertyExt)) {
|
||||
if (($nodeValue = Arr::get($propertyExt, 'value')) !== null) {
|
||||
$child->nodeValue = $nodeValue;
|
||||
}
|
||||
if (($attributes = Arr::get($propertyExt, 'attributes')) !== null) {
|
||||
foreach ($attributes as $name => $property) {
|
||||
$child->appendChild($dom->createAttribute($name))->nodeValue = $property;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a WebDAV property.
|
||||
*
|
||||
* @param string $property
|
||||
* @param string $url
|
||||
* @return array|string|null
|
||||
*/
|
||||
public function getProperty(string $property, string $url = '', array $options = [])
|
||||
{
|
||||
$properties = $this->propfind($property, 0, $options, $url);
|
||||
|
||||
if (($prop = Arr::get($properties, $property)) && is_array($prop)) {
|
||||
$value = $prop[0];
|
||||
|
||||
if (is_string($value)) {
|
||||
$prop = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {DAV:}supported-report-set propfind.
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3253#section-3.1.5
|
||||
*/
|
||||
public function getSupportedReportSet(array $options = []): array
|
||||
{
|
||||
$propName = '{DAV:}supported-report-set';
|
||||
|
||||
$properties = $this->propFind($propName, 0, $options);
|
||||
|
||||
if (($prop = Arr::get($properties, $propName)) && is_array($prop)) {
|
||||
$prop = array_map(function ($supportedReport) {
|
||||
return $this->iterateOver($supportedReport, '{DAV:}supported-report', function ($report) {
|
||||
return $this->iterateOver($report, '{DAV:}report', function ($type) {
|
||||
return Arr::get($type, 'name');
|
||||
});
|
||||
});
|
||||
}, $prop);
|
||||
}
|
||||
|
||||
return $prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over the list, if it contains an item name that match with $name.
|
||||
*
|
||||
* @param array $list
|
||||
* @param string $name
|
||||
* @param callable $callback
|
||||
* @return mixed
|
||||
*/
|
||||
private function iterateOver(array $list, string $name, callable $callback)
|
||||
{
|
||||
if (Arr::get($list, 'name') === $name
|
||||
&& ($value = Arr::get($list, 'value'))) {
|
||||
foreach ($value as $item) {
|
||||
return $callback($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a list of properties on the server.
|
||||
*
|
||||
* The list of properties must have clark-notation properties for the keys,
|
||||
* and the actual (string) value for the value. If the value is null, an
|
||||
* attempt is made to delete the property.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $properties
|
||||
* @return bool
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc2518#section-12.13
|
||||
*/
|
||||
public function propPatch(array $properties, string $url = ''): bool
|
||||
{
|
||||
$propPatch = new PropPatch();
|
||||
$propPatch->properties = $properties;
|
||||
$body = (new Service())->write(
|
||||
'{DAV:}propertyupdate',
|
||||
$propPatch
|
||||
);
|
||||
|
||||
$response = $this->request('PROPPATCH', $url, $body);
|
||||
|
||||
if ($response->status() === 207) {
|
||||
// If it's a 207, the request could still have failed, but the
|
||||
// information is hidden in the response body.
|
||||
$result = self::parseMultiStatus($response->body());
|
||||
|
||||
$errorProperties = [];
|
||||
foreach ($result as $statusList) {
|
||||
foreach ($statusList['properties'] as $status => $properties) {
|
||||
if ($status >= 400) {
|
||||
foreach ($properties as $propName => $propValue) {
|
||||
$errorProperties[] = $propName.' ('.$status.')';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! empty($errorProperties)) {
|
||||
throw new DavClientException('PROPPATCH failed. The following properties errored: '.implode(', ', $errorProperties));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP options request.
|
||||
*
|
||||
* This method returns all the features from the 'DAV:' header as an array.
|
||||
* If there was no DAV header, or no contents this method will return an
|
||||
* empty array.
|
||||
*
|
||||
* @param string $url
|
||||
* @return array
|
||||
*/
|
||||
public function options(string $url = ''): array
|
||||
{
|
||||
$response = $this->request('OPTIONS', $url);
|
||||
|
||||
$dav = $response->header('Dav');
|
||||
if (empty($dav)) {
|
||||
return [];
|
||||
}
|
||||
$davs = explode(', ', $dav);
|
||||
|
||||
return array_map(function ($header) {
|
||||
return trim($header);
|
||||
}, $davs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an actual HTTP request, and returns the result.
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param string|null|resource|\Psr\Http\Message\StreamInterface $body
|
||||
* @param array $headers
|
||||
* @return Response
|
||||
*/
|
||||
public function request(string $method, string $url = '', $body = null, array $headers = [], array $options = []): Response
|
||||
{
|
||||
$request = $this->getRequest()
|
||||
->withHeaders($headers);
|
||||
|
||||
if ($body !== null) {
|
||||
$request = $request->withBody($body, 'application/xml; charset=utf-8');
|
||||
}
|
||||
|
||||
$url = Str::startsWith($url, 'http') ? $url : $this->path($url);
|
||||
|
||||
return $request
|
||||
->send($method, $url, $options)
|
||||
->throw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a WebDAV multistatus response body.
|
||||
*
|
||||
* This method returns an array with the following structure
|
||||
*
|
||||
* [
|
||||
* 'url/to/resource' => [
|
||||
* 'properties' => [
|
||||
* '200' => [
|
||||
* '{DAV:}property1' => 'value1',
|
||||
* '{DAV:}property2' => 'value2',
|
||||
* ],
|
||||
* '404' => [
|
||||
* '{DAV:}property1' => null,
|
||||
* '{DAV:}property2' => null,
|
||||
* ],
|
||||
* ],
|
||||
* 'status' => 200,
|
||||
* ],
|
||||
* 'url/to/resource2' => [
|
||||
* .. etc ..
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* @param string $body xml body
|
||||
* @return array
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc4918#section-9.2.1
|
||||
*/
|
||||
private static function parseMultiStatus(string $body): array
|
||||
{
|
||||
$multistatus = (new Service())
|
||||
->expect('{DAV:}multistatus', $body);
|
||||
|
||||
$result = [];
|
||||
|
||||
if (is_object($multistatus)) {
|
||||
foreach ($multistatus->getResponses() as $response) {
|
||||
$result[$response->getHref()] = [
|
||||
'properties' => $response->getResponseProperties(),
|
||||
'status' => $response->getHttpStatus() ?? '200',
|
||||
];
|
||||
}
|
||||
|
||||
$synctoken = $multistatus->getSyncToken();
|
||||
if (! empty($synctoken)) {
|
||||
$result['synctoken'] = $synctoken;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Element Namespace and add it as document's child.
|
||||
*
|
||||
* @param \DOMDocument $dom
|
||||
* @param string|null $namespace
|
||||
* @param string $qualifiedName
|
||||
* @return \DOMNode
|
||||
*/
|
||||
private static function addElementNS(\DOMDocument $dom, ?string $namespace, string $qualifiedName): \DOMNode
|
||||
{
|
||||
return $dom->appendChild($dom->createElementNS($namespace, $qualifiedName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Element and add it as root's child.
|
||||
*
|
||||
* @param \DOMDocument $dom
|
||||
* @param \DOMNode $root
|
||||
* @param string $name
|
||||
* @param string|null $value
|
||||
* @return \DOMNode
|
||||
*/
|
||||
private static function addElement(\DOMDocument $dom, \DOMNode $root, string $name, ?string $value = null): \DOMNode
|
||||
{
|
||||
return $root->appendChild($dom->createElement($name, $value));
|
||||
}
|
||||
}
|
||||
9
app/Services/DavClient/Utils/Dav/DavClientException.php
Normal file
9
app/Services/DavClient/Utils/Dav/DavClientException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Dav;
|
||||
|
||||
use Exception;
|
||||
|
||||
class DavClientException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Dav;
|
||||
|
||||
class DavServerNotCompliantException extends DavClientException
|
||||
{
|
||||
}
|
||||
81
app/Services/DavClient/Utils/Dav/ServiceUrlQuery.php
Normal file
81
app/Services/DavClient/Utils/Dav/ServiceUrlQuery.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Dav;
|
||||
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Illuminate\Support\Collection;
|
||||
use Http\Client\Exception\RequestException;
|
||||
|
||||
class ServiceUrlQuery
|
||||
{
|
||||
/**
|
||||
* Get service url.
|
||||
*
|
||||
* @return string|null
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6352#section-11
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc2782
|
||||
*/
|
||||
public function execute(string $name, bool $https, string $baseUri, DavClient $client): ?string
|
||||
{
|
||||
try {
|
||||
$host = \Safe\parse_url($baseUri, PHP_URL_HOST);
|
||||
} catch (\Safe\Exceptions\UrlException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entries = $this->dns_get_record($name.'.'.$host, DNS_SRV);
|
||||
|
||||
if ($entries && $entries->count() > 0) {
|
||||
$entries = collect($entries)
|
||||
->groupBy('pri')
|
||||
->sortKeys()
|
||||
->first()
|
||||
->sortByDesc('weight');
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
try {
|
||||
return $this->getUri($entry, $https, $client);
|
||||
} catch (RequestException $e) {
|
||||
// no exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uri from entry.
|
||||
*
|
||||
* @param array $entry
|
||||
* @param bool $https
|
||||
* @param DavClient $client
|
||||
* @return string
|
||||
*
|
||||
* @throws \Http\Client\Exception\RequestException
|
||||
*/
|
||||
private function getUri(array $entry, bool $https, DavClient $client): string
|
||||
{
|
||||
$uri = (new Uri())
|
||||
->withScheme($https ? 'https' : 'http')
|
||||
->withPort($entry['port'])
|
||||
->withHost($entry['target']);
|
||||
|
||||
// Test connection
|
||||
$client->request('GET', $uri);
|
||||
|
||||
return (string) $uri;
|
||||
}
|
||||
|
||||
private function dns_get_record(string $hostname, int $type = DNS_ANY, ?array &$authns = null, ?array &$addtl = null, bool $raw = false): ?Collection
|
||||
{
|
||||
error_clear_last();
|
||||
$result = \dns_get_record($hostname, $type, $authns, $addtl, $raw);
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return collect($result);
|
||||
}
|
||||
}
|
||||
7
app/Services/DavClient/Utils/Model/ContactDeleteDto.php
Normal file
7
app/Services/DavClient/Utils/Model/ContactDeleteDto.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Model;
|
||||
|
||||
class ContactDeleteDto extends ContactDto
|
||||
{
|
||||
}
|
||||
28
app/Services/DavClient/Utils/Model/ContactDto.php
Normal file
28
app/Services/DavClient/Utils/Model/ContactDto.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Model;
|
||||
|
||||
class ContactDto
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $uri;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $etag;
|
||||
|
||||
/**
|
||||
* Create a new ContactDto.
|
||||
*
|
||||
* @param string $uri
|
||||
* @param string|null $etag
|
||||
*/
|
||||
public function __construct(string $uri, ?string $etag = null)
|
||||
{
|
||||
$this->uri = $uri;
|
||||
$this->etag = $etag;
|
||||
}
|
||||
}
|
||||
37
app/Services/DavClient/Utils/Model/ContactPushDto.php
Normal file
37
app/Services/DavClient/Utils/Model/ContactPushDto.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Model;
|
||||
|
||||
class ContactPushDto extends ContactUpdateDto
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $mode;
|
||||
|
||||
public const MODE_MATCH_NONE = 0;
|
||||
|
||||
public const MODE_MATCH_ETAG = 1;
|
||||
|
||||
public const MODE_MATCH_ANY = 2;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $contactId;
|
||||
|
||||
/**
|
||||
* Create a new ContactPushDto.
|
||||
*
|
||||
* @param string $uri
|
||||
* @param string|null $etag
|
||||
* @param string|resource $card
|
||||
* @param int $mode
|
||||
*/
|
||||
public function __construct(string $uri, ?string $etag, $card, int $contact_id, int $mode = self::MODE_MATCH_NONE)
|
||||
{
|
||||
parent::__construct($uri, $etag, $card);
|
||||
$this->mode = $mode;
|
||||
$this->contactId = $contact_id;
|
||||
}
|
||||
}
|
||||
44
app/Services/DavClient/Utils/Model/ContactUpdateDto.php
Normal file
44
app/Services/DavClient/Utils/Model/ContactUpdateDto.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Model;
|
||||
|
||||
use function Safe\fclose;
|
||||
use function Safe\stream_get_contents;
|
||||
|
||||
class ContactUpdateDto extends ContactDto
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $card;
|
||||
|
||||
/**
|
||||
* Create a new ContactUpdateDto.
|
||||
*
|
||||
* @param string $uri
|
||||
* @param string|null $etag
|
||||
* @param string|resource $card
|
||||
*/
|
||||
public function __construct(string $uri, ?string $etag, $card)
|
||||
{
|
||||
parent::__construct($uri, $etag);
|
||||
$this->card = self::transformCard($card);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform card.
|
||||
*
|
||||
* @param string|resource $card
|
||||
* @return string
|
||||
*/
|
||||
protected static function transformCard($card): string
|
||||
{
|
||||
if (is_resource($card)) {
|
||||
$card = tap(stream_get_contents($card), function () use ($card) {
|
||||
fclose($card);
|
||||
});
|
||||
}
|
||||
|
||||
return $card;
|
||||
}
|
||||
}
|
||||
77
app/Services/DavClient/Utils/Model/SyncDto.php
Normal file
77
app/Services/DavClient/Utils/Model/SyncDto.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Model;
|
||||
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
use App\Models\Account\AddressBookSubscription;
|
||||
use App\Services\DavClient\Utils\Dav\DavClient;
|
||||
use App\Http\Controllers\DAV\Backend\CardDAV\CardDAVBackend;
|
||||
|
||||
/**
|
||||
* @method array propFind($properties, int $depth = 0, array $options = [], string $url = '')
|
||||
* @method array|string|null getProperty(string $property, string $url = '', array $options = [])
|
||||
* @method array syncCollection($properties, string $syncToken, array $options = [], string $url = '')
|
||||
* @method array addressbookQuery($properties, array $options = [], string $url = '')
|
||||
*/
|
||||
class SyncDto
|
||||
{
|
||||
use Macroable {
|
||||
__call as macroCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var AddressBookSubscription
|
||||
*/
|
||||
public $subscription;
|
||||
|
||||
/**
|
||||
* @var DavClient
|
||||
*/
|
||||
public $client;
|
||||
|
||||
/**
|
||||
* Sync the address book.
|
||||
*/
|
||||
public function __construct(AddressBookSubscription $subscription, DavClient $client)
|
||||
{
|
||||
$this->subscription = $subscription;
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get address book name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function addressBookName(): string
|
||||
{
|
||||
return $this->subscription->addressbook->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get carddav backend.
|
||||
*
|
||||
* @return CardDAVBackend
|
||||
*/
|
||||
public function backend(): CardDAVBackend
|
||||
{
|
||||
return app(CardDAVBackend::class)->init($this->subscription->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a method against a new dav client instance.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $parameters
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if (static::hasMacro($method)) {
|
||||
return $this->macroCall($method, $parameters);
|
||||
}
|
||||
|
||||
return $this->subscription->getClient()
|
||||
->{$method}(...$parameters);
|
||||
}
|
||||
}
|
||||
19
app/Services/DavClient/Utils/Traits/HasCapability.php
Normal file
19
app/Services/DavClient/Utils/Traits/HasCapability.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Traits;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
trait HasCapability
|
||||
{
|
||||
/**
|
||||
* Check if the subscription has the give capability.
|
||||
*
|
||||
* @param string $capability
|
||||
* @return bool
|
||||
*/
|
||||
private function hasCapability(string $capability): bool
|
||||
{
|
||||
return Arr::get($this->sync->subscription->capabilities, $capability, false);
|
||||
}
|
||||
}
|
||||
24
app/Services/DavClient/Utils/Traits/WithSyncDto.php
Normal file
24
app/Services/DavClient/Utils/Traits/WithSyncDto.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DavClient\Utils\Traits;
|
||||
|
||||
use App\Services\DavClient\Utils\Model\SyncDto;
|
||||
use App\Http\Controllers\DAV\Backend\CardDAV\CardDAVBackend;
|
||||
|
||||
trait WithSyncDto
|
||||
{
|
||||
/**
|
||||
* @var SyncDto
|
||||
*/
|
||||
protected $sync;
|
||||
|
||||
/**
|
||||
* Get carddav backend.
|
||||
*
|
||||
* @return CardDAVBackend
|
||||
*/
|
||||
protected function backend(): CardDAVBackend
|
||||
{
|
||||
return $this->sync->backend();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user