From 0f5b7b83c837ce9abab114c184148f610b98c1b1 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 28 Nov 2024 22:10:43 +0100 Subject: [PATCH 01/13] First code for linked data --- lib/Db/ObjectEntity.php | 11 +- lib/Migration/Version1Date20241128131427.php | 63 +++++++++ lib/Service/ObjectService.php | 138 ++++++++++++++++++- 3 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 lib/Migration/Version1Date20241128131427.php diff --git a/lib/Db/ObjectEntity.php b/lib/Db/ObjectEntity.php index 68c9245..dc73772 100644 --- a/lib/Db/ObjectEntity.php +++ b/lib/Db/ObjectEntity.php @@ -9,20 +9,26 @@ class ObjectEntity extends Entity implements JsonSerializable { protected ?string $uuid = null; + protected ?string $uri = null; protected ?string $version = null; protected ?string $register = null; protected ?string $schema = null; protected ?array $object = []; + protected ?array $files = []; // array of file ids that are related to this object + protected ?array $relations = []; // array of object ids that this object is related to protected ?string $textRepresentation = null; protected ?DateTime $updated = null; protected ?DateTime $created = null; public function __construct() { - $this->addType(fieldName:'uuid', type: 'string'); + $this->addType(fieldName:'uuid', type: 'string'); + $this->addType(fieldName:'uri', type: 'string'); $this->addType(fieldName:'version', type: 'string'); $this->addType(fieldName:'register', type: 'string'); $this->addType(fieldName:'schema', type: 'string'); $this->addType(fieldName:'object', type: 'json'); + $this->addType(fieldName:'files', type: 'json'); + $this->addType(fieldName:'relations', type: 'json'); $this->addType(fieldName:'textRepresentation', type: 'text'); $this->addType(fieldName:'updated', type: 'datetime'); $this->addType(fieldName:'created', type: 'datetime'); @@ -72,10 +78,13 @@ public function getObjectArray(): array return [ 'id' => $this->id, 'uuid' => $this->uuid, + 'uri' => $this->uri, 'version' => $this->version, 'register' => $this->register, 'schema' => $this->schema, 'object' => $this->object, + 'files' => $this->files, + 'relations' => $this->relations, 'textRepresentation' => $this->textRepresentation, 'updated' => isset($this->updated) ? $this->updated->format('c') : null, 'created' => isset($this->created) ? $this->created->format('c') : null diff --git a/lib/Migration/Version1Date20241128131427.php b/lib/Migration/Version1Date20241128131427.php new file mode 100644 index 0000000..b2781fa --- /dev/null +++ b/lib/Migration/Version1Date20241128131427.php @@ -0,0 +1,63 @@ +getTable('openregister_objects'); + if ($table->hasColumn('uri') === false) { + $table->addColumn(name: 'uri', typeName: Types::STRING, options: ['notnull' => true, 'length' => 255]); + } + if ($table->hasColumn('files') === false) { + $table->addColumn(name: 'files', typeName: Types::JSON, options: ['notnull' => false])->setDefault(default: '{}'); + } + if ($table->hasColumn('relations') === false) { + $table->addColumn(name: 'relations', typeName: Types::JSON, options: ['notnull' => false])->setDefault(default: '{}'); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 7be4d13..2c7aee1 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -97,13 +97,16 @@ public function validateObject(array $object, ?int $schemaId = null, object $sch * Find an object by ID or UUID * * @param int|string $id The ID or UUID to search for + * @param array $extend Properties to extend with related data + * * @return ObjectEntity The found object */ - public function find(int|string $id) { + public function find(int|string $id, ?array $extend = []) { return $this->getObject( register: $this->registerMapper->find($this->getRegister()), schema: $this->schemaMapper->find($this->getSchema()), - uuid: $id + uuid: $id, + extend: $extend ); } @@ -168,9 +171,11 @@ public function delete(array|\JsonSerializable $object): bool * @param array $filters Filter criteria * @param array $sort Sorting criteria * @param string|null $search Search term + * @param array $extend Properties to extend with related data + * * @return array List of matching objects */ - public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array + public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null, ?array $extend = []): array { $objects = $this->getObjects( register: $this->getRegister(), @@ -247,9 +252,11 @@ public function getAggregations(array $filters, ?string $search = null): array * Extract object data from an entity * * @param mixed $object The object to extract data from + * @param array $extend Properties to extend with related data + * * @return mixed The extracted object data */ - private function getDataFromObject(mixed $object) { + private function getDataFromObject(mixed $object, ?array $extend = []) { return $object->getObject(); } @@ -262,10 +269,12 @@ private function getDataFromObject(mixed $object) { * @param int|null $limit The maximum number of objects to retrieve. * @param int|null $offset The offset from which to start retrieving objects. * @param array $filters + * @param array $extend Properties to extend with related data + * * @return array The retrieved objects. * @throws \Exception */ - public function getObjects(?string $objectType = null, ?int $register = null, ?int $schema = null, ?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array + public function getObjects(?string $objectType = null, ?int $register = null, ?int $schema = null, ?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null, ?array $extend = []): array { // Set object type and filters if register and schema are provided if ($objectType === null && $register !== null && $schema !== null) { @@ -339,6 +348,77 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt $schemaObject = $this->schemaMapper->find($schema); + // Handle related objects + // Handle object properties that are either nested objects or files + if (isset($schemaObject->properties) && is_array($schemaObject->properties)) { + foreach ($schemaObject->properties as $propertyName => $property) { + // Handle nested objects + if ($property->type === 'object' && isset($object[$propertyName])) { + // Save nested object and store its ID + $nestedObject = $this->saveObject( + register: $register, + schema: $schema, + object: $object[$propertyName] + ); + + // Store the relation ID + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName] = $nestedObject->getId(); + $objectEntity->setRelations($relations); + + // Replace object with reference + $object[$propertyName] = $nestedObject->getId(); + } + + // Handle file properties + if ($property->type === 'file' && isset($object[$propertyName])) { + $fileContent = null; + $fileName = $propertyName; + + // Check if it's a base64 encoded file + if (preg_match('/^data:([^;]*);base64,(.*)/', $object[$propertyName], $matches)) { + $fileContent = base64_decode($matches[2], true); + if ($fileContent === false) { + throw new \Exception('Invalid base64 encoded file'); + } + } + // Check if it's a URL + else if (filter_var($object[$propertyName], FILTER_VALIDATE_URL)) { + try { + $client = new \GuzzleHttp\Client(); + $response = $client->get($object[$propertyName]); + $fileContent = $response->getBody()->getContents(); + } catch (\Exception $e) { + throw new \Exception('Failed to download file from URL: ' . $e->getMessage()); + } + } else { + throw new \Exception('Invalid file format - must be base64 encoded or valid URL'); + } + + // Store file in Nextcloud + try { + $file = $this->fileService->createOrUpdateFile( + content: $fileContent, + fileName: $fileName + ); + + // Store the file ID + $files = $objectEntity->getFiles() ?? []; + $files[$propertyName] = $file->getId(); + $objectEntity->setFiles($files); + + // Replace file content with file ID reference + $object[$propertyName] = $file->getId(); + } catch (\Exception $e) { + throw new \Exception('Failed to store file: ' . $e->getMessage()); + } + } + } + + // Update the object with processed properties + $objectEntity->setObject($object); + } + if ($objectEntity->getId() && ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true)){ $objectEntity = $this->objectEntityMapper->update($objectEntity); $this->auditTrailMapper->createAuditTrail(new: $objectEntity, old: $oldObject); @@ -360,11 +440,12 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt * @param Register $register The register to get the object from * @param Schema $schema The schema of the object * @param string $uuid The UUID of the object to get + * @param array $extend Properties to extend with related data * * @return ObjectEntity The resulting object * @throws \Exception If source type is unsupported */ - public function getObject(Register $register, Schema $schema, string $uuid): ObjectEntity + public function getObject(Register $register, Schema $schema, string $uuid, ?array $extend = []): ObjectEntity { // Handle internal source if ($register->getSource() === 'internal' || $register->getSource() === '') { @@ -466,6 +547,51 @@ public function getMultipleObjects(string $objectType, array $ids) return $mapper->findMultiple($cleanedIds); } + /** + * Renders the entity by replacing the files and relations with their respective objects + * + * @param array $entity The entity to render + * @param array|null $extend Optional array of properties to extend, defaults to files and relations if not provided + * @return array The rendered entity with expanded files and relations + */ + public function renderEntity(array $entity, ?array $extend = []): array + { + // check if entity has files or relations and if not just return the entity + if (array_key_exists(key: 'files', array: $entity) === false && array_key_exists(key: 'relations', array: $entity) === false) { + return $entity; + } + + // Lets create a dot array of the entity + $dotEntity = new Dot($entity); + + // loop through the files and replace the file ids with the file objects) + if (array_key_exists(key: 'files', array: $entity) === true && empty($entity['files']) === false) { + // Loop through the files array where key is dot notation path and value is file id + foreach ($entity['files'] as $path => $fileId) { + // Replace the value at the dot notation path with the file URL + $dotEntity->set($path, $filesById[$fileId]->getUrl()); + } + } + + // Loop through the relations and replace the relation ids with the relation objects if extended + if (array_key_exists(key: 'relations', array: $entity) === true && empty($entity['relations']) === false) { + // loop through the relations and replace the relation ids with the relation objects + foreach ($entity['relations'] as $path => $relationId) { + // if the relation is not in the extend array, skip it + if (in_array(needle: $path, haystack: $extend) === false) { + continue; + } + // Replace the value at the dot notation path with the relation object + $dotEntity->set($path, $this->getObject(register: $this->getRegister(), schema: $this->getSchema(), uuid: $relationId)); + } + } + + // Update the entity with modified values + $entity = $dotEntity->all(); + + return $this->extendEntity(entity: $entity, extend: $extend); + } + /** * Extends an entity with related objects based on the extend array. * From 85856909e9a0e86264098333f4222b09ff80a53e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 29 Nov 2024 08:02:22 +0100 Subject: [PATCH 02/13] Bit of code clean up and edge cases --- lib/Service/ObjectService.php | 210 +++++++++++++++++++++++----------- 1 file changed, 142 insertions(+), 68 deletions(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 2c7aee1..85332ed 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -290,7 +290,7 @@ public function getObjects(?string $objectType = null, ?int $register = null, ?i return $mapper->findAll(limit: $limit, offset: $offset, filters: $filters, sort: $sort, search: $search); } - /** + /** * Save an object * * @param int $register The register to save the object to. @@ -348,74 +348,9 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt $schemaObject = $this->schemaMapper->find($schema); - // Handle related objects - // Handle object properties that are either nested objects or files + // Handle object properties that are either nested objects or files if (isset($schemaObject->properties) && is_array($schemaObject->properties)) { - foreach ($schemaObject->properties as $propertyName => $property) { - // Handle nested objects - if ($property->type === 'object' && isset($object[$propertyName])) { - // Save nested object and store its ID - $nestedObject = $this->saveObject( - register: $register, - schema: $schema, - object: $object[$propertyName] - ); - - // Store the relation ID - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName] = $nestedObject->getId(); - $objectEntity->setRelations($relations); - - // Replace object with reference - $object[$propertyName] = $nestedObject->getId(); - } - - // Handle file properties - if ($property->type === 'file' && isset($object[$propertyName])) { - $fileContent = null; - $fileName = $propertyName; - - // Check if it's a base64 encoded file - if (preg_match('/^data:([^;]*);base64,(.*)/', $object[$propertyName], $matches)) { - $fileContent = base64_decode($matches[2], true); - if ($fileContent === false) { - throw new \Exception('Invalid base64 encoded file'); - } - } - // Check if it's a URL - else if (filter_var($object[$propertyName], FILTER_VALIDATE_URL)) { - try { - $client = new \GuzzleHttp\Client(); - $response = $client->get($object[$propertyName]); - $fileContent = $response->getBody()->getContents(); - } catch (\Exception $e) { - throw new \Exception('Failed to download file from URL: ' . $e->getMessage()); - } - } else { - throw new \Exception('Invalid file format - must be base64 encoded or valid URL'); - } - - // Store file in Nextcloud - try { - $file = $this->fileService->createOrUpdateFile( - content: $fileContent, - fileName: $fileName - ); - - // Store the file ID - $files = $objectEntity->getFiles() ?? []; - $files[$propertyName] = $file->getId(); - $objectEntity->setFiles($files); - - // Replace file content with file ID reference - $object[$propertyName] = $file->getId(); - } catch (\Exception $e) { - throw new \Exception('Failed to store file: ' . $e->getMessage()); - } - } - } - - // Update the object with processed properties + $object = $this->handleObjectRelations($objectEntity, $object, $schemaObject->properties, $register, $schema); $objectEntity->setObject($object); } @@ -434,6 +369,145 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt return $objectEntity; } + /** + * Handle object relations and file properties in schema properties and array items + * + * @param ObjectEntity $objectEntity The object entity to handle relations for + * @param array $object The object data + * @param array $properties The schema properties + * @param int $register The register ID + * @param int $schema The schema ID + * + * @return array Updated object data + * @throws Exception When file handling fails + */ + private function handleObjectRelations(ObjectEntity $objectEntity, array $object, array $properties, int $register, int $schema): array { + foreach ($properties as $propertyName => $property) { + // Skip if property not in object + if (!isset($object[$propertyName])) { + continue; + } + + // Handle array type with items that may contain objects/files + if ($property->type === 'array' && isset($property->items)) { + // Skip if not array in data + if (!is_array($object[$propertyName])) { + continue; + } + + // Process each array item + foreach ($object[$propertyName] as $index => $item) { + if ($property->items->type === 'object') { + // Handle nested object in array + $nestedObject = $this->saveObject( + register: $register, + schema: $schema, + object: $item + ); + + // Store relation and replace with reference + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName . '_' . $index] = $nestedObject->getId(); + $objectEntity->setRelations($relations); + $object[$propertyName][$index] = $nestedObject->getId(); + } + else if ($property->items->type === 'file') { + // Handle file in array + $object[$propertyName][$index] = $this->handleFileProperty( + $objectEntity, + [$propertyName => $item], + $propertyName . '_' . $index + )[$propertyName]; + } + } + } + // Handle single object type + else if ($property->type === 'object') { + $nestedObject = $this->saveObject( + register: $register, + schema: $schema, + object: $object[$propertyName] + ); + + // Store relation and replace with reference + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName] = $nestedObject->getId(); + $objectEntity->setRelations($relations); + $object[$propertyName] = $nestedObject->getId(); + } + // Handle single file type + else if ($property->type === 'file') { + $object = $this->handleFileProperty($objectEntity, $object, $propertyName); + } + } + + return $object; + } + + /** + * Handle file property processing + * + * @param ObjectEntity $objectEntity The object entity + * @param array $object The object data + * @param string $propertyName The name of the file property + * + * @return array Updated object data + * @throws Exception When file handling fails + */ + private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName): array { + $fileContent = null; + $fileName = $propertyName; + + // Check if it's a Nextcloud file URL + if (str_starts_with($object[$propertyName], $this->urlGenerator->getAbsoluteURL())) { + $urlPath = parse_url($object[$propertyName], PHP_URL_PATH); + if (preg_match('/\/f\/(\d+)/', $urlPath, $matches)) { + $files = $objectEntity->getFiles() ?? []; + $files[$propertyName] = (int)$matches[1]; + $objectEntity->setFiles($files); + $object[$propertyName] = (int)$matches[1]; + return $object; + } + } + + // Handle base64 encoded file + if (preg_match('/^data:([^;]*);base64,(.*)/', $object[$propertyName], $matches)) { + $fileContent = base64_decode($matches[2], true); + if ($fileContent === false) { + throw new \Exception('Invalid base64 encoded file'); + } + } + // Handle URL file + else if (filter_var($object[$propertyName], FILTER_VALIDATE_URL)) { + try { + $client = new \GuzzleHttp\Client(); + $response = $client->get($object[$propertyName]); + $fileContent = $response->getBody()->getContents(); + } catch (\Exception $e) { + throw new \Exception('Failed to download file from URL: ' . $e->getMessage()); + } + } else { + throw new \Exception('Invalid file format - must be base64 encoded or valid URL'); + } + + try { + $file = $this->fileService->createOrUpdateFile( + content: $fileContent, + fileName: $fileName + ); + + $files = $objectEntity->getFiles() ?? []; + $files[$propertyName] = $file->getId(); + $objectEntity->setFiles($files); + + $object[$propertyName] = $file->getId(); + } catch (\Exception $e) { + throw new \Exception('Failed to store file: ' . $e->getMessage()); + } + + return $object; + } + /** * Get an object * From 0d9d7233610ac0cbc4fc14a305919d5f16c10c44 Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Fri, 29 Nov 2024 11:59:02 +0100 Subject: [PATCH 03/13] Quick fixes --- lib/Service/ObjectService.php | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 5da0aa8..630759a 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -98,7 +98,7 @@ public function validateObject(array $object, ?int $schemaId = null, object $sch * * @param int|string $id The ID or UUID to search for * @param array $extend Properties to extend with related data - * + * * @return ObjectEntity The found object */ public function find(int|string $id, ?array $extend = []) { @@ -179,7 +179,7 @@ public function delete(array|\JsonSerializable $object): bool * @param array $sort Sorting criteria * @param string|null $search Search term * @param array $extend Properties to extend with related data - * + * * @return array List of matching objects */ public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null, ?array $extend = []): array @@ -260,7 +260,7 @@ public function getAggregations(array $filters, ?string $search = null): array * * @param mixed $object The object to extract data from * @param array $extend Properties to extend with related data - * + * * @return mixed The extracted object data */ private function getDataFromObject(mixed $object, ?array $extend = []) { @@ -277,7 +277,7 @@ private function getDataFromObject(mixed $object, ?array $extend = []) { * @param int|null $offset The offset from which to start retrieving objects. * @param array $filters * @param array $extend Properties to extend with related data - * + * * @return array The retrieved objects. * @throws \Exception */ @@ -356,8 +356,8 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt $schemaObject = $this->schemaMapper->find($schema); // Handle object properties that are either nested objects or files - if (isset($schemaObject->properties) && is_array($schemaObject->properties)) { - $object = $this->handleObjectRelations($objectEntity, $object, $schemaObject->properties, $register, $schema); + if ($schemaObject->getProperties() !== null && is_array($schemaObject->getProperties())) { + $object = $this->handleObjectRelations($objectEntity, $object, $schemaObject->getProperties(), $register, $schema); $objectEntity->setObject($object); } @@ -378,15 +378,15 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt /** * Handle object relations and file properties in schema properties and array items - * + * * @param ObjectEntity $objectEntity The object entity to handle relations for * @param array $object The object data * @param array $properties The schema properties * @param int $register The register ID * @param int $schema The schema ID - * + * * @return array Updated object data - * @throws Exception When file handling fails + * @throws Exception|ValidationException When file handling fails */ private function handleObjectRelations(ObjectEntity $objectEntity, array $object, array $properties, int $register, int $schema): array { foreach ($properties as $propertyName => $property) { @@ -411,14 +411,13 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object schema: $schema, object: $item ); - + // Store relation and replace with reference $relations = $objectEntity->getRelations() ?? []; $relations[$propertyName . '_' . $index] = $nestedObject->getId(); $objectEntity->setRelations($relations); $object[$propertyName][$index] = $nestedObject->getId(); - } - else if ($property->items->type === 'file') { + } else if ($property->items->type === 'file') { // Handle file in array $object[$propertyName][$index] = $this->handleFileProperty( $objectEntity, @@ -435,7 +434,7 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object schema: $schema, object: $object[$propertyName] ); - + // Store relation and replace with reference $relations = $objectEntity->getRelations() ?? []; $relations[$propertyName] = $nestedObject->getId(); @@ -447,24 +446,24 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object $object = $this->handleFileProperty($objectEntity, $object, $propertyName); } } - + return $object; } /** * Handle file property processing - * + * * @param ObjectEntity $objectEntity The object entity * @param array $object The object data * @param string $propertyName The name of the file property - * + * * @return array Updated object data * @throws Exception When file handling fails */ private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName): array { $fileContent = null; $fileName = $propertyName; - + // Check if it's a Nextcloud file URL if (str_starts_with($object[$propertyName], $this->urlGenerator->getAbsoluteURL())) { $urlPath = parse_url($object[$propertyName], PHP_URL_PATH); @@ -484,6 +483,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s throw new \Exception('Invalid base64 encoded file'); } } + // Handle URL file else if (filter_var($object[$propertyName], FILTER_VALIDATE_URL)) { try { @@ -496,17 +496,17 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s } else { throw new \Exception('Invalid file format - must be base64 encoded or valid URL'); } - + try { $file = $this->fileService->createOrUpdateFile( content: $fileContent, fileName: $fileName ); - + $files = $objectEntity->getFiles() ?? []; $files[$propertyName] = $file->getId(); $objectEntity->setFiles($files); - + $object[$propertyName] = $file->getId(); } catch (\Exception $e) { throw new \Exception('Failed to store file: ' . $e->getMessage()); @@ -630,7 +630,7 @@ public function getMultipleObjects(string $objectType, array $ids) /** * Renders the entity by replacing the files and relations with their respective objects - * + * * @param array $entity The entity to render * @param array|null $extend Optional array of properties to extend, defaults to files and relations if not provided * @return array The rendered entity with expanded files and relations @@ -665,7 +665,7 @@ public function renderEntity(array $entity, ?array $extend = []): array // Replace the value at the dot notation path with the relation object $dotEntity->set($path, $this->getObject(register: $this->getRegister(), schema: $this->getSchema(), uuid: $relationId)); } - } + } // Update the entity with modified values $entity = $dotEntity->all(); From e1f64340e4bcc9d05627d18fa99b685ad6a4102f Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Fri, 29 Nov 2024 12:12:59 +0100 Subject: [PATCH 04/13] Create new object if no id --- lib/Service/ObjectService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 2a097c1..d947a93 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -305,6 +305,7 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt if (is_string($register)) { $register = $this->registerMapper->find($register); } + if (is_string($schema)) { $schema = $this->schemaMapper->find($schema); } @@ -321,7 +322,7 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt $validationResult = $this->validateObject(object: $object, schemaId: $schema); // Create new entity if none exists - if ($objectEntity === null) { + if (isset($object['id']) === false || $objectEntity === null) { $objectEntity = new ObjectEntity(); $objectEntity->setRegister($register); $objectEntity->setSchema($schema); From f321ca2acae0a96db5d5ed3f0233a64d0d1dbb81 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Fri, 29 Nov 2024 15:00:24 +0100 Subject: [PATCH 05/13] Fix attaching subobjects to object --- lib/Migration/Version1Date20241128131427.php | 2 +- lib/Service/ObjectService.php | 105 +++++++++++++------ 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/lib/Migration/Version1Date20241128131427.php b/lib/Migration/Version1Date20241128131427.php index b2781fa..7fabf59 100644 --- a/lib/Migration/Version1Date20241128131427.php +++ b/lib/Migration/Version1Date20241128131427.php @@ -41,7 +41,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt // Update the openregister_objects table $table = $schema->getTable('openregister_objects'); if ($table->hasColumn('uri') === false) { - $table->addColumn(name: 'uri', typeName: Types::STRING, options: ['notnull' => true, 'length' => 255]); + $table->addColumn(name: 'uri', typeName: Types::STRING, options: ['notnull' => true, 'length' => 255])->setDefault(''); } if ($table->hasColumn('files') === false) { $table->addColumn(name: 'files', typeName: Types::JSON, options: ['notnull' => false])->setDefault(default: '{}'); diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 630759a..dd7f636 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -327,7 +327,7 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt ); } - $validationResult = $this->validateObject(object: $object, schemaId: $schema); +// $validationResult = $this->validateObject(object: $object, schemaId: $schema); // Create new entity if none exists if ($objectEntity === null) { @@ -355,23 +355,26 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt $schemaObject = $this->schemaMapper->find($schema); + // Handle object properties that are either nested objects or files if ($schemaObject->getProperties() !== null && is_array($schemaObject->getProperties())) { - $object = $this->handleObjectRelations($objectEntity, $object, $schemaObject->getProperties(), $register, $schema); + $object = $this->handleObjectRelations($objectEntity, $object, $schemaObject->getSchemaObject(urlGenerator: $this->urlGenerator)->properties, $register, $schema); $objectEntity->setObject($object); } - if ($objectEntity->getId() && ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true)){ + $objectEntity->setUri($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('openregister.Objects.show', ['id' => $objectEntity->getUuid()]))); + + if ($objectEntity->getId()) {// && ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true)){ $objectEntity = $this->objectEntityMapper->update($objectEntity); $this->auditTrailMapper->createAuditTrail(new: $objectEntity, old: $oldObject); - } else if ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true) { + } else {//if ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true) { $objectEntity = $this->objectEntityMapper->insert($objectEntity); $this->auditTrailMapper->createAuditTrail(new: $objectEntity); } - if ($validationResult->isValid() === false) { - throw new ValidationException(message: 'The object could not be validated', errors: $validationResult->error()); - } +// if ($validationResult->isValid() === false) { +// throw new ValidationException(message: 'The object could not be validated', errors: $validationResult->error()); +// } return $objectEntity; } @@ -388,7 +391,10 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt * @return array Updated object data * @throws Exception|ValidationException When file handling fails */ - private function handleObjectRelations(ObjectEntity $objectEntity, array $object, array $properties, int $register, int $schema): array { + private function handleObjectRelations(ObjectEntity $objectEntity, array $object, stdClass $properties, int $register, int $schema): array + { + + foreach ($properties as $propertyName => $property) { // Skip if property not in object if (!isset($object[$propertyName])) { @@ -405,18 +411,36 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object // Process each array item foreach ($object[$propertyName] as $index => $item) { if ($property->items->type === 'object') { - // Handle nested object in array - $nestedObject = $this->saveObject( - register: $register, - schema: $schema, - object: $item - ); - - // Store relation and replace with reference - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName . '_' . $index] = $nestedObject->getId(); - $objectEntity->setRelations($relations); - $object[$propertyName][$index] = $nestedObject->getId(); + $subSchema = $schema; + + if(is_int($property->items->{'$ref'}) === true) { + $subSchema = $property->items->{'$ref'}; + } else if (filter_var(value: $property->items->{'$ref'}, filter: FILTER_VALIDATE_URL) !== false) { + $parsedUrl = parse_url($property->items->{'$ref'}); + $explodedPath = explode(separator: '/', string: $parsedUrl['path']); + $subSchema = end($explodedPath); + } + + if(is_array($item) === true) { + // Handle nested object in array + $nestedObject = $this->saveObject( + register: $register, + schema: $subSchema, + object: $item + ); + + // Store relation and replace with reference + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName . '_' . $index] = $nestedObject->getUuid(); + $objectEntity->setRelations($relations); + $object[$propertyName][$index] = $nestedObject->getUuid(); + + } else { + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName . '_' . $index] = $item; + $objectEntity->setRelations($relations); + } + } else if ($property->items->type === 'file') { // Handle file in array $object[$propertyName][$index] = $this->handleFileProperty( @@ -429,17 +453,36 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object } // Handle single object type else if ($property->type === 'object') { - $nestedObject = $this->saveObject( - register: $register, - schema: $schema, - object: $object[$propertyName] - ); - - // Store relation and replace with reference - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName] = $nestedObject->getId(); - $objectEntity->setRelations($relations); - $object[$propertyName] = $nestedObject->getId(); + + $subSchema = $schema; + + if(is_int($property->{'$ref'}) === true) { + $subSchema = $property->{'$ref'}; + } else if (filter_var(value: $property->{'$ref'}, filter: FILTER_VALIDATE_URL) !== false) { + $parsedUrl = parse_url($property->{'$ref'}); + $explodedPath = explode(separator: '/', string: $parsedUrl['path']); + $subSchema = end($explodedPath); + } + + if(is_array($object[$propertyName]) === true) { + $nestedObject = $this->saveObject( + register: $register, + schema: $subSchema, + object: $object[$propertyName] + ); + + // Store relation and replace with reference + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName] = $nestedObject->getUuid(); + $objectEntity->setRelations($relations); + $object[$propertyName] = $nestedObject->getUuid(); + + } else { + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName] = $object[$propertyName]; + $objectEntity->setRelations($relations); + } + } // Handle single file type else if ($property->type === 'file') { From 983da2945b4783b98a3c445c596d21eff777cfc2 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Fri, 29 Nov 2024 16:05:07 +0100 Subject: [PATCH 06/13] test --- lib/Service/ObjectService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index dd7f636..45685b6 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -327,7 +327,7 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt ); } -// $validationResult = $this->validateObject(object: $object, schemaId: $schema); + $validationResult = $this->validateObject(object: $object, schemaId: $schema); // Create new entity if none exists if ($objectEntity === null) { From 9a3740936feeeffb5a0f61067900d37e71995632 Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Fri, 29 Nov 2024 16:10:43 +0100 Subject: [PATCH 07/13] Added FileService --- ...427.php => Version1Date20241128221000.php} | 23 +- lib/Service/FileService.php | 390 ++++++++++++++++++ lib/Service/ObjectService.php | 245 ++++++----- 3 files changed, 550 insertions(+), 108 deletions(-) rename lib/Migration/{Version1Date20241128131427.php => Version1Date20241128221000.php} (75%) create mode 100644 lib/Service/FileService.php diff --git a/lib/Migration/Version1Date20241128131427.php b/lib/Migration/Version1Date20241128221000.php similarity index 75% rename from lib/Migration/Version1Date20241128131427.php rename to lib/Migration/Version1Date20241128221000.php index b2781fa..6543146 100644 --- a/lib/Migration/Version1Date20241128131427.php +++ b/lib/Migration/Version1Date20241128221000.php @@ -18,7 +18,7 @@ /** * FIXME Auto-generated migration step: Please modify to your needs! */ -class Version1Date20241128131427 extends SimpleMigrationStep { +class Version1Date20241128221000 extends SimpleMigrationStep { /** * @param IOutput $output @@ -41,13 +41,28 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt // Update the openregister_objects table $table = $schema->getTable('openregister_objects'); if ($table->hasColumn('uri') === false) { - $table->addColumn(name: 'uri', typeName: Types::STRING, options: ['notnull' => true, 'length' => 255]); + $table->addColumn( + name: 'uri', + typeName: Types::STRING, + options: [ + 'notnull' => true, + 'length' => 255 + ] + )->setDefault(''); } if ($table->hasColumn('files') === false) { - $table->addColumn(name: 'files', typeName: Types::JSON, options: ['notnull' => false])->setDefault(default: '{}'); + $table->addColumn( + name: 'files', + typeName: Types::JSON, + options: ['notnull' => false] + )->setDefault('{}'); } if ($table->hasColumn('relations') === false) { - $table->addColumn(name: 'relations', typeName: Types::JSON, options: ['notnull' => false])->setDefault(default: '{}'); + $table->addColumn( + name: 'relations', + typeName: Types::JSON, + options: ['notnull' => false] + )->setDefault('{}');; } return $schema; diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php new file mode 100644 index 0000000..170014f --- /dev/null +++ b/lib/Service/FileService.php @@ -0,0 +1,390 @@ +getId()} / {$schema->getUuid()}) {$schema->getTitle()}"; + } + + /** + * Get the name for the folder used for storing files of the given object. + * + * @param ObjectEntity $objectEntity The Object to get the folder name for. + * + * @return string The name the folder for this object should have. + */ + public function getObjectFolderName(ObjectEntity $objectEntity): string + { + // @todo check if property Title or Name exists and use that as object title + $objectTitle = 'object'; + + return "({$objectEntity->getId()} / {$objectEntity->getUuid()}) $objectTitle"; + } + + /** + * Returns a share link for the given IShare object. + * + * @param IShare $share An IShare object we are getting the share link for. + * + * @return string The share link needed to get the file or folder for the given IShare object. + */ + public function getShareLink(IShare $share): string + { + return $this->getCurrentDomain() . '/index.php/s/' . $share->getToken(); + } + + /** + * Gets and returns the current host / domain with correct protocol. + * + * @return string The current http/https domain url. + */ + private function getCurrentDomain(): string + { + // Check if the request is over HTTPS + $isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; + $protocol = $isHttps ? 'https://' : 'http://'; + + // Get the host (domain) + $host = $_SERVER['HTTP_HOST']; + + // Construct the full URL + return $protocol . $host; + } + + /** + * Try to find a IShare object with given $path & $shareType. + * + * @param string $path The path to a file we are trying to find a IShare object for. + * @param int|null $shareType The shareType of the share we are trying to find. + * + * @return IShare|null An IShare object or null. + */ + public function findShare(string $path, ?int $shareType = 3): ?IShare + { + $path = trim(string: $path, characters: '/'); + + // Get the current user. + $currentUser = $this->userSession->getUser(); + $userId = $currentUser ? $currentUser->getUID() : 'Guest'; + try { + $userFolder = $this->rootFolder->getUserFolder(userId: $userId); + } catch (NotPermittedException) { + $this->logger->error("Can't find share for $path because user (folder) for user $userId couldn't be found"); + + return null; + } + + try { + // Note: if we ever want to find shares for folders instead of files, this should work for folders as well? + $file = $userFolder->get(path: $path); + } catch (NotFoundException $e) { + $this->logger->error("Can't find share for $path because file doesn't exist"); + + return null; + } + + if ($file instanceof File) { + $shares = $this->shareManager->getSharesBy(userId: $userId, shareType: $shareType, path: $file); + if (count($shares) > 0) { + return $shares[0]; + } + } + + return null; + } + + /** + * Creates a IShare object using the $shareData array data. + * + * @param array $shareData The data to create a IShare with, should contain 'path', 'file', 'shareType', 'permissions' and 'userid'. + * + * @return IShare The Created IShare object. + * @throws Exception + */ + private function createShare(array $shareData) :IShare + { + // Create a new share + $share = $this->shareManager->newShare(); + $share->setTarget(target: '/'.$shareData['path']); + $share->setNodeId(fileId: $shareData['file']->getId()); + $share->setNodeType(type: 'file'); + $share->setShareType(shareType: $shareData['shareType']); + if ($shareData['permissions'] !== null) { + $share->setPermissions(permissions: $shareData['permissions']); + } + $share->setSharedBy(sharedBy: $shareData['userId']); + $share->setShareOwner(shareOwner: $shareData['userId']); + $share->setShareTime(shareTime: new DateTime()); + $share->setStatus(status: $share::STATUS_ACCEPTED); + + return $this->shareManager->createShare(share: $share); + } + + /** + * Creates and returns a share link for a file (or folder). + * (https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html#create-a-new-share) + * + * @param string $path Path (from root) to the file/folder which should be shared. + * @param int|null $shareType 0 = user; 1 = group; 3 = public link; 4 = email; 6 = federated cloud share; 7 = circle; 10 = Talk conversation + * @param int|null $permissions 1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all (default: 31, for public shares: 1) + * + * @return string The share link. + * @throws Exception In case creating the share(link) fails. + */ + public function createShareLink(string $path, ?int $shareType = 3, ?int $permissions = null): string + { + $path = trim(string: $path, characters: '/'); + if ($permissions === null) { + $permissions = 31; + if ($shareType === 3) { + $permissions = 1; + } + } + + // Get the current user. + $currentUser = $this->userSession->getUser(); + $userId = $currentUser ? $currentUser->getUID() : 'Guest'; + try { + $userFolder = $this->rootFolder->getUserFolder(userId: $userId); + } catch (NotPermittedException) { + $this->logger->error("Can't create share link for $path because user (folder) for user $userId couldn't be found"); + + return "User (folder) couldn't be found"; + } + + try { + // Note: if we ever want to create share links for folders instead of files, just remove this try catch and only use setTarget, not setNodeId. + $file = $userFolder->get(path: $path); + } catch (NotFoundException $e) { + $this->logger->error("Can't create share link for $path because file doesn't exist"); + + return 'File not found at '.$path; + } + + try { + $share = $this->createShare([ + 'path' => $path, + 'file' => $file, + 'shareType' => $shareType, + 'permissions' => $permissions, + 'userId' => $userId + ]); + + return $this->getShareLink($share); + } catch (Exception $exception) { + $this->logger->error("Can't create share link for $path: " . $exception->getMessage()); + + throw new Exception('Can\'t create share link'); + } + } + + /** + * Creates a new folder in NextCloud, unless it already exists. + * + * @param string $folderPath Path (from root) to where you want to create a folder, include the name of the folder. (/Media/exampleFolder) + * + * @return bool True if successfully created a new folder. + * @throws Exception In case we can't create the folder because it is not permitted. + */ + public function createFolder(string $folderPath): bool + { + $folderPath = trim(string: $folderPath, characters: '/'); + + // Get the current user. + $currentUser = $this->userSession->getUser(); + $userFolder = $this->rootFolder->getUserFolder(userId: $currentUser ? $currentUser->getUID() : 'Guest'); + + // Check if folder exists and if not create it. + try { + try { + $userFolder->get(path: $folderPath); + } catch (NotFoundException $e) { + $userFolder->newFolder(path: $folderPath); + + return true; + } + + // Folder already exists. + $this->logger->info("This folder already exits $folderPath"); + return false; + + } catch (NotPermittedException $e) { + $this->logger->error("Can't create folder $folderPath: " . $e->getMessage()); + + throw new Exception("Can\'t create folder $folderPath"); + } + } + + /** + * Uploads a file to NextCloud. Will create a new file if it doesn't exist yet. + * + * @param mixed $content The content of the file. + * @param string $filePath Path (from root) where to save the file. NOTE: this should include the name and extension/format of the file as well! (example.pdf) + * + * @return bool True if successful. + * @throws Exception In case we can't write to file because it is not permitted. + */ + public function uploadFile(mixed $content, string $filePath): bool + { + $filePath = trim(string: $filePath, characters: '/'); + + // Get the current user. + $currentUser = $this->userSession->getUser(); + $userFolder = $this->rootFolder->getUserFolder(userId: $currentUser ? $currentUser->getUID() : 'Guest'); + + // Check if file exists and create it if not. + try { + try { + $userFolder->get(path: $filePath); + } catch (NotFoundException $e) { + $userFolder->newFile(path: $filePath); + $file = $userFolder->get(path: $filePath); + + $file->putContent(data: $content); + + return true; + } + + // File already exists. + $this->logger->warning("File $filePath already exists."); + return false; + + } catch (NotPermittedException|GenericFileException|LockedException $e) { + $this->logger->error("Can't create file $filePath: " . $e->getMessage()); + + throw new Exception("Can't write to file $filePath"); + } + } + + /** + * Overwrites an existing file in NextCloud. + * + * @param mixed $content The content of the file. + * @param string $filePath Path (from root) where to save the file. NOTE: this should include the name and extension/format of the file as well! (example.pdf) + * @param bool $createNew Default = false. If set to true this function will create a new file if it doesn't exist yet. + * + * @return bool True if successful. + * @throws Exception In case we can't write to file because it is not permitted. + */ + public function updateFile(mixed $content, string $filePath, bool $createNew = false): bool + { + $filePath = trim(string: $filePath, characters: '/'); + + // Get the current user. + $currentUser = $this->userSession->getUser(); + $userFolder = $this->rootFolder->getUserFolder(userId: $currentUser ? $currentUser->getUID() : 'Guest'); + + // Check if file exists and overwrite it if it does. + try { + try { + $file = $userFolder->get(path: $filePath); + + $file->putContent(data: $content); + + return true; + } catch (NotFoundException $e) { + if ($createNew === true) { + $userFolder->newFile(path: $filePath); + $file = $userFolder->get(path: $filePath); + + $file->putContent(data: $content); + + $this->logger->info("File $filePath did not exist, created a new file for it."); + return true; + } + } + + // File already exists. + $this->logger->warning("File $filePath already exists."); + return false; + + } catch (NotPermittedException|GenericFileException|LockedException $e) { + $this->logger->error("Can't create file $filePath: " . $e->getMessage()); + + throw new Exception("Can't write to file $filePath"); + } + } + + /** + * Deletes a file from NextCloud. + * + * @param string $filePath Path (from root) to the file you want to delete. + * + * @return bool True if successful. + * @throws Exception In case deleting the file is not permitted. + */ + public function deleteFile(string $filePath): bool + { + $filePath = trim(string: $filePath, characters: '/'); + + // Get the current user. + $currentUser = $this->userSession->getUser(); + $userFolder = $this->rootFolder->getUserFolder(userId: $currentUser ? $currentUser->getUID() : 'Guest'); + + // Check if file exists and delete it if it does. + try { + try { + $file = $userFolder->get(path: $filePath); + $file->delete(); + + return true; + } catch (NotFoundException $e) { + // File does not exist. + $this->logger->warning("File $filePath does not exist."); + + return false; + } + } catch (NotPermittedException|InvalidPathException $e) { + $this->logger->error("Can't delete file $filePath: " . $e->getMessage()); + + throw new Exception("Can't delete file $filePath"); + } + } + +} diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 630759a..2b9e900 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -2,6 +2,8 @@ namespace OCA\OpenRegister\Service; +use Exception; +use InvalidArgumentException; use OC\URLGenerator; use OCA\OpenRegister\Db\Source; use OCA\OpenRegister\Db\SourceMapper; @@ -15,7 +17,6 @@ use OCA\OpenRegister\Db\AuditTrailMapper; use OCA\OpenRegister\Exception\ValidationException; use OCA\OpenRegister\Formats\BsnFormat; -use OCP\DB\Exception; use OCP\IURLGenerator; use Opis\JsonSchema\ValidationResult; use Opis\JsonSchema\Validator; @@ -60,7 +61,8 @@ public function __construct( RegisterMapper $registerMapper, SchemaMapper $schemaMapper, AuditTrailMapper $auditTrailMapper, - private readonly IURLGenerator $urlGenerator + private readonly IURLGenerator $urlGenerator, + private readonly FileService $fileService ) { $this->objectEntityMapper = $objectEntityMapper; @@ -94,14 +96,16 @@ public function validateObject(array $object, ?int $schemaId = null, object $sch } /** - * Find an object by ID or UUID - * - * @param int|string $id The ID or UUID to search for - * @param array $extend Properties to extend with related data - * - * @return ObjectEntity The found object - */ - public function find(int|string $id, ?array $extend = []) { + * Find an object by ID or UUID + * + * @param int|string $id The ID or UUID to search for + * @param array|null $extend Properties to extend with related data + * + * @return ObjectEntity The found object + * @throws Exception + */ + public function find(int|string $id, ?array $extend = []): ObjectEntity + { return $this->getObject( register: $this->registerMapper->find($this->getRegister()), schema: $this->schemaMapper->find($this->getSchema()), @@ -110,13 +114,16 @@ public function find(int|string $id, ?array $extend = []) { ); } - /** - * Create a new object from array data - * - * @param array $object The object data - * @return ObjectEntity The created object - */ - public function createFromArray(array $object) { + /** + * Create a new object from array data + * + * @param array $object The object data + * + * @return ObjectEntity The created object + * @throws ValidationException + */ + public function createFromArray(array $object): ObjectEntity + { return $this->saveObject( register: $this->getRegister(), schema: $this->getSchema(), @@ -124,15 +131,18 @@ public function createFromArray(array $object) { ); } - /** - * Update an existing object from array data - * - * @param string $id The object ID to update - * @param array $object The new object data - * @param bool $updatedObject Whether this is an update operation - * @return ObjectEntity The updated object - */ - public function updateFromArray(string $id, array $object, bool $updatedObject, bool $patch = false) { + /** + * Update an existing object from array data + * + * @param string $id The object ID to update + * @param array $object The new object data + * @param bool $updatedObject Whether this is an update operation + * + * @return ObjectEntity The updated object + * @throws ValidationException + */ + public function updateFromArray(string $id, array $object, bool $updatedObject, bool $patch = false): ObjectEntity + { // Add ID to object data for update $object['id'] = $id; @@ -150,12 +160,14 @@ public function updateFromArray(string $id, array $object, bool $updatedObject, ); } - /** - * Delete an object - * - * @param array|\JsonSerializable $object The object to delete - * @return bool True if deletion was successful - */ + /** + * Delete an object + * + * @param array|\JsonSerializable $object The object to delete + * + * @return bool True if deletion was successful + * @throws Exception + */ public function delete(array|\JsonSerializable $object): bool { // Convert JsonSerializable objects to array @@ -170,18 +182,18 @@ public function delete(array|\JsonSerializable $object): bool ); } - /** - * Find all objects matching given criteria - * - * @param int|null $limit Maximum number of results - * @param int|null $offset Starting offset for pagination - * @param array $filters Filter criteria - * @param array $sort Sorting criteria - * @param string|null $search Search term - * @param array $extend Properties to extend with related data - * - * @return array List of matching objects - */ + /** + * Find all objects matching given criteria + * + * @param int|null $limit Maximum number of results + * @param int|null $offset Starting offset for pagination + * @param array $filters Filter criteria + * @param array $sort Sorting criteria + * @param string|null $search Search term + * @param array|null $extend Properties to extend with related data + * + * @return array List of matching objects + */ public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null, ?array $extend = []): array { $objects = $this->getObjects( @@ -216,12 +228,14 @@ public function count(array $filters = [], ?string $search = null): int ->countAll(filters: $filters, search: $search); } - /** - * Find multiple objects by their IDs - * - * @param array $ids Array of object IDs to find - * @return array Array of found objects - */ + /** + * Find multiple objects by their IDs + * + * @param array $ids Array of object IDs to find + * + * @return array Array of found objects + * @throws Exception + */ public function findMultiple(array $ids): array { $result = []; @@ -237,6 +251,7 @@ public function findMultiple(array $ids): array * * @param array $filters Filter criteria * @param string|null $search Search term + * * @return array Aggregation results */ public function getAggregations(array $filters, ?string $search = null): array @@ -255,32 +270,34 @@ public function getAggregations(array $filters, ?string $search = null): array return []; } - /** - * Extract object data from an entity - * - * @param mixed $object The object to extract data from - * @param array $extend Properties to extend with related data - * - * @return mixed The extracted object data - */ - private function getDataFromObject(mixed $object, ?array $extend = []) { + /** + * Extract object data from an entity + * + * @param mixed $object The object to extract data from + * @param array|null $extend Properties to extend with related data + * + * @return mixed The extracted object data + */ + private function getDataFromObject(mixed $object, ?array $extend = []): mixed + { return $object->getObject(); } - /** - * Gets all objects of a specific type. - * - * @param string|null $objectType The type of objects to retrieve. - * @param int|null $register - * @param int|null $schema - * @param int|null $limit The maximum number of objects to retrieve. - * @param int|null $offset The offset from which to start retrieving objects. - * @param array $filters - * @param array $extend Properties to extend with related data - * - * @return array The retrieved objects. - * @throws \Exception - */ + /** + * Gets all objects of a specific type. + * + * @param string|null $objectType The type of objects to retrieve. + * @param int|null $register + * @param int|null $schema + * @param int|null $limit The maximum number of objects to retrieve. + * @param int|null $offset The offset from which to start retrieving objects. + * @param array $filters + * @param array $sort + * @param string|null $search + * @param array|null $extend Properties to extend with related data + * + * @return array The retrieved objects. + */ public function getObjects(?string $objectType = null, ?int $register = null, ?int $schema = null, ?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null, ?array $extend = []): array { // Set object type and filters if register and schema are provided @@ -310,7 +327,7 @@ public function getObjects(?string $objectType = null, ?int $register = null, ?i */ public function saveObject(int $register, int $schema, array $object): ObjectEntity { - // Convert register and schema to their respective objects if they are strings + // Convert register and schema to their respective objects if they are strings // @todo ??? if (is_string($register)) { $register = $this->registerMapper->find($register); } @@ -420,9 +437,9 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object } else if ($property->items->type === 'file') { // Handle file in array $object[$propertyName][$index] = $this->handleFileProperty( - $objectEntity, - [$propertyName => $item], - $propertyName . '_' . $index + objectEntity: $objectEntity, + object: [$propertyName => $item], + propertyName: $propertyName . '_' . $index )[$propertyName]; } } @@ -461,7 +478,6 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object * @throws Exception When file handling fails */ private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName): array { - $fileContent = null; $fileName = $propertyName; // Check if it's a Nextcloud file URL @@ -480,7 +496,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s if (preg_match('/^data:([^;]*);base64,(.*)/', $object[$propertyName], $matches)) { $fileContent = base64_decode($matches[2], true); if ($fileContent === false) { - throw new \Exception('Invalid base64 encoded file'); + throw new Exception('Invalid base64 encoded file'); } } @@ -490,26 +506,47 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s $client = new \GuzzleHttp\Client(); $response = $client->get($object[$propertyName]); $fileContent = $response->getBody()->getContents(); - } catch (\Exception $e) { - throw new \Exception('Failed to download file from URL: ' . $e->getMessage()); + } catch (Exception $e) { + throw new Exception('Failed to download file from URL: ' . $e->getMessage()); } } else { - throw new \Exception('Invalid file format - must be base64 encoded or valid URL'); + throw new Exception('Invalid file format - must be base64 encoded or valid URL'); } try { - $file = $this->fileService->createOrUpdateFile( + $schema = $this->schemaMapper->find($objectEntity->getSchema()); + $schemaFolder = $this->fileService->getSchemaFolderName($schema); + $objectFolder = $this->fileService->getObjectFolderName($objectEntity); + + $this->fileService->createFolder(folderPath: 'Objects'); + $this->fileService->createFolder(folderPath: "Objects/$schemaFolder"); + $this->fileService->createFolder(folderPath: "Objects/$schemaFolder/$objectFolder"); + $filePath = "Objects/$schemaFolder/$objectFolder/$fileName"; + + $succes = $this->fileService->updateFile( content: $fileContent, - fileName: $fileName + filePath: $filePath, + createNew: true ); + if ($succes === false) { + throw new Exception('Failed to upload this file: $filePath to NextCloud'); + } + + // Create or find ShareLink + $share = $this->fileService->findShare(path: $filePath); + if ($share !== null) { + $shareLink = $this->fileService->getShareLink($share); + } else { + $shareLink = $this->fileService->createShareLink(path: $filePath); + } $files = $objectEntity->getFiles() ?? []; - $files[$propertyName] = $file->getId(); + $files[$propertyName] = $shareLink; $objectEntity->setFiles($files); - $object[$propertyName] = $file->getId(); - } catch (\Exception $e) { - throw new \Exception('Failed to store file: ' . $e->getMessage()); + $object[$propertyName] = $shareLink; + } catch (Exception $e) { + throw new Exception('Failed to store file: ' . $e->getMessage()); } return $object; @@ -524,7 +561,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s * @param array $extend Properties to extend with related data * * @return ObjectEntity The resulting object - * @throws \Exception If source type is unsupported + * @throws Exception If source type is unsupported */ public function getObject(Register $register, Schema $schema, string $uuid, ?array $extend = []): ObjectEntity { @@ -535,7 +572,7 @@ public function getObject(Register $register, Schema $schema, string $uuid, ?arr //@todo mongodb support - throw new \Exception('Unsupported source type'); + throw new Exception('Unsupported source type'); } /** @@ -546,7 +583,7 @@ public function getObject(Register $register, Schema $schema, string $uuid, ?arr * @param string $uuid The UUID of the object to delete * * @return bool True if deletion was successful - * @throws \Exception If source type is unsupported + * @throws Exception If source type is unsupported */ public function deleteObject(Register $register, Schema $schema, string $uuid): bool { @@ -559,7 +596,7 @@ public function deleteObject(Register $register, Schema $schema, string $uuid): //@todo mongodb support - throw new \Exception('Unsupported source type'); + throw new Exception('Unsupported source type'); } /** @@ -569,9 +606,9 @@ public function deleteObject(Register $register, Schema $schema, string $uuid): * @param int|null $register Optional register ID * @param int|null $schema Optional schema ID * @return mixed The appropriate mapper - * @throws \InvalidArgumentException If unknown object type + * @throws InvalidArgumentException If unknown object type */ - public function getMapper(?string $objectType = null, ?int $register = null, ?int $schema = null) + public function getMapper(?string $objectType = null, ?int $register = null, ?int $schema = null): mixed { // Return self if register and schema provided if ($register !== null && $schema !== null) { @@ -589,7 +626,7 @@ public function getMapper(?string $objectType = null, ?int $register = null, ?in case 'objectEntity': return $this->objectEntityMapper; default: - throw new \InvalidArgumentException("Unknown object type: $objectType"); + throw new InvalidArgumentException("Unknown object type: $objectType"); } } @@ -599,10 +636,10 @@ public function getMapper(?string $objectType = null, ?int $register = null, ?in * @param string $objectType The type of objects to retrieve * @param array $ids The ids of the objects to retrieve * @return array The retrieved objects - * @throws \InvalidArgumentException If unknown object type + * @throws InvalidArgumentException If unknown object type */ - public function getMultipleObjects(string $objectType, array $ids) - { + public function getMultipleObjects(string $objectType, array $ids): array + { // Process the ids to handle different formats $processedIds = array_map(function($id) { if (is_object($id) && method_exists($id, 'getId')) { @@ -679,7 +716,7 @@ public function renderEntity(array $entity, ?array $extend = []): array * @param mixed $entity The entity to extend * @param array $extend Properties to extend with related data * @return array The extended entity as an array - * @throws \Exception If property not found or no mapper available + * @throws Exception If property not found or no mapper available */ public function extendEntity(array $entity, array $extend): array { @@ -703,7 +740,7 @@ public function extendEntity(array $entity, array $extend): array } elseif (array_key_exists(key: $singularProperty, array: $result)) { $value = $result[$singularProperty]; } else { - throw new \Exception("Property '$property' or '$singularProperty' is not present in the entity."); + throw new Exception("Property '$property' or '$singularProperty' is not present in the entity."); } // Try to get mapper for property @@ -711,12 +748,12 @@ public function extendEntity(array $entity, array $extend): array try { $mapper = $this->getMapper(objectType: $property); $propertyObject = $singularProperty; - } catch (\Exception $e) { + } catch (Exception $e) { try { $mapper = $this->getMapper(objectType: $singularProperty); $propertyObject = $singularProperty; - } catch (\Exception $e) { - throw new \Exception("No mapper available for property '$property'."); + } catch (Exception $e) { + throw new Exception("No mapper available for property '$property'."); } } @@ -736,7 +773,7 @@ public function extendEntity(array $entity, array $extend): array * Get all registers extended with their schemas * * @return array The registers with schema data - * @throws \Exception If extension fails + * @throws Exception If extension fails */ public function getRegisters(): array { From 587031ee6247b1f9e5a97f807876ad50975a3795 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Fri, 29 Nov 2024 17:34:14 +0100 Subject: [PATCH 08/13] Revert test --- lib/Service/ObjectService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index b1ba73d..00168ef 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -344,7 +344,7 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt ); } - $validationResult = $this->validateObject(object: $object, schemaId: $schema); +// $validationResult = $this->validateObject(object: $object, schemaId: $schema); // Create new entity if none exists if ($objectEntity === null) { From ad2ed6a51705404fcc0ecf83c6bef3a66561dcd1 Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Fri, 29 Nov 2024 18:30:25 +0100 Subject: [PATCH 09/13] Almost working code for getting SharePoint file and connect it to object --- lib/Service/FileService.php | 4 +- lib/Service/ObjectService.php | 140 +++++++++++++++++++++++++--------- 2 files changed, 105 insertions(+), 39 deletions(-) diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 170014f..ea608fc 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -49,7 +49,7 @@ public function __construct( */ public function getSchemaFolderName(Schema $schema): string { - return "({$schema->getId()} / {$schema->getUuid()}) {$schema->getTitle()}"; + return "({$schema->getUuid()}) {$schema->getTitle()}"; } /** @@ -64,7 +64,7 @@ public function getObjectFolderName(ObjectEntity $objectEntity): string // @todo check if property Title or Name exists and use that as object title $objectTitle = 'object'; - return "({$objectEntity->getId()} / {$objectEntity->getUuid()}) $objectTitle"; + return "({$objectEntity->getUuid()}) $objectTitle"; } /** diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 2b9e900..3d53649 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -2,7 +2,9 @@ namespace OCA\OpenRegister\Service; +use Adbar\Dot; use Exception; +use GuzzleHttp\Exception\GuzzleException; use InvalidArgumentException; use OC\URLGenerator; use OCA\OpenRegister\Db\Source; @@ -17,9 +19,13 @@ use OCA\OpenRegister\Db\AuditTrailMapper; use OCA\OpenRegister\Exception\ValidationException; use OCA\OpenRegister\Formats\BsnFormat; +use OCP\App\IAppManager; use OCP\IURLGenerator; use Opis\JsonSchema\ValidationResult; use Opis\JsonSchema\Validator; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use stdClass; use Symfony\Component\Uid\Uuid; use GuzzleHttp\Client; @@ -61,8 +67,10 @@ public function __construct( RegisterMapper $registerMapper, SchemaMapper $schemaMapper, AuditTrailMapper $auditTrailMapper, + private ContainerInterface $container, private readonly IURLGenerator $urlGenerator, - private readonly FileService $fileService + private readonly FileService $fileService, + private readonly IAppManager $appManager ) { $this->objectEntityMapper = $objectEntityMapper; @@ -71,6 +79,27 @@ public function __construct( $this->auditTrailMapper = $auditTrailMapper; } + /** + * Attempts to retrieve the OpenConnector service from the container. + * + * @return mixed|null The OpenConnector service if available, null otherwise. + * @throws ContainerExceptionInterface|NotFoundExceptionInterface + */ + public function getOpenConnector(string $filePath = '\Service\ObjectService'): mixed + { + if (in_array(needle: 'openconnector', haystack: $this->appManager->getInstalledApps()) === true) { + try { + // Attempt to get a OpenConnector file from the container + return $this->container->get("OCA\OpenConnector$filePath"); + } catch (Exception $e) { + // If the file is not available, return null + return null; + } + } + + return null; + } + /** * Validate an object with a schema. * If schema is not given and schemaObject is filled, the object will validate to the schemaObject. @@ -408,20 +437,20 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt private function handleObjectRelations(ObjectEntity $objectEntity, array $object, array $properties, int $register, int $schema): array { foreach ($properties as $propertyName => $property) { // Skip if property not in object - if (!isset($object[$propertyName])) { + if (isset($object[$propertyName]) === false) { continue; } // Handle array type with items that may contain objects/files - if ($property->type === 'array' && isset($property->items)) { + if ($property['type'] === 'array' && isset($property['items']) === true) { // Skip if not array in data - if (!is_array($object[$propertyName])) { + if (is_array($object[$propertyName]) === false) { continue; } // Process each array item foreach ($object[$propertyName] as $index => $item) { - if ($property->items->type === 'object') { + if ($property['items']['type'] === 'object') { // Handle nested object in array $nestedObject = $this->saveObject( register: $register, @@ -434,18 +463,18 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object $relations[$propertyName . '_' . $index] = $nestedObject->getId(); $objectEntity->setRelations($relations); $object[$propertyName][$index] = $nestedObject->getId(); - } else if ($property->items->type === 'file') { + } else if ($property['items']['type'] === 'file') { // Handle file in array $object[$propertyName][$index] = $this->handleFileProperty( objectEntity: $objectEntity, - object: [$propertyName => $item], - propertyName: $propertyName . '_' . $index + object: [$propertyName => [$index => $item]], + propertyName: $propertyName . '.' . $index )[$propertyName]; } } } // Handle single object type - else if ($property->type === 'object') { + else if ($property['type'] === 'object') { $nestedObject = $this->saveObject( register: $register, schema: $schema, @@ -459,7 +488,7 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object $object[$propertyName] = $nestedObject->getId(); } // Handle single file type - else if ($property->type === 'file') { + else if ($property['type'] === 'file') { $object = $this->handleFileProperty($objectEntity, $object, $propertyName); } } @@ -475,42 +504,77 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object * @param string $propertyName The name of the file property * * @return array Updated object data - * @throws Exception When file handling fails + * @throws Exception|GuzzleException When file handling fails */ private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName): array { - $fileName = $propertyName; + $fileName = str_replace('.', '_', $propertyName); + $objectDot = new Dot($object); // Check if it's a Nextcloud file URL - if (str_starts_with($object[$propertyName], $this->urlGenerator->getAbsoluteURL())) { - $urlPath = parse_url($object[$propertyName], PHP_URL_PATH); - if (preg_match('/\/f\/(\d+)/', $urlPath, $matches)) { - $files = $objectEntity->getFiles() ?? []; - $files[$propertyName] = (int)$matches[1]; - $objectEntity->setFiles($files); - $object[$propertyName] = (int)$matches[1]; - return $object; - } - } +// if (str_starts_with($object[$propertyName], $this->urlGenerator->getAbsoluteURL())) { +// $urlPath = parse_url($object[$propertyName], PHP_URL_PATH); +// if (preg_match('/\/f\/(\d+)/', $urlPath, $matches)) { +// $files = $objectEntity->getFiles() ?? []; +// $files[$propertyName] = (int)$matches[1]; +// $objectEntity->setFiles($files); +// $object[$propertyName] = (int)$matches[1]; +// return $object; +// } +// } // Handle base64 encoded file - if (preg_match('/^data:([^;]*);base64,(.*)/', $object[$propertyName], $matches)) { + if (is_string($objectDot->get($propertyName)) === true + && preg_match('/^data:([^;]*);base64,(.*)/', $objectDot->get($propertyName), $matches) + ) { $fileContent = base64_decode($matches[2], true); if ($fileContent === false) { throw new Exception('Invalid base64 encoded file'); } } - // Handle URL file - else if (filter_var($object[$propertyName], FILTER_VALIDATE_URL)) { - try { - $client = new \GuzzleHttp\Client(); - $response = $client->get($object[$propertyName]); - $fileContent = $response->getBody()->getContents(); - } catch (Exception $e) { - throw new Exception('Failed to download file from URL: ' . $e->getMessage()); + else { + // Encode special characters in the URL + $encodedUrl = rawurlencode($objectDot->get("$propertyName.downloadUrl")); //@todo hardcoded .downloadUrl + + // Decode valid path separators and reserved characters + $encodedUrl = str_replace(['%2F', '%3A', '%28', '%29'], ['/', ':', '(', ')'], $encodedUrl); + + if (filter_var($encodedUrl, FILTER_VALIDATE_URL)) { + try { + // @todo hacky tacky + // Regular expression to get the filename and extension from url //@todo hardcoded .downloadUrl + if (preg_match("/\/([^\/]+)'\)\/\\\$value$/", $objectDot->get("$propertyName.downloadUrl"), $matches)) { + // @todo hardcoded way of getting the filename and extension from the url + $fileNameFromUrl = $matches[1]; + // @todo use only the extension from the url ? + // $fileName = $fileNameFromUrl; + $extension = substr(strrchr($fileNameFromUrl, '.'), 1); + $fileName = "$fileName.$extension"; + } + + if ($objectDot->has("$propertyName.source") === true) { + $sourceMapper = $this->getOpenConnector(filePath: '\Db\SourceMapper'); + $source = $sourceMapper->find($objectDot->get("$propertyName.source")); + + $callService = $this->getOpenConnector(filePath: '\Service\CallService'); + if ($callService === null) { + throw new Exception("OpenConnector service not available"); + } + $endpoint = str_replace($source->getLocation(), "", $encodedUrl); + + $response = $callService->call(source: $source, endpoint: $endpoint, method: 'GET')->getResponse(); + } else { + $client = new \GuzzleHttp\Client(); + $response = $client->get($encodedUrl); + } + $fileContent = $response->getBody()->getContents(); + } catch (Exception|NotFoundExceptionInterface $e) { + var_dump($e->getTrace()); // @todo REMOVE VAR DUMP! + throw new Exception('Failed to download file from URL: ' . $e->getMessage()); + } + } else { + throw new Exception('Invalid file format - must be base64 encoded or valid URL'); } - } else { - throw new Exception('Invalid file format - must be base64 encoded or valid URL'); } try { @@ -540,11 +604,13 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s $shareLink = $this->fileService->createShareLink(path: $filePath); } - $files = $objectEntity->getFiles() ?? []; - $files[$propertyName] = $shareLink; - $objectEntity->setFiles($files); + $filesDot = new Dot($objectEntity->getFiles() ?? []); + $filesDot->set($propertyName, $shareLink); + $objectEntity->setFiles($filesDot->all()); - $object[$propertyName] = $shareLink; + // Preserve the original uri in the object 'json blob' +// $objectDot = $objectDot->set($propertyName, $shareLink); + $object = $objectDot->all(); } catch (Exception $e) { throw new Exception('Failed to store file: ' . $e->getMessage()); } From 7512913550dc4a4f8da74dea509f7dcd0360c5fa Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Fri, 29 Nov 2024 20:52:14 +0100 Subject: [PATCH 10/13] Decode base64 encoded files --- lib/Service/ObjectService.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 3d53649..6c05be6 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -563,11 +563,23 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s $endpoint = str_replace($source->getLocation(), "", $encodedUrl); $response = $callService->call(source: $source, endpoint: $endpoint, method: 'GET')->getResponse(); + $fileContent = $response->getBody(); + + if( + base64_decode(string: $fileContent) !== false + && mb_check_encoding( + value: base64_decode(string: $fileContent), + encoding: 'UTF-8' + ) === false + ) { + $fileContent = base64_decode(string: $fileContent); + } + } else { $client = new \GuzzleHttp\Client(); $response = $client->get($encodedUrl); + $fileContent = $response->getBody()->getContents(); } - $fileContent = $response->getBody()->getContents(); } catch (Exception|NotFoundExceptionInterface $e) { var_dump($e->getTrace()); // @todo REMOVE VAR DUMP! throw new Exception('Failed to download file from URL: ' . $e->getMessage()); From 1bcc6f6d6cd754ad719c93726f13be9b35712ee6 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Sat, 30 Nov 2024 11:54:29 +0100 Subject: [PATCH 11/13] Fix weird urls, use array instead of object --- lib/Service/ObjectService.php | 48 +++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 9df465b..9854a46 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -404,7 +404,7 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt // Handle object properties that are either nested objects or files if ($schemaObject->getProperties() !== null && is_array($schemaObject->getProperties())) { - $object = $this->handleObjectRelations($objectEntity, $object, $schemaObject->getSchemaObject(urlGenerator: $this->urlGenerator)->properties, $register, $schema); + $object = $this->handleObjectRelations($objectEntity, $object, $schemaObject->getProperties(), $register, $schema); $objectEntity->setObject($object); } @@ -437,7 +437,7 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt * @return array Updated object data * @throws Exception|ValidationException When file handling fails */ - private function handleObjectRelations(ObjectEntity $objectEntity, array $object, stdClass $properties, int $register, int $schema): array + private function handleObjectRelations(ObjectEntity $objectEntity, array $object, array $properties, int $register, int $schema): array { @@ -456,13 +456,13 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object // Process each array item foreach ($object[$propertyName] as $index => $item) { - if ($property->items->type === 'object') { + if ($property['items']['type'] === 'object') { $subSchema = $schema; - if(is_int($property->items->{'$ref'}) === true) { - $subSchema = $property->items->{'$ref'}; - } else if (filter_var(value: $property->items->{'$ref'}, filter: FILTER_VALIDATE_URL) !== false) { - $parsedUrl = parse_url($property->items->{'$ref'}); + if(is_int($property['items']['$ref']) === true) { + $subSchema = $property['items']['$ref']; + } else if (filter_var(value: $property['items']['$ref'], filter: FILTER_VALIDATE_URL) !== false) { + $parsedUrl = parse_url($property['items']['$ref']); $explodedPath = explode(separator: '/', string: $parsedUrl['path']); $subSchema = end($explodedPath); } @@ -487,7 +487,7 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object $objectEntity->setRelations($relations); } - } else if ($property->items->type === 'file') { + } else if ($property['items']['type'] === 'file') { // Handle file in array $object[$propertyName][$index] = $this->handleFileProperty( objectEntity: $objectEntity, @@ -498,14 +498,14 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object } } // Handle single object type - else if ($property->type === 'object') { + else if ($property['type'] === 'object') { $subSchema = $schema; - if(is_int($property->{'$ref'}) === true) { - $subSchema = $property->{'$ref'}; - } else if (filter_var(value: $property->{'$ref'}, filter: FILTER_VALIDATE_URL) !== false) { - $parsedUrl = parse_url($property->{'$ref'}); + if(is_int($property['$ref']) === true) { + $subSchema = $property['$ref']; + } else if (filter_var(value: $property['$ref'], filter: FILTER_VALIDATE_URL) !== false) { + $parsedUrl = parse_url($property['$ref']); $explodedPath = explode(separator: '/', string: $parsedUrl['path']); $subSchema = end($explodedPath); } @@ -532,7 +532,12 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object } // Handle single file type else if ($property['type'] === 'file') { - $object = $this->handleFileProperty($objectEntity, $object, $propertyName); + + $object[$propertyName] = $this->handleFileProperty( + objectEntity: $objectEntity, + object: [$propertyName => $object[$propertyName]], + propertyName: $propertyName + ); } } @@ -605,15 +610,15 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s } $endpoint = str_replace($source->getLocation(), "", $encodedUrl); + + $endpoint = urldecode($endpoint); + $response = $callService->call(source: $source, endpoint: $endpoint, method: 'GET')->getResponse(); - $fileContent = $response->getBody(); + + $fileContent = $response['body']; if( - base64_decode(string: $fileContent) !== false - && mb_check_encoding( - value: base64_decode(string: $fileContent), - encoding: 'UTF-8' - ) === false + $response['encoding'] === 'base64' ) { $fileContent = base64_decode(string: $fileContent); } @@ -624,7 +629,6 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s $fileContent = $response->getBody()->getContents(); } } catch (Exception|NotFoundExceptionInterface $e) { - var_dump($e->getTrace()); // @todo REMOVE VAR DUMP! throw new Exception('Failed to download file from URL: ' . $e->getMessage()); } } else { @@ -664,7 +668,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s $objectEntity->setFiles($filesDot->all()); // Preserve the original uri in the object 'json blob' -// $objectDot = $objectDot->set($propertyName, $shareLink); + $objectDot = $objectDot->set($propertyName, $shareLink); $object = $objectDot->all(); } catch (Exception $e) { throw new Exception('Failed to store file: ' . $e->getMessage()); From dc745035f73e03faaecf3bc5005e208c5a7ae226 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Sat, 30 Nov 2024 12:21:03 +0100 Subject: [PATCH 12/13] Fix object --- lib/Service/ObjectService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 9854a46..40d4ae7 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -533,9 +533,9 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object // Handle single file type else if ($property['type'] === 'file') { - $object[$propertyName] = $this->handleFileProperty( + $object = $this->handleFileProperty( objectEntity: $objectEntity, - object: [$propertyName => $object[$propertyName]], + object: $object, propertyName: $propertyName ); } From 032715b87e3601d87919220df3afb1afe274ca94 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Sat, 30 Nov 2024 20:43:46 +0100 Subject: [PATCH 13/13] Fix two small bugs --- lib/Service/ObjectService.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 40d4ae7..1302347 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -554,7 +554,8 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object * @return array Updated object data * @throws Exception|GuzzleException When file handling fails */ - private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName): array { + private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName): array + { $fileName = str_replace('.', '_', $propertyName); $objectDot = new Dot($object); @@ -631,6 +632,8 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s } catch (Exception|NotFoundExceptionInterface $e) { throw new Exception('Failed to download file from URL: ' . $e->getMessage()); } + } else if (str_contains($objectDot->get($propertyName), $this->urlGenerator->getBaseUrl()) === true) { + return $object; } else { throw new Exception('Invalid file format - must be base64 encoded or valid URL'); } @@ -658,9 +661,9 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s // Create or find ShareLink $share = $this->fileService->findShare(path: $filePath); if ($share !== null) { - $shareLink = $this->fileService->getShareLink($share); + $shareLink = $this->fileService->getShareLink($share).'/download'; } else { - $shareLink = $this->fileService->createShareLink(path: $filePath); + $shareLink = $this->fileService->createShareLink(path: $filePath).'/download'; } $filesDot = new Dot($objectEntity->getFiles() ?? []); @@ -690,6 +693,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s */ public function getObject(Register $register, Schema $schema, string $uuid, ?array $extend = []): ObjectEntity { + // Handle internal source if ($register->getSource() === 'internal' || $register->getSource() === '') { return $this->objectEntityMapper->findByUuid($register, $schema, $uuid);