* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\PeopleProviderLocal\Providers\Shared; use KTXF\People\Collection\ICollectionMutable; use KTXF\People\Entity\IEntityMutable; use KTXF\People\Exceptions\InvalidParameterException; use KTXF\People\Exceptions\UnauthorizedException; use KTXF\People\Exceptions\UnsupportedException; use KTXF\People\Filter\Filter; use KTXF\People\Filter\IFilter; use KTXF\People\Range\IRange; use KTXF\People\Range\RangeTally; use KTXF\People\Range\RangeType; use KTXF\People\Service\IServiceBase; use KTXF\People\Service\IServiceCollectionMutable; use KTXF\People\Service\IServiceEntityMutable; use KTXF\People\Sort\ISort; use KTXF\People\Sort\Sort; use KTXM\PeopleProviderLocal\Providers\Personal\Entity; class SharedService implements IServiceBase, IServiceCollectionMutable, IServiceEntityMutable { protected const SERVICE_ID = 'shared'; protected const SERVICE_LABEL = 'Shared Contacts Service'; protected const SERVICE_PROVIDER = 'default'; protected array $serviceCollectionCache = []; protected array $serviceSharesCache = []; protected ?string $serviceTenantId = null; protected ?string $serviceUserId = null; protected ?bool $serviceEnabled = true; protected array $serviceAbilities = [ self::CAPABILITY_COLLECTION_LIST => true, self::CAPABILITY_COLLECTION_LIST_FILTER => [ 'id' => 'a:10:64:192', 'label' => 's:100:256:771', 'description' => 's:100:256:771', ], self::CAPABILITY_COLLECTION_LIST_SORT => [ 'label', 'description' ], self::CAPABILITY_COLLECTION_EXTANT => true, self::CAPABILITY_COLLECTION_FETCH => true, self::CAPABILITY_COLLECTION_CREATE => true, self::CAPABILITY_COLLECTION_MODIFY => true, self::CAPABILITY_COLLECTION_DESTROY => true, self::CAPABILITY_ENTITY_LIST => true, self::CAPABILITY_ENTITY_LIST_FILTER => [ '*' => 's:200:256:771', 'uri' => 's:200:256:771', 'label' => 's:200:256:771', 'phone' => 's:200:256:771', 'email' => 's:200:256:771', 'location' => 's:200:256:771' ], self::CAPABILITY_ENTITY_LIST_SORT => [ 'label', 'phone', 'email', 'location' ], self::CAPABILITY_ENTITY_LIST_RANGE => [ 'tally' => ['absolute', 'relative'] ], self::CAPABILITY_ENTITY_DELTA => true, self::CAPABILITY_ENTITY_EXTANT => true, self::CAPABILITY_ENTITY_FETCH => true, self::CAPABILITY_ENTITY_CREATE => true, self::CAPABILITY_ENTITY_MODIFY => true, self::CAPABILITY_ENTITY_DESTROY => true, self::CAPABILITY_ENTITY_COPY => true, self::CAPABILITY_ENTITY_MOVE => true, ]; public function __construct( private Store $store, //private SharingMapper $sharingStore, ) {} public function jsonSerialize(): mixed { return [ self::JSON_PROPERTY_TYPE => self::JSON_TYPE, self::JSON_PROPERTY_PROVIDER => self::SERVICE_PROVIDER, self::JSON_PROPERTY_ID => self::SERVICE_ID, self::JSON_PROPERTY_LABEL => self::SERVICE_LABEL, self::JSON_PROPERTY_CAPABILITIES => $this->serviceAbilities, self::JSON_PROPERTY_ENABLED => $this->serviceEnabled, ]; } public function init(string $tenantId, string $userId): self { $this->serviceTenantId = $tenantId; $this->serviceUserId = $userId; return $this; } /** * Confirms if specific capability is supported * * @since 1.0.0 * * @inheritDoc */ public function capable(string $value): bool { if (isset($this->serviceAbilities[$value])) { return (bool)$this->serviceAbilities[$value]; } return false; } /** * Lists all supported capabilities * * @since 1.0.0 * * @inheritDoc */ public function capabilities(): array { return $this->serviceAbilities; } /** * Unique identifier of the provider this service belongs to * * @since 1.0.0 * * @inheritDoc */ public function in(): string{ return self::SERVICE_PROVIDER; } /** * Unique arbitrary text string identifying this service (e.g. 1 or service1 or anything else) * * @since 1.0.0 * * @inheritDoc */ public function id(): string { return self::SERVICE_ID; } /** * Gets the localized human friendly name of this service (e.g. ACME Company Mail Service) * * @since 1.0.0 * * @inheritDoc */ public function getLabel(): string { return self::SERVICE_LABEL; } /** * Gets the active status of this service * * @since 1.0.0 * * @inheritDoc */ public function getEnabled(): bool { return (bool)$this->serviceEnabled; } /** * List of accessible collection * * @since 1.0.0 * * @inheritDoc */ public function collectionList(?IFilter $filter = null, ?ISort $sort = null): array { /* $shareEntries = $this->listShareEntries(); foreach ($shareEntries as $key => $shareEntry) { $collectionEntry = $this->store->collectionFetch('system', $shareEntry['resourceid']); $collection = new Collection(); $collection->fromStore($collectionEntry)->fromShareStore($shareEntry); $list[$collection->id()] = $collection; $this->serviceCollectioncache[$collection->id()] = $collection; } */ return $list ?? []; } /** * Returns a filter for collection list * * @since 1.0.0 * * @inheritDoc */ public function collectionListFilter(): Filter { return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []); } /** * Returns a sort for collection list * * @since 1.0.0 * * @inheritDoc */ public function collectionListSort(): Sort { return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []); } /** * Confirms if a collection exists * * @since 1.0.0 * * @inheritDoc */ public function collectionExtant(string|int $id): bool { // validate id if (!is_numeric($id)) { throw new InvalidParameterException("Invalid: Collection identifier '$id' is not valid"); } $id = (int)$id; // determine if collection is cached if (isset($this->serviceCollectioncache[$id])) { return true; } if ($this->fetchShareEntry($id) !== null) { return true; } return false; } /** * Fetches details about a specific collection * * @since 1.0.0 * * @inheritDoc */ public function collectionFetch(string|int $id): ?Collection { // validate access if ($this->collectionExtant($id) === false) { throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$id'"); } $id = (int)$id; // determine if collection is cached if (isset($this->serviceCollectioncache[$id])) { return $this->serviceCollectioncache[$id]; } // retrieve share data $shareEntry = $this->fetchShareEntry($id); // retrieve collection data $collectionEntry = $this->store->collectionFetch('system', $shareEntry['resourceid']); if ($collectionEntry !== null) { $collection = new Collection(); $collection->fromStore($collectionEntry)->fromShareStore($shareEntry); $this->serviceCollectioncache[$collection->id()] = $collection; return $collection; } return null; } /** * Creates a new collection at the specified location * * @since 1.0.0 * * @inheritDoc */ public function collectionCreate(string|int $location, ICollectionMutable $collection, array $options): Collection { throw new UnsupportedException("Unsupported: Shared service does not support collection creation"); } /** * Modifies an existing collection * * @since 1.0.0 * * @inheritDoc */ public function collectionModify(string|int $id, ICollectionMutable $collection): Collection { // validate access if ($this->collectionExtant($id) === false) { throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$id'"); } $id = (int)$id; // convert collection to a native type if needed if (!($collection instanceof Collection)) { $nativeCollection = new Collection(); $nativeCollection->fromJson($collection->toJson()); } else { $nativeCollection = clone $collection; } // convert to store type and force user id $storeEntry = $nativeCollection->toStore(); $storeEntry->setUserId($this->serviceUserId); // modify collection in store $storeEntry = $this->store->collectionModify($storeEntry); $nativeCollection->fromStore($storeEntry); return $nativeCollection; } /** * Destroys a collection * * @since 1.0.0 * * @inheritDoc */ public function collectionDestroy(string|int $id): bool { // validate access if ($this->collectionExtant($id) === false) { throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$id'"); } $id = (int)$id; // destroy collection in store if ($this->store->collectionDestroyById($this->serviceUserId, $id)) { unset($this->serviceCollectioncache[$id]); return true; } return false; } /** * Lists all entities in a specific collection * * @since 1.0.0 * * @inheritDoc */ public function entityList(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $options = null): array { // validate collection access if ($this->collectionExtant($collection) === false) { throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); } $collection = (int)$collection; // retrieve share entry $shareEntry = $this->fetchShareEntry($collection); // retrieve entity entries $entries = $this->store->entityList($shareEntry['resourceid'], $filter, $sort, $range, $options); foreach ($entries as $key => $entry) { $entity = new Entity(); $entity->fromStore($entry); $entities[$key] = $entity; } return $entities ?? []; } /** * Returns a filter for entity list * * @since 1.0.0 * * @inheritDoc */ public function entityListFilter(): Filter { return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []); } /** * Returns a sort for entity list * * @since 1.0.0 * * @inheritDoc */ public function entityListSort(): Sort { return new Sort($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_SORT] ?? []); } /** * Returns a sort for entity list * * @since 1.0.0 * * @inheritDoc */ public function entityListRange(RangeType $type): IRange { // validate type if ($type !== RangeType::TALLY) { throw new InvalidParameterException("Invalid: Entity range of type '{$type->value}' is not valid"); } return new RangeTally(); } /** * Confirms if entity(ies) exist in a collection * * @since 1.0.0 * * @inheritDoc */ public function entityExtant(string|int $collection, string|int ...$identifiers): array { // validate access if ($this->collectionExtant($collection) === false) { throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); } $collection = (int)$collection; // retrieve share entry $shareEntry = $this->fetchShareEntry($collection); // retrieve entity status return $this->store->entityExtant($shareEntry['resourceid'], $identifiers); } /** * Lists of all changes from a specific token * * @since 1.0.0 * * @inheritDoc */ public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): array { // validate access if ($this->collectionExtant($collection) === false) { throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); } $collection = (int)$collection; // retrieve share entry $shareEntry = $this->fetchShareEntry($collection); // retrieve entity delta from store return $this->store->chronicleReminisce($shareEntry['resourceid'], $signature); } /** * Retrieves details about a specific entity(ies) * * @since 1.0.0 * * @inheritDoc */ public function entityFetch(string|int $collection, string|int ...$identifiers): array { // validate collection access if ($this->collectionExtant($collection) === false) { throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); } $collection = (int)$collection; // retrieve share entry $shareEntry = $this->fetchShareEntry($collection); // retrieve entity entry $entries = $this->store->entityFetch($shareEntry['resourceid'], $identifiers); foreach ($entries as $key => $entry) { $entity = new Entity(); $entity->fromStore($entry); $entities[$key] = $entity; } return $entities ?? []; } /** * Creates a fresh entity of the specified type * * @since 1.0.0 * * @inheritDoc */ public function entityFresh(): Entity { return new Entity(); } /** * Creates a new entity in the specified collection * * @since 1.0.0 * * @inheritDoc */ public function entityCreate(string|int $collection, IEntityMutable $entity, array $options): Entity { // validate collection access if ($this->collectionExtant($collection) === false) { throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); } $collection = (int)$collection; // retrieve share entry $shareEntry = $this->fetchShareEntry($collection); // convert enity to a native type if needed if (!($entity instanceof Entity)) { $nativeEntity = $this->entityFresh(); $nativeEntity->fromJson($entity->toJson()); } else { $nativeEntity = clone $entity; } // convert to store type and force address book id $storeEntry = $nativeEntity->toStore(); $storeEntry->setAddressbookid($shareEntry['resourceid']); $storeEntry->setLastmodified(time()); if (isset($options['source']) && $options['source'] === 'dav' && isset($options['uri']) && !empty($options['uri'])) { $storeEntry->setUri($options['uri']); } // create entry in store $storeEntry = $this->store->entityCreate($storeEntry); $nativeEntity->fromStore($storeEntry); return $nativeEntity; } /** * Modifies an existing entity in the specified collection * * @since 1.0.0 * * @inheritDoc */ public function entityModify(string|int $collection, string|int $identifier, IEntityMutable $entity): Entity { // validate collection access if ($this->collectionExtant($collection) === false) { throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); } $collection = (int)$collection; // validate entity identifier if (empty($identifier) || !is_numeric($identifier)) { throw new InvalidParameterException("Invalid: Entity identifier '$identifier' is not valid"); } $identifier = (int)$identifier; // retrieve share entry $shareEntry = $this->fetchShareEntry($collection); // validate entity extant and ownership $extant = $this->store->entityExtant($collection, [$identifier]); if (!isset($extant[$identifier]) || $extant[$identifier] === false) { throw new InvalidParameterException("Invalid: Entity identifier '$identifier' does not exist or does not belong to collection '$collection' or user '{$this->serviceUserId}'"); } // convert enity to a native type if needed if (!($entity instanceof Entity)) { $nativeEntity = $this->entityFresh(); $nativeEntity->fromJson($entity->toJson()); } else { $nativeEntity = clone $entity; } // convert to store type and force address book id $storeEntry = $nativeEntity->toStore(); $storeEntry->setId($identifier); $storeEntry->setAddressbookid($shareEntry['resourceid']); $storeEntry->setLastmodified(time()); // modify entry in store $storeEntry = $this->store->entityModify($shareEntry['resourceid'], $storeEntry); $nativeEntity->fromStore($storeEntry); return $nativeEntity; } /** * Destroys an existing entity in the specified collection * * @since 1.0.0 * * @inheritDoc */ public function entityDestroy(string|int $collection, string|int $identifier): IEntityMutable { // validate collection access if ($this->collectionExtant($collection) === false) { throw new UnauthorizedException("Unauthorized: User '{$this->serviceUserId}' does not have access to collection '$collection'"); } $collection = (int)$collection; // retrieve share entry $shareEntry = $this->fetchShareEntry($collection); // validate entity identifier if (empty($identifier) || !is_numeric($identifier)) { throw new InvalidParameterException("Invalid: Entity identifier '$identifier' is not valid"); } $identifier = (int)$identifier; // validate entity extant and ownership $extant = $this->store->entityExtant($collection, [$identifier]); if (!isset($extant[$identifier]) || $extant[$identifier] === false) { throw new InvalidParameterException("Invalid: Entity identifier '$identifier' does not exist or does not belong to collection '$collection' or user '{$this->serviceUserId}'"); } // destroy entry in store $storeEntry = $this->store->entityDestroyById($shareEntry['resourceid'], $identifier); $nativeEntity = $this->entityFresh(); $nativeEntity->fromStore($storeEntry); return $nativeEntity; } }