diff --git a/lib/Module.php b/lib/Module.php index cfd553e..bc20d0b 100644 --- a/lib/Module.php +++ b/lib/Module.php @@ -16,6 +16,7 @@ use KTXF\Resource\Provider\ProviderInterface; use KTXM\ProviderJmapc\Providers\Mail\Provider as MailProvider; use KTXM\ProviderJmapc\Providers\Chrono\Provider as ChronoProvider; use KTXM\ProviderJmapc\Providers\People\Provider as PeopleProvider; +use KTXM\ProviderJmapc\Providers\Document\Provider as DocumentProvider; /** * JMAP Client Provider Module @@ -70,6 +71,7 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface $this->providerManager->register(ProviderInterface::TYPE_MAIL, 'jmap', MailProvider::class); //$this->providerManager->register(ProviderInterface::TYPE_CHRONO, 'jmap', ChronoProvider::class); //$this->providerManager->register(ProviderInterface::TYPE_PEOPLE, 'jmap', PeopleProvider::class); + $this->providerManager->register(ProviderInterface::TYPE_DOCUMENT, 'jmap', DocumentProvider::class); } public function registerBI(): array { diff --git a/lib/Providers/Document/CollectionProperties.php b/lib/Providers/Document/CollectionProperties.php new file mode 100644 index 0000000..8847e89 --- /dev/null +++ b/lib/Providers/Document/CollectionProperties.php @@ -0,0 +1,47 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderJmapc\Providers\Document; + +use KTXF\Resource\Documents\Collection\CollectionPropertiesMutableAbstract; + +/** + * Document Collection Properties Implementation + */ +class CollectionProperties extends CollectionPropertiesMutableAbstract { + + /** + * Convert JMAP parameters array to document collection properties object + * + * @param array $parameters JMAP parameters array + */ + public function fromJmap(array $parameters): static { + + if (isset($parameters['name'])) { + $this->data['label'] = $parameters['name']; + } + + return $this; + } + + /** + * Convert mail collection properties object to JMAP parameters array + */ + public function toJmap(): array { + + $parameters = []; + + if (isset($this->data['label'])) { + $parameters['name'] = $this->data['label']; + } + + return $parameters; + } + +} diff --git a/lib/Providers/Document/CollectionResource.php b/lib/Providers/Document/CollectionResource.php new file mode 100644 index 0000000..106a618 --- /dev/null +++ b/lib/Providers/Document/CollectionResource.php @@ -0,0 +1,83 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderJmapc\Providers\Document; + +use KTXF\Resource\Documents\Collection\CollectionMutableAbstract; + +/** + * Document Collection Resource Implementation + */ +class CollectionResource extends CollectionMutableAbstract { + + public function __construct( + string $provider = 'jmapc', + string|int|null $service = null, + ) { + parent::__construct($provider, $service); + } + + /** + * Converts JMAP parameters array to document collection object + * + * @param array $parameters JMAP parameters array + */ + public function fromJmap(array $parameters): static { + + if (isset($parameters['parentId'])) { + $this->data['collection'] = $parameters['parentId']; + } + if (isset($parameters['id'])) { + $this->data['identifier'] = $parameters['id']; + } + if (isset($parameters['created'])) { + $this->data['created'] = $parameters['created']; + } + if (isset($parameters['modified'])) { + $this->data['modified'] = $parameters['modified']; + } + if (isset($parameters['signature'])) { + $this->data['signature'] = $parameters['signature']; + } + + $this->getProperties()->fromJmap($parameters); + + return $this; + } + + /** + * Convert document collection object to JMAP parameters array + */ + public function toJmap(): array { + + $parameters = []; + + if (isset($this->data['collection'])) { + $parameters['parentId'] = $this->data['collection']; + } + if (isset($this->data['identifier'])) { + $parameters['id'] = $this->data['identifier']; + } + + $parameters = array_merge($parameters, $this->getProperties()->toJmap()); + + return $parameters; + } + + /** + * @inheritDoc + */ + public function getProperties(): CollectionProperties { + if (!isset($this->properties)) { + $this->properties = new CollectionProperties([]); + } + return $this->properties; + } + +} diff --git a/lib/Providers/Document/EntityProperties.php b/lib/Providers/Document/EntityProperties.php new file mode 100644 index 0000000..945dc61 --- /dev/null +++ b/lib/Providers/Document/EntityProperties.php @@ -0,0 +1,62 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderJmapc\Providers\Document; + +use KTXF\Resource\Documents\Entity\EntityPropertiesMutableAbstract; + +/** + * Document Collection Properties Implementation + */ +class EntityProperties extends EntityPropertiesMutableAbstract { + + /** + * Convert JMAP parameters array to document entity properties object + * + * @param array $parameters JMAP parameters array + */ + public function fromJmap(array $parameters): static { + + if (isset($data['size'])) { + $this->data[self::JSON_PROPERTY_SIZE] = (int) $data['size']; + } + + if (isset($data['label'])) { + $this->data[self::JSON_PROPERTY_LABEL] = (string) $data['name']; + } + + if (isset($data['mime'])) { + $this->data[self::JSON_PROPERTY_MIME] = (string) $data['type']; + } + + if (isset($data['format'])) { + $this->data[self::JSON_PROPERTY_FORMAT] = null; + } + + if (isset($data['encoding'])) { + $this->data[self::JSON_PROPERTY_ENCODING] = null; + } + + return $this; + } + + /** + * Convert document entity properties object to JMAP parameters array + */ + public function toJmap(): array { + + $parameters = array_filter([ + 'name' => $this->data[self::JSON_PROPERTY_LABEL], + 'type' => $this->data[self::JSON_PROPERTY_MIME] ?? 'application/octet-stream' + ], static fn($value) => $value !== null); + + return $parameters; + } + +} diff --git a/lib/Providers/Document/EntityResource.php b/lib/Providers/Document/EntityResource.php new file mode 100644 index 0000000..e2e6faa --- /dev/null +++ b/lib/Providers/Document/EntityResource.php @@ -0,0 +1,83 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderJmapc\Providers\Document; + +use KTXF\Resource\Documents\Entity\EntityMutableAbstract; + +/** + * Mail Entity Resource Implementation + */ +class EntityResource extends EntityMutableAbstract { + + public function __construct( + string $provider = 'jmapc', + string|int|null $service = null, + ) { + parent::__construct($provider, $service); + } + + /** + * Convert JMAP parameters array to mail entity object + * + * @param array $parameters JMAP parameters array + */ + public function fromJmap(array $parameters): static { + + if (isset($parameters['parentId'])) { + $this->data['collection'] = $parameters['parentId']; + } + if (isset($parameters['id'])) { + $this->data['identifier'] = $parameters['id']; + } + if (isset($parameters['signature'])) { + $this->data['signature'] = $parameters['signature']; + } + if (isset($parameters['created'])) { + $this->data['created'] = $parameters['created'] ?? $parameters['created']; + } + if (isset($parameters['modified'])) { + $this->data['modified'] = $parameters['modified']; + } + if (isset($parameters['accessed'])) + + $this->getProperties()->fromJmap($parameters); + + return $this; + } + + /** + * Convert mail entity object to JMAP parameters array + */ + public function toJmap(): array { + + $parameters = []; + + if (isset($this->data['collection'])) { + $parameters['parentId'] = $this->data['collection']; + } + if (isset($this->data['identifier'])) { + $parameters['id'] = $this->data['identifier']; + } + + $parameters = array_merge($parameters, $this->getProperties()->toJmap()); + + return $parameters; + } + + /** + * @inheritDoc + */ + public function getProperties(): EntityProperties { + if (!isset($this->properties)) { + $this->properties = new EntityProperties([]); + } + return $this->properties; + } +} diff --git a/lib/Providers/Document/Provider.php b/lib/Providers/Document/Provider.php new file mode 100644 index 0000000..a5577d8 --- /dev/null +++ b/lib/Providers/Document/Provider.php @@ -0,0 +1,201 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderJmapc\Providers\Document; + +use KTXF\Resource\Documents\Provider\ProviderBaseInterface; +use KTXF\Resource\Documents\Provider\ProviderServiceMutateInterface; +use KTXF\Resource\Documents\Provider\ProviderServiceTestInterface; +use KTXF\Resource\Documents\Service\ServiceBaseInterface; +use KTXF\Resource\Documents\Service\ServiceMutableInterface; +use KTXF\Resource\Provider\ResourceServiceLocationInterface; +use KTXF\Resource\Provider\ResourceServiceMutateInterface; +use KTXM\ProviderJmapc\Service\Discovery; +use KTXM\ProviderJmapc\Service\Remote\RemoteService; +use KTXM\ProviderJmapc\Stores\ServiceStore; + +/** + * JMAP Mail Provider + */ +class Provider implements ProviderServiceMutateInterface, ProviderServiceTestInterface +{ + + public const JSON_TYPE = ProviderBaseInterface::JSON_TYPE; + protected const PROVIDER_IDENTIFIER = 'jmap'; + protected const PROVIDER_LABEL = 'JMAP Documents Provider'; + protected const PROVIDER_DESCRIPTION = 'Provides documents services via JMAP protocol (RFC 8620)'; + protected const PROVIDER_ICON = 'mdi-file-sync'; + + protected array $providerAbilities = [ + self::CAPABILITY_SERVICE_LIST => true, + self::CAPABILITY_SERVICE_FETCH => true, + self::CAPABILITY_SERVICE_EXTANT => true, + self::CAPABILITY_SERVICE_CREATE => true, + self::CAPABILITY_SERVICE_MODIFY => true, + self::CAPABILITY_SERVICE_DESTROY => true, + self::CAPABILITY_SERVICE_TEST => true, + ]; + + public function __construct( + private readonly ServiceStore $serviceStore, + ) {} + + public function jsonSerialize(): array + { + return [ + self::JSON_PROPERTY_TYPE => self::JSON_TYPE, + self::JSON_PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER, + self::JSON_PROPERTY_LABEL => self::PROVIDER_LABEL, + self::JSON_PROPERTY_CAPABILITIES => $this->providerAbilities, + ]; + } + + public function jsonDeserialize(array|string $data): static + { + return $this; + } + + public function type(): string + { + return self::TYPE_MAIL; + } + + public function identifier(): string + { + return self::PROVIDER_IDENTIFIER; + } + + public function label(): string + { + return self::PROVIDER_LABEL; + } + + public function description(): string + { + return self::PROVIDER_DESCRIPTION; + } + + public function icon(): string + { + return self::PROVIDER_ICON; + } + + public function capable(string $value): bool + { + return !empty($this->providerAbilities[$value]); + } + + public function capabilities(): array + { + return $this->providerAbilities; + } + + public function serviceList(string $tenantId, string $userId, array $filter = []): array + { + $list = $this->serviceStore->list($tenantId, $userId, $filter); + foreach ($list as $serviceData) { + $serviceInstance = $this->serviceFresh()->fromStore($serviceData); + $list[$serviceInstance->identifier()] = $serviceInstance; + } + return $list; + } + + public function serviceFetch(string $tenantId, string $userId, string|int $identifier): ?Service + { + $serviceData = $this->serviceStore->fetch($tenantId, $userId, $identifier); + if ($serviceData === null) { + return null; + } + $serviceInstance = $this->serviceFresh()->fromStore($serviceData); + return $serviceInstance; + } + + public function serviceExtant(string $tenantId, string $userId, string|int ...$identifiers): array + { + return $this->serviceStore->extant($tenantId, $userId, $identifiers); + } + + public function serviceFresh(): ServiceMutableInterface + { + return new Service(); + } + + public function serviceCreate(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string + { + if (!($service instanceof Service)) { + throw new \InvalidArgumentException('Service must be instance of JMAP Service'); + } + + $created = $this->serviceStore->create($tenantId, $userId, $service); + return (string) $created['id']; + } + + public function serviceModify(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string + { + if (!($service instanceof Service)) { + throw new \InvalidArgumentException('Service must be instance of JMAP Service'); + } + + $updated = $this->serviceStore->modify($tenantId, $userId, $service); + return (string) $updated['id']; + } + + public function serviceDestroy(string $tenantId, string $userId, ResourceServiceMutateInterface $service): bool + { + if (!($service instanceof Service)) { + return false; + } + + return $this->serviceStore->delete($tenantId, $userId, $service->identifier()); + } + + public function serviceDiscover( + string $tenantId, + string $userId, + string $identity, + ?string $location = null, + ?string $secret = null + ): ResourceServiceLocationInterface|null { + $discovery = new Discovery(); + + // TODO: Make SSL verification configurable based on tenant/user settings + $verifySSL = true; + + return $discovery->discover($identity, $location, $secret, $verifySSL); + } + + public function serviceTest(ServiceBaseInterface $service, array $options = []): array { + $startTime = microtime(true); + + try { + if (!($service instanceof Service)) { + throw new \InvalidArgumentException('Service must be instance of JMAP Service'); + } + + $client = RemoteService::freshClient($service); + $session = $client->connect(); + + $latency = round((microtime(true) - $startTime) * 1000); // ms4 + + return [ + 'success' => true, + 'message' => 'JMAP connection successful' + . ' (Account ID: ' . ($session->username() ?? 'N/A') . ')' + . ' (Latency: ' . $latency . ' ms)', + ]; + } catch (\Exception $e) { + $latency = round((microtime(true) - $startTime) * 1000); + return [ + 'success' => false, + 'message' => 'Test failed: ' . $e->getMessage(), + ]; + } + } + +} diff --git a/lib/Providers/Document/Service.php b/lib/Providers/Document/Service.php new file mode 100644 index 0000000..adaff01 --- /dev/null +++ b/lib/Providers/Document/Service.php @@ -0,0 +1,482 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderJmapc\Providers\Document; + +use Generator; +use KTXF\Resource\Provider\ResourceServiceIdentityInterface; +use KTXF\Resource\Provider\ResourceServiceLocationInterface; +use KTXF\Resource\Delta\Delta; +use KTXF\Resource\Documents\Collection\CollectionBaseInterface; +use KTXF\Resource\Documents\Collection\CollectionMutableInterface; +use KTXF\Resource\Documents\Service\ServiceBaseInterface; +use KTXF\Resource\Documents\Service\ServiceCollectionMutableInterface; +use KTXF\Resource\Documents\Service\ServiceConfigurableInterface; +use KTXF\Resource\Documents\Service\ServiceMutableInterface; +use KTXF\Resource\Filter\Filter; +use KTXF\Resource\Filter\IFilter; +use KTXF\Resource\Range\IRange; +use KTXF\Resource\Range\Range; +use KTXF\Resource\Range\RangeType; +use KTXF\Resource\Sort\ISort; +use KTXF\Resource\Sort\Sort; +use KTXM\ProviderJmapc\Providers\ServiceIdentityBasic; +use KTXM\ProviderJmapc\Providers\ServiceLocation; +use KTXM\ProviderJmapc\Service\Remote\RemoteFilesService; +use KTXM\ProviderJmapc\Service\Remote\RemoteService; + +class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface +{ + public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE; + + private const PROVIDER_IDENTIFIER = 'jmap'; + + private ?string $serviceTenantId = null; + private ?string $serviceUserId = null; + private ?string $serviceIdentifier = null; + private ?string $serviceLabel = null; + private bool $serviceEnabled = false; + private bool $serviceDebug = false; + private ?ServiceLocation $location = null; + private ?ServiceIdentityBasic $identity = null; + private array $auxiliary = []; + + private array $serviceAbilities = [ + self::CAPABILITY_COLLECTION_LIST => true, + self::CAPABILITY_COLLECTION_LIST_FILTER => [ + self::CAPABILITY_COLLECTION_FILTER_LABEL => 's:100:256:256', + ], + self::CAPABILITY_COLLECTION_LIST_SORT => [ + self::CAPABILITY_COLLECTION_SORT_LABEL, + ], + self::CAPABILITY_COLLECTION_EXTANT => true, + self::CAPABILITY_COLLECTION_FETCH => true, + self::CAPABILITY_COLLECTION_CREATE => true, + self::CAPABILITY_COLLECTION_UPDATE => true, + self::CAPABILITY_COLLECTION_DELETE => true, + self::CAPABILITY_ENTITY_LIST => true, + self::CAPABILITY_ENTITY_LIST_FILTER => [ + self::CAPABILITY_ENTITY_FILTER_ALL => 's:200:256:256', + ], + self::CAPABILITY_ENTITY_LIST_SORT => [ + self::CAPABILITY_ENTITY_SORT_LABEL + ], + self::CAPABILITY_ENTITY_LIST_RANGE => [ + 'tally' => ['absolute', 'relative'] + ], + self::CAPABILITY_ENTITY_DELTA => true, + self::CAPABILITY_ENTITY_EXTANT => true, + self::CAPABILITY_ENTITY_FETCH => true, + ]; + + private readonly RemoteFilesService $remoteService; + + public function __construct( + ) {} + + private function initialize(): void { + if (!isset($this->remoteService)) { + $client = RemoteService::freshClient($this); + $this->remoteService = RemoteService::documentsService($client); + } + } + + public function toStore(): array { + return array_filter([ + 'tid' => $this->serviceTenantId, + 'uid' => $this->serviceUserId, + 'sid' => $this->serviceIdentifier, + 'label' => $this->serviceLabel, + 'enabled' => $this->serviceEnabled, + 'debug' => $this->serviceDebug, + 'location' => $this->location?->toStore(), + 'identity' => $this->identity?->toStore(), + 'auxiliary' => $this->auxiliary, + ], fn($v) => $v !== null); + } + + public function fromStore(array $data): static { + $this->serviceTenantId = $data['tid'] ?? null; + $this->serviceUserId = $data['uid'] ?? null; + $this->serviceIdentifier = $data['sid']; + $this->serviceLabel = $data['label'] ?? ''; + $this->serviceEnabled = $data['enabled'] ?? false; + $this->serviceDebug = $data['debug'] ?? false; + + if (isset($data['location'])) { + $this->location = (new ServiceLocation())->fromStore($data['location']); + } + + if (isset($data['identity'])) { + $this->identity = (new ServiceIdentityBasic())->fromStore($data['identity']); + } + if (isset($data['auxiliary']) && is_array($data['auxiliary'])) { + $this->auxiliary = $data['auxiliary']; + } + + return $this; + } + + public function jsonSerialize(): array { + return array_filter([ + self::JSON_PROPERTY_TYPE => self::JSON_TYPE, + self::JSON_PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER, + self::JSON_PROPERTY_IDENTIFIER => $this->serviceIdentifier, + self::JSON_PROPERTY_LABEL => $this->serviceLabel, + self::JSON_PROPERTY_ENABLED => $this->serviceEnabled, + self::JSON_PROPERTY_CAPABILITIES => $this->serviceAbilities, + self::JSON_PROPERTY_LOCATION => $this->location?->jsonSerialize(), + self::JSON_PROPERTY_IDENTITY => $this->identity?->jsonSerialize(), + self::JSON_PROPERTY_AUXILIARY => $this->auxiliary, + ], fn($v) => $v !== null); + } + + public function jsonDeserialize(array|string $data): static { + if (is_string($data)) { + $data = json_decode($data, true, 512, JSON_THROW_ON_ERROR); + } + + if (isset($data[self::JSON_PROPERTY_LABEL])) { + $this->setLabel($data[self::JSON_PROPERTY_LABEL]); + } + if (isset($data[self::JSON_PROPERTY_ENABLED])) { + $this->setEnabled($data[self::JSON_PROPERTY_ENABLED]); + } + if (isset($data[self::JSON_PROPERTY_LOCATION])) { + $this->setLocation($this->freshLocation(null, $data[self::JSON_PROPERTY_LOCATION])); + } + if (isset($data[self::JSON_PROPERTY_IDENTITY])) { + $this->setIdentity($this->freshIdentity(null, $data[self::JSON_PROPERTY_IDENTITY])); + } + if (isset($data[self::JSON_PROPERTY_AUXILIARY]) && is_array($data[self::JSON_PROPERTY_AUXILIARY])) { + $this->setAuxiliary($data[self::JSON_PROPERTY_AUXILIARY]); + } + + return $this; + } + + public function capable(string $value): bool + { + return isset($this->serviceAbilities[$value]); + } + + public function capabilities(): array { + return $this->serviceAbilities; + } + + public function provider(): string + { + return self::PROVIDER_IDENTIFIER; + } + + public function identifier(): string|int + { + return $this->serviceIdentifier; + } + + public function getLabel(): string|null + { + return $this->serviceLabel; + } + + public function setLabel(string $label): static + { + $this->serviceLabel = $label; + return $this; + } + + public function getEnabled(): bool + { + return $this->serviceEnabled; + } + + public function setEnabled(bool $enabled): static + { + $this->serviceEnabled = $enabled; + return $this; + } + + public function getLocation(): ServiceLocation + { + return $this->location; + } + + public function setLocation(ResourceServiceLocationInterface $location): static + { + $this->location = $location; + return $this; + } + + public function freshLocation(string|null $type = null, array $data = []): ServiceLocation + { + $location = new ServiceLocation(); + $location->jsonDeserialize($data); + return $location; + } + + public function getIdentity(): ServiceIdentityBasic + { + return $this->identity; + } + + public function setIdentity(ResourceServiceIdentityInterface $identity): static + { + $this->identity = $identity; + return $this; + } + + public function freshIdentity(string|null $type, array $data = []): ServiceIdentityBasic + { + $identity = new ServiceIdentityBasic(); + $identity->jsonDeserialize($data); + return $identity; + } + + public function getDebug(): bool + { + return $this->serviceDebug; + } + + public function setDebug(bool $debug): static + { + $this->serviceDebug = $debug; + return $this; + } + + public function getAuxiliary(): array + { + return $this->auxiliary; + } + + public function setAuxiliary(array $auxiliary): static + { + $this->auxiliary = $auxiliary; + return $this; + } + + // Collection operations + + public function collectionList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null): array + { + $this->initialize(); + + $collections = $this->remoteService->collectionList($location, $filter, $sort); + + foreach ($collections as &$collection) { + if (is_array($collection) && isset($collection['id'])) { + $object = new CollectionResource(provider: $this->provider(), service: $this->identifier()); + $object->fromJmap($collection); + $collection = $object; + } + } + + return $collections; + } + + public function collectionListFilter(): Filter + { + return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []); + } + + public function collectionListSort(): Sort + { + return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []); + } + + public function collectionExtant(string|int|null $location, string|int ...$identifiers): array + { + $this->initialize(); + + return $this->remoteService->collectionExtant($location, ...$identifiers); + } + + public function collectionFetch(string|int|null $identifier): ?CollectionBaseInterface + { + $this->initialize(); + + $collection = $this->remoteService->collectionFetch($identifier); + + if (is_array($collection) && isset($collection['id'])) { + $object = new CollectionResource(provider: $this->provider(), service: $this->identifier()); + $object->fromJmap($collection); + $collection = $object; + } + + return $collection; + } + + public function collectionFresh(): CollectionMutableInterface + { + return new CollectionResource(provider: $this->provider(), service: $this->identifier()); + } + + public function collectionCreate(string|int|null $location, CollectionMutableInterface $collection, array $options = []): CollectionBaseInterface + { + $this->initialize(); + + if ($collection instanceof CollectionResource === false) { + $object = new CollectionResource(provider: $this->provider(), service: $this->identifier()); + $object->jsonDeserialize($collection->jsonSerialize()); + $collection = $object; + } + + $collection = $collection->toJmap(); + $collection = $this->remoteService->collectionCreate($location, $collection, $options); + + $object = new CollectionResource(provider: $this->provider(), service: $this->identifier()); + $object->fromJmap($collection); + + return $object; + } + + public function collectionUpdate(string|int $identifier, CollectionMutableInterface $collection): CollectionBaseInterface + { + $this->initialize(); + + if ($collection instanceof CollectionResource === false) { + $object = new CollectionResource(provider: $this->provider(), service: $this->identifier()); + $object->jsonDeserialize($collection->jsonSerialize()); + $collection = $object; + } + + $collection = $collection->toJmap(); + $collection = $this->remoteService->collectionModify($identifier, $collection); + + $object = new CollectionResource(provider: $this->provider(), service: $this->identifier()); + $object->fromJmap($collection); + + return $object; + } + + public function collectionDelete(string|int $identifier, bool $force = false, bool $recursive = false): bool + { + $this->initialize(); + + return $this->remoteService->collectionDestroy($identifier, $force, $recursive) !== null; + } + + public function collectionMove(string|int $identifier, string|int|null $targetLocation): CollectionBaseInterface + { + // TODO: Implement collection move + $this->initialize(); + $collection = new CollectionResource(provider: $this->provider(), service: $this->identifier()); + return $collection; + } + + // Entity operations + + public function entityList(string|int|null $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array + { + $this->initialize(); + + $result = $this->remoteService->entityList($collection, $filter, $sort, $range, $properties); + + $list = []; + foreach ($result['list'] as $index => $entry) { + if (is_array($entry) && isset($entry['id'])) { + $object = new EntityResource(provider: $this->provider(), service: $this->identifier()); + $object->fromJmap($entry); + $list[$object->identifier()] = $object; + } + unset($result['list'][$index]); + } + + return $list; + } + + public function entityListStream(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): Generator + { + $this->initialize(); + + $result = $this->remoteService->entityList($collection, $filter, $sort, $range, $properties); + + foreach ($result['list'] as $index => $entry) { + if (is_array($entry) && isset($entry['id'])) { + $object = new EntityResource(provider: $this->provider(), service: $this->identifier()); + $object->fromJmap($entry); + yield $object; + } + unset($result['list'][$index]); + } + } + + public function entityListFilter(): Filter + { + return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []); + } + + public function entityListSort(): Sort + { + return new Sort($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_SORT] ?? []); + } + + public function entityListRange(RangeType $type): IRange + { + return new Range(); + } + + public function entityDelta(string|int|null $collection, string $signature, string $detail = 'ids'): Delta + { + $this->initialize(); + + return $this->remoteService->entityDelta($collection, $signature, $detail); + } + + public function entityExtant(string|int|null $collection, string|int ...$identifiers): array + { + $this->initialize(); + + return $this->remoteService->entityExtant(...$identifiers); + } + + public function entityFetch(string|int|null $collection, string|int ...$identifiers): array + { + $this->initialize(); + + $entities = $this->remoteService->entityFetch(...$identifiers); + + foreach ($entities as &$entity) { + if (is_array($entity) && isset($entity['id'])) { + $object = new EntityResource(provider: $this->provider(), service: $this->identifier()); + $object->fromJmap($entity); + $entity = $object; + } + } + + return $entities; + } + + public function entityRead(string|int|null $collection, string|int $identifier): ?string + { + return null; + } + + // Node operations + + public function nodeList(string|int|null $collection, bool $recursive = false, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array + { + return []; + } + + public function nodeListFilter(): IFilter + { + return new Filter(['']); + } + + public function nodeListSort(): ISort + { + return new Sort(['']); + } + + public function nodeDelta(string|int|null $collection, string $signature, string $detail = 'ids'): Delta + { + return new Delta(); + } + +} diff --git a/lib/Providers/Mail/Provider.php b/lib/Providers/Mail/Provider.php index a78ae5f..ee3f979 100644 --- a/lib/Providers/Mail/Provider.php +++ b/lib/Providers/Mail/Provider.php @@ -99,17 +99,21 @@ class Provider implements ProviderServiceMutateInterface, ProviderServiceDiscove public function serviceList(string $tenantId, string $userId, array $filter = []): array { $list = $this->serviceStore->list($tenantId, $userId, $filter); - foreach ($list as $entry) { - $service = new Service(); - $service->fromStore($entry); - $list[$service->identifier()] = $service; + foreach ($list as $serviceData) { + $serviceInstance = $this->serviceFresh()->fromStore($serviceData); + $list[$serviceInstance->identifier()] = $serviceInstance; } return $list; } public function serviceFetch(string $tenantId, string $userId, string|int $identifier): ?Service { - return $this->serviceStore->fetch($tenantId, $userId, $identifier); + $serviceData = $this->serviceStore->fetch($tenantId, $userId, $identifier); + if ($serviceData === null) { + return null; + } + $serviceInstance = $this->serviceFresh()->fromStore($serviceData); + return $serviceInstance; } public function serviceFindByAddress(string $tenantId, string $userId, string $address): ?Service @@ -129,7 +133,7 @@ class Provider implements ProviderServiceMutateInterface, ProviderServiceDiscove return $this->serviceStore->extant($tenantId, $userId, $identifiers); } - public function serviceFresh(): ResourceServiceMutateInterface + public function serviceFresh(): Service { return new Service(); } @@ -141,7 +145,7 @@ class Provider implements ProviderServiceMutateInterface, ProviderServiceDiscove } $created = $this->serviceStore->create($tenantId, $userId, $service); - return (string) $created->identifier(); + return (string) $created['id']; } public function serviceModify(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string @@ -151,7 +155,7 @@ class Provider implements ProviderServiceMutateInterface, ProviderServiceDiscove } $updated = $this->serviceStore->modify($tenantId, $userId, $service); - return (string) $updated->identifier(); + return (string) $updated['id']; } public function serviceDestroy(string $tenantId, string $userId, ResourceServiceMutateInterface $service): bool diff --git a/lib/Service/Remote/RemoteFilesService.php b/lib/Service/Remote/RemoteFilesService.php new file mode 100644 index 0000000..da2eeeb --- /dev/null +++ b/lib/Service/Remote/RemoteFilesService.php @@ -0,0 +1,726 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\ProviderJmapc\Service\Remote; + +use Exception; +use JmapClient\Client; +use JmapClient\Requests\Files\NodeGet; +use JmapClient\Requests\Files\NodeQuery; +use JmapClient\Requests\Files\NodeParameters as NodeParametersRequest; +use JmapClient\Requests\Files\NodeQueryChanges; +use JmapClient\Requests\Files\NodeSet; +use JmapClient\Responses\Files\NodeParameters as NodeParametersResponse; +use JmapClient\Responses\ResponseException; +use KTXF\Resource\Delta\Delta; +use KTXF\Resource\Delta\DeltaCollection; +use KTXF\Resource\Filter\Filter; +use KTXF\Resource\Filter\IFilter; +use KTXF\Resource\Range\IRange; +use KTXF\Resource\Range\RangeAnchorType; +use KTXF\Resource\Range\RangeTally; +use KTXF\Resource\Sort\ISort; +use KTXF\Resource\Sort\Sort; +use KTXM\ProviderJmapc\Exception\JmapUnknownMethod; + +class RemoteFilesService { + + private const ROOT_ID = '00000000-0000-0000-0000-000000000000'; + private const COLLECTION_FILTER_ATTRIBUTES = ['any', 'label', 'role', 'roles', 'createdBefore', 'createdAfter', 'modifiedBefore', 'modifiedAfter']; + private const COLLECTION_SORT_ATTRIBUTES = ['tree']; + private const ENTITY_FILTER_ATTRIBUTES = ['any', 'label', 'createdBefore', 'createdAfter', 'modifiedBefore', 'modifiedAfter']; + private const ENTITY_SORT_ATTRIBUTES = ['tree']; + + protected Client $dataStore; + protected string $dataAccount; + + protected ?string $resourceNamespace = null; + protected ?string $resourceCollectionLabel = null; + protected ?string $resourceEntityLabel = null; + + 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('filenode'); + } + $this->dataAccount = $account !== null ? $account->id() : ''; + } else { + $this->dataAccount = $dataAccount; + } + + } + + /** + * list of collections in remote storage + * + * @since Release 1.0.0 + */ + public function collectionList(?string $location = null, IFilter|null $filter = null, ISort|null $sort = null, IRange|null $range = null): array { + // construct request + $r0 = new NodeQuery($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $r0->filter()->is(false); + $r0->depth(1); + // define location + if (!empty($location) && $location !== self::ROOT_ID) { + $r0->filter()->in($location); + } + // define filter + if ($filter !== null) { + foreach ($filter->conditions() as $condition) { + $value = $condition['value']; + match($condition['attribute']) { + 'label' => $r0->filter()->labelMatches($value), + default => null + }; + } + } + // define order + if ($sort !== null) { + foreach ($sort->conditions() as $condition) { + $direction = $condition['direction']; + match($condition['attribute']) { + 'tree' => $r0->sort()->tree($direction), + default => null + }; + } + } + // construct request + $r1 = new NodeGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + // define target + //$r1->targetFromRequest($r0, '/ids'); + if (!empty($location) && $location !== self::ROOT_ID) { + $r1->target($location); + } + // transceive + //$bundle = $this->dataStore->perform([$r1]); + $bundle = $this->dataStore->perform([$r0, $r1]); + // extract response + //$response = $bundle->response(0); + $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 jmap objects to collection objects + $list = []; + foreach ($response->objects() as $so) { + if (!$so instanceof NodeParametersResponse) { + continue; + } + $id = $so->id(); + $list[$id] = $so->parametersRaw(); + $list[$id]['signature'] = $response->state(); + } + // return collection of collections + return $list; + } + + /** + * fresh instance of collection filter object + * + * @since Release 1.0.0 + */ + public function collectionListFilter(): Filter { + return new Filter(self::COLLECTION_FILTER_ATTRIBUTES); + } + + /** + * fresh instance of collection sort object + * + * @since Release 1.0.0 + */ + public function collectionListSort(): Sort { + return new Sort([self::COLLECTION_SORT_ATTRIBUTES]); + } + + /** + * check existence of collections in remote storage + * + * @since Release 1.0.0 + */ + public function collectionExtant(string ...$identifiers): array { + $extant = []; + // construct request + $r0 = new NodeGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $r0->target(...$identifiers); + $r0->property('id'); + // 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 + foreach ($response->objects() as $so) { + if (!$so instanceof NodeParametersResponse) { + continue; + } + $extant[$so->id()] = true; + } + return $extant; + } + + /** + * retrieve properties for specific collection + * + * @since Release 1.0.0 + */ + public function collectionFetch(string $identifier): ?array { + // construct request + $r0 = new NodeGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $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 NodeParametersResponse) { + $to = $so->parametersRaw(); + $to['signature'] = $response->state(); + } + return $to; + } + + /** + * create collection in remote storage + * + * @since Release 1.0.0 + */ + public function collectionCreate(string|null $location, array $so): ?array { + // convert entity + $to = new NodeParametersRequest(); + $to->parametersRaw($so); + // define location + if (!empty($location) && $location !== self::ROOT_ID) { + $to->in($location); + } + $id = uniqid(); + // construct request + $r0 = new NodeSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $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 array_merge($so, $result); + } + // 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, array $so): ?array { + // convert entity + $to = new NodeParametersRequest(); + $to->parametersRaw($so); + // construct request + $r0 = new NodeSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $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 array_merge($so, $result); + } + // 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 collectionDestroy(string $identifier, bool $force = false, bool $recursive = false): ?string { + // construct request + $r0 = new NodeSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $r0->delete($identifier); + if ($force) { + $r0->destroyContents(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 + */ + public function entityList(?string $location = null, IFilter|null $filter = null, ISort|null $sort = null, IRange|null $range = null, string|null $granularity = null): array { + // construct request + $r0 = new NodeQuery($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']) { + 'any' => $r0->filter()->text($value), + 'in' => $r0->filter()->in($value), + 'label' => $r0->filter()->labelMatches($value), + 'format' => $r0->filter()->formatIs($value), + default => null + }; + } + } + // define order + if ($sort !== null) { + foreach ($sort->conditions() as $condition) { + $direction = $condition['direction']; + match($condition['attribute']) { + 'type' => $r0->sort()->type($direction), + default => null + }; + } + } + // define range + if ($range !== null) { + if ($range instanceof RangeTally && $range->getAnchor() === RangeAnchorType::ABSOLUTE) { + $r0->limitAbsolute($range->getPosition(), $range->getTally()); + } + if ($range instanceof RangeTally && $range->getAnchor() === RangeAnchorType::RELATIVE) { + $r0->limitRelative($range->getPosition(), $range->getTally()); + } + } + // construct get request + $r1 = new NodeGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + // set target to query request + //$r1->targetFromRequest($r0, '/ids'); + if (!empty($location)) { + $r1->target($location); + } + // transmit request and receive response + $bundle = $this->dataStore->perform([$r1]); + //$bundle = $this->dataStore->perform([$r0, $r1]); + // extract response + $response = $bundle->response(0); + //$response = $bundle->response(1); + // convert json objects to message objects + $state = $response->state(); + $list = $response->objects(); + foreach ($list as $id => $entry) { + if (!$entry instanceof NodeParametersResponse) { + continue; + } + $list[$id] = $entry->parametersRaw(); + } + // return message collection + return ['list' => $list, 'state' => $state]; + } + + /** + * fresh instance of object filter + * + * @since Release 1.0.0 + */ + public function entityListFilter(): Filter { + return new Filter(self::ENTITY_FILTER_ATTRIBUTES); + } + + /** + * fresh instance of object sort + * + * @since Release 1.0.0 + */ + public function entityListSort(): Sort { + return new Sort(self::ENTITY_SORT_ATTRIBUTES); + } + + /** + * fresh instance of object range + * + * @since Release 1.0.0 + */ + public function entityListRange(): RangeTally { + return new RangeTally(); + } + + /** + * check existence of entities in remote storage + * + * @since Release 1.0.0 + */ + public function entityExtant(string ...$identifiers): array { + $extant = []; + // construct request + $r0 = new NodeGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $r0->target(...$identifiers); + $r0->property('id'); + // transmit request and receive response + $bundle = $this->dataStore->perform([$r0]); + // extract response + $response = $bundle->response(0); + // convert json objects to message objects + foreach ($response->objects() as $so) { + if (!$so instanceof NodeParametersResponse) { + continue; + } + $extant[$so->id()] = true; + } + return $extant; + } + + /** + * delta for entities in remote storage + * + * @since Release 1.0.0 + * + * @return Delta + */ + public function entityDelta(?string $location, string $state, string $granularity = 'D'): Delta { + + if (empty($state)) { + $results = $this->entityList($location, null, null, null, 'B'); + $delta = new Delta(); + $delta->signature = $results['state']; + foreach ($results['list'] as $entry) { + $delta->additions[] = $entry['id']; + } + return $delta; + } + if (empty($location)) { + return $this->entityDeltaDefault($state, $granularity); + } else { + return $this->entityDeltaSpecific($location, $state, $granularity); + } + } + + /** + * delta of changes for specific collection in remote storage + * + * @since Release 1.0.0 + * + */ + public function entityDeltaSpecific(?string $location, string $state, string $granularity = 'D'): Delta { + // construct set request + $r0 = new NodeQueryChanges($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 Delta(); + $delta->signature = $response->stateNew(); + $delta->additions = new DeltaCollection(array_column($response->added(), 'id')); + $delta->modifications = new DeltaCollection([]); + $delta->deletions = new DeltaCollection(array_column($response->removed(), 'id')); + + return $delta; + } + + /** + * delta of changes in remote storage + * + * @since Release 1.0.0 + * + */ + public function entityDeltaDefault(string $state, string $granularity = 'D'): Delta { + // construct set request + $r0 = new NodeQueryChanges($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 Delta(); + $delta->signature = $response->stateNew(); + $delta->additions = new DeltaCollection(array_column($response->added(), 'id')); + $delta->modifications = new DeltaCollection([]); + $delta->deletions = new DeltaCollection(array_column($response->removed(), 'id')); + + return $delta; + } + + /** + * retrieve entity from remote storage + * + * @since Release 1.0.0 + */ + public function entityFetch(string ...$identifiers): ?array { + // construct request + $r0 = new NodeGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $r0->target(...$identifiers); + // transmit request and receive response + $bundle = $this->dataStore->perform([$r0]); + // extract response + $response = $bundle->response(0); + // convert json objects to message objects + $list = []; + foreach ($response->objects() as $so) { + if (!$so instanceof NodeParametersResponse) { + continue; + } + $id = $so->id(); + $list[$id] = $so->parametersRaw(); + $list[$id]['signature'] = $response->state(); + } + // return message collection + return $list; + } + + /** + * create entity in remote storage + * + * @since Release 1.0.0 + */ + public function entityCreate(string $location, array $so): ?array { + // convert entity + $to = new NodeParametersRequest(); + $to->parametersRaw($so); + $to->in($location); + $id = uniqid(); + // construct request + $r0 = new NodeSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $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 array_merge($so, $result); + } + // 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(array $so): ?array { + // extract entity id + $id = $so['id']; + // convert entity + $to = new NodeParametersRequest(); + $to->parametersRaw($so); + // construct request + $r0 = new NodeSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $r0->update($id, $to); + // transmit request and receive response + $bundle = $this->dataStore->perform([$r0]); + // extract response + $response = $bundle->response(0); + // determine if command succeeded + if (array_key_exists($id, $response->updated())) { + // update entity + $ro = $response->updated()[$id]; + $so = array_merge($so, $ro); + return $so; + } + return null; + } + + /** + * delete entity from remote storage + * + * @since Release 1.0.0 + */ + public function entityDelete(string $id): ?string { + // construct set request + $r0 = new NodeSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + // construct object + $r0->delete($id); + // transmit request and receive response + $bundle = $this->dataStore->perform([$r0]); + // extract response + $response = $bundle->response(0); + // determine if command succeeded + if (array_search($id, $response->deleted()) !== false) { + return $response->stateNew(); + } + return null; + } + + /** + * copy entity in remote storage + * + * @since Release 1.0.0 + * + */ + public function entityCopy(string $location, MailMessageObject $so): ?MailMessageObject { + return null; + } + + /** + * move entity in remote storage + * + * @since Release 1.0.0 + * + */ + public function entityMove(string $location, array $so): ?array { + // extract entity id + $id = $so['id']; + // construct request + $r0 = new NodeSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); + $r0->update($id)->in($location); + // transmit request and receive response + $bundle = $this->dataStore->perform([$r0]); + // extract response + $response = $bundle->response(0); + // determine if command succeeded + if (array_key_exists($id, $response->updated())) { + $so = array_merge($so, ['mailboxIds' => [$location => true]]); + return $so; + } + return null; + } + +} diff --git a/lib/Service/Remote/RemoteService.php b/lib/Service/Remote/RemoteService.php index d1ecd4d..48d1360 100644 --- a/lib/Service/Remote/RemoteService.php +++ b/lib/Service/Remote/RemoteService.php @@ -14,12 +14,17 @@ use JmapClient\Authentication\Bearer; use JmapClient\Authentication\JsonBasic; use JmapClient\Authentication\JsonBasicCookie; use JmapClient\Client as JmapClient; +use KTXC\Server; use KTXF\Resource\Provider\ResourceServiceBaseInterface; use KTXF\Resource\Provider\ResourceServiceIdentityBasic; use KTXF\Resource\Provider\ResourceServiceIdentityBearer; use KTXF\Resource\Provider\ResourceServiceIdentityOAuth; use KTXF\Resource\Provider\ResourceServiceLocationUri; -use KTXM\ProviderJmapc\Providers\Mail\Service; +use KTXM\ProviderJmapc\Providers\Mail\Service as MailService; +use KTXM\ProviderJmapc\Providers\Contacts\Service as ContactsService; +use KTXM\ProviderJmapc\Providers\Events\Service as EventsService; +use KTXM\ProviderJmapc\Providers\Tasks\Service as TasksService; +use KTXM\ProviderJmapc\Providers\Document\Service as FilesService; use KTXM\ProviderJmapc\Service\Remote\FM\RemoteContactsServiceFM; use KTXM\ProviderJmapc\Service\Remote\FM\RemoteCoreServiceFM; use KTXM\ProviderJmapc\Service\Remote\FM\RemoteEventsServiceFM; @@ -33,7 +38,7 @@ class RemoteService { * * @since Release 1.0.0 */ - public static function freshClient(Service $service): JmapClient { + public static function freshClient(MailService|FilesService $service): JmapClient { // defaults $client = new JmapClient(); @@ -98,7 +103,7 @@ class RemoteService { } // construct service based on capabilities if ($Client->sessionCapable('https://www.fastmail.com/dev/user', false)) { - $service = new RemoteCoreServiceFM(); + //$service = new RemoteCoreServiceFM(); } else { $service = new RemoteCoreService(); } @@ -176,6 +181,21 @@ class RemoteService { return $service; } + /** + * Appropriate Documents Service for Connection + * + * @since Release 1.0.0 + */ + public static function documentsService(JmapClient $Client, ?string $dataAccount = null): RemoteFilesService { + // determine if client is connected + if (!$Client->sessionStatus()) { + $Client->connect(); + } + $service = new RemoteFilesService(); + $service->initialize($Client, $dataAccount); + return $service; + } + public static function cookieStoreRetrieve(mixed $id): ?array { $file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . (string)$id . '.jmapc'; @@ -185,7 +205,7 @@ class RemoteService { } $data = file_get_contents($file); - $crypto = \OC::$server->get(\OCP\Security\ICrypto::class); + $crypto = Server::getInstance()->container()->get(\KTXF\Security\Crypto::class); $data = $crypto->decrypt($data); if (!empty($data)) { @@ -202,7 +222,7 @@ class RemoteService { return; } - $crypto = \OC::$server->get(\OCP\Security\ICrypto::class); + $crypto = Server::getInstance()->container()->get(\KTXF\Security\Crypto::class); $data = $crypto->encrypt(json_encode($value)); $file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . (string)$id . '.jmapc'; diff --git a/lib/Stores/ServiceStore.php b/lib/Stores/ServiceStore.php index 7be82d3..5d478de 100644 --- a/lib/Stores/ServiceStore.php +++ b/lib/Stores/ServiceStore.php @@ -91,7 +91,7 @@ class ServiceStore /** * Retrieve a single service by ID */ - public function fetch(string $tenantId, string $userId, string|int $serviceId): ?Service + public function fetch(string $tenantId, string $userId, string|int $serviceId): ?array { $document = $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne([ 'tid' => $tenantId, @@ -107,13 +107,13 @@ class ServiceStore $document['identity']['secret'] = $this->crypto->decrypt($document['identity']['secret']); } - return (new Service())->fromStore($document); + return $document; } /** * Create a new service */ - public function create(string $tenantId, string $userId, Service $service): Service + public function create(string $tenantId, string $userId, Service $service): array { $document = $service->toStore(); @@ -129,15 +129,15 @@ class ServiceStore $result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->insertOne($document); - return (new Service())->fromStore($document); + return $document; } /** * Modify an existing service */ - public function modify(string $tenantId, string $userId, Service $service): Service + public function modify(string $tenantId, string $userId, Service $service): array { - $serviceId = $service->id(); + $serviceId = $service->identifier(); if (empty($serviceId)) { throw new \InvalidArgumentException('Service ID is required for update'); } @@ -159,7 +159,7 @@ class ServiceStore ['$set' => $document] ); - return (new Service())->fromStore($document); + return $document; } /**