diff --git a/src/Bundle/ChillDocStore/.gitignore b/src/Bundle/ChillDocStore/.gitignore new file mode 100644 index 000000000..57872d0f1 --- /dev/null +++ b/src/Bundle/ChillDocStore/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/src/Bundle/ChillDocStore/.gitlab-ci.yml b/src/Bundle/ChillDocStore/.gitlab-ci.yml new file mode 100644 index 000000000..e9826c3fb --- /dev/null +++ b/src/Bundle/ChillDocStore/.gitlab-ci.yml @@ -0,0 +1,25 @@ +.test_definition: &test_definition + services: + - chill/database:latest + before_script: + - composer config github-oauth.github.com $GITHUB_TOKEN + - composer install + - cp Resources/test/Fixtures/App/app/config/parameters.gitlab-ci.yml Resources/test/Fixtures/App/app/config/parameters.yml + - php Resources/test/Fixtures/App/app/console --env=test cache:warmup + - php Resources/test/Fixtures/App/app/console doctrine:migrations:migrate --env=test --no-interaction + - php Resources/test/Fixtures/App/app/console doctrine:fixtures:load --env=test --no-interaction + + +stages: + - deploy + +deploy-packagist: + stage: deploy + image: chill/ci-image:php-7.2 + before_script: + # test that PACKAGIST USERNAME and PACKAGIST_TOKEN variable are set + - if [ -z ${PACKAGIST_USERNAME+x} ]; then echo "Please set PACKAGIST_USERNAME variable"; exit -1; fi + - if [ -z ${PACKAGIST_TOKEN+x} ]; then echo "Please set PACKAGIST_TOKEN variable"; exit -1; fi + script: + - STATUSCODE=$(curl -XPOST -H'content-type:application/json' "https://packagist.org/api/update-package?username=$PACKAGIST_USERNAME&apiToken=$PACKAGIST_TOKEN" -d"{\"repository\":{\"url\":\"$CI_PROJECT_URL.git\"}}" --silent --output /dev/stderr --write-out "%{http_code}") + - if [ $STATUSCODE = "202" ]; then exit 0; else exit $STATUSCODE; fi diff --git a/src/Bundle/ChillDocStore/CHANGELOG.md b/src/Bundle/ChillDocStore/CHANGELOG.md new file mode 100644 index 000000000..1d9c05143 --- /dev/null +++ b/src/Bundle/ChillDocStore/CHANGELOG.md @@ -0,0 +1,39 @@ + +Version 1.5.1 +============= + +- adding .gitlab-ci to upgrade automatically packagist +- adding fixtures for ACL and DocumentCategory + +Version 1.5.2 +============= + +- fix some missing translations on update / create document and "any document" in list +- use dropzone to upload a document with a better UI + +You must add `"dropzone": "^5.5.1"` to your dependencies in `packages.json` at the root project. + +Version 1.5.3 +============= + +- the javascript for uploading a file now works within collections, listening to collection events. + +Version 1.5.4 +============= + +- replace default message on download button below dropzone ; +- launch event when dropzone is initialized, to allow to customize events on dropzone; +- add privacy events to document index / show +- add privacy events to document edit / update +- remove dump message + +Version 1.5.5 +============= + +- add button to remove existing document in form, and improve UI in this part +- fix error when document is removed in form + +Master branch +============= + +- fix capitalization of person document pages diff --git a/src/Bundle/ChillDocStore/ChillDocStoreBundle.php b/src/Bundle/ChillDocStore/ChillDocStoreBundle.php new file mode 100644 index 000000000..60649a163 --- /dev/null +++ b/src/Bundle/ChillDocStore/ChillDocStoreBundle.php @@ -0,0 +1,9 @@ +getDoctrine()->getManager(); + $categories = $em->getRepository("ChillDocStoreBundle:DocumentCategory")->findAll(); + + return $this->render( + 'ChillDocStoreBundle:DocumentCategory:index.html.twig', + ['document_categories' => $categories]); + } + + /** + * @Route("/new", name="document_category_new", methods="GET|POST") + */ + public function new(Request $request): Response + { + $em = $this->getDoctrine()->getManager(); + $documentCategory = new DocumentCategory(); + $documentCategory + ->setBundleId('Chill\DocStoreBundle\ChillDocStoreBundle'); + $documentCategory + ->setIdInsideBundle( + $em->getRepository("ChillDocStoreBundle:DocumentCategory") + ->nextIdInsideBundle()); + $documentCategory + ->setDocumentClass(PersonDocument::class); + + $form = $this->createForm(DocumentCategoryType::class, $documentCategory); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $em = $this->getDoctrine()->getManager(); + $em->persist($documentCategory); + $em->flush(); + + return $this->redirectToRoute('document_category_index'); + } else { + $documentCategory->setBundleId( + 'Chill\DocStoreBundle\ChillDocStoreBundle'); + } + + return $this->render('ChillDocStoreBundle:DocumentCategory:new.html.twig', [ + 'document_category' => $documentCategory, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{bundleId}/{idInsideBundle}", name="document_category_show", methods="GET") + */ + public function show($bundleId, $idInsideBundle): Response + { + $em = $this->getDoctrine()->getManager(); + $documentCategory = $em + ->getRepository("ChillDocStoreBundle:DocumentCategory") + ->findOneBy( + ['bundleId' => $bundleId, 'idInsideBundle' => $idInsideBundle]); + + return $this->render( + 'ChillDocStoreBundle:DocumentCategory:show.html.twig', + ['document_category' => $documentCategory]); + } + + /** + * @Route("/{bundleId}/{idInsideBundle}/edit", name="document_category_edit", methods="GET|POST") + */ + public function edit(Request $request, $bundleId, $idInsideBundle): Response + { + $em = $this->getDoctrine()->getManager(); + $documentCategory = $em + ->getRepository("ChillDocStoreBundle:DocumentCategory") + ->findOneBy( + ['bundleId' => $bundleId, 'idInsideBundle' => $idInsideBundle]); + + $form = $this->createForm(DocumentCategoryType::class, $documentCategory); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + + return $this->redirectToRoute('document_category_edit', [ + 'bundleId' => $documentCategory->getBundleId(), + 'idInsideBundle' => $documentCategory->getIdInsideBundle(),]); + } + + return $this->render('ChillDocStoreBundle:DocumentCategory:edit.html.twig', [ + 'document_category' => $documentCategory, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{bundleId}/{idInsideBundle}", name="document_category_delete", methods="DELETE") + */ + public function delete(Request $request, $bundleId, $idInsideBundle): Response + { + $em = $this->getDoctrine()->getManager(); + $documentCategory = $em + ->getRepository("ChillDocStoreBundle:DocumentCategory") + ->findOneBy( + ['bundleId' => $bundleId, 'idInsideBundle' => $idInsideBundle]); + + if ($this->isCsrfTokenValid('delete'.$bundleId.$idInsideBundle, $request->request->get('_token'))) { + $em->remove($documentCategory); + $em->flush(); + } + + return $this->redirectToRoute('document_category_index'); + } +} diff --git a/src/Bundle/ChillDocStore/Controller/DocumentPersonController.php b/src/Bundle/ChillDocStore/Controller/DocumentPersonController.php new file mode 100644 index 000000000..f6f074610 --- /dev/null +++ b/src/Bundle/ChillDocStore/Controller/DocumentPersonController.php @@ -0,0 +1,226 @@ +translator = $translator; + $this->eventDispatcher = $eventDispatcher; + } + + /** + * @Route("/", name="person_document_index", methods="GET") + */ + public function index(Person $person): Response + { + $em = $this->getDoctrine()->getManager(); + + if ($person === NULL) { + throw $this->createNotFoundException('Person not found'); + } + + $this->denyAccessUnlessGranted(PersonVoter::SEE, $person); + + $reachableScopes = $this->get('chill.main.security.authorization.helper') + ->getReachableScopes( + $this->getUser(), new Role(PersonDocumentVoter::SEE), + $person->getCenter()); + + $documents = $em + ->getRepository("ChillDocStoreBundle:PersonDocument") + ->findBy( + array('person' => $person, 'scope' => $reachableScopes), + array('date' => 'DESC') + ); + + $event = new PrivacyEvent($person, array( + 'element_class' => PersonDocument::class, + 'action' => 'index' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->render( + 'ChillDocStoreBundle:PersonDocument:index.html.twig', + [ + 'documents' => $documents, + 'person' => $person + ]); + } + + /** + * @Route("/new", name="person_document_new", methods="GET|POST") + */ + public function new(Request $request, Person $person): Response + { + if ($person === NULL) { + throw $this->createNotFoundException('person not found'); + } + + $this->denyAccessUnlessGranted(PersonVoter::SEE, $person); + + $document = new PersonDocument(); + $document->setUser($this->getUser()); + $document->setPerson($person); + $document->setDate(new \DateTime('Now')); + + $form = $this->createForm(PersonDocumentType::class, $document, array( + 'center' => $document->getCenter(), + 'role' => new Role('CHILL_PERSON_DOCUMENT_CREATE') + )); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->denyAccessUnlessGranted( + 'CHILL_PERSON_DOCUMENT_CREATE', $document, + 'creation of this activity not allowed'); + + $em = $this->getDoctrine()->getManager(); + $em->persist($document); + $em->flush(); + + $this->addFlash('success', $this->translator->trans("The document is successfully registered")); + + return $this->redirectToRoute('person_document_index', ['person' => $person->getId()]); + } elseif ($form->isSubmitted() and !$form->isValid()) { + $this->addFlash('error', $this->translator->trans("This form contains errors")); + } + + return $this->render('ChillDocStoreBundle:PersonDocument:new.html.twig', [ + 'document' => $document, + 'form' => $form->createView(), + 'person' => $person, + ]); + } + + /** + * @Route("/{id}", name="person_document_show", methods="GET") + */ + public function show(Person $person, PersonDocument $document): Response + { + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); + $this->denyAccessUnlessGranted('CHILL_PERSON_DOCUMENT_SEE', $document); + + $event = new PrivacyEvent($person, array( + 'element_class' => PersonDocument::class, + 'element_id' => $document->getId(), + 'action' => 'show' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->render( + 'ChillDocStoreBundle:PersonDocument:show.html.twig', + ['document' => $document, 'person' => $person]); + } + + /** + * @Route("/{id}/edit", name="person_document_edit", methods="GET|POST") + */ + public function edit(Request $request, Person $person, PersonDocument $document): Response + { + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); + $this->denyAccessUnlessGranted('CHILL_PERSON_DOCUMENT_UPDATE', $document); + + $document->setUser($this->getUser()); + $document->setDate(new \DateTime('Now')); + + $form = $this->createForm( + PersonDocumentType::class, $document, array( + 'center' => $document->getCenter(), + 'role' => new Role('CHILL_PERSON_DOCUMENT_UPDATE'), + )); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + + $this->addFlash('success', $this->translator->trans("The document is successfully updated")); + + $event = new PrivacyEvent($person, array( + 'element_class' => PersonDocument::class, + 'element_id' => $document->getId(), + 'action' => 'update' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->redirectToRoute( + 'person_document_edit', + ['id' => $document->getId(), 'person' => $person->getId()]); + + } elseif ($form->isSubmitted() and !$form->isValid()) { + $this->addFlash('error', $this->translator->trans("This form contains errors")); + } + + $event = new PrivacyEvent($person, array( + 'element_class' => PersonDocument::class, + 'element_id' => $document->getId(), + 'action' => 'edit' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->render( + 'ChillDocStoreBundle:PersonDocument:edit.html.twig', + [ + 'document' => $document, + 'form' => $form->createView(), + 'person' => $person, + ]); + } + + /** + * @Route("/{id}", name="person_document_delete", methods="DELETE") + */ + public function delete(Request $request, Person $person, PersonDocument $document): Response + { + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); + $this->denyAccessUnlessGranted('CHILL_PERSON_DOCUMENT_DELETE', $document); + + if ($this->isCsrfTokenValid('delete'.$document->getId(), $request->request->get('_token'))) { + $em = $this->getDoctrine()->getManager(); + $em->remove($document); + $em->flush(); + } + + return $this->redirectToRoute( + 'person_document_index', ['person' => $person->getId()]); + } +} diff --git a/src/Bundle/ChillDocStore/DataFixtures/ORM/LoadDocumentACL.php b/src/Bundle/ChillDocStore/DataFixtures/ORM/LoadDocumentACL.php new file mode 100644 index 000000000..c17e795e6 --- /dev/null +++ b/src/Bundle/ChillDocStore/DataFixtures/ORM/LoadDocumentACL.php @@ -0,0 +1,92 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\DocStoreBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Common\Persistence\ObjectManager; +use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup; +use Chill\MainBundle\Entity\RoleScope; +use Chill\MainBundle\DataFixtures\ORM\LoadScopes; +use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; + +/** + * Adding acl for person document + * + */ +class LoadDocumentACL extends AbstractFixture implements OrderedFixtureInterface +{ + public function getOrder() + { + return 35000; + } + + + public function load(ObjectManager $manager) + { + foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) { + $permissionsGroup = $this->getReference($permissionsGroupRef); + printf("processing permission group %s \n", $permissionsGroup->getName()); + foreach (LoadScopes::$references as $scopeRef){ + $scope = $this->getReference($scopeRef); + printf("processing scope %s \n", $scope->getName()['en']); + //create permission group + switch ($permissionsGroup->getName()) { + case 'social': + if ($scope->getName()['en'] === 'administrative') { + printf("denying power on administrative \n"); + break 2; // we do not want any power on administrative + } + break; + case 'administrative': + case 'direction': + if (in_array($scope->getName()['en'], array('administrative', 'social'))) { + printf("denying power on %s\n", $scope->getName()['en']); + break 2; // we do not want any power on social or administrative + } + break; + } + + printf("Adding Person report acl to %s " + . "permission group, scope '%s' \n", + $permissionsGroup->getName(), $scope->getName()['en']); + $roleScopeUpdate = (new RoleScope()) + ->setRole(PersonDocumentVoter::CREATE) + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeUpdate); + $roleScopeCreate = (new RoleScope()) + ->setRole(PersonDocumentVoter::UPDATE) + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeCreate); + $roleScopeDelete = (new RoleScope()) + ->setRole(PersonDocumentVoter::DELETE) + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeDelete); + $manager->persist($roleScopeUpdate); + $manager->persist($roleScopeCreate); + $manager->persist($roleScopeDelete); + } + + } + + $manager->flush(); + } + +} diff --git a/src/Bundle/ChillDocStore/DataFixtures/ORM/LoadDocumentCategory.php b/src/Bundle/ChillDocStore/DataFixtures/ORM/LoadDocumentCategory.php new file mode 100644 index 000000000..37e55c5b7 --- /dev/null +++ b/src/Bundle/ChillDocStore/DataFixtures/ORM/LoadDocumentCategory.php @@ -0,0 +1,60 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\DocStoreBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Common\Persistence\ObjectManager; +use Chill\DocStoreBundle\Entity\DocumentCategory; + +/** + * + * + */ +class LoadDocumentCategory extends AbstractFixture implements OrderedFixtureInterface +{ + public function getOrder() + { + return 35010; + } + + public function load(ObjectManager $manager) + { + $category = (new DocumentCategory('chill-doc-store', 10)) + ->setDocumentClass(\Chill\DocStoreBundle\Entity\PersonDocument::class) + ->setName([ + 'fr' => "Document d'identité", + 'en' => "Identity" + ]) + ; + + $manager->persist($category); + + $category = (new DocumentCategory('chill-doc-store', 20)) + ->setDocumentClass(\Chill\DocStoreBundle\Entity\PersonDocument::class) + ->setName([ + 'fr' => "Courrier reçu", + 'en' => "Received email" + ]) + ; + + $manager->persist($category); + + $manager->flush(); + } +} diff --git a/src/Bundle/ChillDocStore/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStore/DependencyInjection/ChillDocStoreExtension.php new file mode 100644 index 000000000..ed36f1845 --- /dev/null +++ b/src/Bundle/ChillDocStore/DependencyInjection/ChillDocStoreExtension.php @@ -0,0 +1,75 @@ +processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.yml'); + $loader->load('services/media.yml'); + $loader->load('services/controller.yml'); + $loader->load('services/menu.yml'); + $loader->load('services/fixtures.yml'); + $loader->load('services/form.yml'); + } + + public function prepend(ContainerBuilder $container) + { + $this->prependRoute($container); + $this->prependAuthorization($container); + $this->prependTwig($container); + } + + protected function prependRoute(ContainerBuilder $container) + { + //declare routes for task bundle + $container->prependExtensionConfig('chill_main', array( + 'routing' => array( + 'resources' => array( + '@ChillDocStoreBundle/Resources/config/routing.yml', + '@ChampsLibresAsyncUploaderBundle/Resources/config/routing.yml' + ) + ) + )); + } + + protected function prependAuthorization(ContainerBuilder $container) + { + $container->prependExtensionConfig('security', array( + 'role_hierarchy' => array( + PersonDocumentVoter::UPDATE => [PersonDocumentVoter::SEE_DETAILS], + PersonDocumentVoter::CREATE => [PersonDocumentVoter::SEE_DETAILS], + PersonDocumentVoter::DELETE => [PersonDocumentVoter::SEE_DETAILS], + PersonDocumentVoter::SEE_DETAILS => [PersonDocumentVoter::SEE], + ) + )); + } + + protected function prependTwig(ContainerBuilder $container) + { + $twigConfig = array( + 'form_themes' => array('@ChillDocStore/Form/fields.html.twig') + ); + $container->prependExtensionConfig('twig', $twigConfig); + } +} diff --git a/src/Bundle/ChillDocStore/DependencyInjection/Configuration.php b/src/Bundle/ChillDocStore/DependencyInjection/Configuration.php new file mode 100644 index 000000000..162f6ce99 --- /dev/null +++ b/src/Bundle/ChillDocStore/DependencyInjection/Configuration.php @@ -0,0 +1,29 @@ +root('chill_doc_store'); + + // Here you should define the parameters that are allowed to + // configure your bundle. See the documentation linked above for + // more information on that topic. + + return $treeBuilder; + } +} diff --git a/src/Bundle/ChillDocStore/Entity/Document.php b/src/Bundle/ChillDocStore/Entity/Document.php new file mode 100644 index 000000000..586d52035 --- /dev/null +++ b/src/Bundle/ChillDocStore/Entity/Document.php @@ -0,0 +1,171 @@ +id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription($description): self + { + $this->description = (string) $description; + + return $this; + } + + /** + * @return DocumentCategory + */ + public function getCategory(): ?DocumentCategory + { + return $this->category; + } + + public function setCategory(DocumentCategory $category): self + { + $this->category = $category; + + return $this; + } + + /** + * Get scope + * + * @return \Chill\MainBundle\Entity\Scope + */ + public function getScope() + { + return $this->scope; + } + + public function setScope($scope): self + { + $this->scope = $scope; + + return $this; + } + + public function getUser() + { + return $this->user; + } + + public function setUser($user): self + { + $this->user = $user; + + return $this; + } + + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): self + { + $this->date = $date; + + return $this; + } + + public function getObject(): ?StoredObject + { + return $this->object; + } + + public function setObject(StoredObject $object = null) + { + $this->object = $object; + + return $this; + } +} diff --git a/src/Bundle/ChillDocStore/Entity/DocumentCategory.php b/src/Bundle/ChillDocStore/Entity/DocumentCategory.php new file mode 100644 index 000000000..a2a8167cc --- /dev/null +++ b/src/Bundle/ChillDocStore/Entity/DocumentCategory.php @@ -0,0 +1,93 @@ +bundleId = $bundleId; + $this->idInsideBundle = $idInsideBundle; + } + + public function getBundleId() // ::class BundleClass (FQDN) + { + return $this->bundleId; + } + + public function getIdInsideBundle() + { + return $this->idInsideBundle; + } + + public function getDocumentClass() + { + return $this->documentClass; + } + + public function setDocumentClass($documentClass): self + { + $this->documentClass = $documentClass; + + return $this; + } + + public function getName($locale = null) + { + if ($locale) { + if (isset($this->name[$locale])) { + return $this->name[$locale]; + } else { + foreach ($this->name as $name) { + if (!empty($name)) { + return $name; + } + } + } + return ''; + } else { + return $this->name; + } + } + + public function setName($name): self + { + $this->name = $name; + + return $this; + } +} diff --git a/src/Bundle/ChillDocStore/Entity/PersonDocument.php b/src/Bundle/ChillDocStore/Entity/PersonDocument.php new file mode 100644 index 000000000..b38023ddb --- /dev/null +++ b/src/Bundle/ChillDocStore/Entity/PersonDocument.php @@ -0,0 +1,46 @@ +person; + } + + public function setPerson($person): self + { + $this->person = $person; + + return $this; + } + + public function getCenter() + { + return $this->getPerson()->getCenter(); + } +} diff --git a/src/Bundle/ChillDocStore/Entity/StoredObject.php b/src/Bundle/ChillDocStore/Entity/StoredObject.php new file mode 100644 index 000000000..e145ff1ab --- /dev/null +++ b/src/Bundle/ChillDocStore/Entity/StoredObject.php @@ -0,0 +1,157 @@ + + * + * @ORM\Entity() + * @ORM\Table("chill_doc.stored_object") + * @AsyncFileExists( + * message="The file is not stored properly" + * ) + */ +class StoredObject implements AsyncFileInterface +{ + /** + * @ORM\Id() + * @ORM\GeneratedValue() + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @ORM\Column(type="text") + */ + private $filename; + + /** + * @ORM\Column(type="json_array", name="key") + * @var array + */ + private $keyInfos = array(); + + /** + * + * @var int[] + * @ORM\Column(type="json_array", name="iv") + */ + private $iv = array(); + + /** + * + * @var \DateTime + * @ORM\Column(type="datetime", name="creation_date") + */ + private $creationDate; + + /** + * + * @var string + * @ORM\Column(type="text", name="type") + */ + private $type = ''; + + /** + * + * @var array + * @ORM\Column(type="json_array", name="datas") + */ + private $datas = []; + + public function __construct() + { + $this->creationDate = new \DateTime(); + } + + public function getId() + { + return $this->id; + } + + public function getFilename() + { + return $this->filename; + } + + public function getCreationDate(): \DateTime + { + return $this->creationDate; + } + + public function getType() + { + return $this->type; + } + + public function getDatas() + { + return $this->datas; + } + + public function setFilename($filename) + { + $this->filename = $filename; + + return $this; + } + + public function setCreationDate(\DateTime $creationDate) + { + $this->creationDate = $creationDate; + return $this; + } + + public function setType($type) + { + $this->type = $type; + + return $this; + } + + public function setDatas(array $datas) + { + $this->datas = $datas; + + return $this; + } + + public function getObjectName() + { + return $this->getFilename(); + } + + public function getKeyInfos() + { + return $this->keyInfos; + } + + public function getIv() + { + return $this->iv; + } + + public function setKeyInfos($keyInfos) + { + $this->keyInfos = $keyInfos; + + return $this; + } + + public function setIv($iv) + { + $this->iv = $iv; + + return $this; + } + + +} diff --git a/src/Bundle/ChillDocStore/EntityRepository/DocumentCategoryRepository.php b/src/Bundle/ChillDocStore/EntityRepository/DocumentCategoryRepository.php new file mode 100644 index 000000000..d803405e4 --- /dev/null +++ b/src/Bundle/ChillDocStore/EntityRepository/DocumentCategoryRepository.php @@ -0,0 +1,39 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\DocStoreBundle\EntityRepository; + +use Doctrine\ORM\EntityRepository; +use Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option; + +/** + * Get an available idInsideBUndle + */ +class DocumentCategoryRepository extends EntityRepository +{ + public function nextIdInsideBundle() + { + $array_res = $this->getEntityManager() + ->createQuery( + 'SELECT MAX(c.idInsideBundle) + 1 FROM ChillDocStoreBundle:DocumentCategory c') + ->getSingleResult(); + + return $array_res[1] ?: 0; + } +} diff --git a/src/Bundle/ChillDocStore/Form/DocumentCategoryType.php b/src/Bundle/ChillDocStore/Form/DocumentCategoryType.php new file mode 100644 index 000000000..9a3f75d8f --- /dev/null +++ b/src/Bundle/ChillDocStore/Form/DocumentCategoryType.php @@ -0,0 +1,50 @@ + $value) { + if(substr($key, 0, 5) === 'Chill') { + $this->chillBundlesFlipped[$value] = $key; + } + } + } + + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('bundleId', ChoiceType::class, array( + 'choices' => $this->chillBundlesFlipped, + 'disabled' => true, + )) + ->add('idInsideBundle', null, array( + 'disabled' => true, + )) + ->add('documentClass', null, array( + 'disabled' => true, + )) // cahcerh par default PersonDocument + ->add('name', TranslatableStringFormType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => DocumentCategory::class, + ]); + } +} diff --git a/src/Bundle/ChillDocStore/Form/PersonDocumentType.php b/src/Bundle/ChillDocStore/Form/PersonDocumentType.php new file mode 100644 index 000000000..ffb494ec8 --- /dev/null +++ b/src/Bundle/ChillDocStore/Form/PersonDocumentType.php @@ -0,0 +1,103 @@ +translatableStringHelper = $translatableStringHelper; + } + + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('title', TextType::class) + ->add('description', TextareaType::class, [ + 'required' => false + ]) + ->add('object', StoredObjectType::class, [ + 'error_bubbling' => true + ]) + ->add('scope', ScopePickerType::class, [ + 'center' => $options['center'], + 'role' => $options['role'] + ]) + ->add('date', ChillDateType::class) + ->add('category', EntityType::class, array( + 'placeholder' => 'Choose a document category', + 'class' => 'ChillDocStoreBundle:DocumentCategory', + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('c') + ->where('c.documentClass = :docClass') + ->setParameter('docClass', PersonDocument::class); + }, + 'choice_label' => function ($entity = null) { + return $entity ? $this->translatableStringHelper->localize($entity->getName()) : ''; + }, + )) + ; + + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Document::class, + ]); + + $resolver->setRequired(['role', 'center']) + ->setAllowedTypes('role', [ \Symfony\Component\Security\Core\Role\Role::class ]) + ->setAllowedTypes('center', [ \Chill\MainBundle\Entity\Center::class ]) + ; + } +} diff --git a/src/Bundle/ChillDocStore/Form/StoredObjectType.php b/src/Bundle/ChillDocStore/Form/StoredObjectType.php new file mode 100644 index 000000000..64cc32e83 --- /dev/null +++ b/src/Bundle/ChillDocStore/Form/StoredObjectType.php @@ -0,0 +1,108 @@ +em = $em; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $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'] + )); + } + + public function getBlockPrefix() + { + return 'stored_object'; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefault('data_class', StoredObject::class) + ; + } + + public function reverseTransform($value) + { + if ($value === null) { + return null; + } + + return \json_decode($value, true); + } + + public function transform($object) + { + if ($object === null) { + return null; + } + + return \json_encode($object); + } + + public function transformObject($object = null) + { + return $object; + } + + 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; + } +} diff --git a/src/Bundle/ChillDocStore/Menu/MenuBuilder.php b/src/Bundle/ChillDocStore/Menu/MenuBuilder.php new file mode 100644 index 000000000..03a8b7038 --- /dev/null +++ b/src/Bundle/ChillDocStore/Menu/MenuBuilder.php @@ -0,0 +1,87 @@ + + */ +class MenuBuilder implements LocalMenuBuilderInterface +{ + /** + * + * @var TokenStorageInterface + */ + protected $tokenStorage; + + /** + * + * @var AuthorizationHelper + */ + protected $authorizationHelper; + + /** + * + * @var TranslatorInterface + */ + protected $translator; + + public function __construct( + TokenStorageInterface $tokenStorage, + AuthorizationHelper $authorizationHelper, + TranslatorInterface $translator + ){ + $this->tokenStorage = $tokenStorage; + $this->authorizationHelper = $authorizationHelper; + $this->translator = $translator; + } + + + public function buildMenu($menuId, MenuItem $menu, array $parameters) + { + switch($menuId) { + case 'person': + $this->buildMenuPerson($menu, $parameters); + break; + default: + throw new \LogicException("this menuid $menuId is not implemented"); + } + } + + protected function buildMenuPerson(MenuItem $menu, array $parameters) + { + /* @var $person \Chill\PersonBundle\Entity\Person */ + $person = $parameters['person']; + $user = $this->tokenStorage->getToken()->getUser(); + + if ($this->authorizationHelper->userHasAccess($user, + $person->getCenter(), PersonDocumentVoter::SEE)) { + + $menu->addChild($this->translator->trans('Documents'), [ + 'route' => 'person_document_index', + 'routeParameters' => [ + 'person' => $person->getId() + ] + ]) + ->setExtras([ + 'order'=> 350 + ]); + } + + } + + public static function getMenuIds(): array + { + return [ 'person' ]; + } +} diff --git a/src/Bundle/ChillDocStore/Object/ObjectToAsyncFileTransformer.php b/src/Bundle/ChillDocStore/Object/ObjectToAsyncFileTransformer.php new file mode 100644 index 000000000..c5542d667 --- /dev/null +++ b/src/Bundle/ChillDocStore/Object/ObjectToAsyncFileTransformer.php @@ -0,0 +1,49 @@ + + */ +class ObjectToAsyncFileTransformer implements AsyncFileTransformerInterface +{ + /** + * + * @var EntityManagerInterface + */ + protected $em; + + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + } + + public function toAsyncFile($data) + { + if ($data instanceof StoredObject) { + return $data; + } + } + + public function toData(AsyncFileInterface $asyncFile) + { + $object = $this->em + ->getRepository(StoredObject::class) + ->findByFilename($asyncFile->getObjectName()) + ; + + return $object ?? (new StoredObject()) + ->setFilename($asyncFile->getObjectName()) + ; + } +} diff --git a/src/Bundle/ChillDocStore/Object/PersistenceChecker.php b/src/Bundle/ChillDocStore/Object/PersistenceChecker.php new file mode 100644 index 000000000..3e0c58189 --- /dev/null +++ b/src/Bundle/ChillDocStore/Object/PersistenceChecker.php @@ -0,0 +1,40 @@ + + */ +class PersistenceChecker implements PersistenceCheckerInterface +{ + /** + * + * @var EntityManagerInterface + */ + protected $em; + + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + } + + + public function isPersisted($object_name): bool + { + $qb = $this->em->createQueryBuilder(); + $qb->select('COUNT(m)') + ->from(StoredObject::class, 'm') + ->where($qb->expr()->eq('m.filename', ':object_name')) + ->setParameter('object_name', $object_name) + ; + + return 1 === $qb->getQuery()->getSingleScalarResult(); + } +} diff --git a/src/Bundle/ChillDocStore/Resources/config/routing.yml b/src/Bundle/ChillDocStore/Resources/config/routing.yml new file mode 100644 index 000000000..c02dc76ab --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/config/routing.yml @@ -0,0 +1,3 @@ +app: + resource: "@ChillDocStoreBundle/Controller/" + type: annotation diff --git a/src/Bundle/ChillDocStore/Resources/config/services.yml b/src/Bundle/ChillDocStore/Resources/config/services.yml new file mode 100644 index 000000000..4439c8dbb --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/config/services.yml @@ -0,0 +1,24 @@ +parameters: +# cl_chill_person.example.class: Chill\PersonBundle\Example + +services: + Chill\DocStoreBundle\Form\DocumentCategoryType: + class: Chill\DocStoreBundle\Form\DocumentCategoryType + arguments: ['%kernel.bundles%'] + tags: + - { name: form.type } + + Chill\DocStoreBundle\Form\PersonDocumentType: + class: Chill\DocStoreBundle\Form\PersonDocumentType + arguments: + - "@chill.main.helper.translatable_string" + tags: + - { name: form.type, alias: chill_docstorebundle_form_document } + + Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter: + class: Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter + arguments: + - "@chill.main.security.authorization.helper" + tags: + - { name: security.voter } + - { name: chill.role } diff --git a/src/Bundle/ChillDocStore/Resources/config/services/controller.yml b/src/Bundle/ChillDocStore/Resources/config/services/controller.yml new file mode 100644 index 000000000..0a2798508 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/config/services/controller.yml @@ -0,0 +1,9 @@ +services: + Chill\DocStoreBundle\Controller\: + resource: '../../../Controller' + tags: ['controller.service_arguments'] + + Chill\DocStoreBundle\Controller\DocumentPersonController: + autowire: true + arguments: + $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' diff --git a/src/Bundle/ChillDocStore/Resources/config/services/fixtures.yml b/src/Bundle/ChillDocStore/Resources/config/services/fixtures.yml new file mode 100644 index 000000000..c9bc451d6 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/config/services/fixtures.yml @@ -0,0 +1,4 @@ +services: + Chill\DocStoreBundle\DataFixtures\ORM\: + resource: ../../../DataFixtures/ORM + tags: [ 'doctrine.fixture.orm' ] diff --git a/src/Bundle/ChillDocStore/Resources/config/services/form.yml b/src/Bundle/ChillDocStore/Resources/config/services/form.yml new file mode 100644 index 000000000..c84507e51 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/config/services/form.yml @@ -0,0 +1,6 @@ +services: + Chill\DocStoreBundle\Form\StoredObjectType: + arguments: + $em: '@Doctrine\ORM\EntityManagerInterface' + tags: + - { name: form.type } diff --git a/src/Bundle/ChillDocStore/Resources/config/services/media.yml b/src/Bundle/ChillDocStore/Resources/config/services/media.yml new file mode 100644 index 000000000..e6afe2155 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/config/services/media.yml @@ -0,0 +1,9 @@ +services: + chill_doc_store.persistence_checker: + class: Chill\DocStoreBundle\Object\PersistenceChecker + arguments: + $em: '@Doctrine\ORM\EntityManagerInterface' + + Chill\DocStoreBundle\Object\ObjectToAsyncFileTransformer: + arguments: + $em: '@Doctrine\ORM\EntityManagerInterface' diff --git a/src/Bundle/ChillDocStore/Resources/config/services/menu.yml b/src/Bundle/ChillDocStore/Resources/config/services/menu.yml new file mode 100644 index 000000000..069cd64d4 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/config/services/menu.yml @@ -0,0 +1,8 @@ +services: + Chill\DocStoreBundle\Menu\MenuBuilder: + arguments: + $tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' + $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' + $translator: '@Symfony\Component\Translation\TranslatorInterface' + tags: + - { name: 'chill.menu_builder' } diff --git a/src/Bundle/ChillDocStore/Resources/migrations/Version20180605102533.php b/src/Bundle/ChillDocStore/Resources/migrations/Version20180605102533.php new file mode 100644 index 000000000..5661277e2 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/migrations/Version20180605102533.php @@ -0,0 +1,39 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE SCHEMA chill_doc'); + $this->addSql('CREATE SEQUENCE chill_doc.person_document_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_doc.document_category (bundle_id VARCHAR(255) NOT NULL, id_inside_bundle INT NOT NULL, document_class VARCHAR(255) NOT NULL, name JSON NOT NULL, PRIMARY KEY(bundle_id, id_inside_bundle))'); + $this->addSql('COMMENT ON COLUMN chill_doc.document_category.name IS \'(DC2Type:json_array)\''); + $this->addSql('CREATE TABLE chill_doc.person_document (id INT NOT NULL, category_bundle_id VARCHAR(255) DEFAULT NULL, category_id_inside_bundle INT DEFAULT NULL, scope_id INT DEFAULT NULL, user_id INT DEFAULT NULL, person_id INT DEFAULT NULL, title TEXT NOT NULL, description TEXT NOT NULL, content TEXT NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_41DA53C369A0BE36EF62EFC ON chill_doc.person_document (category_bundle_id, category_id_inside_bundle)'); + $this->addSql('CREATE INDEX IDX_41DA53C682B5931 ON chill_doc.person_document (scope_id)'); + $this->addSql('CREATE INDEX IDX_41DA53CA76ED395 ON chill_doc.person_document (user_id)'); + $this->addSql('CREATE INDEX IDX_41DA53C217BBB47 ON chill_doc.person_document (person_id)'); + $this->addSql('ALTER TABLE chill_doc.person_document ADD CONSTRAINT FK_41DA53C369A0BE36EF62EFC FOREIGN KEY (category_bundle_id, category_id_inside_bundle) REFERENCES chill_doc.document_category (bundle_id, id_inside_bundle) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_doc.person_document ADD CONSTRAINT FK_41DA53C682B5931 FOREIGN KEY (scope_id) REFERENCES scopes (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_doc.person_document ADD CONSTRAINT FK_41DA53CA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_doc.person_document ADD CONSTRAINT FK_41DA53C217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema) : void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('DROP SCHEMA chill_doc CASCADE'); + + } +} diff --git a/src/Bundle/ChillDocStore/Resources/migrations/Version20180606133338.php b/src/Bundle/ChillDocStore/Resources/migrations/Version20180606133338.php new file mode 100644 index 000000000..43dae7aa4 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/migrations/Version20180606133338.php @@ -0,0 +1,38 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE SEQUENCE chill_doc.stored_object_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_doc.stored_object (id INT NOT NULL, filename TEXT NOT NULL, key JSON NOT NULL, iv JSON NOT NULL, creation_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, type TEXT NOT NULL, datas JSON NOT NULL, PRIMARY KEY(id))'); + $this->addSql('COMMENT ON COLUMN chill_doc.stored_object.key IS \'(DC2Type:json_array)\''); + $this->addSql('COMMENT ON COLUMN chill_doc.stored_object.iv IS \'(DC2Type:json_array)\''); + $this->addSql('COMMENT ON COLUMN chill_doc.stored_object.datas IS \'(DC2Type:json_array)\''); + $this->addSql('ALTER TABLE chill_doc.person_document ADD object_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_doc.person_document DROP content'); + $this->addSql('ALTER TABLE chill_doc.person_document ADD CONSTRAINT FK_41DA53C232D562B FOREIGN KEY (object_id) REFERENCES chill_doc.stored_object (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_41DA53C232D562B ON chill_doc.person_document (object_id)'); + } + + public function down(Schema $schema) : void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('ALTER TABLE chill_doc.person_document DROP CONSTRAINT FK_41DA53C232D562B'); + $this->addSql('DROP SEQUENCE chill_doc.stored_object_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_doc.stored_object'); + $this->addSql('ALTER TABLE chill_doc.person_document ADD content TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_doc.person_document DROP object_id'); + } +} diff --git a/src/Bundle/ChillDocStore/Resources/public/module/async_upload/downloader.js b/src/Bundle/ChillDocStore/Resources/public/module/async_upload/downloader.js new file mode 100644 index 000000000..deeb2828f --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/public/module/async_upload/downloader.js @@ -0,0 +1,114 @@ +var mime = require('mime-types'); + +var algo = 'AES-CBC'; + +var initializeButtons = (root) => { + var + buttons = root.querySelectorAll('a[data-download-button]'); + + for (let i = 0; i < buttons.length; i ++) { + initialize(buttons[i]); + } +}; + +var initialize = (button) => { + button.addEventListener('click', onClick); +}; + +var onClick = e => download(e.target); + +var download = (button) => { + var + keyData = JSON.parse(button.dataset.key), + ivData = JSON.parse(button.dataset.iv), + iv = new Uint8Array(ivData), + urlGenerator = button.dataset.tempUrlGetGenerator, + hasFilename = 'filename' in button.dataset, + filename = button.dataset.filename, + labelPreparing = button.dataset.labelPreparing, + labelReady = button.dataset.labelReady, + mimeType = button.dataset.mimeType, + extension = mime.extension(mimeType), + decryptError = "Error while decrypting file", + fetchError = "Error while fetching file", + key, url + ; + + button.textContent = labelPreparing; + + window.fetch(urlGenerator) + .then((r) => { + if (r.ok) { + return r.json(); + } else { + throw new Error("error while downloading url " + r.status + " " + r.statusText); + } + }) + .then(data => { + url = data.url; + + return window.crypto.subtle.importKey('jwk', keyData, { name: algo, iv: iv}, false, ['decrypt']); + }) + .catch(e => { + console.error("error while importing key"); + console.error(e); + button.appendChild(document.createTextNode(decryptError)); + }) + .then(nKey => { + key = nKey; + + return window.fetch(url); + }) + .catch(e => { + console.error("error while fetching data"); + console.error(e); + button.textContent = ""; + button.appendChild(document.createTextNode(fetchError)); + }) + .then(r => { + if (r.ok) { + return r.arrayBuffer(); + } else { + throw new Error(r.status + r.statusText); + } + }) + .then(buffer => { + return window.crypto.subtle.decrypt({ name: algo, iv: iv }, key, buffer); + }) + .catch(e => { + console.error("error while importing key"); + console.error(e); + button.textContent = ""; + button.appendChild(document.createTextNode(decryptError)); + }) + .then(decrypted => { + var + blob = new Blob([decrypted], { type: mimeType }), + url = window.URL.createObjectURL(blob) + ; + button.href = url; + button.target = '_blank'; + button.type = mimeType; + button.textContent = labelReady; + if (hasFilename) { + button.download = filename; + if (extension !== false) { + button.download = button.download + '.' + extension; + } + } + button.removeEventListener('click', onClick); + button.click(); + }) + .catch(error => { + console.log(error); + button.textContent = ""; + button.appendChild(document.createTextNode("error while handling decrypted file")); + }) + ; +}; + +window.addEventListener('load', function(e) { + initializeButtons(e.target); +}); + +module.exports = initializeButtons; diff --git a/src/Bundle/ChillDocStore/Resources/public/module/async_upload/index.js b/src/Bundle/ChillDocStore/Resources/public/module/async_upload/index.js new file mode 100644 index 000000000..9fa7deeeb --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/public/module/async_upload/index.js @@ -0,0 +1,2 @@ +require('./uploader.js'); +require('./downloader.js'); \ No newline at end of file diff --git a/src/Bundle/ChillDocStore/Resources/public/module/async_upload/index.scss b/src/Bundle/ChillDocStore/Resources/public/module/async_upload/index.scss new file mode 100644 index 000000000..54299efce --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/public/module/async_upload/index.scss @@ -0,0 +1,34 @@ +// override dropzone from dropzoneJS +.dropzone { + margin-bottom: 0.5rem; + + .dz-preview { + display: initial; + margin-left: auto; + margin-right: auto; + + .dz-image { + margin-left: auto; + margin-right: auto; + } + + .dz-details, .dz-progress, .dz-success-mark, .dz-error-mark { + position: initial; + margin-left: auto; + margin-right: auto; + } + } +} + +.chill-dropzone__below-zone { + + display: flex; + + & > *:not(:last-child) { + margin-right: 1rem; + } + + .sc-button.dz-bt-below-dropzone { + width: 100%; + } +} diff --git a/src/Bundle/ChillDocStore/Resources/public/module/async_upload/uploader.js b/src/Bundle/ChillDocStore/Resources/public/module/async_upload/uploader.js new file mode 100644 index 000000000..9e96070bb --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/public/module/async_upload/uploader.js @@ -0,0 +1,372 @@ +var algo = 'AES-CBC'; +var Dropzone = require('dropzone'); +var initializeDownload = require('./downloader.js'); + + +/** + * + * define a dropzone for chill usage + * + * An event is launched when dropzone is initialize, allowing to customize events + * on dropzone : + * + * ``` + * window.addEventListener("chill_dropzone_initialized", (e) => { + * // do something with dropzone: + * e.detail.dropzone.on("success", (e) => { + * // see https://www.dropzonejs.com/#event-success + * }); + * }); + * ``` + * + */ + +// load css +//require('dropzone/dist/basic.css'); +require('dropzone/dist/dropzone.css'); +require('./index.scss'); +// + +// disable dropzone autodiscover +Dropzone.autoDiscover = false; + +var keyDefinition = { + name: algo, + length: 256 +}; + +var searchForZones = function(root) { + var zones = root.querySelectorAll('div[data-stored-object]'); + + for(let i=0; i < zones.length; i++) { + initialize(zones[i]); + } +}; + +var getUploadUrl = function(zoneData, files) { + var + generateTempUrlPost = zoneData.zone.querySelector('input[data-async-file-upload]').dataset.generateTempUrlPost, + oReq = new XMLHttpRequest() + ; + + // arg, dropzone, you cannot handle async upload... + oReq.open("GET", generateTempUrlPost, false); + oReq.send(); + + if (oReq.readyState !== XMLHttpRequest.DONE) { + throw new Error("Error while fetching url to upload"); + } + + zoneData.params = JSON.parse(oReq.responseText); + + return zoneData.params.url; +}; + +var encryptFile = function(originalFile, zoneData, done) { + var + iv = crypto.getRandomValues(new Uint8Array(16)), + reader = new FileReader(), + jsKey, rawKey + ; + + zoneData.originalType = originalFile.type; + + reader.onload = e => { + window.crypto.subtle.generateKey(keyDefinition, true, [ "encrypt", "decrypt" ]) + .then(key => { + jsKey = key; + + // we register the key somwhere + return window.crypto.subtle.exportKey('jwk', key); + }).then(exportedKey => { + rawKey = exportedKey; + + // we start encryption + return window.crypto.subtle.encrypt({ name: algo, iv: iv}, jsKey, e.target.result); + }) + .then(encrypted => { + zoneData.crypto = { + jsKey: jsKey, + rawKey: rawKey, + iv: iv + }; + + done(new File( [ encrypted ], zoneData.suffix)); + }); + }; + + reader.readAsArrayBuffer(originalFile); +}; + +var addBelowButton = (btn, zone, zoneData) => { + let + belowZone = zone.querySelector('.chill-dropzone__below-zone'); + + if (belowZone === null) { + belowZone = document.createElement('div'); + belowZone.classList.add('chill-dropzone__below-zone'); + zone.appendChild(belowZone); + } + + belowZone.appendChild(btn); +}; + +var createZone = (zone, zoneData) => { + var + created = document.createElement('div'), + initMessage = document.createElement('div'), + initContent = zone.dataset.labelInitMessage, + dropzoneI; + + created.classList.add('dropzone'); + initMessage.classList.add('dz-message'); + initMessage.appendChild(document.createTextNode(initContent)); + + dropzoneI = new Dropzone(created, { + url: function(files) { + return getUploadUrl(zoneData, files); + }, + dictDefaultMessage: zone.dataset.dictDefaultMessage, + dictFileTooBig: zone.dataset.dictFileTooBig, + dictRemoveFile: zone.dataset.dictRemoveFile, + dictMaxFilesExceeded: zone.dataset.dictMaxFilesExceeded, + dictCancelUpload: zone.dataset.dictCancelUpload, + dictCancelUploadConfirm: zone.dataset.dictCancelUploadConfirm, + dictUploadCanceled: zone.dataset.dictUploadCanceled, + maxFiles: 1, + addRemoveLinks: true, + transformFile: function(file, done) { + encryptFile(file, zoneData, done); + }, + renameFile: function(file) { + return zoneData.suffix; + } + }); + + dropzoneI.on("sending", function(file, xhr, formData) { + formData.append("redirect", zoneData.params.redirect); + formData.append("max_file_size", zoneData.params.max_file_size); + formData.append("max_file_count", zoneData.params.max_file_count); + formData.append("expires", zoneData.params.expires); + formData.append("signature", zoneData.params.signature); + }); + + dropzoneI.on("success", function(file, response) { + zoneData.currentFile = file; + storeDataInForm(zone, zoneData); + }); + + dropzoneI.on("addedfile", function(file) { + if (zoneData.hasOwnProperty('currentFile')) { + dropzoneI.removeFile(zoneData.currentFile); + } + }); + + dropzoneI.on("removedfile", function(file) { + removeDataInForm(zone, zoneData); + }); + + zone.insertBefore(created, zone.firstChild); + + let event = new CustomEvent("chill_dropzone_initialized", { + detail: { + dropzone: dropzoneI, + zoneData: zoneData + } + }); + window.dispatchEvent(event); +}; + + +var initialize = function(zone) { + var + allowRemove = zone.dataset.allowRemove, + zoneData = { zone: zone, suffix: createFilename(), allowRemove: allowRemove, old: null } + ; + + if (hasDataInForm(zone, zoneData)) { + insertRemoveButton(zone, zoneData); + insertDownloadButton(zone, zoneData); + } else { + createZone(zone, zoneData); + } +}; + +var createFilename = () => { + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (let i = 0; i < 7; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + + return text; +}; + +var storeDataInForm = (zone, zoneData) => { + var + inputKey = zone.querySelector('input[data-stored-object-key]'), + inputIv = zone.querySelector('input[data-stored-object-iv]'), + inputObject = zone.querySelector('input[data-async-file-upload]'), + inputType = zone.querySelector('input[data-async-file-type]') + ; + + inputKey.value = JSON.stringify(zoneData.crypto.rawKey); + inputIv.value = JSON.stringify(Array.from(zoneData.crypto.iv)); + inputType.value = zoneData.originalType; + inputObject.value = zoneData.params.prefix + zoneData.suffix; + + insertDownloadButton(zone); +}; + +const restoreDataInForm = (zone, zoneData) => { + var + inputKey = zone.querySelector('input[data-stored-object-key]'), + inputIv = zone.querySelector('input[data-stored-object-iv]'), + inputObject = zone.querySelector('input[data-async-file-upload]'), + inputType = zone.querySelector('input[data-async-file-type]') + ; + + if (zoneData.old === null) { + console.log('should not have restored data'); + return; + } + + inputKey.value = zoneData.old.key; + inputIv.value = zoneData.old.iv; + inputType.value = zoneData.old.type; + inputObject.value = zoneData.old.obj; + + insertDownloadButton(zone); +}; + +const hasDataInForm = (zone, zoneData) => { + var + inputObject = zone.querySelector('input[data-async-file-upload]') + ; + + return inputObject.value.length > 0; +}; + +var removeDataInForm = (zone, zoneData) => { + var + inputKey = zone.querySelector('input[data-stored-object-key]'), + inputIv = zone.querySelector('input[data-stored-object-iv]'), + inputObject = zone.querySelector('input[data-async-file-upload]'), + inputType = zone.querySelector('input[data-async-file-type]') + ; + + // store data for future usage + zoneData.old = { + key: inputKey.value, + iv: inputIv.value, + obj: inputObject.value, + type: inputType.value + }; + // set blank values + inputKey.value = ""; + inputIv.value = ""; + inputType.value = ""; + inputObject.value = ""; + + insertDownloadButton(zone); +}; + +var insertRemoveButton = (zone, zoneData) => { + var + removeButton = document.createElement('a'), + cancelButton = document.createElement('a'), + labelRemove = zone.dataset.dictRemove, + labelCancel = 'Restaurer' + ; + + removeButton.classList.add('sc-button', 'bt-delete'); + removeButton.textContent = labelRemove; + + cancelButton.classList.add('sc-button'); + cancelButton.textContent = labelCancel; + + removeButton.addEventListener('click', (e) => { + e.preventDefault(); + if (zoneData.allowRemove === 'true') { + removeDataInForm(zone, zoneData); + cancelButton.addEventListener('click', (e) => { + e.preventDefault(); + + restoreDataInForm(zone, zoneData); + + cancelButton.remove(); + zone.querySelector('.dropzone').remove(); + + initialize(zone); + }); + } + addBelowButton(cancelButton, zone, zoneData); + //zone.appendChild(cancelButton); + removeButton.remove(); + createZone(zone, zoneData); + }); + + addBelowButton(removeButton, zone, zoneData); + // zone.appendChild(removeButton); +}; + +const removeDownloadButton = (zone, zoneData) => { + var + existingButtons = zone.querySelectorAll('a[data-download-button]') + ; + + // remove existing + existingButtons.forEach(function(b) { + b.remove(); + }); +}; + +var insertDownloadButton = (zone, zoneData) => { + var + existingButtons = zone.querySelectorAll('a[data-download-button]'), + newButton = document.createElement('a'), + inputKey = zone.querySelector('input[data-stored-object-key]'), + inputIv = zone.querySelector('input[data-stored-object-iv]'), + inputObject = zone.querySelector('input[data-async-file-upload]'), + inputType = zone.querySelector('input[data-async-file-type]'), + labelPreparing = zone.dataset.labelPreparing, + labelQuietButton = zone.dataset.labelQuietButton, + labelReady = zone.dataset.labelReady, + tempUrlGenerator = zone.dataset.tempUrlGenerator, + tempUrlGeneratorParams = new URLSearchParams() + ; + + // remove existing + existingButtons.forEach(function(b) { + b.remove(); + }); + + if (inputObject.value === '') { + return; + } + + tempUrlGeneratorParams.append('object_name', inputObject.value); + + newButton.dataset.downloadButton = true; + newButton.dataset.key = inputKey.value; + newButton.dataset.iv = inputIv.value; + newButton.dataset.mimeType = inputType.value; + newButton.dataset.labelPreparing = labelPreparing; + newButton.dataset.labelReady = labelReady; + newButton.dataset.tempUrlGetGenerator = tempUrlGenerator + '?' + tempUrlGeneratorParams.toString(); + newButton.classList.add('sc-button', 'bt-download', 'dz-bt-below-dropzone'); + newButton.textContent = labelQuietButton; + + addBelowButton(newButton, zone, zoneData); + //zone.appendChild(newButton); + initializeDownload(zone); +}; + +window.addEventListener('load', function(e) { + searchForZones(document); +}); + +window.addEventListener('collection-add-entry', function(e) { + searchForZones(e.detail.entry); +}); diff --git a/src/Bundle/ChillDocStore/Resources/translations/messages.fr.yml b/src/Bundle/ChillDocStore/Resources/translations/messages.fr.yml new file mode 100644 index 000000000..3d69655d7 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/translations/messages.fr.yml @@ -0,0 +1,28 @@ +Document: Document +Documents for %name%: Documents de %name% +Preparing: En préparation +Ready to show: Prêt à être visualisé +Download: Télécharger +Download existing file: Télécharger le fichier existant +Create new document: Créer un nouveau document +New document for %name%: Nouveau document pour %name% +Editing document for %name%: Modification d'un document pour %name% +Edit Document: Modification d'un document +Existing document: Document existant +The document is successfully updated: Le document est mis à jour +No document to download: Aucun document à télécharger +'Choose a document category': Choisissez une catégorie de document +Any document found: Aucun document trouvé +The document is successfully registered: Le document est enregistré +The document is successfully updated: Le document est mis à jour +Any description: Aucune description + +# dropzone upload +File too big: Fichier trop volumineux +Drop your file or click here: Cliquez ici ou faites glissez votre nouveau fichier dans cette zone +Remove file in order to upload a new one: Supprimer ce fichier pour en insérer un autre +Max files exceeded. Remove previous files: Nombre maximum de fichier atteint. Supprimez les précédents +Cancel upload: Annuler le téléversement +Are you sure you want to cancel this upload ?: Êtes-vous sûrs de vouloir annuler ce téléversement ? +Upload canceled: Téléversement annulé +Remove existing file: Supprimer le document existant diff --git a/src/Bundle/ChillDocStore/Resources/translations/validators.fr.yml b/src/Bundle/ChillDocStore/Resources/translations/validators.fr.yml new file mode 100644 index 000000000..38f21d4ab --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/translations/validators.fr.yml @@ -0,0 +1,2 @@ +The file is not stored properly: Le fichier n'est pas téléchargé correctement +Upload a document: Téléversez un document \ No newline at end of file diff --git a/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/_delete_form.html.twig b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/_delete_form.html.twig new file mode 100644 index 000000000..5d77512dd --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/_delete_form.html.twig @@ -0,0 +1,21 @@ +{# + * Copyright (C) 2018, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} +
+ + + +
diff --git a/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/_form.html.twig b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/_form.html.twig new file mode 100644 index 000000000..e2a0a004d --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/_form.html.twig @@ -0,0 +1,20 @@ +{# + * Copyright (C) 2018, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} diff --git a/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/edit.html.twig b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/edit.html.twig new file mode 100644 index 000000000..68c74a29d --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/edit.html.twig @@ -0,0 +1,31 @@ +{# + * Copyright (C) 2018, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} +{% extends "ChillMainBundle::Admin/layout.html.twig" %} + +{% block title %}{{ 'Document category edit'|trans }}{% endblock title %} + +{% block admin_content %} +

{{ 'Document category edit'|trans }}

+ + {{ include('ChillDocStoreBundle:DocumentCategory:_form.html.twig', {'button_label': 'Update'}) }} + + + {{ 'Back to the category list' | trans }} + + + {{ include('ChillDocStoreBundle:DocumentCategory:_delete_form.html.twig') }} +{% endblock %} diff --git a/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/index.html.twig b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/index.html.twig new file mode 100644 index 000000000..3aaa7fcf0 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/index.html.twig @@ -0,0 +1,60 @@ +{# + * Copyright (C) 2018, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} +{% extends "ChillMainBundle::Admin/layout.html.twig" %} + +{% block title %}{{ 'Document category list' | trans }}{% endblock title %} + +{% block admin_content %} +

{{ 'Document category list' | trans }}

+ + + + + + + + + + + + + {% for document_category in document_categories %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
{{ 'Creator bundle id' | trans }}{{ 'Internal id inside creator bundle' | trans }}{{ 'Document class' | trans }}{{ 'Name' | trans }}{{ 'Actions' | trans }}
{{ document_category.bundleId }}{{ document_category.idInsideBundle }}{{ document_category.documentClass }}{{ document_category.name | localize_translatable_string}} + + {{ 'show' | trans }} + + + {{ 'edit' | trans }} + +
{{ 'no records found' | trans }}
+ + {{ 'Create new' | trans }} +{% endblock %} diff --git a/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/new.html.twig b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/new.html.twig new file mode 100644 index 000000000..a87d42585 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/new.html.twig @@ -0,0 +1,29 @@ +{# + * Copyright (C) 2018, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} +{% extends "ChillMainBundle::Admin/layout.html.twig" %} + +{% block title %}{{ 'Create new document category' | trans }}{% endblock title %} + +{% block admin_content %} +

{{ 'Create new DocumentCategory' | trans }}

+ + {{ include('ChillDocStoreBundle:DocumentCategory:_form.html.twig') }} + + + {{ 'Back to the category list' | trans }} + +{% endblock %} diff --git a/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/show.html.twig b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/show.html.twig new file mode 100644 index 000000000..cf323781d --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/DocumentCategory/show.html.twig @@ -0,0 +1,54 @@ +{# + * Copyright (C) 2018, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} +{% extends "ChillMainBundle::Admin/layout.html.twig" %} + +{% block title %}{{ 'Document category show'|trans }}{% endblock title %} + +{% block admin_content %} +

Document category

+ + + + + + + + + + + + + + + + + + + + +
{{ 'Creator bundle id' | trans }}{{ document_category.bundleId }}
{{ 'Internal id inside creator bundle' | trans }}{{ document_category.idInsideBundle }}
{{ 'Document class' | trans }}{{ document_category.documentClass }}
{{ 'Name' | trans }}{{ document_category.name | localize_translatable_string }}
+ + + {{ 'Back to the category list' | trans }} + + + + {{ 'Edit' | trans }} + + + {{ include('ChillDocStoreBundle:DocumentCategory:_delete_form.html.twig') }} +{% endblock %} diff --git a/src/Bundle/ChillDocStore/Resources/views/Form/fields.html.twig b/src/Bundle/ChillDocStore/Resources/views/Form/fields.html.twig new file mode 100644 index 000000000..3ab0ff255 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/Form/fields.html.twig @@ -0,0 +1,22 @@ +{% block stored_object_widget %} +
+ {{ form_widget(form.filename) }} + {{ form_widget(form.keyInfos, { 'attr': { 'data-stored-object-key': 1 } }) }} + {{ form_widget(form.iv, { 'attr': { 'data-stored-object-iv': 1 } }) }} + {{ form_widget(form.type, { 'attr': { 'data-async-file-type': 1 } }) }} +
+{% endblock %} diff --git a/src/Bundle/ChillDocStore/Resources/views/Macro/macro.html.twig b/src/Bundle/ChillDocStore/Resources/views/Macro/macro.html.twig new file mode 100644 index 000000000..e42012a0b --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/Macro/macro.html.twig @@ -0,0 +1,15 @@ +{% macro download_button(storedObject, filename = null) %} + {% if storedObject is null %} + + {% else %} + + {{ 'Download'|trans }} + {% endif %} +{% endmacro %} diff --git a/src/Bundle/ChillDocStore/Resources/views/PersonDocument/_delete_form.html.twig b/src/Bundle/ChillDocStore/Resources/views/PersonDocument/_delete_form.html.twig new file mode 100644 index 000000000..80ae22935 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/PersonDocument/_delete_form.html.twig @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/src/Bundle/ChillDocStore/Resources/views/PersonDocument/edit.html.twig b/src/Bundle/ChillDocStore/Resources/views/PersonDocument/edit.html.twig new file mode 100644 index 000000000..704031abb --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/PersonDocument/edit.html.twig @@ -0,0 +1,64 @@ +{# + * Copyright (C) 2018, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} + +{% extends "ChillPersonBundle::layout.html.twig" %} + +{% set activeRouteKey = '' %} + +{% block title %}{{ 'Editing document for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %} +{% block personcontent %} +

{{ 'Edit Document' | trans }}

+ + {{ form_errors(form) }} + + {{ form_start(form) }} + + {{ form_row(form.title) }} + {{ form_row(form.date) }} + {{ form_row(form.category) }} + {{ form_row(form.scope) }} + {{ form_row(form.description) }} + {{ form_row(form.object, { 'label': 'Document', 'existing': document.object }) }} + +
    +
  • + + {{ 'Back to the list' | trans }} + +
  • +
  • + +
  • + {# {% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %} +
  • + {{ include('ChillDocStoreBundle:PersonDocument:_delete_form.html.twig') }} +
  • + {% endif %} #} +
+ + {{ form_end(form) }} + + +{% endblock %} + +{% block js %} + +{% endblock %} + +{% block css %} + +{% endblock %} diff --git a/src/Bundle/ChillDocStore/Resources/views/PersonDocument/index.html.twig b/src/Bundle/ChillDocStore/Resources/views/PersonDocument/index.html.twig new file mode 100644 index 000000000..5f6f3632a --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/PersonDocument/index.html.twig @@ -0,0 +1,83 @@ +{# + * Copyright (C) 2018, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} + +{% extends "ChillPersonBundle::layout.html.twig" %} + +{% set activeRouteKey = '' %} + +{% import "@ChillDocStore/Macro/macro.html.twig" as m %} + +{% block title %}{{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %} + +{% block js %} + +{% endblock %} + +{% block personcontent %} +

{{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}

+ + + + + + + + + + + + {% for document in documents %} + + + + + + + {% else %} + + + + {% endfor %} + +
{{ 'Title' | trans }}{{ 'Category'|trans }}{{ 'Circle' | trans }}{{ 'Actions' | trans }}
{{ document.title }}{{ document.category.name|localize_translatable_string }}{{ document.scope.name|localize_translatable_string }} +
    + {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %} +
  • + {{ m.download_button(document.object, document.title) }} +
  • +
  • + +
  • + {% endif %} + {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %} +
  • + +
  • + {% endif %} +
+
{{ 'Any document found'|trans }}
+ + {% if is_granted('CHILL_PERSON_DOCUMENT_CREATE', person) %} + + {% endif %} +{% endblock %} diff --git a/src/Bundle/ChillDocStore/Resources/views/PersonDocument/new.html.twig b/src/Bundle/ChillDocStore/Resources/views/PersonDocument/new.html.twig new file mode 100644 index 000000000..7a643b985 --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/PersonDocument/new.html.twig @@ -0,0 +1,56 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} +{% extends "ChillPersonBundle::layout.html.twig" %} + +{% set activeRouteKey = '' %} + +{% block title %}{{ 'New document for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %} + +{% block personcontent %} +

{{ 'New document for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}

+ + {{ form_errors(form) }} + + {{ form_start(form) }} + + {{ form_row(form.title) }} + {{ form_row(form.date) }} + {{ form_row(form.category) }} + {{ form_row(form.scope) }} + {{ form_row(form.description) }} + {{ form_row(form.object, { 'label': 'Document', 'existing': document.object }) }} + + + {{ form_end(form) }} +{% endblock %} + +{% block js %} + +{% endblock %} + +{% block css %} + +{% endblock %} diff --git a/src/Bundle/ChillDocStore/Resources/views/PersonDocument/show.html.twig b/src/Bundle/ChillDocStore/Resources/views/PersonDocument/show.html.twig new file mode 100644 index 000000000..5a929d5ea --- /dev/null +++ b/src/Bundle/ChillDocStore/Resources/views/PersonDocument/show.html.twig @@ -0,0 +1,75 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +#} +{% extends "ChillPersonBundle::layout.html.twig" %} + +{% set activeRouteKey = '' %} + +{% import "@ChillDocStore/Macro/macro.html.twig" as m %} + +{% block title %}{{ 'Detail of document of %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %} + +{% block js %} + +{% endblock %} + +{% block personcontent %} +

{{ 'Document %title%' | trans({ '%title%': document.title }) }}

+ +
+
{{ 'Title'|trans }}
+
{{ document.title }}
+ +
{{ 'Scope' | trans }}
+
{{ document.scope.name | localize_translatable_string }}
+ +
{{ 'Category'|trans }}
+
{{ document.category.name|localize_translatable_string }}
+ +
{{ 'Description' | trans }}
+
+ {% if document.description is empty %} + {{ 'Any description'|trans }} + {% else %} +
+ {{ document.description }} +
+ {% endif %} +
+ +
+ +
    +
  • + + {{ 'Back to the list' | trans }} + +
  • + +
  • + {{ m.download_button(document.object, document.title) }} +
  • + + {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %} +
  • + + {{ 'Edit' | trans }} + +
  • + {% endif %} + + {# {{ include('ChillDocStoreBundle:PersonDocument:_delete_form.html.twig') }} #} +{% endblock %} diff --git a/src/Bundle/ChillDocStore/Security/Authorization/PersonDocumentVoter.php b/src/Bundle/ChillDocStore/Security/Authorization/PersonDocumentVoter.php new file mode 100644 index 000000000..47bfed38b --- /dev/null +++ b/src/Bundle/ChillDocStore/Security/Authorization/PersonDocumentVoter.php @@ -0,0 +1,93 @@ +. + */ + +namespace Chill\DocStoreBundle\Security\Authorization; + +use Chill\MainBundle\Security\Authorization\AbstractChillVoter; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; +use Chill\DocStoreBundle\Entity\PersonDocument; +use Chill\PersonBundle\Entity\Person; + +/** + * + */ +class PersonDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface +{ + const CREATE = 'CHILL_PERSON_DOCUMENT_CREATE'; + const SEE = 'CHILL_PERSON_DOCUMENT_SEE'; + const SEE_DETAILS = 'CHILL_PERSON_DOCUMENT_SEE_DETAILS'; + const UPDATE = 'CHILL_PERSON_DOCUMENT_UPDATE'; + const DELETE = 'CHILL_PERSON_DOCUMENT_DELETE'; + + /** + * + * @var AuthorizationHelper + */ + protected $helper; + + public function __construct(AuthorizationHelper $helper) + { + $this->helper = $helper; + } + + public function getRoles() + { + return [ + self::CREATE, + self::SEE, + self::SEE_DETAILS, + self::UPDATE, + self::DELETE + ]; + } + + protected function supports($attribute, $subject) + { + if (\in_array($attribute, $this->getRoles()) && $subject instanceof PersonDocument) { + return true; + } + + if ($subject instanceof Person && $attribute === self::CREATE) { + return true; + } + + return false; + } + + protected function isGranted($attribute, $report, $user = null) + { + if (! $user instanceof \Chill\MainBundle\Entity\User){ + return false; + } + + return $this->helper->userHasAccess($user, $report, $attribute); + } + + public function getRolesWithoutScope() + { + return array(); + } + + + public function getRolesWithHierarchy() + { + return ['PersonDocument' => $this->getRoles() ]; + } +} diff --git a/src/Bundle/ChillDocStore/chill.webpack.config.js b/src/Bundle/ChillDocStore/chill.webpack.config.js new file mode 100644 index 000000000..e98df7a23 --- /dev/null +++ b/src/Bundle/ChillDocStore/chill.webpack.config.js @@ -0,0 +1,5 @@ +module.exports = function(encore) { + let file = __dirname + '/Resources/public/module/async_upload/index.js'; + + encore.addEntry('async_upload', file); +}; diff --git a/src/Bundle/ChillDocStore/composer.json b/src/Bundle/ChillDocStore/composer.json new file mode 100644 index 000000000..32cc7695d --- /dev/null +++ b/src/Bundle/ChillDocStore/composer.json @@ -0,0 +1,13 @@ +{ + "name": "chill-project/chill-doc-store", + "description": "A Chill bundle to store documents", + "type": "symfony-bundle", + "autoload": { + "psr-4": { "Chill\\DocStoreBundle\\" : "" } + }, + "require": { + "chill-project/person": "~1.5.0", + "champs-libres/async-uploader-bundle": "~1.0" + }, + "license": "AGPL-3.0" +}