Files
provider_jmapc/lib/Service/Remote/RemoteFilesService.php
Sebastian Krupinski 4730b75a05
All checks were successful
Build Test / test (pull_request) Successful in 1m44s
JS Unit Tests / test (pull_request) Successful in 1m45s
PHP Unit Tests / test (pull_request) Successful in 2m24s
refactor: improvemets
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-03-24 19:12:26 -04:00

727 lines
21 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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;
}
}