From 3d49c959e00335c3628c75e2e64078f569345fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 2 Sep 2024 16:24:23 +0200 Subject: [PATCH] Update DropFile to handle object versioning --- .gitlab-ci.yml | 2 +- .../TempUrlOpenstackGenerator.php | 8 +- .../Controller/AsyncUploadController.php | 10 +- .../Entity/StoredObject.php | 21 +-- .../Entity/StoredObjectVersion.php | 2 +- .../DataMapper/StoredObjectDataMapper.php | 11 +- .../StoredObjectDataTransformer.php | 4 +- .../Repository/StoredObjectRepository.php | 5 + .../StoredObjectRepositoryInterface.php | 2 + .../helper.ts => js/async-upload/uploader.ts} | 18 +- .../public/module/async_upload/index.ts | 14 +- .../Resources/public/types.ts | 25 ++- .../vuejs/DocumentActionButtonsGroup.vue | 55 ++++-- .../public/vuejs/DocumentSignature/App.vue | 8 +- .../public/vuejs/DropFileWidget/DropFile.vue | 52 +++--- .../vuejs/DropFileWidget/DropFileModal.vue | 69 +++++++ .../vuejs/DropFileWidget/DropFileWidget.vue | 14 +- .../StoredObjectButton/ConvertButton.vue | 4 +- .../StoredObjectButton/DesktopEditButton.vue | 3 + .../StoredObjectButton/DownloadButton.vue | 28 +-- .../StoredObjectButton/WopiEditButton.vue | 5 +- .../vuejs/StoredObjectButton/helpers.ts | 53 ++++-- .../vuejs/_components/AddAsyncUpload.vue | 174 ------------------ .../_components/AddAsyncUploadDownloader.vue | 45 ----- .../Normalizer/StoredObjectDenormalizer.php | 60 ++++-- .../Normalizer/StoredObjectNormalizer.php | 2 +- .../StoredObjectVersionNormalizer.php | 45 +++++ .../Service/StoredObjectManager.php | 35 +++- .../Service/StoredObjectManagerInterface.php | 6 + .../TempUrlOpenstackGeneratorTest.php | 80 +++++++- .../OpenstackObjectStore/file-to-upload.txt | 1 + .../Tests/Controller/WebdavControllerTest.php | 13 +- .../Tests/Entity/StoredObjectTest.php | 39 +--- .../Tests/Form/StoredObjectTypeTest.php | 62 ++++++- .../StoredObjectDenormalizerTest.php | 144 +++++++++++++++ .../StoredObjectVersionNormalizerTest.php | 27 ++- .../Constraints/AsyncFileExistsValidator.php | 63 ++----- .../WorkflowAddSignatureController.php | 64 +++++++ .../Controller/WorkflowController.php | 37 ---- .../components/FormEvaluation.vue | 41 ++--- .../vuejs/AccompanyingCourseWorkEdit/store.js | 14 +- tsconfig.json | 31 ++++ 42 files changed, 857 insertions(+), 539 deletions(-) rename src/Bundle/ChillDocStoreBundle/Resources/public/{vuejs/_components/helper.ts => js/async-upload/uploader.ts} (75%) create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DropFileWidget/DropFileModal.vue delete mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/_components/AddAsyncUpload.vue delete mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/_components/AddAsyncUploadDownloader.vue create mode 100644 src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectVersionNormalizer.php create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/file-to-upload.txt create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectDenormalizerTest.php create mode 100644 src/Bundle/ChillMainBundle/Controller/WorkflowAddSignatureController.php create mode 100644 tsconfig.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bb5c8dd5d..e493e6556 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -122,7 +122,7 @@ unit_tests: - php tests/console chill:db:sync-views --env=test - php -d memory_limit=2G tests/console cache:clear --env=test - php -d memory_limit=3G tests/console doctrine:fixtures:load -n --env=test - - php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive + - php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive --exclude-group openstack-integration artifacts: expire_in: 1 day paths: diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php index 3ba1f0dea..19a52f71d 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php @@ -182,21 +182,19 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf return \hash_hmac('sha512', $body, $this->key, false); } - private function generateSignature($method, $url, \DateTimeImmutable $expires) + private function generateSignature(string $method, $url, \DateTimeImmutable $expires) { if ('POST' === $method) { return $this->generateSignaturePost($url, $expires); } $path = \parse_url((string) $url, PHP_URL_PATH); - $body = sprintf( "%s\n%s\n%s", - $method, + strtoupper($method), $expires->format('U'), $path - ) - ; + ); $this->logger->debug( 'generate signature GET', diff --git a/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php b/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php index 52e0882a1..cdbb94d29 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/AsyncUploadController.php @@ -15,6 +15,7 @@ use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObjectVersion; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; +use Chill\MainBundle\Entity\User; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -99,8 +100,15 @@ final readonly class AsyncUploadController $request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null ); + $user = $this->security->getUser(); + $userId = match ($user instanceof User) { + true => $user->getId(), + false => $user->getUserIdentifier(), + }; + $this->chillLogger->notice('[Privacy Event] a request to see a document has been granted', [ - 'doc_uuid' => $storedObject->getUuid(), + 'doc_uuid' => $storedObject->getUuid()->toString(), + 'user_id' => $userId, ]); return new JsonResponse( diff --git a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php index 6ed52716d..dabcec8eb 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php @@ -23,6 +23,7 @@ use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Random\RandomException; use Symfony\Component\Serializer\Annotation as Serializer; +use Symfony\Component\Validator\Constraints as Assert; /** * Represent a document stored in an object store. @@ -37,7 +38,6 @@ use Symfony\Component\Serializer\Annotation as Serializer; */ #[ORM\Entity] #[ORM\Table('stored_object', schema: 'chill_doc')] -#[AsyncFileExists(message: 'The file is not stored properly')] class StoredObject implements Document, TrackCreationInterface { use TrackCreationTrait; @@ -91,7 +91,7 @@ class StoredObject implements Document, TrackCreationInterface /** * @var Collection */ - #[ORM\OneToMany(targetEntity: StoredObjectVersion::class, cascade: ['persist'], mappedBy: 'storedObject', orphanRemoval: true)] + #[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)] private Collection $versions; /** @@ -127,6 +127,8 @@ class StoredObject implements Document, TrackCreationInterface return \DateTime::createFromImmutable($this->createdAt); } + #[AsyncFileExists(message: 'The file is not stored properly')] + #[Assert\NotNull(message: 'The store object version must be present')] public function getCurrentVersion(): ?StoredObjectVersion { $maxVersion = null; @@ -350,20 +352,7 @@ class StoredObject implements Document, TrackCreationInterface /** * @deprecated */ - public function saveHistory(): void - { - if ('' === $this->getFilename()) { - return; - } - - $this->datas['history'][] = [ - 'filename' => $this->getFilename(), - 'iv' => $this->getIv(), - 'key_infos' => $this->getKeyInfos(), - 'type' => $this->getType(), - 'before' => (new \DateTimeImmutable('now'))->getTimestamp(), - ]; - } + public function saveHistory(): void {} public static function generatePrefix(): string { diff --git a/src/Bundle/ChillDocStoreBundle/Entity/StoredObjectVersion.php b/src/Bundle/ChillDocStoreBundle/Entity/StoredObjectVersion.php index 84b10688f..8526cd9a9 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/StoredObjectVersion.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/StoredObjectVersion.php @@ -86,7 +86,7 @@ class StoredObjectVersion implements TrackCreationInterface { try { $suffix = base_convert(bin2hex(random_bytes(8)), 16, 36); - } catch (RandomException $e) { + } catch (RandomException) { $suffix = uniqid(more_entropy: true); } diff --git a/src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php b/src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php index 37d28c7a4..264f52365 100644 --- a/src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php +++ b/src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php @@ -55,15 +55,8 @@ class StoredObjectDataMapper implements DataMapperInterface return; } - /** @var StoredObject $viewData */ - if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) { - $viewData->registerVersion( - $forms['stored_object']->getData()['iv'], - $forms['stored_object']->getData()['keyInfos'], - $forms['stored_object']->getData()['type'], - $forms['stored_object']->getData()['filename'], - ); - } + /* @var StoredObject $viewData */ + $viewData = $forms['stored_object']->getData(); if (array_key_exists('title', $forms)) { $viewData->setTitle($forms['title']->getData()); diff --git a/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php b/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php index b5c7c3930..8df3e3eb9 100644 --- a/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php +++ b/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php @@ -19,7 +19,7 @@ use Symfony\Component\Serializer\SerializerInterface; class StoredObjectDataTransformer implements DataTransformerInterface { public function __construct( - private readonly SerializerInterface $serializer + private readonly SerializerInterface $serializer, ) {} public function transform(mixed $value): mixed @@ -41,6 +41,6 @@ class StoredObjectDataTransformer implements DataTransformerInterface return null; } - return json_decode((string) $value, true, 10, JSON_THROW_ON_ERROR); + return $this->serializer->deserialize($value, StoredObject::class, 'json'); } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php index 2f39f2021..d48792bc9 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php @@ -64,6 +64,11 @@ final readonly class StoredObjectRepository implements StoredObjectRepositoryInt return $qb->getQuery()->toIterable(hydrationMode: Query::HYDRATE_OBJECT); } + public function findOneByUUID(string $uuid): ?StoredObject + { + return $this->repository->findOneBy(['uuid' => $uuid]); + } + public function getClassName(): string { return StoredObject::class; diff --git a/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php index 45dcfcf94..877847677 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php @@ -23,4 +23,6 @@ interface StoredObjectRepositoryInterface extends ObjectRepository * @return iterable */ public function findByExpired(\DateTimeImmutable $expiredAtDate): iterable; + + public function findOneByUUID(string $uuid): ?StoredObject; } diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/_components/helper.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/js/async-upload/uploader.ts similarity index 75% rename from src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/_components/helper.ts rename to src/Bundle/ChillDocStoreBundle/Resources/public/js/async-upload/uploader.ts index 6d07e769d..592bd8111 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/_components/helper.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/js/async-upload/uploader.ts @@ -1,5 +1,5 @@ import {makeFetch} from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; -import {PostStoreObjectSignature} from "../../types"; +import {PostStoreObjectSignature, StoredObject} from "../../types"; const algo = 'AES-CBC'; @@ -21,11 +21,22 @@ const createFilename = (): string => { return text; }; -export const uploadFile = async (uploadFile: ArrayBuffer): Promise => { +/** + * Fetches a new stored object from the server. + * + * @async + * @function fetchNewStoredObject + * @returns {Promise} A Promise that resolves to the newly created StoredObject. + */ +export const fetchNewStoredObject = async (): Promise => { + return makeFetch("POST", '/api/1.0/doc-store/stored-object/create', null); +} + +export const uploadVersion = async (uploadFile: ArrayBuffer, storedObject: StoredObject): Promise => { const params = new URLSearchParams(); params.append('expires_delay', "180"); params.append('submit_delay', "180"); - const asyncData: PostStoreObjectSignature = await makeFetch("GET", URL_POST + "?" + params.toString()); + const asyncData: PostStoreObjectSignature = await makeFetch("GET", `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` + "?" + params.toString()); const suffix = createFilename(); const filename = asyncData.prefix + suffix; const formData = new FormData(); @@ -50,7 +61,6 @@ export const uploadFile = async (uploadFile: ArrayBuffer): Promise => { } export const encryptFile = async (originalFile: ArrayBuffer): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => { - console.log('encrypt', originalFile); const iv = crypto.getRandomValues(new Uint8Array(16)); const key = await window.crypto.subtle.generateKey(keyDefinition, true, [ "encrypt", "decrypt" ]); const exportedKey = await window.crypto.subtle.exportKey('jwk', key); diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts index b7df11323..50849d635 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts @@ -1,7 +1,7 @@ import {CollectionEventPayload} from "../../../../../ChillMainBundle/Resources/public/module/collection"; import {createApp} from "vue"; import DropFileWidget from "../../vuejs/DropFileWidget/DropFileWidget.vue" -import {StoredObject, StoredObjectCreated} from "../../types"; +import {StoredObject, StoredObjectVersion} from "../../types"; import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n"; const i18n = _createI18n({}); @@ -30,15 +30,17 @@ const startApp = (divElement: HTMLDivElement, collectionEntry: null|HTMLLIElemen DropFileWidget, }, methods: { - addDocument: function(object: StoredObjectCreated): void { - console.log('object added', object); - this.$data.existingDoc = object; - input_stored_object.value = JSON.stringify(object); + addDocument: function({stored_object, stored_object_version}: {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void { + console.log('object added', stored_object); + console.log('version added', stored_object_version); + this.$data.existingDoc = stored_object; + this.$data.existingDoc.currentVersion = stored_object_version; + input_stored_object.value = JSON.stringify(this.$data.existingDoc); }, removeDocument: function(object: StoredObject): void { console.log('catch remove document', object); input_stored_object.value = ""; - this.$data.existingDoc = null; + this.$data.existingDoc = undefined; console.log('collectionEntry', collectionEntry); if (null !== collectionEntry) { diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts index 235b375ce..840421991 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts @@ -4,11 +4,11 @@ export type StoredObjectStatus = "empty"|"ready"|"failure"|"pending"; export interface StoredObject { id: number, - title: string, + title: string|null, uuid: string, prefix: string, status: StoredObjectStatus, - currentVersion: null|StoredObjectVersion, + currentVersion: null|StoredObjectVersionCreated|StoredObjectVersionPersisted, totalVersions: number, datas: object, /** @deprecated */ @@ -32,21 +32,20 @@ export interface StoredObjectVersion { * filename of the object in the object storage */ filename: string, - version: number, - id: number, iv: number[], - keyInfos: object, + keyInfos: JsonWebKey, type: string, - createdAt: DateTime|null, - createdBy: User|null, } -export interface StoredObjectCreated { - status: "stored_object_created", - filename: string, - iv: Uint8Array, - keyInfos: object, - type: string, +export interface StoredObjectVersionCreated extends StoredObjectVersion { + persisted: false, +} + +export interface StoredObjectVersionPersisted extends StoredObjectVersionCreated { + version: number, + id: number, + createdAt: DateTime|null, + createdBy: User|null, } export interface StoredObjectStatusChange { diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue index b4c53eacd..66546f2a5 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue @@ -1,20 +1,20 @@