1230 lines
37 KiB
PHP
1230 lines
37 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* @copyright Copyright (c) 2023 Sebastian Krupinski <krupinski01@gmail.com>
|
|
*
|
|
* @author Sebastian Krupinski <krupinski01@gmail.com>
|
|
*
|
|
* @license AGPL-3.0-or-later
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
namespace KTXM\ProviderJmapc\Service\Remote;
|
|
|
|
use DateTimeImmutable;
|
|
use DateTimeZone;
|
|
use Exception;
|
|
|
|
use JmapClient\Client;
|
|
use JmapClient\Requests\Contacts\AddressBookGet;
|
|
use JmapClient\Requests\Contacts\AddressBookParameters as AddressBookParametersRequest;
|
|
use JmapClient\Requests\Contacts\AddressBookSet;
|
|
use JmapClient\Requests\Contacts\ContactChanges;
|
|
use JmapClient\Requests\Contacts\ContactGet;
|
|
use JmapClient\Requests\Contacts\ContactParameters as ContactParametersRequest;
|
|
use JmapClient\Requests\Contacts\ContactQuery;
|
|
use JmapClient\Requests\Contacts\ContactQueryChanges;
|
|
use JmapClient\Requests\Contacts\ContactSet;
|
|
use JmapClient\Responses\Contacts\AddressBookParameters as AddressBookParametersResponse;
|
|
use JmapClient\Responses\Contacts\ContactParameters as ContactParametersResponse;
|
|
use JmapClient\Responses\ResponseException;
|
|
use JmapClient\Session\Account;
|
|
use OCA\JMAPC\Exceptions\JmapUnknownMethod;
|
|
use OCA\JMAPC\Objects\BaseStringCollection;
|
|
use OCA\JMAPC\Objects\Contact\ContactAliasObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactAnniversaryObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactCollectionObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactCryptoObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactEmailObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactNoteObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactObject as ContactObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactOrganizationObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactPhoneObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactPhysicalLocationObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactTitleObject;
|
|
use OCA\JMAPC\Objects\Contact\ContactTitleTypes;
|
|
use OCA\JMAPC\Objects\DeltaObject;
|
|
use OCA\JMAPC\Objects\OriginTypes;
|
|
use OCA\JMAPC\Store\Common\Filters\IFilter;
|
|
use OCA\JMAPC\Store\Common\Range\IRangeTally;
|
|
use OCA\JMAPC\Store\Common\Range\RangeAnchorType;
|
|
use OCA\JMAPC\Store\Common\Sort\ISort;
|
|
use OCA\JMAPC\Store\Remote\Filters\ContactFilter;
|
|
use OCA\JMAPC\Store\Remote\Sort\ContactSort;
|
|
|
|
class RemoteContactsService {
|
|
protected Client $dataStore;
|
|
protected string $dataAccount;
|
|
|
|
protected ?string $resourceNamespace = null;
|
|
protected ?string $resourceCollectionLabel = null;
|
|
protected ?string $resourceEntityLabel = null;
|
|
|
|
protected array $collectionPropertiesDefault = [];
|
|
protected array $collectionPropertiesBasic = [];
|
|
protected array $entityPropertiesDefault = [];
|
|
protected array $entityPropertiesBasic = [
|
|
'id', 'addressbookId', 'uid'
|
|
];
|
|
|
|
public function __construct() {
|
|
}
|
|
|
|
public function initialize(Client $dataStore, ?string $dataAccount = null) {
|
|
|
|
$this->dataStore = $dataStore;
|
|
// evaluate if client is connected
|
|
if (!$this->dataStore->sessionStatus()) {
|
|
$this->dataStore->connect();
|
|
}
|
|
// determine account
|
|
if ($dataAccount === null) {
|
|
if ($this->resourceNamespace !== null) {
|
|
$account = $dataStore->sessionAccountDefault($this->resourceNamespace, false);
|
|
} else {
|
|
$account = $dataStore->sessionAccountDefault('contacts');
|
|
}
|
|
$this->dataAccount = $account !== null ? $account->id() : '';
|
|
} else {
|
|
$this->dataAccount = $dataAccount;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* list of collections in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
* @param string|null $location Id of parent collection
|
|
* @param string|null $granularity Amount of detail to return
|
|
* @param int|null $depth Depth of sub collections to return
|
|
*
|
|
* @return array<string,ContactCollectionObject>
|
|
*/
|
|
public function collectionList(?string $location = null, ?string $granularity = null, ?int $depth = null): array {
|
|
// construct request
|
|
$r0 = new AddressBookGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceCollectionLabel);
|
|
// set target to query request
|
|
if ($location !== null) {
|
|
$r0->target($location);
|
|
}
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// convert jmap objects to collection objects
|
|
$list = [];
|
|
foreach ($response->objects() as $so) {
|
|
if (!$so instanceof AddressBookParametersResponse) {
|
|
continue;
|
|
}
|
|
$to = $this->toContactCollection($so);
|
|
$to->Signature = $response->state();
|
|
$list[] = $to;
|
|
}
|
|
// return collection of collections
|
|
return $list;
|
|
}
|
|
|
|
/**
|
|
* retrieve properties for specific collection
|
|
*
|
|
* @since Release 1.0.0
|
|
*/
|
|
public function collectionFetch(string $identifier): ?ContactCollectionObject {
|
|
// construct request
|
|
$r0 = new AddressBookGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceCollectionLabel);
|
|
$r0->target($identifier);
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// convert jmap object to collection object
|
|
$so = $response->object(0);
|
|
$to = null;
|
|
if ($so instanceof AddressBookParametersResponse) {
|
|
$to = $this->toContactCollection($so);
|
|
$to->Signature = $response->state();
|
|
}
|
|
return $to;
|
|
}
|
|
|
|
/**
|
|
* create collection in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*/
|
|
public function collectionCreate(ContactCollectionObject $so): ?string {
|
|
// convert entity
|
|
$to = $this->fromContactCollection($so);
|
|
$id = uniqid();
|
|
// construct request
|
|
$r0 = new AddressBookSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceCollectionLabel);
|
|
$r0->create($id, $to);
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// check for success
|
|
$result = $response->createSuccess($id);
|
|
if ($result !== null) {
|
|
return (string)$result['id'];
|
|
}
|
|
// check for failure
|
|
$result = $response->createFailure($id);
|
|
if ($result !== null) {
|
|
$type = $result['type'] ?? 'unknownError';
|
|
$description = $result['description'] ?? 'An unknown error occurred during collection creation.';
|
|
throw new Exception("$type: $description", 1);
|
|
}
|
|
// return null if creation failed without failure reason
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* modify collection in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*/
|
|
public function collectionModify(string $identifier, ContactCollectionObject $so): ?string {
|
|
// convert entity
|
|
$to = $this->fromContactCollection($so);
|
|
// construct request
|
|
$r0 = new AddressBookSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceCollectionLabel);
|
|
$r0->update($identifier, $to);
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// check for success
|
|
$result = $response->updateSuccess($identifier);
|
|
if ($result !== null) {
|
|
return (string)$result['id'];
|
|
}
|
|
// check for failure
|
|
$result = $response->updateFailure($identifier);
|
|
if ($result !== null) {
|
|
$type = $result['type'] ?? 'unknownError';
|
|
$description = $result['description'] ?? 'An unknown error occurred during collection modification.';
|
|
throw new Exception("$type: $description", 1);
|
|
}
|
|
// return null if modification failed without failure reason
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* delete collection in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
*/
|
|
public function collectionDelete(string $identifier): ?string {
|
|
// construct request
|
|
$r0 = new AddressBookSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceCollectionLabel);
|
|
$r0->delete($identifier);
|
|
$r0->deleteContents(true);
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// check for success
|
|
$result = $response->deleteSuccess($identifier);
|
|
if ($result !== null) {
|
|
return (string)$result['id'];
|
|
}
|
|
// check for failure
|
|
$result = $response->deleteFailure($identifier);
|
|
if ($result !== null) {
|
|
$type = $result['type'] ?? 'unknownError';
|
|
$description = $result['description'] ?? 'An unknown error occurred during collection deletion.';
|
|
throw new Exception("$type: $description", 1);
|
|
}
|
|
// return null if deletion failed without failure reason
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* retrieve entities from remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
* @param string|null $location Id of parent collection
|
|
* @param string|null $granularity Amount of detail to return
|
|
* @param IRange|null $range Range of collections to return
|
|
* @param IFilter|null $filter Properties to filter by
|
|
* @param ISort|null $sort Properties to sort by
|
|
*/
|
|
public function entityList(?string $location = null, ?string $granularity = null, ?IRangeTally $range = null, ?IFilter $filter = null, ?ISort $sort = null, ?int $depth = null): array {
|
|
// construct request
|
|
$r0 = new ContactQuery($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
|
// define location
|
|
if (!empty($location)) {
|
|
$r0->filter()->in($location);
|
|
}
|
|
// define filter
|
|
if ($filter !== null) {
|
|
foreach ($filter->conditions() as $condition) {
|
|
$value = $condition['value'];
|
|
match($condition['attribute']) {
|
|
'createBefore' => $r0->filter()->createdBefore($value),
|
|
'createAfter' => $r0->filter()->createdAfter($value),
|
|
'modifiedBefore' => $r0->filter()->updatedBefore($value),
|
|
'modifiedAfter' => $r0->filter()->updatedAfter($value),
|
|
'uid' => $r0->filter()->uid($value),
|
|
'kind' => $r0->filter()->kind($value),
|
|
'member' => $r0->filter()->member($value),
|
|
'text' => $r0->filter()->text($value),
|
|
'name' => $r0->filter()->name($value),
|
|
'nameGiven' => $r0->filter()->nameGiven($value),
|
|
'nameSurname' => $r0->filter()->nameSurname($value),
|
|
'nameAlias' => $r0->filter()->nameAlias($value),
|
|
'organization' => $r0->filter()->organization($value),
|
|
'email' => $r0->filter()->mail($value),
|
|
'phone' => $r0->filter()->phone($value),
|
|
'address' => $r0->filter()->address($value),
|
|
'note' => $r0->filter()->note($value),
|
|
default => null
|
|
};
|
|
}
|
|
}
|
|
// define sort
|
|
if ($sort !== null) {
|
|
foreach ($sort->conditions() as $condition) {
|
|
$direction = $condition['direction'];
|
|
match($condition['attribute']) {
|
|
'created' => $r0->sort()->created($direction),
|
|
'modified' => $r0->sort()->updated($direction),
|
|
'nameGiven' => $r0->sort()->nameGiven($direction),
|
|
'nameSurname' => $r0->sort()->nameSurname($direction),
|
|
default => null
|
|
};
|
|
}
|
|
}
|
|
// define range
|
|
if ($range !== null) {
|
|
if ($range->anchor() === RangeAnchorType::ABSOLUTE) {
|
|
$r0->limitAbsolute($range->getPosition(), $range->getCount());
|
|
}
|
|
if ($range->anchor() === RangeAnchorType::RELATIVE) {
|
|
$r0->limitRelative($range->getPosition(), $range->getCount());
|
|
}
|
|
}
|
|
// construct get request
|
|
$r1 = new ContactGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
|
// set target to query request
|
|
$r1->targetFromRequest($r0, '/ids');
|
|
// select properties to return
|
|
if ($granularity === 'B') {
|
|
$r1->property(...$this->entityPropertiesBasic);
|
|
}
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0, $r1]);
|
|
// extract response
|
|
$response = $bundle->response(1);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// convert json objects to contact objects
|
|
$state = $response->state();
|
|
$list = $response->objects();
|
|
foreach ($list as $id => $entry) {
|
|
$eo = $this->toContactObject($entry);
|
|
$eo->Signature = $this->generateSignature($eo);
|
|
$list[$id] = $eo;
|
|
}
|
|
// return status object
|
|
return ['list' => $list, 'state' => $state];
|
|
}
|
|
|
|
public function entityListFilter(): ContactFilter {
|
|
return new ContactFilter();
|
|
}
|
|
|
|
public function entityListSort(): ContactSort {
|
|
return new ContactSort();
|
|
}
|
|
|
|
/**
|
|
* delta for entities in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
* @return DeltaObject
|
|
*/
|
|
public function entityDelta(?string $location, string $state): DeltaObject {
|
|
|
|
if (empty($state)) {
|
|
$results = $this->entityList($location, 'B');
|
|
$delta = new DeltaObject();
|
|
$delta->signature = $results['state'];
|
|
foreach ($results['list'] as $entry) {
|
|
$delta->additions[] = $entry->ID;
|
|
}
|
|
return $delta;
|
|
}
|
|
if (empty($location)) {
|
|
return $this->entityDeltaDefault($state);
|
|
} else {
|
|
return $this->entityDeltaSpecific($location, $state);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* delta of changes for specific collection in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
*/
|
|
public function entityDeltaSpecific(?string $location, string $state): DeltaObject {
|
|
// construct set request
|
|
$r0 = new ContactQueryChanges($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
|
// set location constraint
|
|
if (!empty($location)) {
|
|
$r0->filter()->in($location);
|
|
}
|
|
// set state constraint
|
|
if (!empty($state)) {
|
|
$r0->state($state);
|
|
} else {
|
|
$r0->state('0');
|
|
}
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// convert jmap object to delta object
|
|
$delta = new DeltaObject();
|
|
$delta->signature = $response->stateNew();
|
|
$delta->additions = new BaseStringCollection($response->added());
|
|
$delta->modifications = new BaseStringCollection($response->updated());
|
|
$delta->deletions = new BaseStringCollection($response->removed());
|
|
|
|
return $delta;
|
|
}
|
|
|
|
/**
|
|
* delta of changes in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
*/
|
|
public function entityDeltaDefault(string $state, string $granularity = 'D'): DeltaObject {
|
|
// construct set request
|
|
$r0 = new ContactChanges($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
|
// set state constraint
|
|
if (!empty($state)) {
|
|
$r0->state($state);
|
|
} else {
|
|
$r0->state('');
|
|
}
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// convert jmap object to delta object
|
|
$delta = new DeltaObject();
|
|
$delta->signature = $response->stateNew();
|
|
$delta->additions = new BaseStringCollection($response->created());
|
|
$delta->modifications = new BaseStringCollection($response->updated());
|
|
$delta->deletions = new BaseStringCollection($response->deleted());
|
|
|
|
return $delta;
|
|
}
|
|
|
|
/**
|
|
* retrieve entity from remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
* @param string $location Id of collection
|
|
* @param string $identifier Id of entity
|
|
* @param string $granularity Amount of detail to return
|
|
*
|
|
* @return EventObject|null
|
|
*/
|
|
public function entityFetch(string $location, string $identifier, string $granularity = 'D'): ?ContactObject {
|
|
// construct request
|
|
$r0 = new ContactGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
|
$r0->target($identifier);
|
|
// select properties to return
|
|
if ($granularity === 'B') {
|
|
$r0->property(...$this->entityPropertiesBasic);
|
|
}
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// convert jmap object to event object
|
|
$so = $response->object(0);
|
|
if ($so instanceof ContactParametersResponse) {
|
|
$to = $this->toContactObject($so);
|
|
$to->Signature = $this->generateSignature($to);
|
|
}
|
|
|
|
return $to ?? null;
|
|
}
|
|
|
|
/**
|
|
* retrieve entity(ies) from remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
* @param string $location Id of collection
|
|
* @param array<string> $identifiers Id of entity
|
|
* @param string $granularity Amount of detail to return
|
|
*
|
|
* @return array<string,ContactObject>
|
|
*/
|
|
public function entityFetchMultiple(string $location, array $identifiers, string $granularity = 'D'): array {
|
|
// construct request
|
|
$r0 = new ContactGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
|
$r0->target(...$identifiers);
|
|
// select properties to return
|
|
if ($granularity === 'B') {
|
|
$r0->property(...$this->entityPropertiesBasic);
|
|
}
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// convert jmap object(s) to event object
|
|
$list = $response->objects();
|
|
foreach ($list as $id => $so) {
|
|
if (!$so instanceof ContactParametersResponse) {
|
|
continue;
|
|
}
|
|
$to = $this->toContactObject($so);
|
|
$to->Signature = $this->generateSignature($to);
|
|
$list[$id] = $so;
|
|
}
|
|
// return object(s)
|
|
return $list;
|
|
}
|
|
|
|
/**
|
|
* create entity in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
*/
|
|
public function entityCreate(string $location, ContactObject $so): ?ContactObject {
|
|
// convert entity
|
|
$entity = $this->fromContactObject($so);
|
|
$id = uniqid();
|
|
// construct set request
|
|
$r0 = new ContactSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
|
$r0->create($id, $entity)->in($location);
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// check for success
|
|
$result = $response->createSuccess($id);
|
|
if ($result !== null) {
|
|
$ro = clone $so;
|
|
$ro->Origin = OriginTypes::External;
|
|
$ro->ID = $result['id'];
|
|
$ro->CreatedOn = isset($result['updated']) ? new DateTimeImmutable($result['updated']) : null;
|
|
$ro->ModifiedOn = $ro->CreatedOn;
|
|
$ro->Signature = $this->generateSignature($ro);
|
|
return $ro;
|
|
}
|
|
// check for failure
|
|
$result = $response->createFailure($id);
|
|
if ($result !== null) {
|
|
$type = $result['type'] ?? 'unknownError';
|
|
$description = $result['description'] ?? 'An unknown error occurred during collection creation.';
|
|
throw new Exception("$type: $description", 1);
|
|
}
|
|
// return null if creation failed without failure reason
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* update entity in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
*/
|
|
public function entityModify(string $location, string $identifier, ContactObject $so): ?ContactObject {
|
|
// convert entity
|
|
$entity = $this->fromContactObject($so);
|
|
// construct set request
|
|
$r0 = new ContactSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
|
$r0->update($identifier, $entity)->in($location);
|
|
// transceive
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// check for success
|
|
$result = $response->updateSuccess($identifier);
|
|
if ($result !== null) {
|
|
$ro = clone $so;
|
|
$ro->Origin = OriginTypes::External;
|
|
$ro->ID = $identifier;
|
|
$ro->ModifiedOn = isset($result['updated']) ? new DateTimeImmutable($result['updated']) : null;
|
|
$ro->Signature = $this->generateSignature($ro);
|
|
return $ro;
|
|
}
|
|
// check for failure
|
|
$result = $response->updateFailure($identifier);
|
|
if ($result !== null) {
|
|
$type = $result['type'] ?? 'unknownError';
|
|
$description = $result['description'] ?? 'An unknown error occurred during collection modification.';
|
|
throw new Exception("$type: $description", 1);
|
|
}
|
|
// return null if modification failed without failure reason
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* delete entity from remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
*/
|
|
public function entityDelete(string $location, string $identifier): ?string {
|
|
// construct set request
|
|
$r0 = new ContactSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
|
// construct object
|
|
$r0->delete($identifier);
|
|
// transmit request and receive response
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// check for command error
|
|
if ($response instanceof ResponseException) {
|
|
if ($response->type() === 'unknownMethod') {
|
|
throw new JmapUnknownMethod($response->description(), 1);
|
|
} else {
|
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
|
}
|
|
}
|
|
// check for success
|
|
$result = $response->deleteSuccess($identifier);
|
|
if ($result !== null) {
|
|
return (string)$result['id'];
|
|
}
|
|
// check for failure
|
|
$result = $response->deleteFailure($identifier);
|
|
if ($result !== null) {
|
|
$type = $result['type'] ?? 'unknownError';
|
|
$description = $result['description'] ?? 'An unknown error occurred during collection deletion.';
|
|
throw new Exception("$type: $description", 1);
|
|
}
|
|
// return null if deletion failed without failure reason
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* copy entity in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
*/
|
|
public function entityCopy(string $sourceLocation, string $identifier, string $destinationLocation): string {
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* move entity in remote storage
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
*/
|
|
public function entityMove(string $sourceLocation, string $identifier, string $destinationLocation): string {
|
|
// construct set request
|
|
$r0 = new ContactSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
|
// construct object
|
|
$m0 = $r0->update($identifier);
|
|
$m0->in($destinationLocation);
|
|
// transmit request and receive response
|
|
$bundle = $this->dataStore->perform([$r0]);
|
|
// extract response
|
|
$response = $bundle->response(0);
|
|
// return collection information
|
|
return array_key_exists($identifier, $response->updated()) ? (string)$identifier : '';
|
|
}
|
|
|
|
private function toContactCollection(AddressBookParametersResponse $so): ContactCollectionObject {
|
|
$to = new ContactCollectionObject();
|
|
$to->Id = $so->id();
|
|
$to->Label = $so->label();
|
|
$to->Description = $so->description();
|
|
$to->Priority = $so->priority();
|
|
return $to;
|
|
}
|
|
|
|
public function fromContactCollection(ContactCollectionObject $so): AddressBookParametersRequest {
|
|
// create object
|
|
$to = new AddressBookParametersRequest();
|
|
|
|
if ($so->Label !== null) {
|
|
$to->label($so->Label);
|
|
}
|
|
if ($so->Description !== null) {
|
|
$to->description($so->Description);
|
|
}
|
|
if ($so->Priority !== null) {
|
|
$to->priority($so->Priority);
|
|
}
|
|
|
|
return $to;
|
|
}
|
|
|
|
/**
|
|
* convert jmap object to contact object
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
*/
|
|
public function toContactObject($so): ContactObject {
|
|
|
|
// create object
|
|
$do = new ContactObject();
|
|
// source origin
|
|
$do->Origin = OriginTypes::External;
|
|
// collection id
|
|
if ($so->in() !== null) {
|
|
$do->CID = $so->in()[0];
|
|
}
|
|
// entity id
|
|
if ($so->id() !== null) {
|
|
$do->ID = $so->id();
|
|
}
|
|
// universal id
|
|
if ($so->uid() !== null) {
|
|
$do->UUID = $so->uid();
|
|
}
|
|
// creation date time
|
|
if ($so->created() !== null) {
|
|
$do->CreatedOn = $so->created();
|
|
}
|
|
// modification date time
|
|
if ($so->updated() !== null) {
|
|
$do->ModifiedOn = $so->updated();
|
|
}
|
|
// kind
|
|
if ($so->kind() !== null) {
|
|
$do->Kind = $so->kind();
|
|
}
|
|
// name
|
|
if ($so->name() !== null) {
|
|
$nameParams = $so->name();
|
|
foreach ($nameParams->components() as $component) {
|
|
$kind = $component->kind();
|
|
$value = $component->value();
|
|
if ($kind === 'surname') {
|
|
$do->Name->Last = $value;
|
|
} elseif ($kind === 'given') {
|
|
$do->Name->First = $value;
|
|
} elseif ($kind === 'additional') {
|
|
$do->Name->Other = $value;
|
|
} elseif ($kind === 'prefix') {
|
|
$do->Name->Prefix = $value;
|
|
} elseif ($kind === 'suffix') {
|
|
$do->Name->Suffix = $value;
|
|
}
|
|
}
|
|
}
|
|
// aliases
|
|
if ($so->aliases() !== null) {
|
|
foreach ($so->aliases() as $id => $entry) {
|
|
$entity = new ContactAliasObject();
|
|
$entity->Id = (string)$id;
|
|
$entity->Label = $entry->name();
|
|
$do->Name->Aliases[$id] = $entity;
|
|
}
|
|
}
|
|
// anniversaries
|
|
if ($so->anniversaries() !== null) {
|
|
foreach ($so->anniversaries() as $id => $entry) {
|
|
$entity = new ContactAnniversaryObject();
|
|
if ($entry->date() !== null) {
|
|
$dateParams = $entry->date();
|
|
if (method_exists($dateParams, 'value')) {
|
|
$entity->When = $dateParams->value();
|
|
}
|
|
}
|
|
$do->Anniversaries[$id] = $entity;
|
|
}
|
|
}
|
|
// emails
|
|
if ($so->emails() !== null) {
|
|
foreach ($so->emails() as $id => $entry) {
|
|
$entity = new ContactEmailObject();
|
|
$entity->Id = (string)$id;
|
|
$entity->Address = $entry->address();
|
|
$entity->Priority = $entry->priority();
|
|
$entity->Context = !empty($entry->context()) ? $entry->context()[0] : null;
|
|
$do->Email[$id] = $entity;
|
|
}
|
|
}
|
|
// phones
|
|
if ($so->phones() !== null) {
|
|
foreach ($so->phones() as $id => $entry) {
|
|
$entity = new ContactPhoneObject();
|
|
$entity->Id = (string)$id;
|
|
$entity->Number = $entry->number();
|
|
$entity->Label = $entry->label();
|
|
$entity->Priority = $entry->priority();
|
|
$entity->Context = !empty($entry->context()) ? $entry->context()[0] : null;
|
|
$do->Phone[$id] = $entity;
|
|
}
|
|
}
|
|
// addresses
|
|
if ($so->addresses() !== null) {
|
|
foreach ($so->addresses() as $id => $entry) {
|
|
$entity = new ContactPhysicalLocationObject();
|
|
$entity->Id = (string)$id;
|
|
$entity->Coordinates = $entry->coordinates();
|
|
if ($entry->timeZone() !== null) {
|
|
$entity->TimeZone = $entry->timeZone()->getName();
|
|
}
|
|
$entity->Country = $entry->country();
|
|
// parse components
|
|
foreach ($entry->components() as $component) {
|
|
$kind = $component->kind();
|
|
$value = $component->value();
|
|
if ($kind === 'pobox') {
|
|
$entity->Box = $value;
|
|
} elseif ($kind === 'unit') {
|
|
$entity->Unit = $value;
|
|
} elseif ($kind === 'street') {
|
|
$entity->Street = $value;
|
|
} elseif ($kind === 'locality') {
|
|
$entity->Locality = $value;
|
|
} elseif ($kind === 'region') {
|
|
$entity->Region = $value;
|
|
} elseif ($kind === 'code') {
|
|
$entity->Code = $value;
|
|
} elseif ($kind === 'country') {
|
|
$entity->Country = $value;
|
|
}
|
|
}
|
|
$do->PhysicalLocations[$id] = $entity;
|
|
}
|
|
}
|
|
// organizations
|
|
if ($so->organizations() !== null) {
|
|
foreach ($so->organizations() as $id => $entry) {
|
|
$entity = new ContactOrganizationObject();
|
|
$entity->Id = (string)$id;
|
|
$entity->Label = $entry->name();
|
|
$entity->SortName = $entry->sorting();
|
|
if ($entry->units() !== null) {
|
|
foreach ($entry->units() as $unit) {
|
|
$entity->Units[] = $unit->name();
|
|
}
|
|
}
|
|
$do->Organizations[$id] = $entity;
|
|
}
|
|
}
|
|
// titles
|
|
if ($so->titles() !== null) {
|
|
foreach ($so->titles() as $id => $entry) {
|
|
$entity = new ContactTitleObject();
|
|
$entity->Id = (string)$id;
|
|
$entity->Label = $entry->name();
|
|
$entity->Relation = $entry->relation();
|
|
$entity->Kind = match ($entry->kind() ?? 'title') {
|
|
'role' => ContactTitleTypes::Role,
|
|
default => ContactTitleTypes::Title,
|
|
};
|
|
$do->Titles[$id] = $entity;
|
|
}
|
|
}
|
|
// tags
|
|
if ($so->tags() !== null) {
|
|
foreach ($so->tags() as $tag) {
|
|
$do->Tags[] = $tag;
|
|
}
|
|
}
|
|
// notes
|
|
if ($so->notes() !== null) {
|
|
foreach ($so->notes() as $id => $entry) {
|
|
$entity = new ContactNoteObject();
|
|
$entity->Id = (string)$id;
|
|
$entity->Content = $entry->value();
|
|
$entity->Date = $entry->created();
|
|
$do->Notes[$id] = $entity;
|
|
}
|
|
}
|
|
// crypto keys
|
|
if ($so->crypto() !== null) {
|
|
foreach ($so->crypto() as $id => $entry) {
|
|
$entity = new ContactCryptoObject();
|
|
$entity->Id = (string)$id;
|
|
$entity->Type = $entry->kind();
|
|
$entity->Data = $entry->uri();
|
|
$do->Crypto[$id] = $entity;
|
|
}
|
|
}
|
|
|
|
return $do;
|
|
|
|
}
|
|
|
|
/**
|
|
* convert contact object to jmap object
|
|
*
|
|
* @since Release 1.0.0
|
|
*
|
|
*/
|
|
public function fromContactObject(ContactObject $so): mixed {
|
|
|
|
// create object
|
|
$to = new ContactParametersRequest();
|
|
// universal id
|
|
if ($so->UUID !== null) {
|
|
$to->uid($so->UUID);
|
|
}
|
|
// creation date time
|
|
if ($so->CreatedOn !== null) {
|
|
$to->created($so->CreatedOn);
|
|
}
|
|
// modification date time
|
|
if ($so->ModifiedOn !== null) {
|
|
$to->updated($so->ModifiedOn);
|
|
}
|
|
// kind
|
|
if ($so->Kind !== null) {
|
|
$to->kind($so->Kind);
|
|
}
|
|
// name
|
|
if ($so->Name !== null) {
|
|
$nameParams = $to->name();
|
|
if ($so->Name->First !== null || $so->Name->Last !== null ||
|
|
$so->Name->Other !== null || $so->Name->Prefix !== null ||
|
|
$so->Name->Suffix !== null) {
|
|
// Build name components
|
|
if ($so->Name->Prefix !== null) {
|
|
$component = $nameParams->components();
|
|
$component->kind('prefix');
|
|
$component->value($so->Name->Prefix);
|
|
}
|
|
if ($so->Name->First !== null) {
|
|
$component = $nameParams->components();
|
|
$component->kind('given');
|
|
$component->value($so->Name->First);
|
|
}
|
|
if ($so->Name->Other !== null) {
|
|
$component = $nameParams->components();
|
|
$component->kind('additional');
|
|
$component->value($so->Name->Other);
|
|
}
|
|
if ($so->Name->Last !== null) {
|
|
$component = $nameParams->components();
|
|
$component->kind('surname');
|
|
$component->value($so->Name->Last);
|
|
}
|
|
if ($so->Name->Suffix !== null) {
|
|
$component = $nameParams->components();
|
|
$component->kind('suffix');
|
|
$component->value($so->Name->Suffix);
|
|
}
|
|
}
|
|
}
|
|
// aliases
|
|
foreach ($so->Name->Aliases ?? [] as $id => $entry) {
|
|
$aliasParams = $to->aliases((string)$id);
|
|
if ($entry->Label !== null) {
|
|
$aliasParams->name($entry->Label);
|
|
}
|
|
}
|
|
// anniversaries
|
|
foreach ($so->Anniversaries ?? [] as $id => $entry) {
|
|
$annivParams = $to->anniversaries((string)$id);
|
|
if ($entry->When !== null) {
|
|
$annivParams->dateStamp()->value($entry->When);
|
|
}
|
|
}
|
|
// emails
|
|
foreach ($so->Email ?? [] as $id => $entry) {
|
|
$emailParams = $to->emails((string)$id);
|
|
if ($entry->Address !== null) {
|
|
$emailParams->address($entry->Address);
|
|
}
|
|
if ($entry->Priority !== null) {
|
|
$emailParams->priority($entry->Priority);
|
|
}
|
|
if ($entry->Context !== null) {
|
|
$emailParams->context($entry->Context);
|
|
}
|
|
}
|
|
// phones
|
|
foreach ($so->Phone ?? [] as $id => $entry) {
|
|
$phoneParams = $to->phones((string)$id);
|
|
if ($entry->Number !== null) {
|
|
$phoneParams->number($entry->Number);
|
|
}
|
|
if ($entry->Label !== null) {
|
|
$phoneParams->label($entry->Label);
|
|
}
|
|
if ($entry->Priority !== null) {
|
|
$phoneParams->priority($entry->Priority);
|
|
}
|
|
if ($entry->Context !== null) {
|
|
$phoneParams->context($entry->Context);
|
|
}
|
|
}
|
|
// addresses
|
|
foreach ($so->PhysicalLocations ?? [] as $id => $entry) {
|
|
$addressParams = $to->addresses((string)$id);
|
|
if ($entry->Box !== null || $entry->Unit !== null || $entry->Street !== null ||
|
|
$entry->Locality !== null || $entry->Region !== null || $entry->Code !== null ||
|
|
$entry->Country !== null) {
|
|
// Build address components
|
|
if ($entry->Box !== null) {
|
|
$component = $addressParams->components();
|
|
$component->kind('pobox');
|
|
$component->value($entry->Box);
|
|
}
|
|
if ($entry->Unit !== null) {
|
|
$component = $addressParams->components();
|
|
$component->kind('unit');
|
|
$component->value($entry->Unit);
|
|
}
|
|
if ($entry->Street !== null) {
|
|
$component = $addressParams->components();
|
|
$component->kind('street');
|
|
$component->value($entry->Street);
|
|
}
|
|
if ($entry->Locality !== null) {
|
|
$component = $addressParams->components();
|
|
$component->kind('locality');
|
|
$component->value($entry->Locality);
|
|
}
|
|
if ($entry->Region !== null) {
|
|
$component = $addressParams->components();
|
|
$component->kind('region');
|
|
$component->value($entry->Region);
|
|
}
|
|
if ($entry->Code !== null) {
|
|
$component = $addressParams->components();
|
|
$component->kind('code');
|
|
$component->value($entry->Code);
|
|
}
|
|
if ($entry->Country !== null) {
|
|
$component = $addressParams->components();
|
|
$component->kind('country');
|
|
$component->value($entry->Country);
|
|
}
|
|
}
|
|
if ($entry->Country !== null) {
|
|
$addressParams->country($entry->Country);
|
|
}
|
|
if ($entry->Coordinates !== null) {
|
|
// Parse coordinates in format "geo:latitude,longitude"
|
|
if (preg_match('/geo:([-\d.]+),([-\d.]+)/', $entry->Coordinates, $matches)) {
|
|
$addressParams->coordinates((float)$matches[1], (float)$matches[2]);
|
|
}
|
|
}
|
|
if ($entry->TimeZone !== null) {
|
|
try {
|
|
$addressParams->timeZone(new DateTimeZone($entry->TimeZone));
|
|
} catch (\Exception $e) {
|
|
// Invalid timezone, skip
|
|
}
|
|
}
|
|
}
|
|
// organizations
|
|
foreach ($so->Organizations ?? [] as $id => $entry) {
|
|
$orgParams = $to->organizations((string)$id);
|
|
if ($entry->Label !== null) {
|
|
$orgParams->name($entry->Label);
|
|
}
|
|
if ($entry->SortName !== null) {
|
|
$orgParams->sorting($entry->SortName);
|
|
}
|
|
foreach ($entry->Units ?? [] as $unit) {
|
|
$unitParams = $orgParams->units();
|
|
$unitParams->name($unit);
|
|
}
|
|
}
|
|
// titles
|
|
foreach ($so->Titles ?? [] as $id => $entry) {
|
|
$titleParams = $to->titles((string)$id);
|
|
if ($entry->Label !== null) {
|
|
$titleParams->name($entry->Label);
|
|
}
|
|
if ($entry->Kind !== null) {
|
|
$titleParams->kind(match ($entry->Kind ?? ContactTitleTypes::Title) {
|
|
ContactTitleTypes::Role => 'role',
|
|
default => 'title',
|
|
});
|
|
}
|
|
if ($entry->Relation !== null) {
|
|
$titleParams->relation($entry->Relation);
|
|
}
|
|
}
|
|
// tags
|
|
if (!empty($so->Tags)) {
|
|
$tags = [];
|
|
foreach ($so->Tags as $tag) {
|
|
if ($tag->Value !== null) {
|
|
$tags[] = $tag->Value;
|
|
}
|
|
}
|
|
if (!empty($tags)) {
|
|
$to->tags(...$tags);
|
|
}
|
|
}
|
|
// notes
|
|
foreach ($so->Notes ?? [] as $id => $entry) {
|
|
$noteParams = $to->notes((string)$id);
|
|
if ($entry->Content !== null) {
|
|
$noteParams->contents($entry->Content);
|
|
}
|
|
if ($entry->Date !== null) {
|
|
$noteParams->created($entry->Date);
|
|
}
|
|
}
|
|
// crypto keys
|
|
foreach ($so->Crypto ?? [] as $id => $entry) {
|
|
$cryptoParams = $to->crypto((string)$id);
|
|
if ($entry->Type !== null) {
|
|
$cryptoParams->kind($entry->Type);
|
|
}
|
|
if ($entry->Data !== null) {
|
|
$cryptoParams->uri($entry->Data);
|
|
}
|
|
}
|
|
|
|
return $to;
|
|
|
|
}
|
|
|
|
public function generateSignature(ContactObject $eo): string {
|
|
|
|
// clone self
|
|
$o = clone $eo;
|
|
// remove non needed values
|
|
unset(
|
|
$o->Origin,
|
|
$o->ID,
|
|
$o->CID,
|
|
$o->Signature,
|
|
$o->CCID,
|
|
$o->CEID,
|
|
$o->CESN,
|
|
$o->UUID,
|
|
$o->CreatedOn,
|
|
$o->ModifiedOn
|
|
);
|
|
// generate signature
|
|
return md5(json_encode($o, JSON_PARTIAL_OUTPUT_ON_ERROR));
|
|
|
|
}
|
|
|
|
}
|