diff --git a/composer.json b/composer.json index 3195f732b..37282788e 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ ], "require": { "php": "^8.2", + "ext-dom": "*", "ext-json": "*", "ext-openssl": "*", "ext-redis": "*", @@ -75,7 +76,7 @@ "phpunit/phpunit": ">= 7.5", "psalm/plugin-phpunit": "^0.18.4", "psalm/plugin-symfony": "^4.0.2", - "rector/rector": "^0.17.7", + "rector/rector": "^1.1.0", "symfony/debug-bundle": "^5.1", "symfony/dotenv": "^4.4", "symfony/maker-bundle": "^1.20", diff --git a/phpstan-baseline-2024-05.neon b/phpstan-baseline-2024-05.neon new file mode 100644 index 000000000..728ec4edc --- /dev/null +++ b/phpstan-baseline-2024-05.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$records of method League\\\\Csv\\\\Writer\\:\\:insertAll\\(\\) expects iterable\\\\>, iterable\\\\> given\\.$#" + count: 1 + path: src/Bundle/ChillMainBundle/Controller/UserExportController.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 62dbe0468..4e6745469 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -31,4 +31,5 @@ includes: - phpstan-baseline-level-3.neon - phpstan-baseline-level-4.neon - phpstan-baseline-level-5.neon + - phpstan-baseline-2024-05.neon diff --git a/rector.php b/rector.php index 0dd8e355c..8aa62bdd0 100644 --- a/rector.php +++ b/rector.php @@ -45,9 +45,6 @@ return static function (RectorConfig $rectorConfig): void { // skip some path... $rectorConfig->skip([ - // we need to discuss this: are we going to have FALSE in tests instead of an error ? - \Rector\Php71\Rector\FuncCall\CountOnNullRector::class, - // we must adapt service definition \Rector\Symfony\Symfony28\Rector\MethodCall\GetToConstructorInjectionRector::class, \Rector\Symfony\Symfony34\Rector\Closure\ContainerGetNameToTypeInTestsRector::class, diff --git a/src/Bundle/ChillActivityBundle/Form/ActivityType.php b/src/Bundle/ChillActivityBundle/Form/ActivityType.php index fbcf60bf0..9e2358e8b 100644 --- a/src/Bundle/ChillActivityBundle/Form/ActivityType.php +++ b/src/Bundle/ChillActivityBundle/Form/ActivityType.php @@ -15,11 +15,10 @@ use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\ActivityPresence; use Chill\ActivityBundle\Form\Type\PickActivityReasonType; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; -use Chill\DocStoreBundle\Form\StoredObjectType; +use Chill\DocStoreBundle\Form\CollectionStoredObjectType; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\User; -use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\CommentType; use Chill\MainBundle\Form\Type\PickUserDynamicType; @@ -276,16 +275,9 @@ class ActivityType extends AbstractType } if ($activityType->isVisible('documents')) { - $builder->add('documents', ChillCollectionType::class, [ - 'entry_type' => StoredObjectType::class, + $builder->add('documents', CollectionStoredObjectType::class, [ 'label' => $activityType->getLabel('documents'), 'required' => $activityType->isRequired('documents'), - 'allow_add' => true, - 'allow_delete' => true, - 'button_add_label' => 'activity.Insert a document', - 'button_remove_label' => 'activity.Remove a document', - 'empty_collection_explain' => 'No documents', - 'entry_options' => ['has_title' => true], ]); } diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/edit.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/edit.html.twig index a000b0c7e..d986f9150 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/edit.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/edit.html.twig @@ -92,7 +92,9 @@ {% endif %} {%- if edit_form.documents is defined -%} - {{ form_row(edit_form.documents) }} + {{ form_label(edit_form.documents) }} + {{ form_errors(edit_form.documents) }} + {{ form_widget(edit_form.documents) }}
{% endif %} @@ -127,4 +129,4 @@ {% block css %} {{ encore_entry_link_tags('mod_pickentity_type') }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Tests/Chill/DocStoreBundle/Tests/Security/Guard/DavTokenAuthenticationEventSubscriberTest.php b/src/Bundle/ChillAsideActivityBundle/src/Tests/Chill/DocStoreBundle/Tests/Security/Guard/DavTokenAuthenticationEventSubscriberTest.php new file mode 100644 index 000000000..875e40a65 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Tests/Chill/DocStoreBundle/Tests/Security/Guard/DavTokenAuthenticationEventSubscriberTest.php @@ -0,0 +1,66 @@ + 1, + 'so' => '1234', + 'e' => 1, + ], $token); + + $eventSubscriber->onJWTAuthenticated($event); + + self::assertTrue($token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)); + self::assertTrue($token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)); + self::assertEquals('1234', $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)); + self::assertEquals(StoredObjectRoleEnum::EDIT, $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)); + } + + public function testOnJWTAuthenticatedWithDavNoDataInPayload(): void + { + $eventSubscriber = new DavTokenAuthenticationEventSubscriber(); + $token = new class () extends AbstractToken { + public function getCredentials() + { + return null; + } + }; + $event = new JWTAuthenticatedEvent([], $token); + + $eventSubscriber->onJWTAuthenticated($event); + + self::assertFalse($token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)); + self::assertFalse($token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)); + } +} diff --git a/src/Bundle/ChillCustomFieldsBundle/CustomFields/CustomFieldInterface.php b/src/Bundle/ChillCustomFieldsBundle/CustomFields/CustomFieldInterface.php index ff89c04d2..eb2d3b011 100644 --- a/src/Bundle/ChillCustomFieldsBundle/CustomFields/CustomFieldInterface.php +++ b/src/Bundle/ChillCustomFieldsBundle/CustomFields/CustomFieldInterface.php @@ -49,20 +49,17 @@ interface CustomFieldInterface /** * Return if the value can be considered as empty. - * - * @param mixed $value the value passed throug the deserialize function */ - public function isEmptyValue($value, CustomField $customField); + public function isEmptyValue(mixed $value, CustomField $customField); /** * Return a repsentation of the value of the CustomField. * - * @param mixed $value the raw value, **not deserialized** (= as stored in the db) * @param \Chill\CustomFieldsBundle\CustomField\CustomField $customField * * @return string an html representation of the value */ - public function render($value, CustomField $customField, $documentType = 'html'); + public function render(mixed $value, CustomField $customField, $documentType = 'html'); /** * Transform the value into a format that can be stored in DB. diff --git a/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsChoiceTest.php b/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsChoiceTest.php index 48d4847d6..b9fe45820 100644 --- a/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsChoiceTest.php +++ b/src/Bundle/ChillCustomFieldsBundle/Tests/CustomFields/CustomFieldsChoiceTest.php @@ -399,8 +399,6 @@ final class CustomFieldsChoiceTest extends KernelTestCase /** * @dataProvider emptyDataProvider - * - * @param mixed $data deserialized data */ public function testIsEmptyValueEmpty(mixed $data) { diff --git a/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php b/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php new file mode 100644 index 000000000..70aecbe1e --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php @@ -0,0 +1,252 @@ +requestAnalyzer = new PropfindRequestAnalyzer(); + } + + /** + * @Route("/dav/{access_token}/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get") + */ + public function getDirectory(StoredObject $storedObject, string $access_token): Response + { + if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { + throw new AccessDeniedHttpException(); + } + + return new DavResponse( + $this->engine->render('@ChillDocStore/Webdav/directory.html.twig', [ + 'stored_object' => $storedObject, + 'access_token' => $access_token, + ]) + ); + } + + /** + * @Route("/dav/{access_token}/get/{uuid}/", methods={"OPTIONS"}) + */ + public function optionsDirectory(StoredObject $storedObject): Response + { + if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { + throw new AccessDeniedHttpException(); + } + + $response = (new DavResponse('')) + ->setEtag($this->storedObjectManager->etag($storedObject)) + ; + + // $response->headers->add(['Allow' => 'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT,PROPPATCH,COPY,MOVE,REPORT,PATCH,POST,TRACE']); + $response->headers->add(['Allow' => 'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT']); + + return $response; + } + + /** + * @Route("/dav/{access_token}/get/{uuid}/", methods={"PROPFIND"}) + */ + public function propfindDirectory(StoredObject $storedObject, string $access_token, Request $request): Response + { + if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { + throw new AccessDeniedHttpException(); + } + + $depth = $request->headers->get('depth'); + + if ('0' !== $depth && '1' !== $depth) { + throw new BadRequestHttpException('only 1 and 0 are accepted for Depth header'); + } + + [$properties, $lastModified, $etag, $length] = $this->parseDavRequest($request->getContent(), $storedObject); + + $response = new DavResponse( + $this->engine->render('@ChillDocStore/Webdav/directory_propfind.xml.twig', [ + 'stored_object' => $storedObject, + 'properties' => $properties, + 'last_modified' => $lastModified, + 'etag' => $etag, + 'content_length' => $length, + 'depth' => (int) $depth, + 'access_token' => $access_token, + ]), + 207 + ); + + $response->headers->add([ + 'Content-Type' => 'text/xml', + ]); + + return $response; + } + + /** + * @Route("/dav/{access_token}/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"}) + */ + public function getDocument(StoredObject $storedObject): Response + { + if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { + throw new AccessDeniedHttpException(); + } + + return (new DavResponse($this->storedObjectManager->read($storedObject))) + ->setEtag($this->storedObjectManager->etag($storedObject)); + } + + /** + * @Route("/dav/{access_token}/get/{uuid}/d", methods={"HEAD"}) + */ + public function headDocument(StoredObject $storedObject): Response + { + if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { + throw new AccessDeniedHttpException(); + } + + $response = new DavResponse(''); + + $response->headers->add( + [ + 'Content-Length' => $this->storedObjectManager->getContentLength($storedObject), + 'Content-Type' => $storedObject->getType(), + 'Etag' => $this->storedObjectManager->etag($storedObject), + ] + ); + + return $response; + } + + /** + * @Route("/dav/{access_token}/get/{uuid}/d", methods={"OPTIONS"}) + */ + public function optionsDocument(StoredObject $storedObject): Response + { + if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { + throw new AccessDeniedHttpException(); + } + + $response = (new DavResponse('')) + ->setEtag($this->storedObjectManager->etag($storedObject)) + ; + + $response->headers->add(['Allow' => 'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT']); + + return $response; + } + + /** + * @Route("/dav/{access_token}/get/{uuid}/d", methods={"PROPFIND"}) + */ + public function propfindDocument(StoredObject $storedObject, string $access_token, Request $request): Response + { + if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { + throw new AccessDeniedHttpException(); + } + + [$properties, $lastModified, $etag, $length] = $this->parseDavRequest($request->getContent(), $storedObject); + + $response = new DavResponse( + $this->engine->render( + '@ChillDocStore/Webdav/doc_props.xml.twig', + [ + 'stored_object' => $storedObject, + 'properties' => $properties, + 'etag' => $etag, + 'last_modified' => $lastModified, + 'content_length' => $length, + 'access_token' => $access_token, + ] + ), + 207 + ); + + $response + ->headers->add([ + 'Content-Type' => 'text/xml', + ]); + + return $response; + } + + /** + * @Route("/dav/{access_token}/get/{uuid}/d", methods={"PUT"}) + */ + public function putDocument(StoredObject $storedObject, Request $request): Response + { + if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) { + throw new AccessDeniedHttpException(); + } + + $this->storedObjectManager->write($storedObject, $request->getContent()); + + return new DavResponse('', Response::HTTP_NO_CONTENT); + } + + /** + * @return array{0: array, 1: \DateTimeInterface, 2: string, 3: int} properties, lastModified, etag, length + */ + private function parseDavRequest(string $content, StoredObject $storedObject): array + { + $xml = new \DOMDocument(); + $xml->loadXML($content); + + $properties = $this->requestAnalyzer->getRequestedProperties($xml); + $requested = array_keys(array_filter($properties, fn ($item) => true === $item)); + + if ( + in_array('lastModified', $requested, true) + || in_array('etag', $requested, true) + ) { + $lastModified = $this->storedObjectManager->getLastModified($storedObject); + $etag = $this->storedObjectManager->etag($storedObject); + } + if (in_array('contentLength', $requested, true)) { + $length = $this->storedObjectManager->getContentLength($storedObject); + } + + return [ + $properties, + $lastModified ?? null, + $etag ?? null, + $length ?? null, + ]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Dav/Exception/ParseRequestException.php b/src/Bundle/ChillDocStoreBundle/Dav/Exception/ParseRequestException.php new file mode 100644 index 000000000..70fff1866 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Dav/Exception/ParseRequestException.php @@ -0,0 +1,16 @@ +} + */ +class PropfindRequestAnalyzer +{ + private const KNOWN_PROPS = [ + 'resourceType', + 'contentType', + 'lastModified', + 'creationDate', + 'contentLength', + 'etag', + 'supportedLock', + ]; + + /** + * @return davProperties + */ + public function getRequestedProperties(\DOMDocument $request): array + { + $propfinds = $request->getElementsByTagNameNS('DAV:', 'propfind'); + + if (0 === $propfinds->count()) { + throw new ParseRequestException('any propfind element found'); + } + + if (1 < $propfinds->count()) { + throw new ParseRequestException('too much propfind element found'); + } + + $propfind = $propfinds->item(0); + + if (0 === $propfind->childNodes->count()) { + throw new ParseRequestException('no element under propfind'); + } + + $unknows = []; + $props = []; + + foreach ($propfind->childNodes->getIterator() as $prop) { + /** @var \DOMNode $prop */ + if (XML_ELEMENT_NODE !== $prop->nodeType) { + continue; + } + + if ('propname' === $prop->nodeName) { + return $this->baseProps(true); + } + + foreach ($prop->childNodes->getIterator() as $getProp) { + if (XML_ELEMENT_NODE !== $getProp->nodeType) { + continue; + } + + if ('DAV:' !== $getProp->lookupNamespaceURI(null)) { + $unknows[] = ['xmlns' => $getProp->lookupNamespaceURI(null), 'prop' => $getProp->nodeName]; + continue; + } + + $props[] = match ($getProp->nodeName) { + 'resourcetype' => 'resourceType', + 'getcontenttype' => 'contentType', + 'getlastmodified' => 'lastModified', + default => '', + }; + } + } + + $props = array_filter(array_values($props), fn (string $item) => '' !== $item); + + return [...$this->baseProps(false), ...array_combine($props, array_fill(0, count($props), true)), 'unknowns' => $unknows]; + } + + /** + * @return davProperties + */ + private function baseProps(bool $default = false): array + { + return + [ + ...array_combine( + self::KNOWN_PROPS, + array_fill(0, count(self::KNOWN_PROPS), $default) + ), + 'unknowns' => [], + ]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php b/src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php new file mode 100644 index 000000000..32332d20a --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php @@ -0,0 +1,24 @@ +headers->add(['DAV' => '1']); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php index 4a16d33f4..aae003e09 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php @@ -48,14 +48,14 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa /** * @ORM\Column(type="json", name="datas") * - * @Serializer\Groups({"read", "write"}) + * @Serializer\Groups({"write"}) */ private array $datas = []; /** * @ORM\Column(type="text") * - * @Serializer\Groups({"read", "write"}) + * @Serializer\Groups({"write"}) */ private string $filename = ''; @@ -66,7 +66,7 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa * * @ORM\Column(type="integer") * - * @Serializer\Groups({"read", "write"}) + * @Serializer\Groups({"write"}) */ private ?int $id = null; @@ -75,35 +75,35 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa * * @ORM\Column(type="json", name="iv") * - * @Serializer\Groups({"read", "write"}) + * @Serializer\Groups({"write"}) */ private array $iv = []; /** * @ORM\Column(type="json", name="key") * - * @Serializer\Groups({"read", "write"}) + * @Serializer\Groups({"write"}) */ private array $keyInfos = []; /** * @ORM\Column(type="text", name="title") * - * @Serializer\Groups({"read", "write"}) + * @Serializer\Groups({"write"}) */ private string $title = ''; /** * @ORM\Column(type="text", name="type", options={"default": ""}) * - * @Serializer\Groups({"read", "write"}) + * @Serializer\Groups({"write"}) */ private string $type = ''; /** * @ORM\Column(type="uuid", unique=true) * - * @Serializer\Groups({"read", "write"}) + * @Serializer\Groups({"write"}) */ private UuidInterface $uuid; @@ -137,8 +137,6 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa */ public function __construct(/** * @ORM\Column(type="text", options={"default": "ready"}) - * - * @Serializer\Groups({"read"}) */ private string $status = 'ready' ) { diff --git a/src/Bundle/ChillDocStoreBundle/Form/AccompanyingCourseDocumentType.php b/src/Bundle/ChillDocStoreBundle/Form/AccompanyingCourseDocumentType.php index 0fced39d3..331e3cb98 100644 --- a/src/Bundle/ChillDocStoreBundle/Form/AccompanyingCourseDocumentType.php +++ b/src/Bundle/ChillDocStoreBundle/Form/AccompanyingCourseDocumentType.php @@ -14,47 +14,21 @@ namespace Chill\DocStoreBundle\Form; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\Document; use Chill\DocStoreBundle\Entity\DocumentCategory; -use Chill\MainBundle\Entity\User; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillTextareaType; -use Chill\MainBundle\Security\Authorization\AuthorizationHelper; -use Chill\MainBundle\Templating\TranslatableStringHelper; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectManager; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -class AccompanyingCourseDocumentType extends AbstractType +final class AccompanyingCourseDocumentType extends AbstractType { - /** - * @var AuthorizationHelper - */ - protected $authorizationHelper; - - /** - * @var ObjectManager - */ - protected $om; - - /** - * @var TranslatableStringHelper - */ - protected $translatableStringHelper; - - /** - * the user running this form. - * - * @var User - */ - protected $user; - public function __construct( - TranslatableStringHelper $translatableStringHelper + private readonly TranslatableStringHelperInterface $translatableStringHelper ) { - $this->translatableStringHelper = $translatableStringHelper; } public function buildForm(FormBuilderInterface $builder, array $options) diff --git a/src/Bundle/ChillDocStoreBundle/Form/CollectionStoredObjectType.php b/src/Bundle/ChillDocStoreBundle/Form/CollectionStoredObjectType.php new file mode 100644 index 000000000..fd14897bf --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Form/CollectionStoredObjectType.php @@ -0,0 +1,37 @@ +setDefault('entry_type', StoredObjectType::class) + ->setDefault('allow_add', true) + ->setDefault('allow_delete', true) + ->setDefault('button_add_label', 'stored_object.Insert a document') + ->setDefault('button_remove_label', 'stored_object.Remove a document') + ->setDefault('empty_collection_explain', 'No documents') + ->setDefault('entry_options', ['has_title' => true]) + ->setDefault('js_caller', 'data-collection-stored-object'); + } + + public function getParent() + { + return ChillCollectionType::class; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php b/src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php new file mode 100644 index 000000000..3ec52134f --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Form/DataMapper/StoredObjectDataMapper.php @@ -0,0 +1,75 @@ +setData($viewData->getTitle()); + } + $forms['stored_object']->setData($viewData); + } + + /** + * @param FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances + */ + public function mapFormsToData($forms, &$viewData) + { + $forms = iterator_to_array($forms); + + if (!(null === $viewData || $viewData instanceof StoredObject)) { + throw new Exception\UnexpectedTypeException($viewData, StoredObject::class); + } + + if (null === $forms['stored_object']->getData()) { + return; + } + + /** @var StoredObject $viewData */ + if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) { + // we do not want to erase the previous object + $viewData = new StoredObject(); + } + + $viewData->setFilename($forms['stored_object']->getData()['filename']); + $viewData->setIv($forms['stored_object']->getData()['iv']); + $viewData->setKeyInfos($forms['stored_object']->getData()['keyInfos']); + $viewData->setType($forms['stored_object']->getData()['type']); + + 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 new file mode 100644 index 000000000..520b644b3 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Form/DataTransformer/StoredObjectDataTransformer.php @@ -0,0 +1,52 @@ +serializer->serialize($value, 'json', [ + 'groups' => [ + StoredObjectNormalizer::ADD_DAV_EDIT_LINK_CONTEXT, + ], + ]); + } + + throw new UnexpectedTypeException($value, StoredObject::class); + } + + public function reverseTransform(mixed $value): mixed + { + if ('' === $value || null === $value) { + return null; + } + + return json_decode((string) $value, true, 10, JSON_THROW_ON_ERROR); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Form/StoredObjectType.php b/src/Bundle/ChillDocStoreBundle/Form/StoredObjectType.php index 77484208f..5badc458d 100644 --- a/src/Bundle/ChillDocStoreBundle/Form/StoredObjectType.php +++ b/src/Bundle/ChillDocStoreBundle/Form/StoredObjectType.php @@ -11,11 +11,10 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Form; -use ChampsLibres\AsyncUploaderBundle\Form\Type\AsyncUploaderType; use Chill\DocStoreBundle\Entity\StoredObject; -use Doctrine\ORM\EntityManagerInterface; +use Chill\DocStoreBundle\Form\DataMapper\StoredObjectDataMapper; +use Chill\DocStoreBundle\Form\DataTransformer\StoredObjectDataTransformer; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -24,16 +23,12 @@ use Symfony\Component\OptionsResolver\OptionsResolver; /** * Form type which allow to join a document. */ -class StoredObjectType extends AbstractType +final class StoredObjectType extends AbstractType { - /** - * @var EntityManagerInterface - */ - protected $em; - - public function __construct(EntityManagerInterface $em) - { - $this->em = $em; + public function __construct( + private readonly StoredObjectDataTransformer $storedObjectDataTransformer, + private readonly StoredObjectDataMapper $storedObjectDataMapper, + ) { } public function buildForm(FormBuilderInterface $builder, array $options) @@ -45,30 +40,9 @@ class StoredObjectType extends AbstractType ]); } - $builder - ->add('filename', AsyncUploaderType::class) - ->add('type', HiddenType::class) - ->add('keyInfos', HiddenType::class) - ->add('iv', HiddenType::class); - - $builder - ->get('keyInfos') - ->addModelTransformer(new CallbackTransformer( - $this->transform(...), - $this->reverseTransform(...) - )); - $builder - ->get('iv') - ->addModelTransformer(new CallbackTransformer( - $this->transform(...), - $this->reverseTransform(...) - )); - - $builder - ->addModelTransformer(new CallbackTransformer( - $this->transformObject(...), - $this->reverseTransformObject(...) - )); + $builder->add('stored_object', HiddenType::class); + $builder->get('stored_object')->addModelTransformer($this->storedObjectDataTransformer); + $builder->setDataMapper($this->storedObjectDataMapper); } public function configureOptions(OptionsResolver $resolver) @@ -80,43 +54,4 @@ class StoredObjectType extends AbstractType ->setDefault('has_title', false) ->setAllowedTypes('has_title', ['bool']); } - - public function reverseTransform($value) - { - if (null === $value) { - return null; - } - - return \json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR); - } - - public function reverseTransformObject($object) - { - if (null === $object) { - return null; - } - - if (null === $object->getFilename()) { - // remove the original object - $this->em->remove($object); - - return null; - } - - return $object; - } - - public function transform($object) - { - if (null === $object) { - return null; - } - - return \json_encode($object, JSON_THROW_ON_ERROR); - } - - public function transformObject($object = null) - { - return $object; - } } diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.js b/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index-old.js similarity index 100% rename from src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.js rename to src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index-old.js diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts new file mode 100644 index 000000000..b7df11323 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts @@ -0,0 +1,86 @@ +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 {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n"; +const i18n = _createI18n({}); + +const startApp = (divElement: HTMLDivElement, collectionEntry: null|HTMLLIElement): void => { + console.log('app started', divElement); + const input_stored_object: HTMLInputElement|null = divElement.querySelector("input[data-stored-object]"); + if (null === input_stored_object) { + throw new Error('input to stored object not found'); + } + + let existingDoc: StoredObject|null = null; + if (input_stored_object.value !== "") { + existingDoc = JSON.parse(input_stored_object.value); + } + const app_container = document.createElement("div"); + divElement.appendChild(app_container); + + const app = createApp({ + template: '', + data(vm) { + return { + existingDoc: existingDoc, + } + }, + components: { + DropFileWidget, + }, + methods: { + addDocument: function(object: StoredObjectCreated): void { + console.log('object added', object); + this.$data.existingDoc = object; + input_stored_object.value = JSON.stringify(object); + }, + removeDocument: function(object: StoredObject): void { + console.log('catch remove document', object); + input_stored_object.value = ""; + this.$data.existingDoc = null; + console.log('collectionEntry', collectionEntry); + + if (null !== collectionEntry) { + console.log('will remove collection'); + collectionEntry.remove(); + } + } + } + }); + + app.use(i18n).mount(app_container); +} +window.addEventListener('collection-add-entry', ((e: CustomEvent) => { + const detail = e.detail; + const divElement: null|HTMLDivElement = detail.entry.querySelector('div[data-stored-object]'); + + if (null === divElement) { + throw new Error('div[data-stored-object] not found'); + } + + startApp(divElement, detail.entry); +}) as EventListener); + +window.addEventListener('DOMContentLoaded', () => { + const upload_inputs: NodeListOf = document.querySelectorAll('div[data-stored-object]'); + + upload_inputs.forEach((input: HTMLDivElement): void => { + // test for a parent to check if this is a collection entry + let collectionEntry: null|HTMLLIElement = null; + let parent = input.parentElement; + console.log('parent', parent); + if (null !== parent) { + let grandParent = parent.parentElement; + console.log('grandParent', grandParent); + if (null !== grandParent) { + if (grandParent.tagName.toLowerCase() === 'li' && grandParent.classList.contains('entry')) { + collectionEntry = grandParent as HTMLLIElement; + } + } + } + startApp(input, collectionEntry); + }) +}); + +export {} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts index 4180808dd..77eb8c2c9 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts @@ -17,18 +17,22 @@ window.addEventListener('DOMContentLoaded', function (e) { canEdit: string, storedObject: string, buttonSmall: string, + davLink: string, + davLinkExpiration: string, }; const storedObject = JSON.parse(datasets.storedObject) as StoredObject, filename = datasets.filename, canEdit = datasets.canEdit === '1', - small = datasets.buttonSmall === '1' + small = datasets.buttonSmall === '1', + davLink = 'davLink' in datasets && datasets.davLink !== '' ? datasets.davLink : null, + davLinkExpiration = 'davLinkExpiration' in datasets ? Number.parseInt(datasets.davLinkExpiration) : null ; - return { storedObject, filename, canEdit, small }; + return { storedObject, filename, canEdit, small, davLink, davLinkExpiration }; }, - template: '', + template: '', methods: { onStoredObjectStatusChange: function(newStatus: StoredObjectStatusChange): void { this.$data.storedObject.status = newStatus.status; diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts index 825055973..25d956312 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts @@ -17,6 +17,20 @@ export interface StoredObject { type: string, uuid: string, status: StoredObjectStatus, + _links?: { + dav_link?: { + href: string + expiration: number + }, + } +} + +export interface StoredObjectCreated { + status: "stored_object_created", + filename: string, + iv: Uint8Array, + keyInfos: object, + type: string, } export interface StoredObjectStatusChange { @@ -33,3 +47,18 @@ export type WopiEditButtonExecutableBeforeLeaveFunction = { (): Promise } +/** + * Object containing information for performering a POST request to a swift object store + */ +export interface PostStoreObjectSignature { + method: "POST", + max_file_size: number, + max_file_count: 1, + expires: number, + submit_delay: 180, + redirect: string, + prefix: string, + url: string, + signature: string, +} + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue index 88587e90f..192cdd271 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue @@ -1,5 +1,5 @@