mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-03 18:58:24 +00:00 
			
		
		
		
	Compare commits
	
		
			190 Commits
		
	
	
		
			v3.0.0-RC7
			...
			signature-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					bf61324c1d | ||
| 
						 | 
					abf20b0cf2 | ||
| 
						 | 
					86896a12e6 | ||
| 
						 | 
					3a959b7044 | ||
| 
						 | 
					f8d95384ea | ||
| 
						
						
							
						
						8a374864fa
	
				 | 
					
					
						|||
| 
						
						
							
						
						bb848746d5
	
				 | 
					
					
						|||
| 
						
						
							
						
						3738c110f8
	
				 | 
					
					
						|||
| 
						
						
							
						
						f57fdb2b4c
	
				 | 
					
					
						|||
| 
						
						
							
						
						b57824fc7e
	
				 | 
					
					
						|||
| 
						
						
							
						
						6b4e1ed2d3
	
				 | 
					
					
						|||
| 
						
						
							
						
						b0485dbcc8
	
				 | 
					
					
						|||
| 
						
						
							
						
						c16219dc6d
	
				 | 
					
					
						|||
| ad47804c91 | |||
| 85e2466611 | |||
| 94d6b5eff8 | |||
| d87f380f16 | |||
| 58bf722fae | |||
| 50fb79ebbf | |||
| 58912f1d98 | |||
| 9604ba5f4b | |||
| b689a51a48 | |||
| 8c0d2f58ba | |||
| 212230448b | |||
| 2bfb8fe387 | |||
| 6362b98a00 | |||
| 6e2a08cae8 | |||
| 305105faae | |||
| 85811cc6ae | |||
| 7eee995627 | |||
| c0c448fb39 | |||
| 6445342136 | |||
| d52e54fd2a | |||
| 547a9d1369 | |||
| 288a02f5b7 | |||
| 2f9884072c | |||
| ee45ff61a6 | |||
| 5dfd8daf3a | |||
| 
						
						
							
						
						564813ef3d
	
				 | 
					
					
						|||
| 
						
						
							
						
						5fed42a623
	
				 | 
					
					
						|||
| b19dd4fc11 | |||
| 
						
						
							
						
						44226d6f7f
	
				 | 
					
					
						|||
| d75607a1d2 | |||
| bf66af0f25 | |||
| 15f3e474a0 | |||
| 5623cf946e | |||
| 0a6f3a99da | |||
| 50bd9f32c3 | |||
| 1396304af5 | |||
| c33e4adeec | |||
| 7351a35c42 | |||
| 72e3325626 | |||
| 0a46b5304d | |||
| 
						 | 
					e57d52d00e | ||
| 64e527672d | |||
| 
						
						
							
						
						123168a5ee
	
				 | 
					
					
						|||
| 
						
						
							
						
						3836d0dc9b
	
				 | 
					
					
						|||
| 
						
						
							
						
						51ab4bef38
	
				 | 
					
					
						|||
| 
						 | 
					567ca8a26f | ||
| 
						 | 
					111305d09c | ||
| 
						 | 
					67395f52b5 | ||
| 
						 | 
					421226c0dc | ||
| 
						 | 
					77da2c1ac6 | ||
| 
						 | 
					39d3ba2f40 | ||
| 
						 | 
					fb62e54d63 | ||
| 
						 | 
					c968d6c541 | ||
| 
						 | 
					c428e6665f | ||
| 
						 | 
					5b7e3f0336 | ||
| 
						 | 
					0c8ef37860 | ||
| 
						 | 
					794c479b9e | ||
| 
						 | 
					1bee3114ac | ||
| 
						 | 
					1344b65dd4 | ||
| 
						 | 
					68dcf4dd28 | ||
| 
						 | 
					b0a8fd54a8 | ||
| 
						 | 
					0f589ec57e | ||
| 
						 | 
					2d4fc45a0c | ||
| 
						 | 
					c80f23f0db | ||
| 
						 | 
					c950400fe2 | ||
| 
						 | 
					21c1e77d36 | ||
| 
						 | 
					bbfd0caf10 | ||
| 
						 | 
					9192883217 | ||
| 3836622d27 | |||
| cc2c4be1b0 | |||
| 873940786f | |||
| db73dcffc7 | |||
| 
						
						
							
						
						8aec69f0f9
	
				 | 
					
					
						|||
| 
						
						
							
						
						9f88eef249
	
				 | 
					
					
						|||
| 
						
						
							
						
						d689ce9aef
	
				 | 
					
					
						|||
| 
						
						
							
						
						d5e4991982
	
				 | 
					
					
						|||
| 
						
						
							
						
						ca68b58246
	
				 | 
					
					
						|||
| 
						
						
							
						
						747a1de321
	
				 | 
					
					
						|||
| 
						
						
							
						
						9e92ede16f
	
				 | 
					
					
						|||
| 
						
						
							
						
						31f842471a
	
				 | 
					
					
						|||
| 
						
						
							
						
						7d0f9175be
	
				 | 
					
					
						|||
| 
						
						
							
						
						e83307ca6d
	
				 | 
					
					
						|||
| 215eba41b7 | |||
| 52a3d1be1b | |||
| 8d543be5cc | |||
| 0474b25859 | |||
| 
						
						
							
						
						db94af0958
	
				 | 
					
					
						|||
| 3e8805bdda | |||
| 
						
						
							
						
						a887602f4f
	
				 | 
					
					
						|||
| 
						
						
							
						
						c1cf27c42d
	
				 | 
					
					
						|||
| 
						
						
							
						
						fe6b4848e6
	
				 | 
					
					
						|||
| 
						
						
							
						
						b5af9f7b63
	
				 | 
					
					
						|||
| 
						
						
							
						
						7f3de62b2c
	
				 | 
					
					
						|||
| cfa51cd659 | |||
| facc4affed | |||
| f9122341d1 | |||
| 7dd5f542a6 | |||
| 3b80d9a93b | |||
| 790576863f | |||
| 25e89571f7 | |||
| 435836c7d1 | |||
| af4db22184 | |||
| 2adc8b3bf6 | |||
| 21b79c1981 | |||
| 428494ca1f | |||
| 5d57ec8a3b | |||
| 719fabc878 | |||
| e9a9a3430f | |||
| c648a560cc | |||
| 3d7c8596ee | |||
| 345f379650 | |||
| 3262a1dd02 | |||
| a9f4f8c973 | |||
| c19c597ba0 | |||
| 03800029c9 | |||
| 064dfc5a56 | |||
| ba95687f46 | |||
| 
						
						
							
						
						a309cc0774
	
				 | 
					
					
						|||
| 5b0babb9b0 | |||
| ac2f314395 | |||
| 8c92d11722 | |||
| 
						
						
							
						
						3db4fff80d
	
				 | 
					
					
						|||
| fb743b522d | |||
| d1653a074b | |||
| 254122d125 | |||
| c9d2e37cee | |||
| 
						
						
							
						
						c9d54a5fea
	
				 | 
					
					
						|||
| 
						
						
							
						
						86c862e69d
	
				 | 
					
					
						|||
| 
						
						
							
						
						9bc6fe6aff
	
				 | 
					
					
						|||
| 
						
						
							
						
						18a03fd740
	
				 | 
					
					
						|||
| e9d4b9e2ab | |||
| efaad1981d | |||
| 742f2540f6 | |||
| bab6528ed6 | |||
| a25f2c7539 | |||
| c06e76a0ee | |||
| 4607c36b57 | |||
| 7c03a25f1a | |||
| cce04ee490 | |||
| e54633d14d | |||
| d9892f6822 | |||
| f75c7a0232 | |||
| 062afd6695 | |||
| 830dace1ba | |||
| 2ce9810243 | |||
| 26b3d84d62 | |||
| 30078db841 | |||
| aaac80be84 | |||
| a0fead48e1 | |||
| 2d09efb2e0 | |||
| 3a87513a11 | |||
| d3956319ca | |||
| bd36735cb1 | |||
| 1310d53589 | |||
| 
						
						
							
						
						610239930b
	
				 | 
					
					
						|||
| b65e2c62c4 | |||
| 
						
						
							
						
						89f5231649
	
				 | 
					
					
						|||
| 73797b98f6 | |||
| 3d40db7493 | |||
| 760d65b972 | |||
| d26fa6bde6 | |||
| 427f232ab8 | |||
| 
						
						
							
						
						99818c211d
	
				 | 
					
					
						|||
| 
						
						
							
						
						a9f0059743
	
				 | 
					
					
						|||
| 
						
						
							
						
						5bc542a567
	
				 | 
					
					
						|||
| 482f279dc5 | |||
| e0828b1f0f | |||
| e015f71bb0 | |||
| 04a48f22ad | |||
| ad4fe80240 | |||
| 4b82e67952 | |||
| 
						
						
							
						
						c8ccce83fd
	
				 | 
					
					
						|||
| e9a9262fae | |||
| d9e37d0958 | |||
| 65c41e6fa9 | |||
| 7923b5a1ef | |||
| 4a229ebf6b | 
							
								
								
									
										8
									
								
								.changes/unreleased/Feature-20240614-153236.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.changes/unreleased/Feature-20240614-153236.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: |-
 | 
			
		||||
  Electronic signature
 | 
			
		||||
 | 
			
		||||
  Implementation of the electronic signature for documents within chill.
 | 
			
		||||
time: 2024-06-14T15:32:36.875891692+02:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: ""
 | 
			
		||||
							
								
								
									
										7
									
								
								.changes/unreleased/Feature-20240614-153537.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.changes/unreleased/Feature-20240614-153537.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: The behavoir of the voters for stored objects is adjusted so as to limit edit
 | 
			
		||||
  and delete possibilities to users related to the activity, social action or workflow
 | 
			
		||||
  entity.
 | 
			
		||||
time: 2024-06-14T15:35:37.582159301+02:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "286"
 | 
			
		||||
							
								
								
									
										5
									
								
								.changes/unreleased/Feature-20240718-151233.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/unreleased/Feature-20240718-151233.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: Metadata form added for person signatures
 | 
			
		||||
time: 2024-07-18T15:12:33.8134266+02:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "288"
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
## v2.23.1 - 2024-07-25
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix export activities linked to accompanying period between two dates 
 | 
			
		||||
							
								
								
									
										5
									
								
								.changes/v3.0.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/v3.0.0.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
## v3.0.0 - 2024-08-26
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix delete action for accompanying periods in draft state 
 | 
			
		||||
* Fix connection to azure when making an calendar event in chill 
 | 
			
		||||
* CollectionType js fixes for remove button and adding multiple entries 
 | 
			
		||||
@@ -138,4 +138,4 @@ release:
 | 
			
		||||
        - echo "running release_job"
 | 
			
		||||
    release:
 | 
			
		||||
        tag_name: '$CI_COMMIT_TAG'
 | 
			
		||||
        description: "./.changes/v$CI_COMMIT_TAG.md"
 | 
			
		||||
        description: "./.changes/$CI_COMMIT_TAG.md"
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
 | 
			
		||||
and is generated by [Changie](https://github.com/miniscruff/changie).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## v3.0.0 - 2024-08-26
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix delete action for accompanying periods in draft state 
 | 
			
		||||
* Fix connection to azure when making an calendar event in chill 
 | 
			
		||||
* CollectionType js fixes for remove button and adding multiple entries 
 | 
			
		||||
 | 
			
		||||
## v2.23.0 - 2024-07-23
 | 
			
		||||
### Feature
 | 
			
		||||
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles 
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,7 @@
 | 
			
		||||
        "phpoffice/phpspreadsheet": "^1.16",
 | 
			
		||||
        "ramsey/uuid-doctrine": "^1.7",
 | 
			
		||||
        "sensio/framework-extra-bundle": "^5.5",
 | 
			
		||||
        "smalot/pdfparser": "^2.10",
 | 
			
		||||
        "spomky-labs/base64url": "^2.0",
 | 
			
		||||
        "symfony/asset": "^5.4",
 | 
			
		||||
        "symfony/browser-kit": "^5.4",
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,7 @@
 | 
			
		||||
    "marked": "^12.0.2",
 | 
			
		||||
    "masonry-layout": "^4.2.2",
 | 
			
		||||
    "mime": "^4.0.0",
 | 
			
		||||
    "pdfjs-dist": "^4.3.136",
 | 
			
		||||
    "swagger-ui": "^4.15.5",
 | 
			
		||||
    "vis-network": "^9.1.0",
 | 
			
		||||
    "vue": "^3.2.37",
 | 
			
		||||
 
 | 
			
		||||
@@ -69,9 +69,8 @@ return static function (RectorConfig $rectorConfig): void {
 | 
			
		||||
 | 
			
		||||
    // skip some path...
 | 
			
		||||
    $rectorConfig->skip([
 | 
			
		||||
        // we must adapt service definition
 | 
			
		||||
        \Rector\Symfony\Symfony28\Rector\MethodCall\GetToConstructorInjectionRector::class,
 | 
			
		||||
        \Rector\Symfony\Symfony34\Rector\Closure\ContainerGetNameToTypeInTestsRector::class,
 | 
			
		||||
        // waiting for fixing this bug: https://github.com/rectorphp/rector-doctrine/issues/342
 | 
			
		||||
        \Rector\Doctrine\CodeQuality\Rector\Property\ImproveDoctrineCollectionDocTypeInEntityRector::class,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $rectorConfig->ruleWithConfiguration(AnnotationToAttributeRector::class, [
 | 
			
		||||
 
 | 
			
		||||
@@ -80,7 +80,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
 | 
			
		||||
    private \DateTime $date;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<StoredObject>
 | 
			
		||||
     * @var Collection<int, StoredObject>
 | 
			
		||||
     */
 | 
			
		||||
    #[Assert\Valid(traverse: true)]
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist'])]
 | 
			
		||||
@@ -107,7 +107,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
 | 
			
		||||
    private ?Person $person = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<Person>
 | 
			
		||||
     * @var Collection<int, \Chill\PersonBundle\Entity\Person>
 | 
			
		||||
     */
 | 
			
		||||
    #[Groups(['read', 'docgen:read'])]
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: Person::class)]
 | 
			
		||||
@@ -117,7 +117,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
 | 
			
		||||
    private PrivateCommentEmbeddable $privateComment;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<ActivityReason>
 | 
			
		||||
     * @var Collection<int, ActivityReason>
 | 
			
		||||
     */
 | 
			
		||||
    #[Groups(['docgen:read'])]
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: ActivityReason::class)]
 | 
			
		||||
@@ -132,7 +132,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
 | 
			
		||||
    private string $sentReceived = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<SocialAction>
 | 
			
		||||
     * @var Collection<int, \Chill\PersonBundle\Entity\SocialWork\SocialAction>
 | 
			
		||||
     */
 | 
			
		||||
    #[Groups(['read', 'docgen:read'])]
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: SocialAction::class)]
 | 
			
		||||
@@ -140,7 +140,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
 | 
			
		||||
    private Collection $socialActions;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<SocialIssue>
 | 
			
		||||
     * @var Collection<int, SocialIssue>
 | 
			
		||||
     */
 | 
			
		||||
    #[Groups(['read', 'docgen:read'])]
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: SocialIssue::class)]
 | 
			
		||||
@@ -148,7 +148,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
 | 
			
		||||
    private Collection $socialIssues;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<ThirdParty>
 | 
			
		||||
     * @var Collection<int, ThirdParty>
 | 
			
		||||
     */
 | 
			
		||||
    #[Groups(['read', 'docgen:read'])]
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: ThirdParty::class)]
 | 
			
		||||
@@ -162,7 +162,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
 | 
			
		||||
    private ?User $user = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[Groups(['read', 'docgen:read'])]
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
 
 | 
			
		||||
@@ -40,9 +40,9 @@ class ActivityReasonCategory implements \Stringable
 | 
			
		||||
    /**
 | 
			
		||||
     * Array of ActivityReason.
 | 
			
		||||
     *
 | 
			
		||||
     * @var Collection<ActivityReason>
 | 
			
		||||
     * @var Collection<int, ActivityReason>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: ActivityReason::class, mappedBy: 'category')]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'category', targetEntity: ActivityReason::class)]
 | 
			
		||||
    private Collection $reasons;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -73,15 +73,13 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt
 | 
			
		||||
 | 
			
		||||
        $qb->andWhere(
 | 
			
		||||
            $qb->expr()->exists(
 | 
			
		||||
                'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = activity.accompanyingPeriod AND {$alias}.id = activity.id"
 | 
			
		||||
                'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = activity.accompanyingPeriod"
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $qb
 | 
			
		||||
            ->setParameter($from, $this->rollingDateConverter->convert($data['start_date']))
 | 
			
		||||
            ->setParameter($to, $this->rollingDateConverter->convert($data['end_date']));
 | 
			
		||||
 | 
			
		||||
        dump($qb->getQuery()->getResult());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function applyOn()
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,8 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\ActivityBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\ActivityBundle\Entity\Activity;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 | 
			
		||||
@@ -23,7 +25,7 @@ use Doctrine\Persistence\ManagerRegistry;
 | 
			
		||||
 * @method Activity[]    findAll()
 | 
			
		||||
 * @method Activity[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 | 
			
		||||
 */
 | 
			
		||||
class ActivityRepository extends ServiceEntityRepository
 | 
			
		||||
class ActivityRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(ManagerRegistry $registry)
 | 
			
		||||
    {
 | 
			
		||||
@@ -97,4 +99,16 @@ class ActivityRepository extends ServiceEntityRepository
 | 
			
		||||
 | 
			
		||||
        return $qb->getQuery()->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?Activity
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->createQueryBuilder('a');
 | 
			
		||||
        $query = $qb
 | 
			
		||||
            ->leftJoin('a.documents', 'ad')
 | 
			
		||||
            ->where('ad.id = :storedObjectId')
 | 
			
		||||
            ->setParameter('storedObjectId', $storedObject->getId())
 | 
			
		||||
            ->getQuery();
 | 
			
		||||
 | 
			
		||||
        return $query->getOneOrNullResult();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\ActivityBundle\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\ActivityBundle\Entity\Activity;
 | 
			
		||||
use Chill\ActivityBundle\Repository\ActivityRepository;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly ActivityRepository $repository,
 | 
			
		||||
        Security $security,
 | 
			
		||||
        WorkflowStoredObjectPermissionHelper $workflowDocumentService
 | 
			
		||||
    ) {
 | 
			
		||||
        parent::__construct($security, $workflowDocumentService);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getRepository(): AssociatedEntityToStoredObjectInterface
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getClass(): string
 | 
			
		||||
    {
 | 
			
		||||
        return Activity::class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function attributeToRole(StoredObjectRoleEnum $attribute): string
 | 
			
		||||
    {
 | 
			
		||||
        return match ($attribute) {
 | 
			
		||||
            StoredObjectRoleEnum::EDIT => ActivityVoter::UPDATE,
 | 
			
		||||
            StoredObjectRoleEnum::SEE => ActivityVoter::SEE_DETAILS,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function canBeAssociatedWithWorkflow(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -22,9 +22,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
 | 
			
		||||
class AsideActivityCategory
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<AsideActivityCategory>
 | 
			
		||||
     * @var Collection<int, AsideActivityCategory>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: AsideActivityCategory::class, mappedBy: 'parent')]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'parent', targetEntity: AsideActivityCategory::class)]
 | 
			
		||||
    private Collection $children;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
 | 
			
		||||
    private int $dateTimeVersion = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<CalendarDoc>
 | 
			
		||||
     * @var Collection<int, \Chill\CalendarBundle\Entity\CalendarDoc>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'calendar', targetEntity: CalendarDoc::class, orphanRemoval: true)]
 | 
			
		||||
    private Collection $documents;
 | 
			
		||||
@@ -120,7 +120,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection&Selectable<int, Invite>
 | 
			
		||||
     * @var \Doctrine\Common\Collections\Collection<int, \Chill\CalendarBundle\Entity\Invite>&Selectable
 | 
			
		||||
     */
 | 
			
		||||
    #[Serializer\Groups(['read', 'docgen:read'])]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'calendar', targetEntity: Invite::class, cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)]
 | 
			
		||||
@@ -143,7 +143,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
 | 
			
		||||
    private ?Person $person = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<Person>
 | 
			
		||||
     * @var Collection<int, Person>
 | 
			
		||||
     */
 | 
			
		||||
    #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
 | 
			
		||||
    #[Assert\Count(min: 1, minMessage: 'calendar.At least {{ limit }} person is required.')]
 | 
			
		||||
@@ -157,7 +157,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
 | 
			
		||||
    private PrivateCommentEmbeddable $privateComment;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<ThirdParty>
 | 
			
		||||
     * @var Collection<int, ThirdParty>
 | 
			
		||||
     */
 | 
			
		||||
    #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: ThirdParty::class)]
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ final class CalendarContextTest extends TestCase
 | 
			
		||||
    {
 | 
			
		||||
        $expected =
 | 
			
		||||
            [
 | 
			
		||||
                'track_datetime' => true,
 | 
			
		||||
                'trackDatetime' => true,
 | 
			
		||||
                'askMainPerson' => true,
 | 
			
		||||
                'mainPersonLabel' => 'docgen.calendar.Destinee',
 | 
			
		||||
                'askThirdParty' => false,
 | 
			
		||||
@@ -61,7 +61,7 @@ final class CalendarContextTest extends TestCase
 | 
			
		||||
    {
 | 
			
		||||
        $expected =
 | 
			
		||||
            [
 | 
			
		||||
                'track_datetime' => true,
 | 
			
		||||
                'trackDatetime' => true,
 | 
			
		||||
                'askMainPerson' => true,
 | 
			
		||||
                'mainPersonLabel' => 'docgen.calendar.Destinee',
 | 
			
		||||
                'askThirdParty' => false,
 | 
			
		||||
 
 | 
			
		||||
@@ -23,9 +23,9 @@ class Option
 | 
			
		||||
    private bool $active = true;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<Option>
 | 
			
		||||
     * @var Collection<int, Option>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: Option::class, mappedBy: 'parent')]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'parent', targetEntity: Option::class)]
 | 
			
		||||
    private Collection $children;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
 
 | 
			
		||||
@@ -32,9 +32,9 @@ class CustomFieldsGroup
 | 
			
		||||
     * The custom fields of the group.
 | 
			
		||||
     * The custom fields are asc-ordered regarding to their property "ordering".
 | 
			
		||||
     *
 | 
			
		||||
     * @var Collection<CustomField>
 | 
			
		||||
     * @var Collection<int, CustomField>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: CustomField::class, mappedBy: 'customFieldGroup')]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'customFieldGroup', targetEntity: CustomField::class)]
 | 
			
		||||
    #[ORM\OrderBy(['ordering' => \Doctrine\Common\Collections\Criteria::ASC])]
 | 
			
		||||
    private Collection $customFields;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -89,6 +89,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
 | 
			
		||||
        $g = new SignedUrlPost(
 | 
			
		||||
            $url = $this->generateUrl($object_name),
 | 
			
		||||
            $expires,
 | 
			
		||||
            $object_name,
 | 
			
		||||
            $this->max_post_file_size,
 | 
			
		||||
            $max_file_count,
 | 
			
		||||
            $submit_delay,
 | 
			
		||||
@@ -127,7 +128,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
 | 
			
		||||
        ];
 | 
			
		||||
        $url = $url.'?'.\http_build_query($args);
 | 
			
		||||
 | 
			
		||||
        $signature = new SignedUrl(strtoupper($method), $url, $expires);
 | 
			
		||||
        $signature = new SignedUrl(strtoupper($method), $url, $expires, $object_name);
 | 
			
		||||
 | 
			
		||||
        $this->event_dispatcher->dispatch(
 | 
			
		||||
            new TempUrlGenerateEvent($signature)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,8 @@ readonly class SignedUrl
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
        public string $url,
 | 
			
		||||
        public \DateTimeImmutable $expires,
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
        public string $object_name,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ readonly class SignedUrlPost extends SignedUrl
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        string $url,
 | 
			
		||||
        \DateTimeImmutable $expires,
 | 
			
		||||
        string $object_name,
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
        public int $max_file_size,
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
@@ -31,6 +32,6 @@ readonly class SignedUrlPost extends SignedUrl
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
        public string $signature,
 | 
			
		||||
    ) {
 | 
			
		||||
        parent::__construct('POST', $url, $expires);
 | 
			
		||||
        parent::__construct('POST', $url, $expires, $object_name);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class DocumentPersonController.
 | 
			
		||||
@@ -40,6 +42,8 @@ class DocumentPersonController extends AbstractController
 | 
			
		||||
        protected TranslatorInterface $translator,
 | 
			
		||||
        protected EventDispatcherInterface $eventDispatcher,
 | 
			
		||||
        protected AuthorizationHelper $authorizationHelper,
 | 
			
		||||
        protected PDFSignatureZoneParser $PDFSignatureZoneParser,
 | 
			
		||||
        protected StoredObjectManagerInterface $storedObjectManagerInterface,
 | 
			
		||||
        private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
@@ -197,4 +201,36 @@ class DocumentPersonController extends AbstractController
 | 
			
		||||
            ['document' => $document, 'person' => $person]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Route(path: '/{id}/signature', name: 'person_document_signature', methods: 'GET')]
 | 
			
		||||
    public function signature(Person $person, PersonDocument $document): Response
 | 
			
		||||
    {
 | 
			
		||||
        $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
 | 
			
		||||
        $this->denyAccessUnlessGranted('CHILL_PERSON_DOCUMENT_SEE', $document);
 | 
			
		||||
 | 
			
		||||
        $event = new PrivacyEvent($person, [
 | 
			
		||||
            'element_class' => PersonDocument::class,
 | 
			
		||||
            'element_id' => $document->getId(),
 | 
			
		||||
            'action' => 'show',
 | 
			
		||||
        ]);
 | 
			
		||||
        $this->eventDispatcher->dispatch($event, PrivacyEvent::PERSON_PRIVACY_EVENT);
 | 
			
		||||
 | 
			
		||||
        $storedObject = $document->getObject();
 | 
			
		||||
        $content = $this->storedObjectManagerInterface->read($storedObject);
 | 
			
		||||
        $zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
 | 
			
		||||
 | 
			
		||||
        $signature = [];
 | 
			
		||||
        $signature['id'] = 1;
 | 
			
		||||
        $signature['storedObject'] = [ // TEMP
 | 
			
		||||
            'filename' => $storedObject->getFilename(),
 | 
			
		||||
            'iv' => $storedObject->getIv(),
 | 
			
		||||
            'keyInfos' => $storedObject->getKeyInfos(),
 | 
			
		||||
        ];
 | 
			
		||||
        $signature['zones'] = $zones;
 | 
			
		||||
 | 
			
		||||
        return $this->render(
 | 
			
		||||
            '@ChillDocStore/PersonDocument/signature.html.twig',
 | 
			
		||||
            ['document' => $document, 'person' => $person, 'signature' => $signature]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,67 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\Messenger\MessageBusInterface;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
 | 
			
		||||
class SignatureRequestController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly MessageBusInterface $messageBus,
 | 
			
		||||
        private readonly StoredObjectManagerInterface $storedObjectManager,
 | 
			
		||||
        private readonly EntityWorkflowManager $entityWorkflowManager,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
 | 
			
		||||
    public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse
 | 
			
		||||
    {
 | 
			
		||||
        $entityWorkflow = $signature->getStep()->getEntityWorkflow();
 | 
			
		||||
        $storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
 | 
			
		||||
        $content = $this->storedObjectManager->read($storedObject);
 | 
			
		||||
 | 
			
		||||
        $data = \json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR); // TODO parse payload: json_decode ou, mieux, dataTransfertObject
 | 
			
		||||
        $zone = new PDFSignatureZone(
 | 
			
		||||
            $data['zone']['index'],
 | 
			
		||||
            $data['zone']['x'],
 | 
			
		||||
            $data['zone']['y'],
 | 
			
		||||
            $data['zone']['height'],
 | 
			
		||||
            $data['zone']['width'],
 | 
			
		||||
            new PDFPage($data['zone']['PDFPage']['index'], $data['zone']['PDFPage']['width'], $data['zone']['PDFPage']['height'])
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->messageBus->dispatch(new RequestPdfSignMessage(
 | 
			
		||||
            $signature->getId(),
 | 
			
		||||
            $zone,
 | 
			
		||||
            $data['zone']['index'],
 | 
			
		||||
            'test signature', // reason (string)
 | 
			
		||||
            'Mme Caroline Diallo', // signerText (string)
 | 
			
		||||
            $content
 | 
			
		||||
        ));
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(null, JsonResponse::HTTP_OK, []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/document/workflow/{id}/check-signature', name: 'chill_docstore_check_signature')]
 | 
			
		||||
    public function checkSignature(EntityWorkflowStepSignature $signature): JsonResponse
 | 
			
		||||
    {
 | 
			
		||||
        return new JsonResponse($signature->getState(), JsonResponse::HTTP_OK, []);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\DependencyInjection;
 | 
			
		||||
use Chill\DocStoreBundle\Controller\StoredObjectApiController;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
 | 
			
		||||
use Symfony\Component\Config\FileLocator;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
 | 
			
		||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
 | 
			
		||||
@@ -35,6 +36,8 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
 | 
			
		||||
 | 
			
		||||
        $container->setParameter('chill_doc_store', $config);
 | 
			
		||||
 | 
			
		||||
        $container->registerForAutoconfiguration(StoredObjectVoterInterface::class)->addTag('stored_object_voter');
 | 
			
		||||
 | 
			
		||||
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
 | 
			
		||||
        $loader->load('services.yaml');
 | 
			
		||||
        $loader->load('services/controller.yaml');
 | 
			
		||||
@@ -42,6 +45,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
 | 
			
		||||
        $loader->load('services/fixtures.yaml');
 | 
			
		||||
        $loader->load('services/form.yaml');
 | 
			
		||||
        $loader->load('services/templating.yaml');
 | 
			
		||||
        $loader->load('services/security.yaml');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function prepend(ContainerBuilder $container)
 | 
			
		||||
 
 | 
			
		||||
@@ -12,13 +12,14 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\DocStoreBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
class AccompanyingCourseDocumentRepository implements ObjectRepository
 | 
			
		||||
class AccompanyingCourseDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
 | 
			
		||||
{
 | 
			
		||||
    private readonly EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
@@ -45,6 +46,16 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
 | 
			
		||||
        return $qb->getQuery()->getSingleScalarResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->repository->createQueryBuilder('d');
 | 
			
		||||
        $query = $qb->where('d.object = :storedObject')
 | 
			
		||||
            ->setParameter('storedObject', $storedObject)
 | 
			
		||||
            ->getQuery();
 | 
			
		||||
 | 
			
		||||
        return $query->getOneOrNullResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function find($id): ?AccompanyingCourseDocument
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->find($id);
 | 
			
		||||
@@ -55,7 +66,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
 | 
			
		||||
    }
 | 
			
		||||
@@ -65,7 +76,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
 | 
			
		||||
        return $this->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName()
 | 
			
		||||
    public function getClassName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return AccompanyingCourseDocument::class;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
 | 
			
		||||
interface AssociatedEntityToStoredObjectInterface
 | 
			
		||||
{
 | 
			
		||||
    public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object;
 | 
			
		||||
}
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\DocStoreBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\PersonDocument;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
@@ -19,7 +20,7 @@ use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
/**
 | 
			
		||||
 * @template ObjectRepository<PersonDocument::class>
 | 
			
		||||
 */
 | 
			
		||||
readonly class PersonDocumentRepository implements ObjectRepository
 | 
			
		||||
readonly class PersonDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
 | 
			
		||||
{
 | 
			
		||||
    private EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
@@ -53,4 +54,14 @@ readonly class PersonDocumentRepository implements ObjectRepository
 | 
			
		||||
    {
 | 
			
		||||
        return PersonDocument::class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->repository->createQueryBuilder('d');
 | 
			
		||||
        $query = $qb->where('d.object = :storedObject')
 | 
			
		||||
            ->setParameter('storedObject', $storedObject)
 | 
			
		||||
            ->getQuery();
 | 
			
		||||
 | 
			
		||||
        return $query->getOneOrNullResult();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,11 +26,11 @@ export interface StoredObject {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface StoredObjectCreated {
 | 
			
		||||
    status: "stored_object_created",
 | 
			
		||||
    filename: string,
 | 
			
		||||
    iv: Uint8Array,
 | 
			
		||||
    keyInfos: object,
 | 
			
		||||
    type: string,
 | 
			
		||||
  status: "stored_object_created",
 | 
			
		||||
  filename: string,
 | 
			
		||||
  iv: Uint8Array,
 | 
			
		||||
  keyInfos: object,
 | 
			
		||||
  type: string,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface StoredObjectStatusChange {
 | 
			
		||||
@@ -51,14 +51,37 @@ export type WopiEditButtonExecutableBeforeLeaveFunction = {
 | 
			
		||||
 * 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,
 | 
			
		||||
  method: "POST",
 | 
			
		||||
  max_file_size: number,
 | 
			
		||||
  max_file_count: 1,
 | 
			
		||||
  expires: number,
 | 
			
		||||
  submit_delay: 180,
 | 
			
		||||
  redirect: string,
 | 
			
		||||
  prefix: string,
 | 
			
		||||
  url: string,
 | 
			
		||||
  signature: string,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PDFPage {
 | 
			
		||||
  index: number,
 | 
			
		||||
  width: number,
 | 
			
		||||
  height: number,
 | 
			
		||||
}
 | 
			
		||||
export interface SignatureZone {
 | 
			
		||||
  index: number | null,
 | 
			
		||||
  x: number,
 | 
			
		||||
  y: number,
 | 
			
		||||
  width: number,
 | 
			
		||||
  height: number,
 | 
			
		||||
  PDFPage: PDFPage,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Signature {
 | 
			
		||||
  id: number,
 | 
			
		||||
  storedObject: StoredObject,
 | 
			
		||||
  zones: SignatureZone[],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error';
 | 
			
		||||
 | 
			
		||||
export type CanvasEvent = 'select' | 'add';
 | 
			
		||||
@@ -0,0 +1,559 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <teleport to="body">
 | 
			
		||||
    <modal v-if="modalOpen" @close="modalOpen = false">
 | 
			
		||||
      <template v-slot:header>
 | 
			
		||||
        <h2>{{ $t("signature_confirmation") }}</h2>
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:body>
 | 
			
		||||
        <div class="signature-modal-body text-center" v-if="loading">
 | 
			
		||||
          <p>{{ $t("electronic_signature_in_progress") }}</p>
 | 
			
		||||
          <div class="loading">
 | 
			
		||||
            <i
 | 
			
		||||
              class="fa fa-circle-o-notch fa-spin fa-3x"
 | 
			
		||||
              :title="$t('loading')"
 | 
			
		||||
            ></i>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="signature-modal-body text-center" v-else>
 | 
			
		||||
          <p>{{ $t("you_are_going_to_sign") }}</p>
 | 
			
		||||
          <p>{{ $t("are_you_sure") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:footer>
 | 
			
		||||
        <button class="btn btn-action" @click.prevent="confirmSign">
 | 
			
		||||
          {{ $t("yes") }}
 | 
			
		||||
        </button>
 | 
			
		||||
      </template>
 | 
			
		||||
    </modal>
 | 
			
		||||
  </teleport>
 | 
			
		||||
  <div class="col-12">
 | 
			
		||||
    <div
 | 
			
		||||
      class="row justify-content-center mb-2"
 | 
			
		||||
      v-if="signature.zones.length > 1"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="col-4 gap-2 d-grid">
 | 
			
		||||
        <button
 | 
			
		||||
          :disabled="userSignatureZone === null || userSignatureZone?.index < 1"
 | 
			
		||||
          class="btn btn-light btn-sm"
 | 
			
		||||
          @click="turnSignature(-1)"
 | 
			
		||||
        >
 | 
			
		||||
          {{ $t("last_sign_zone") }}
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-4 gap-2 d-grid">
 | 
			
		||||
        <button
 | 
			
		||||
          :disabled="userSignatureZone?.index >= signature.zones.length - 1"
 | 
			
		||||
          class="btn btn-light btn-sm"
 | 
			
		||||
          @click="turnSignature(1)"
 | 
			
		||||
        >
 | 
			
		||||
          {{ $t("next_sign_zone") }}
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div
 | 
			
		||||
      id="turn-page"
 | 
			
		||||
      class="row justify-content-center mb-2"
 | 
			
		||||
      v-if="pageCount > 1"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="col-6-sm col-3-md text-center">
 | 
			
		||||
        <button
 | 
			
		||||
          class="btn btn-light btn-sm"
 | 
			
		||||
          :disabled="page <= 1"
 | 
			
		||||
          @click="turnPage(-1)"
 | 
			
		||||
        >
 | 
			
		||||
          ❮
 | 
			
		||||
        </button>
 | 
			
		||||
        <span>page {{ page }} / {{ pageCount }}</span>
 | 
			
		||||
        <button
 | 
			
		||||
          class="btn btn-light btn-sm"
 | 
			
		||||
          :disabled="page >= pageCount"
 | 
			
		||||
          @click="turnPage(1)"
 | 
			
		||||
        >
 | 
			
		||||
          ❯
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div v-if="multiPage" class="col-12 text-center">
 | 
			
		||||
    <canvas
 | 
			
		||||
      v-for="p in pageCount"
 | 
			
		||||
      :key="p"
 | 
			
		||||
      class="m-auto"
 | 
			
		||||
      :id="`canvas-${p}`"
 | 
			
		||||
    ></canvas>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div v-else class="col-12 text-center">
 | 
			
		||||
    <canvas class="m-auto" :id="canvas"></canvas>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="col-12 p-4" id="action-buttons" v-if="signedState !== 'signed'">
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
      <div class="col-12 d-flex justify-content-end">
 | 
			
		||||
        <div class="col-4 col-xl-3 gap-2 d-grid">
 | 
			
		||||
          <button
 | 
			
		||||
            v-if="adding"
 | 
			
		||||
            class="btn btn-misc btn-cancel me-2 btn-sm"
 | 
			
		||||
            @click="removeNewZone()"
 | 
			
		||||
          >
 | 
			
		||||
            {{ $t("remove_sign_zone") }}
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-4 gap-2 d-grid">
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-create btn-sm"
 | 
			
		||||
            :class="{ active: canvasEvent === 'add' }"
 | 
			
		||||
            @click="toggleAddZone()"
 | 
			
		||||
          >
 | 
			
		||||
            {{ $t("add_sign_zone") }}
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="row">
 | 
			
		||||
      <div class="col-4">
 | 
			
		||||
        <button
 | 
			
		||||
          class="btn btn-action me-2"
 | 
			
		||||
          :disabled="!userSignatureZone"
 | 
			
		||||
          @click="sign"
 | 
			
		||||
        >
 | 
			
		||||
          {{ $t("sign") }}
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-8 d-flex justify-content-end">
 | 
			
		||||
        <button
 | 
			
		||||
          class="btn btn-misc me-2"
 | 
			
		||||
          :hidden="!userSignatureZone"
 | 
			
		||||
          @click="undoSign"
 | 
			
		||||
          v-if="signature.zones.length > 1"
 | 
			
		||||
        >
 | 
			
		||||
          {{ $t("choose_another_signature") }}
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
          class="btn btn-misc me-2"
 | 
			
		||||
          :hidden="!userSignatureZone"
 | 
			
		||||
          @click="undoSign"
 | 
			
		||||
          v-else
 | 
			
		||||
        >
 | 
			
		||||
          {{ $t("cancel") }}
 | 
			
		||||
        </button>
 | 
			
		||||
        <button class="btn btn-delete" @click="undoSign">
 | 
			
		||||
          {{ $t("cancel_signing") }}
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, Ref, reactive } from "vue";
 | 
			
		||||
import { useToast } from "vue-toast-notification";
 | 
			
		||||
import "vue-toast-notification/dist/theme-sugar.css";
 | 
			
		||||
import {
 | 
			
		||||
  CanvasEvent,
 | 
			
		||||
  Signature,
 | 
			
		||||
  SignatureZone,
 | 
			
		||||
  SignedState,
 | 
			
		||||
} from "../../types";
 | 
			
		||||
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
 | 
			
		||||
import * as pdfjsLib from "pdfjs-dist";
 | 
			
		||||
import {
 | 
			
		||||
  PDFDocumentProxy,
 | 
			
		||||
  PDFPageProxy,
 | 
			
		||||
} from "pdfjs-dist/types/src/display/api";
 | 
			
		||||
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import * as PdfWorker from "pdfjs-dist/build/pdf.worker.mjs";
 | 
			
		||||
console.log(PdfWorker); // incredible but this is needed
 | 
			
		||||
 | 
			
		||||
// import { PdfWorker } from 'pdfjs-dist/build/pdf.worker.mjs'
 | 
			
		||||
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
 | 
			
		||||
 | 
			
		||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
 | 
			
		||||
import {
 | 
			
		||||
  build_download_info_link,
 | 
			
		||||
  download_and_decrypt_doc,
 | 
			
		||||
} from "../StoredObjectButton/helpers";
 | 
			
		||||
 | 
			
		||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
 | 
			
		||||
 | 
			
		||||
const multiPage: Ref<boolean> = ref(true);
 | 
			
		||||
const modalOpen: Ref<boolean> = ref(false);
 | 
			
		||||
const loading: Ref<boolean> = ref(false);
 | 
			
		||||
const adding: Ref<boolean> = ref(false);
 | 
			
		||||
const canvasEvent: Ref<CanvasEvent> = ref("select");
 | 
			
		||||
const signedState: Ref<SignedState> = ref("pending");
 | 
			
		||||
const page: Ref<number> = ref(1);
 | 
			
		||||
const pageCount: Ref<number> = ref(0);
 | 
			
		||||
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
 | 
			
		||||
let pdf = {} as PDFDocumentProxy;
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface Window {
 | 
			
		||||
    signature: Signature;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const $toast = useToast();
 | 
			
		||||
 | 
			
		||||
const signature = window.signature;
 | 
			
		||||
const urlInfo = build_download_info_link(signature.storedObject.filename);
 | 
			
		||||
 | 
			
		||||
console.log(signature);
 | 
			
		||||
 | 
			
		||||
const mountPdf = async (url: string) => {
 | 
			
		||||
  const loadingTask = pdfjsLib.getDocument(url);
 | 
			
		||||
  pdf = await loadingTask.promise;
 | 
			
		||||
  pageCount.value = pdf.numPages;
 | 
			
		||||
  if (multiPage.value) {
 | 
			
		||||
    await setAllPages();
 | 
			
		||||
  } else {
 | 
			
		||||
    await setPage(1);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getRenderContext = (pdfPage: PDFPageProxy) => {
 | 
			
		||||
  const scale = 1;
 | 
			
		||||
  const viewport = pdfPage.getViewport({ scale });
 | 
			
		||||
  let canvas;
 | 
			
		||||
  if (multiPage.value) {
 | 
			
		||||
    canvas = document.getElementById(
 | 
			
		||||
      `canvas-${pdfPage.pageNumber}`
 | 
			
		||||
    ) as HTMLCanvasElement;
 | 
			
		||||
  } else {
 | 
			
		||||
    canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
 | 
			
		||||
  }
 | 
			
		||||
  const context = canvas.getContext("2d") as CanvasRenderingContext2D;
 | 
			
		||||
  canvas.height = viewport.height;
 | 
			
		||||
  canvas.width = viewport.width;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    canvasContext: context,
 | 
			
		||||
    viewport: viewport,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setAllPages = async () =>
 | 
			
		||||
  Array.from(Array(pageCount.value).keys()).map((p) => setPage(p + 1));
 | 
			
		||||
 | 
			
		||||
const setPage = async (page: number) => {
 | 
			
		||||
  const pdfPage = await pdf.getPage(page);
 | 
			
		||||
  const renderContext = getRenderContext(pdfPage);
 | 
			
		||||
  await pdfPage.render(renderContext);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function downloadAndOpen(): Promise<Blob> {
 | 
			
		||||
  let raw;
 | 
			
		||||
  try {
 | 
			
		||||
    raw = await download_and_decrypt_doc(
 | 
			
		||||
      urlInfo,
 | 
			
		||||
      signature.storedObject.keyInfos,
 | 
			
		||||
      new Uint8Array(signature.storedObject.iv)
 | 
			
		||||
    );
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error("error while downloading and decrypting document", e);
 | 
			
		||||
    throw e;
 | 
			
		||||
  }
 | 
			
		||||
  await mountPdf(URL.createObjectURL(raw));
 | 
			
		||||
  initPdf();
 | 
			
		||||
  return raw;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const initPdf = () => {
 | 
			
		||||
  const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
 | 
			
		||||
  canvas.addEventListener("pointerup", canvasClick, false);
 | 
			
		||||
  setTimeout(() => addZones(page.value), 800);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) =>
 | 
			
		||||
  Math.round((x * canvasWidth) / PDFWidth);
 | 
			
		||||
 | 
			
		||||
const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
 | 
			
		||||
  Math.round((h * canvasHeight) / PDFHeight);
 | 
			
		||||
 | 
			
		||||
const hitSignature = (
 | 
			
		||||
  zone: SignatureZone,
 | 
			
		||||
  xy: number[],
 | 
			
		||||
  canvasWidth: number,
 | 
			
		||||
  canvasHeight: number
 | 
			
		||||
) =>
 | 
			
		||||
  scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
 | 
			
		||||
  xy[0] <
 | 
			
		||||
    scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
 | 
			
		||||
  zone.PDFPage.height -
 | 
			
		||||
    scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
 | 
			
		||||
    xy[1] &&
 | 
			
		||||
  xy[1] <
 | 
			
		||||
    scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
 | 
			
		||||
      zone.PDFPage.height;
 | 
			
		||||
 | 
			
		||||
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
 | 
			
		||||
  userSignatureZone.value = z;
 | 
			
		||||
  const ctx = canvas.getContext("2d");
 | 
			
		||||
  if (ctx) {
 | 
			
		||||
    setPage(page.value);
 | 
			
		||||
    setTimeout(() => drawZone(z, ctx, canvas.width, canvas.height), 200);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const selectZoneOnCanvas = (e: PointerEvent, canvas: HTMLCanvasElement) =>
 | 
			
		||||
  signature.zones
 | 
			
		||||
    .filter((z) => z.PDFPage.index + 1 === page.value)
 | 
			
		||||
    .map((z) => {
 | 
			
		||||
      if (
 | 
			
		||||
        hitSignature(z, [e.offsetX, e.offsetY], canvas.width, canvas.height)
 | 
			
		||||
      ) {
 | 
			
		||||
        if (userSignatureZone.value === null) {
 | 
			
		||||
          selectZone(z, canvas);
 | 
			
		||||
        } else {
 | 
			
		||||
          if (userSignatureZone.value.index === z.index) {
 | 
			
		||||
            sign();
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
const canvasClick = (e: PointerEvent) => {
 | 
			
		||||
  const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
 | 
			
		||||
  canvasEvent.value === "select"
 | 
			
		||||
    ? selectZoneOnCanvas(e, canvas)
 | 
			
		||||
    : addZoneOnCanvas(e, canvas);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const turnPage = async (upOrDown: number) => {
 | 
			
		||||
  userSignatureZone.value = null;
 | 
			
		||||
  page.value = page.value + upOrDown;
 | 
			
		||||
  await setPage(page.value);
 | 
			
		||||
  setTimeout(() => addZones(page.value), 200);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const turnSignature = async (upOrDown: number) => {
 | 
			
		||||
  let zoneIndex = userSignatureZone.value?.index ?? -1;
 | 
			
		||||
  if (zoneIndex < -1) {
 | 
			
		||||
    zoneIndex = -1;
 | 
			
		||||
  }
 | 
			
		||||
  if (zoneIndex < signature.zones.length) {
 | 
			
		||||
    zoneIndex = zoneIndex + upOrDown;
 | 
			
		||||
  } else {
 | 
			
		||||
    zoneIndex = 0;
 | 
			
		||||
  }
 | 
			
		||||
  let currentZone = signature.zones[zoneIndex];
 | 
			
		||||
  if (currentZone) {
 | 
			
		||||
    page.value = currentZone.PDFPage.index + 1;
 | 
			
		||||
    userSignatureZone.value = currentZone;
 | 
			
		||||
    const canvas = document.querySelectorAll("canvas")[0];
 | 
			
		||||
    selectZone(currentZone, canvas);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const drawZone = (
 | 
			
		||||
  zone: SignatureZone,
 | 
			
		||||
  ctx: CanvasRenderingContext2D,
 | 
			
		||||
  canvasWidth: number,
 | 
			
		||||
  canvasHeight: number
 | 
			
		||||
) => {
 | 
			
		||||
  const unselectedBlue = "#007bff";
 | 
			
		||||
  const selectedBlue = "#034286";
 | 
			
		||||
  ctx.strokeStyle =
 | 
			
		||||
    userSignatureZone.value?.index === zone.index
 | 
			
		||||
      ? selectedBlue
 | 
			
		||||
      : unselectedBlue;
 | 
			
		||||
  ctx.lineWidth = 2;
 | 
			
		||||
  ctx.lineJoin = "bevel";
 | 
			
		||||
  ctx.strokeRect(
 | 
			
		||||
    scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
 | 
			
		||||
    zone.PDFPage.height -
 | 
			
		||||
      scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
 | 
			
		||||
    scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
 | 
			
		||||
    scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
 | 
			
		||||
  );
 | 
			
		||||
  ctx.font = "bold 16px serif";
 | 
			
		||||
  ctx.textAlign = "center";
 | 
			
		||||
  ctx.fillStyle = "black";
 | 
			
		||||
  const xText =
 | 
			
		||||
    scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
 | 
			
		||||
    scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
 | 
			
		||||
  const yText =
 | 
			
		||||
    zone.PDFPage.height -
 | 
			
		||||
    scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
 | 
			
		||||
    scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
 | 
			
		||||
  if (userSignatureZone.value?.index === zone.index) {
 | 
			
		||||
    ctx.fillStyle = selectedBlue;
 | 
			
		||||
    ctx.fillText("Signer ici", xText, yText);
 | 
			
		||||
  } else {
 | 
			
		||||
    ctx.fillStyle = unselectedBlue;
 | 
			
		||||
    ctx.fillText("Choisir cette", xText, yText - 12);
 | 
			
		||||
    ctx.fillText("zone de signature", xText, yText + 12);
 | 
			
		||||
    // ctx.strokeStyle = "#c6c6c6"; // halo
 | 
			
		||||
    // ctx.strokeText("Choisir cette", xText, yText - 12);
 | 
			
		||||
    // ctx.strokeText("zone de signature", xText, yText + 12);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addZones = (page: number) => {
 | 
			
		||||
  const canvas = document.querySelectorAll("canvas")[0];
 | 
			
		||||
  const ctx = canvas.getContext("2d");
 | 
			
		||||
  if (ctx) {
 | 
			
		||||
    signature.zones
 | 
			
		||||
      .filter((z) => z.PDFPage.index + 1 === page)
 | 
			
		||||
      .map((z) => drawZone(z, ctx, canvas.width, canvas.height));
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const checkSignature = () => {
 | 
			
		||||
  const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
 | 
			
		||||
  return makeFetch("GET", url)
 | 
			
		||||
    .then((r) => {
 | 
			
		||||
      signedState.value = r as SignedState;
 | 
			
		||||
      checkForReady();
 | 
			
		||||
    })
 | 
			
		||||
    .catch((error) => {
 | 
			
		||||
      signedState.value = "error";
 | 
			
		||||
      console.log("Error while checking the signature", error);
 | 
			
		||||
      $toast.error(
 | 
			
		||||
        `Erreur lors de la vérification de la signature: ${error.txt}`
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const maxTryForReady = 60; //2 minutes for trying to sign
 | 
			
		||||
let tryForReady = 0;
 | 
			
		||||
 | 
			
		||||
const stopTrySigning = () => {
 | 
			
		||||
  loading.value = false;
 | 
			
		||||
  modalOpen.value = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const checkForReady = () => {
 | 
			
		||||
  if (tryForReady > maxTryForReady) {
 | 
			
		||||
    stopTrySigning();
 | 
			
		||||
    tryForReady = 0;
 | 
			
		||||
    console.log("Reached the maximum number of tentative to try signing");
 | 
			
		||||
    $toast.error(
 | 
			
		||||
      "Le nombre maximum de tentatives pour essayer de signer est atteint"
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  if (signedState.value === "rejected") {
 | 
			
		||||
    stopTrySigning();
 | 
			
		||||
    console.log("Signature rejected by the server");
 | 
			
		||||
    $toast.error("Signature rejetée par le serveur");
 | 
			
		||||
  }
 | 
			
		||||
  if (signedState.value === "canceled") {
 | 
			
		||||
    stopTrySigning();
 | 
			
		||||
    console.log("Signature canceled");
 | 
			
		||||
    $toast.error("Signature annulée");
 | 
			
		||||
  }
 | 
			
		||||
  if (signedState.value === "pending") {
 | 
			
		||||
    tryForReady = tryForReady + 1;
 | 
			
		||||
    setTimeout(() => checkSignature(), 2000);
 | 
			
		||||
  } else {
 | 
			
		||||
    stopTrySigning();
 | 
			
		||||
    if (signedState.value === "signed") {
 | 
			
		||||
      userSignatureZone.value = null;
 | 
			
		||||
      downloadAndOpen();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sign = () => (modalOpen.value = true);
 | 
			
		||||
 | 
			
		||||
const confirmSign = () => {
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
  const url = `/api/1.0/document/workflow/${signature.id}/signature-request`;
 | 
			
		||||
  const body = {
 | 
			
		||||
    storedObject: signature.storedObject,
 | 
			
		||||
    zone: userSignatureZone.value,
 | 
			
		||||
  };
 | 
			
		||||
  makeFetch("POST", url, body)
 | 
			
		||||
    .then((r) => {
 | 
			
		||||
      checkForReady();
 | 
			
		||||
    })
 | 
			
		||||
    .catch((error) => {
 | 
			
		||||
      console.log("Error while posting the signature", error);
 | 
			
		||||
      stopTrySigning();
 | 
			
		||||
      $toast.error(
 | 
			
		||||
        `Erreur lors de la soumission de la signature: ${error.txt}`
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const undoSign = async () => {
 | 
			
		||||
  signature.zones = signature.zones.filter((z) => z.index !== null);
 | 
			
		||||
  await setPage(page.value);
 | 
			
		||||
  setTimeout(() => addZones(page.value), 200);
 | 
			
		||||
  userSignatureZone.value = null;
 | 
			
		||||
  adding.value = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const toggleAddZone = () => {
 | 
			
		||||
  canvasEvent.value === "select"
 | 
			
		||||
    ? (canvasEvent.value = "add")
 | 
			
		||||
    : (canvasEvent.value = "select");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addZoneOnCanvas = (e: PointerEvent, canvas: HTMLCanvasElement) => {
 | 
			
		||||
  const BOX_WIDTH = 180;
 | 
			
		||||
  const BOX_HEIGHT = 90;
 | 
			
		||||
  const PDFPageHeight = canvas.height;
 | 
			
		||||
  const PDFPageWidth = canvas.width;
 | 
			
		||||
 | 
			
		||||
  const x = e.offsetX;
 | 
			
		||||
  const y = e.offsetY;
 | 
			
		||||
  const newZone: SignatureZone = {
 | 
			
		||||
    index: null,
 | 
			
		||||
    x:
 | 
			
		||||
      scaleXToCanvas(x, canvas.width, PDFPageWidth) -
 | 
			
		||||
      scaleXToCanvas(BOX_WIDTH / 2, canvas.width, PDFPageWidth),
 | 
			
		||||
    y:
 | 
			
		||||
      PDFPageHeight -
 | 
			
		||||
      scaleYToCanvas(y, canvas.height, PDFPageHeight) +
 | 
			
		||||
      scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight),
 | 
			
		||||
    width: BOX_WIDTH,
 | 
			
		||||
    height: BOX_HEIGHT,
 | 
			
		||||
    PDFPage: {
 | 
			
		||||
      index: page.value - 1,
 | 
			
		||||
      width: PDFPageWidth,
 | 
			
		||||
      height: PDFPageHeight,
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
  signature.zones.push(newZone);
 | 
			
		||||
 | 
			
		||||
  setTimeout(() => addZones(page.value), 200);
 | 
			
		||||
  canvasEvent.value = "select";
 | 
			
		||||
  adding.value = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const removeNewZone = async () => {
 | 
			
		||||
  signature.zones = signature.zones.filter((z) => z.index !== null);
 | 
			
		||||
  userSignatureZone.value = null;
 | 
			
		||||
  await setPage(page.value);
 | 
			
		||||
  setTimeout(() => addZones(page.value), 200);
 | 
			
		||||
  canvasEvent.value = "select";
 | 
			
		||||
  adding.value = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
downloadAndOpen();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
canvas {
 | 
			
		||||
  box-shadow: 0 20px 20px rgba(0, 0, 0, 0.1);
 | 
			
		||||
}
 | 
			
		||||
div#action-buttons {
 | 
			
		||||
  position: sticky;
 | 
			
		||||
  bottom: 0px;
 | 
			
		||||
  background-color: white;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
}
 | 
			
		||||
div#turn-page {
 | 
			
		||||
  span {
 | 
			
		||||
    font-size: 0.8rem;
 | 
			
		||||
    margin: 0 0.4rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
div.signature-modal-body {
 | 
			
		||||
  height: 8rem;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
import { createApp } from "vue";
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
 | 
			
		||||
import App from "./App.vue";
 | 
			
		||||
 | 
			
		||||
const appMessages = {
 | 
			
		||||
    fr: {
 | 
			
		||||
        yes: 'Oui',
 | 
			
		||||
        are_you_sure: 'Êtes-vous sûr·e?',
 | 
			
		||||
        you_are_going_to_sign: 'Vous allez signer le document',
 | 
			
		||||
        signature_confirmation: 'Confirmation de la signature',
 | 
			
		||||
        sign: 'Signer',
 | 
			
		||||
        choose_another_signature: 'Choisir une autre zone de signature',
 | 
			
		||||
        cancel: 'Annuler',
 | 
			
		||||
        cancel_signing: 'Refuser de signer',
 | 
			
		||||
        last_sign_zone: 'Zone de signature précédente',
 | 
			
		||||
        next_sign_zone: 'Zone de signature suivante',
 | 
			
		||||
        electronic_signature_in_progress: 'Signature électronique en cours...',
 | 
			
		||||
        loading: 'Chargement...',
 | 
			
		||||
        add_sign_zone: 'Ajouter une zone de signature',
 | 
			
		||||
        remove_sign_zone: 'Enlever la zone',
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const i18n = _createI18n(appMessages);
 | 
			
		||||
 | 
			
		||||
const app = createApp({
 | 
			
		||||
    template: `<app></app>`,
 | 
			
		||||
})
 | 
			
		||||
    .use(i18n)
 | 
			
		||||
    .component("app", App)
 | 
			
		||||
    .mount("#document-signature");
 | 
			
		||||
@@ -71,7 +71,7 @@
 | 
			
		||||
                </li>
 | 
			
		||||
                {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
 | 
			
		||||
                        {{ document.object|chill_document_button_group(document.title) }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
 | 
			
		||||
@@ -90,7 +90,7 @@
 | 
			
		||||
            {% else %}
 | 
			
		||||
                {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
 | 
			
		||||
                        {{ document.object|chill_document_button_group(document.title) }}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="fr">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta http-equiv="x-ua-compatible" content="ie=edge">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
 | 
			
		||||
    <title>Signature</title>
 | 
			
		||||
 | 
			
		||||
    {{ encore_entry_link_tags('mod_bootstrap') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_forkawesome') }}
 | 
			
		||||
    {{ encore_entry_link_tags('chill') }}
 | 
			
		||||
    {{ encore_entry_link_tags('vue_document_signature') }}
 | 
			
		||||
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
    {% block js %}
 | 
			
		||||
        {{ encore_entry_script_tags('mod_document_action_buttons_group') }}
 | 
			
		||||
        <script type="text/javascript">
 | 
			
		||||
            window.signature = {{ signature|json_encode|raw }};
 | 
			
		||||
        </script>
 | 
			
		||||
        {{ encore_entry_script_tags('vue_document_signature') }}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
 | 
			
		||||
    <div class="content" id="content">
 | 
			
		||||
        <div class="container-xxl">
 | 
			
		||||
            <div class="row">
 | 
			
		||||
               <div class="col-xs-12 col-md-12 col-lg-9 my-5 m-auto">
 | 
			
		||||
                   <h4>{{ 'Document %title%' | trans({ '%title%': document.title }) }}</h4>
 | 
			
		||||
                   <div class="row" id="document-signature"></div>
 | 
			
		||||
               </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -70,12 +70,12 @@ class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements Prov
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function supports($attribute, $subject): bool
 | 
			
		||||
    public function supports($attribute, $subject): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->voterHelper->supports($attribute, $subject);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
 | 
			
		||||
    public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (!$token->getUser() instanceof User) {
 | 
			
		||||
            return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
@@ -22,6 +23,7 @@ final class AsyncUploadVoter extends Voter
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly Security $security,
 | 
			
		||||
        private readonly StoredObjectRepository $storedObjectRepository
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    protected function supports($attribute, $subject): bool
 | 
			
		||||
@@ -32,10 +34,16 @@ final class AsyncUploadVoter extends Voter
 | 
			
		||||
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
 | 
			
		||||
    {
 | 
			
		||||
        /** @var SignedUrl $subject */
 | 
			
		||||
        if (!in_array($subject->method, ['POST', 'GET', 'HEAD'], true)) {
 | 
			
		||||
        if (!in_array($subject->method, ['POST', 'GET', 'HEAD', 'PUT'], true)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN');
 | 
			
		||||
        $storedObject = $this->storedObjectRepository->findOneBy(['filename' => $subject->object_name]);
 | 
			
		||||
 | 
			
		||||
        return match ($subject->method) {
 | 
			
		||||
            'GET', 'HEAD' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject),
 | 
			
		||||
            'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject),
 | 
			
		||||
            'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,10 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Voter for the content of a stored object.
 | 
			
		||||
@@ -23,6 +24,10 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
 | 
			
		||||
 */
 | 
			
		||||
class StoredObjectVoter extends Voter
 | 
			
		||||
{
 | 
			
		||||
    public const LOG_PREFIX = '[stored object voter] ';
 | 
			
		||||
 | 
			
		||||
    public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters, private readonly LoggerInterface $logger) {}
 | 
			
		||||
 | 
			
		||||
    protected function supports($attribute, $subject): bool
 | 
			
		||||
    {
 | 
			
		||||
        return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
 | 
			
		||||
@@ -32,24 +37,28 @@ class StoredObjectVoter extends Voter
 | 
			
		||||
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
 | 
			
		||||
    {
 | 
			
		||||
        /** @var StoredObject $subject */
 | 
			
		||||
        if (
 | 
			
		||||
            !$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
 | 
			
		||||
            || $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
 | 
			
		||||
        ) {
 | 
			
		||||
            return false;
 | 
			
		||||
        $attributeAsEnum = StoredObjectRoleEnum::from($attribute);
 | 
			
		||||
 | 
			
		||||
        // Loop through context-specific voters
 | 
			
		||||
        foreach ($this->storedObjectVoters as $storedObjectVoter) {
 | 
			
		||||
            if ($storedObjectVoter->supports($attributeAsEnum, $subject)) {
 | 
			
		||||
                $grant = $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token);
 | 
			
		||||
 | 
			
		||||
                if (false === $grant) {
 | 
			
		||||
                    $this->logger->debug(self::LOG_PREFIX.'deny access by storedObjectVoter', ['stored_object_voter' => $storedObjectVoter::class]);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return $grant;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        // User role-based fallback
 | 
			
		||||
        if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) {
 | 
			
		||||
            // TODO: this maybe considered as a security issue, as all authenticated users can reach a stored object which
 | 
			
		||||
            // is potentially detached from an existing entity.
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $askedRole = StoredObjectRoleEnum::from($attribute);
 | 
			
		||||
        $tokenRoleAuthorization =
 | 
			
		||||
            $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS);
 | 
			
		||||
 | 
			
		||||
        return match ($askedRole) {
 | 
			
		||||
            StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization,
 | 
			
		||||
            StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization
 | 
			
		||||
        };
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
 | 
			
		||||
{
 | 
			
		||||
    abstract protected function getRepository(): AssociatedEntityToStoredObjectInterface;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return class-string
 | 
			
		||||
     */
 | 
			
		||||
    abstract protected function getClass(): string;
 | 
			
		||||
 | 
			
		||||
    abstract protected function attributeToRole(StoredObjectRoleEnum $attribute): string;
 | 
			
		||||
 | 
			
		||||
    abstract protected function canBeAssociatedWithWorkflow(): bool;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly Security $security,
 | 
			
		||||
        private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
 | 
			
		||||
    {
 | 
			
		||||
        $class = $this->getClass();
 | 
			
		||||
 | 
			
		||||
        return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
 | 
			
		||||
    {
 | 
			
		||||
        // Retrieve the related accompanying course document
 | 
			
		||||
        $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
 | 
			
		||||
 | 
			
		||||
        // Determine the attribute to pass to AccompanyingCourseDocumentVoter
 | 
			
		||||
        $voterAttribute = $this->attributeToRole($attribute);
 | 
			
		||||
 | 
			
		||||
        if (false === $this->security->isGranted($voterAttribute, $entity)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (StoredObjectRoleEnum::SEE !== $attribute && $this->canBeAssociatedWithWorkflow()) {
 | 
			
		||||
            if (null === $this->workflowDocumentService) {
 | 
			
		||||
                throw new \LogicException('Provide a workflow document service');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $this->workflowDocumentService->notBlockedByWorkflow($entity);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly AccompanyingCourseDocumentRepository $repository,
 | 
			
		||||
        Security $security,
 | 
			
		||||
        WorkflowStoredObjectPermissionHelper $workflowDocumentService
 | 
			
		||||
    ) {
 | 
			
		||||
        parent::__construct($security, $workflowDocumentService);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getRepository(): AssociatedEntityToStoredObjectInterface
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function attributeToRole(StoredObjectRoleEnum $attribute): string
 | 
			
		||||
    {
 | 
			
		||||
        return match ($attribute) {
 | 
			
		||||
            StoredObjectRoleEnum::EDIT => AccompanyingCourseDocumentVoter::UPDATE,
 | 
			
		||||
            StoredObjectRoleEnum::SEE => AccompanyingCourseDocumentVoter::SEE_DETAILS,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getClass(): string
 | 
			
		||||
    {
 | 
			
		||||
        return AccompanyingCourseDocument::class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function canBeAssociatedWithWorkflow(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\PersonDocument;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly PersonDocumentRepository $repository,
 | 
			
		||||
        Security $security,
 | 
			
		||||
        WorkflowStoredObjectPermissionHelper $workflowDocumentService
 | 
			
		||||
    ) {
 | 
			
		||||
        parent::__construct($security, $workflowDocumentService);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getRepository(): AssociatedEntityToStoredObjectInterface
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getClass(): string
 | 
			
		||||
    {
 | 
			
		||||
        return PersonDocument::class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function attributeToRole(StoredObjectRoleEnum $attribute): string
 | 
			
		||||
    {
 | 
			
		||||
        return match ($attribute) {
 | 
			
		||||
            StoredObjectRoleEnum::EDIT => PersonDocumentVoter::UPDATE,
 | 
			
		||||
            StoredObjectRoleEnum::SEE => PersonDocumentVoter::SEE_DETAILS,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function canBeAssociatedWithWorkflow(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
 | 
			
		||||
interface StoredObjectVoterInterface
 | 
			
		||||
{
 | 
			
		||||
    public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
 | 
			
		||||
 | 
			
		||||
    public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool;
 | 
			
		||||
}
 | 
			
		||||
@@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
 | 
			
		||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
@@ -32,7 +33,8 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider,
 | 
			
		||||
        private readonly UrlGeneratorInterface $urlGenerator
 | 
			
		||||
        private readonly UrlGeneratorInterface $urlGenerator,
 | 
			
		||||
        private readonly Security $security
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function normalize($object, ?string $format = null, array $context = [])
 | 
			
		||||
@@ -55,13 +57,13 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
 | 
			
		||||
        // deprecated property
 | 
			
		||||
        $datas['creationDate'] = $datas['createdAt'];
 | 
			
		||||
 | 
			
		||||
        $canDavSee = in_array(self::ADD_DAV_SEE_LINK_CONTEXT, $context['groups'] ?? [], true);
 | 
			
		||||
        $canDavEdit = in_array(self::ADD_DAV_EDIT_LINK_CONTEXT, $context['groups'] ?? [], true);
 | 
			
		||||
        $canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object);
 | 
			
		||||
        $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object);
 | 
			
		||||
 | 
			
		||||
        if ($canDavSee || $canDavEdit) {
 | 
			
		||||
        if ($canSee || $canEdit) {
 | 
			
		||||
            $accessToken = $this->JWTDavTokenProvider->createToken(
 | 
			
		||||
                $object,
 | 
			
		||||
                $canDavEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
 | 
			
		||||
                $canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            $datas['_links'] = [
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Message which is received when a pdf is signed.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class PdfSignedMessage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public readonly int $signatureId,
 | 
			
		||||
        public readonly string $content
 | 
			
		||||
    ) {}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * log prefix.
 | 
			
		||||
     */
 | 
			
		||||
    private const P = '[pdf signed message] ';
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private LoggerInterface $logger,
 | 
			
		||||
        private EntityWorkflowManager $entityWorkflowManager,
 | 
			
		||||
        private StoredObjectManagerInterface $storedObjectManager,
 | 
			
		||||
        private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
        private ClockInterface $clock,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function __invoke(PdfSignedMessage $message): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]);
 | 
			
		||||
 | 
			
		||||
        $signature = $this->entityWorkflowStepSignatureRepository->find($message->signatureId);
 | 
			
		||||
 | 
			
		||||
        if (null === $signature) {
 | 
			
		||||
            throw new \RuntimeException('no signature found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($signature->getStep()->getEntityWorkflow());
 | 
			
		||||
 | 
			
		||||
        if (null === $storedObject) {
 | 
			
		||||
            throw new \RuntimeException('no stored object found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->storedObjectManager->write($storedObject, $message->content);
 | 
			
		||||
 | 
			
		||||
        $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate($this->clock->now());
 | 
			
		||||
        $this->entityManager->flush();
 | 
			
		||||
        $this->entityManager->clear();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,66 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Messenger\Envelope;
 | 
			
		||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
 | 
			
		||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Decode (and requeue) @see{PdfSignedMessage}, which comes from an external producer.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class PdfSignedMessageSerializer implements SerializerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function decode(array $encodedEnvelope): Envelope
 | 
			
		||||
    {
 | 
			
		||||
        $body = $encodedEnvelope['body'];
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $decoded = json_decode((string) $body, true, 512, JSON_THROW_ON_ERROR);
 | 
			
		||||
        } catch (\JsonException $e) {
 | 
			
		||||
            throw new MessageDecodingFailedException('Could not deserialize message', previous: $e);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!array_key_exists('signatureId', $decoded) || !array_key_exists('content', $decoded)) {
 | 
			
		||||
            throw new MessageDecodingFailedException('Could not find expected keys: signatureId or content');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $content = base64_decode((string) $decoded['content'], true);
 | 
			
		||||
 | 
			
		||||
        if (false === $content) {
 | 
			
		||||
            throw new MessageDecodingFailedException('Invalid character found in the base64 encoded content');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $message = new PdfSignedMessage($decoded['signatureId'], $content);
 | 
			
		||||
 | 
			
		||||
        return new Envelope($message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function encode(Envelope $envelope): array
 | 
			
		||||
    {
 | 
			
		||||
        $message = $envelope->getMessage();
 | 
			
		||||
 | 
			
		||||
        if (!$message instanceof PdfSignedMessage) {
 | 
			
		||||
            throw new MessageDecodingFailedException('Expected a PdfSignedMessage');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $data = [
 | 
			
		||||
            'signatureId' => $message->signatureId,
 | 
			
		||||
            'content' => base64_encode($message->content),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'body' => json_encode($data, JSON_THROW_ON_ERROR),
 | 
			
		||||
            'headers' => [],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Message which is sent when we request a signature on a pdf.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class RequestPdfSignMessage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public int $signatureId,
 | 
			
		||||
        public PDFSignatureZone $PDFSignatureZone,
 | 
			
		||||
        public int $signatureZoneIndex,
 | 
			
		||||
        public string $reason,
 | 
			
		||||
        public string $signerText,
 | 
			
		||||
        public string $content,
 | 
			
		||||
    ) {}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,105 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
 | 
			
		||||
use Symfony\Component\Messenger\Envelope;
 | 
			
		||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
 | 
			
		||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
 | 
			
		||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Serialize a RequestPdfSignMessage, for external consumer.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class RequestPdfSignMessageSerializer implements SerializerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private NormalizerInterface $normalizer,
 | 
			
		||||
        private DenormalizerInterface $denormalizer,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function decode(array $encodedEnvelope): Envelope
 | 
			
		||||
    {
 | 
			
		||||
        $body = $encodedEnvelope['body'];
 | 
			
		||||
        $headers = $encodedEnvelope['headers'];
 | 
			
		||||
 | 
			
		||||
        if (RequestPdfSignMessage::class !== ($headers['Message'] ?? null)) {
 | 
			
		||||
            throw new MessageDecodingFailedException('serializer does not support this message');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $data = json_decode((string) $body, true);
 | 
			
		||||
 | 
			
		||||
        $zoneSignature = $this->denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [
 | 
			
		||||
            AbstractNormalizer::GROUPS => ['write'],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $content = base64_decode((string) $data['content'], true);
 | 
			
		||||
 | 
			
		||||
        if (false === $content) {
 | 
			
		||||
            throw new MessageDecodingFailedException('the content could not be converted from base64 encoding');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $message = new RequestPdfSignMessage(
 | 
			
		||||
            $data['signatureId'],
 | 
			
		||||
            $zoneSignature,
 | 
			
		||||
            $data['signatureZoneIndex'],
 | 
			
		||||
            $data['reason'],
 | 
			
		||||
            $data['signerText'],
 | 
			
		||||
            $content,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // in case of redelivery, unserialize any stamps
 | 
			
		||||
        $stamps = [];
 | 
			
		||||
        if (isset($headers['stamps'])) {
 | 
			
		||||
            $stamps = unserialize($headers['stamps']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new Envelope($message, $stamps);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function encode(Envelope $envelope): array
 | 
			
		||||
    {
 | 
			
		||||
        $message = $envelope->getMessage();
 | 
			
		||||
 | 
			
		||||
        if (!$message instanceof RequestPdfSignMessage) {
 | 
			
		||||
            throw new MessageDecodingFailedException('Message is not a RequestPdfSignMessage');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $data = [
 | 
			
		||||
            'signatureId' => $message->signatureId,
 | 
			
		||||
            'signatureZoneIndex' => $message->signatureZoneIndex,
 | 
			
		||||
            'signatureZone' => $this->normalizer->normalize($message->PDFSignatureZone, 'json', [AbstractNormalizer::GROUPS => ['read']]),
 | 
			
		||||
            'reason' => $message->reason,
 | 
			
		||||
            'signerText' => $message->signerText,
 | 
			
		||||
            'content' => base64_encode($message->content),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $allStamps = [];
 | 
			
		||||
        foreach ($envelope->all() as $stamp) {
 | 
			
		||||
            if ($stamp instanceof NonSendableStampInterface) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            $allStamps = [...$allStamps, ...$stamp];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'body' => json_encode($data, JSON_THROW_ON_ERROR, 512),
 | 
			
		||||
            'headers' => [
 | 
			
		||||
                'stamps' => serialize($allStamps),
 | 
			
		||||
                'Message' => RequestPdfSignMessage::class,
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Service\Signature;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Serializer\Annotation\Groups;
 | 
			
		||||
 | 
			
		||||
final readonly class PDFPage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public int $index,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $width,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $height,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function equals(self $page): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $page->index === $this->index
 | 
			
		||||
            && round($page->width, 2) === round($this->width, 2)
 | 
			
		||||
            && round($page->height, 2) === round($this->height, 2);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Service\Signature;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Serializer\Annotation\Groups;
 | 
			
		||||
 | 
			
		||||
final readonly class PDFSignatureZone
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public int $index,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $x,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $y,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $height,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public float $width,
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public PDFPage $PDFPage,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function equals(self $other): bool
 | 
			
		||||
    {
 | 
			
		||||
        return
 | 
			
		||||
            $this->index == $other->index
 | 
			
		||||
            && $this->x == $other->x
 | 
			
		||||
            && $this->y == $other->y
 | 
			
		||||
            && $this->height == $other->height
 | 
			
		||||
            && $this->width == $other->width
 | 
			
		||||
            && $this->PDFPage->equals($other->PDFPage);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,60 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Service\Signature;
 | 
			
		||||
 | 
			
		||||
use Smalot\PdfParser\Parser;
 | 
			
		||||
 | 
			
		||||
class PDFSignatureZoneParser
 | 
			
		||||
{
 | 
			
		||||
    public const ZONE_SIGNATURE_START = 'signature_zone';
 | 
			
		||||
 | 
			
		||||
    private readonly Parser $parser;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public float $defaultHeight = 90.0,
 | 
			
		||||
        public float $defaultWidth = 180.0,
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->parser = new Parser();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return list<PDFSignatureZone>
 | 
			
		||||
     */
 | 
			
		||||
    public function findSignatureZones(string $fileContent): array
 | 
			
		||||
    {
 | 
			
		||||
        $pdf = $this->parser->parseContent($fileContent);
 | 
			
		||||
        $zones = [];
 | 
			
		||||
 | 
			
		||||
        $defaults = $pdf->getObjectsByType('Pages');
 | 
			
		||||
        $defaultPage = reset($defaults);
 | 
			
		||||
        $defaultPageDetails = $defaultPage->getDetails();
 | 
			
		||||
        $zoneIndex = 0;
 | 
			
		||||
 | 
			
		||||
        foreach ($pdf->getPages() as $index => $page) {
 | 
			
		||||
            $details = $page->getDetails();
 | 
			
		||||
            $pdfPage = new PDFPage(
 | 
			
		||||
                $index,
 | 
			
		||||
                (float) ($details['MediaBox'][2] ?? $defaultPageDetails['MediaBox'][2]),
 | 
			
		||||
                (float) ($details['MediaBox'][3] ?? $defaultPageDetails['MediaBox'][3]),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            foreach ($page->getDataTm() as $dataTm) {
 | 
			
		||||
                if (str_starts_with((string) $dataTm[1], self::ZONE_SIGNATURE_START)) {
 | 
			
		||||
                    $zones[] = new PDFSignatureZone($zoneIndex, (float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
 | 
			
		||||
                    ++$zoneIndex;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $zones;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Service;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
class WorkflowStoredObjectPermissionHelper
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {}
 | 
			
		||||
 | 
			
		||||
    public function notBlockedByWorkflow(object $entity): bool
 | 
			
		||||
    {
 | 
			
		||||
        $workflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
 | 
			
		||||
        $currentUser = $this->security->getUser();
 | 
			
		||||
 | 
			
		||||
        foreach ($workflows as $workflow) {
 | 
			
		||||
            if ($workflow->isFinal()) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
 | 
			
		||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
@@ -128,6 +129,7 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
 | 
			
		||||
        private NormalizerInterface $normalizer,
 | 
			
		||||
        private JWTDavTokenProviderInterface $davTokenProvider,
 | 
			
		||||
        private UrlGeneratorInterface $urlGenerator,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -148,8 +150,10 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
 | 
			
		||||
     * @throws \Twig\Error\RuntimeError
 | 
			
		||||
     * @throws \Twig\Error\SyntaxError
 | 
			
		||||
     */
 | 
			
		||||
    public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string
 | 
			
		||||
    public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $showEditButtons = true, array $options = []): string
 | 
			
		||||
    {
 | 
			
		||||
        $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $document) && $showEditButtons;
 | 
			
		||||
 | 
			
		||||
        $accessToken = $this->davTokenProvider->createToken(
 | 
			
		||||
            $document,
 | 
			
		||||
            $canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
 | 
			
		||||
 
 | 
			
		||||
@@ -122,7 +122,8 @@ class TempUrlOpenstackGeneratorTest extends TestCase
 | 
			
		||||
        $signedUrl = new SignedUrl(
 | 
			
		||||
            'GET',
 | 
			
		||||
            'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
 | 
			
		||||
            \DateTimeImmutable::createFromFormat('U', '1702043543')
 | 
			
		||||
            \DateTimeImmutable::createFromFormat('U', '1702043543'),
 | 
			
		||||
            $objectName
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        foreach ($baseUrls as $baseUrl) {
 | 
			
		||||
@@ -153,6 +154,7 @@ class TempUrlOpenstackGeneratorTest extends TestCase
 | 
			
		||||
        $signedUrl = new SignedUrlPost(
 | 
			
		||||
            'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
 | 
			
		||||
            \DateTimeImmutable::createFromFormat('U', '1702043543'),
 | 
			
		||||
            $objectName,
 | 
			
		||||
            150,
 | 
			
		||||
            1,
 | 
			
		||||
            1800,
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ class AsyncUploadExtensionTest extends KernelTestCase
 | 
			
		||||
    {
 | 
			
		||||
        $generator = $this->prophesize(TempUrlGeneratorInterface::class);
 | 
			
		||||
        $generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any())
 | 
			
		||||
            ->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
 | 
			
		||||
            ->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'), $args[1]));
 | 
			
		||||
 | 
			
		||||
        $urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
 | 
			
		||||
        $urlGenerator->generate('async_upload.generate_url', Argument::type('array'))
 | 
			
		||||
 
 | 
			
		||||
@@ -73,6 +73,7 @@ class AsyncUploadControllerTest extends TestCase
 | 
			
		||||
                return new SignedUrlPost(
 | 
			
		||||
                    'https://object.store.example',
 | 
			
		||||
                    new \DateTimeImmutable('1 hour'),
 | 
			
		||||
                    'abc',
 | 
			
		||||
                    150,
 | 
			
		||||
                    1,
 | 
			
		||||
                    1800,
 | 
			
		||||
@@ -87,7 +88,8 @@ class AsyncUploadControllerTest extends TestCase
 | 
			
		||||
                return new SignedUrl(
 | 
			
		||||
                    $method,
 | 
			
		||||
                    'https://object.store.example',
 | 
			
		||||
                    new \DateTimeImmutable('1 hour')
 | 
			
		||||
                    new \DateTimeImmutable('1 hour'),
 | 
			
		||||
                    $object_name
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Component\Form\PreloadedExtension;
 | 
			
		||||
use Symfony\Component\Form\Test\TypeTestCase;
 | 
			
		||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
 | 
			
		||||
use Symfony\Component\Serializer\Serializer;
 | 
			
		||||
 | 
			
		||||
@@ -80,11 +81,15 @@ class StoredObjectTypeTest extends TypeTestCase
 | 
			
		||||
        $urlGenerator->generate('chill_docstore_dav_document_get', Argument::type('array'), UrlGeneratorInterface::ABSOLUTE_URL)
 | 
			
		||||
            ->willReturn('http://url/fake');
 | 
			
		||||
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->isGranted(Argument::cetera())->willReturn(true);
 | 
			
		||||
 | 
			
		||||
        $serializer = new Serializer(
 | 
			
		||||
            [
 | 
			
		||||
                new StoredObjectNormalizer(
 | 
			
		||||
                    $jwtTokenProvider->reveal(),
 | 
			
		||||
                    $urlGenerator->reveal(),
 | 
			
		||||
                    $security->reveal()
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            [
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,168 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class AbstractStoredObjectVoterTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    private AssociatedEntityToStoredObjectInterface $repository;
 | 
			
		||||
    private Security $security;
 | 
			
		||||
    private WorkflowStoredObjectPermissionHelper $workflowDocumentService;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class);
 | 
			
		||||
        $this->security = $this->createMock(Security::class);
 | 
			
		||||
        $this->workflowDocumentService = $this->createMock(WorkflowStoredObjectPermissionHelper::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null): AbstractStoredObjectVoter
 | 
			
		||||
    {
 | 
			
		||||
        // Anonymous class extending the abstract class
 | 
			
		||||
        return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
 | 
			
		||||
            public function __construct(
 | 
			
		||||
                private readonly bool $canBeAssociatedWithWorkflow,
 | 
			
		||||
                private readonly AssociatedEntityToStoredObjectInterface $repository,
 | 
			
		||||
                Security $security,
 | 
			
		||||
                ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null
 | 
			
		||||
            ) {
 | 
			
		||||
                parent::__construct($security, $workflowDocumentService);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            protected function attributeToRole($attribute): string
 | 
			
		||||
            {
 | 
			
		||||
                return 'SOME_ROLE';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            protected function getRepository(): AssociatedEntityToStoredObjectInterface
 | 
			
		||||
            {
 | 
			
		||||
                return $this->repository;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            protected function getClass(): string
 | 
			
		||||
            {
 | 
			
		||||
                return \stdClass::class;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            protected function canBeAssociatedWithWorkflow(): bool
 | 
			
		||||
            {
 | 
			
		||||
                return $this->canBeAssociatedWithWorkflow;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function setupMockObjects(): array
 | 
			
		||||
    {
 | 
			
		||||
        $user = new User();
 | 
			
		||||
        $token = $this->createMock(TokenInterface::class);
 | 
			
		||||
        $subject = new StoredObject();
 | 
			
		||||
        $entity = new \stdClass();
 | 
			
		||||
 | 
			
		||||
        return [$user, $token, $subject, $entity];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForEntity, object $entity, bool $workflowAllowed): void
 | 
			
		||||
    {
 | 
			
		||||
        // Set up token to return user
 | 
			
		||||
        $token->method('getUser')->willReturn($user);
 | 
			
		||||
 | 
			
		||||
        // Mock the return of an AccompanyingCourseDocument by the repository
 | 
			
		||||
        $this->repository->method('findAssociatedEntityToStoredObject')->willReturn($entity);
 | 
			
		||||
 | 
			
		||||
        // Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument
 | 
			
		||||
        $this->security->method('isGranted')->willReturn($isGrantedForEntity);
 | 
			
		||||
 | 
			
		||||
        // Mock case where user is blocked or not by workflow
 | 
			
		||||
        $this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testSupportsOnAttribute(): void
 | 
			
		||||
    {
 | 
			
		||||
        [$user, $token, $subject, $entity] = $this->setupMockObjects();
 | 
			
		||||
 | 
			
		||||
        // Setup mocks for voteOnAttribute method
 | 
			
		||||
        $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
 | 
			
		||||
        $voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
 | 
			
		||||
 | 
			
		||||
        self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, $subject));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testVoteOnAttributeAllowedAndWorkflowAllowed(): void
 | 
			
		||||
    {
 | 
			
		||||
        [$user, $token, $subject, $entity] = $this->setupMockObjects();
 | 
			
		||||
 | 
			
		||||
        // Setup mocks for voteOnAttribute method
 | 
			
		||||
        $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
 | 
			
		||||
        $voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
 | 
			
		||||
 | 
			
		||||
        // The voteOnAttribute method should return True when workflow is allowed
 | 
			
		||||
        self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testVoteOnAttributeNotAllowed(): void
 | 
			
		||||
    {
 | 
			
		||||
        [$user, $token, $subject, $entity] = $this->setupMockObjects();
 | 
			
		||||
 | 
			
		||||
        // Setup mocks for voteOnAttribute method where isGranted() returns false
 | 
			
		||||
        $this->setupMocksForVoteOnAttribute($user, $token, false, $entity, true);
 | 
			
		||||
        $voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
 | 
			
		||||
 | 
			
		||||
        // The voteOnAttribute method should return True when workflow is allowed
 | 
			
		||||
        self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testVoteOnAttributeAllowedWorkflowNotAllowed(): void
 | 
			
		||||
    {
 | 
			
		||||
        [$user, $token, $subject, $entity] = $this->setupMockObjects();
 | 
			
		||||
 | 
			
		||||
        // Setup mocks for voteOnAttribute method
 | 
			
		||||
        $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
 | 
			
		||||
        $voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
 | 
			
		||||
 | 
			
		||||
        // Test voteOnAttribute method
 | 
			
		||||
        $attribute = StoredObjectRoleEnum::EDIT;
 | 
			
		||||
        $result = $voter->voteOnAttribute($attribute, $subject, $token);
 | 
			
		||||
 | 
			
		||||
        // Assert that access is denied when workflow is not allowed
 | 
			
		||||
        $this->assertFalse($result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testVoteOnAttributeAllowedWorkflowAllowedToSeeDocument(): void
 | 
			
		||||
    {
 | 
			
		||||
        [$user, $token, $subject, $entity] = $this->setupMockObjects();
 | 
			
		||||
 | 
			
		||||
        // Setup mocks for voteOnAttribute method
 | 
			
		||||
        $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
 | 
			
		||||
        $voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
 | 
			
		||||
 | 
			
		||||
        // Test voteOnAttribute method
 | 
			
		||||
        $attribute = StoredObjectRoleEnum::SEE;
 | 
			
		||||
        $result = $voter->voteOnAttribute($attribute, $subject, $token);
 | 
			
		||||
 | 
			
		||||
        // Assert that access is denied when workflow is not allowed
 | 
			
		||||
        $this->assertTrue($result);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,11 +14,14 @@ namespace Chill\DocStoreBundle\Tests\Security\Authorization;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Psr\Log\NullLogger;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
 | 
			
		||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
@@ -27,97 +30,93 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
 | 
			
		||||
 */
 | 
			
		||||
class StoredObjectVoterTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider provideDataVote
 | 
			
		||||
     */
 | 
			
		||||
    public function testVote(TokenInterface $token, ?object $subject, string $attribute, mixed $expected): void
 | 
			
		||||
    public function testVote(array $storedObjectVotersDefinition, object $subject, string $attribute, bool $fallbackSecurityExpected, bool $securityIsGrantedResult, mixed $expected): void
 | 
			
		||||
    {
 | 
			
		||||
        $voter = new StoredObjectVoter();
 | 
			
		||||
        $storedObjectVoters = array_map(fn (array $definition) => $this->buildStoredObjectVoter($definition[0], $definition[1], $definition[2]), $storedObjectVotersDefinition);
 | 
			
		||||
        $token = new UsernamePasswordToken(new User(), 'chill_main', ['ROLE_USER']);
 | 
			
		||||
 | 
			
		||||
        $security = $this->createMock(Security::class);
 | 
			
		||||
        $security->expects($fallbackSecurityExpected ? $this->atLeastOnce() : $this->never())
 | 
			
		||||
            ->method('isGranted')
 | 
			
		||||
            ->with($this->logicalOr($this->identicalTo('ROLE_USER'), $this->identicalTo('ROLE_ADMIN')))
 | 
			
		||||
            ->willReturn($securityIsGrantedResult);
 | 
			
		||||
 | 
			
		||||
        $voter = new StoredObjectVoter($security, $storedObjectVoters, new NullLogger());
 | 
			
		||||
 | 
			
		||||
        self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function provideDataVote(): iterable
 | 
			
		||||
    private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface
 | 
			
		||||
    {
 | 
			
		||||
        $storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class);
 | 
			
		||||
        $storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports')
 | 
			
		||||
            ->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class))
 | 
			
		||||
            ->willReturn($supports);
 | 
			
		||||
        $storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute')
 | 
			
		||||
            ->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class))
 | 
			
		||||
            ->willReturn($voteOnAttribute);
 | 
			
		||||
 | 
			
		||||
        return $storedObjectVoter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function provideDataVote(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::EDIT, new StoredObject()),
 | 
			
		||||
            // we try with something else than a SToredObject, the voter should abstain
 | 
			
		||||
            [[false, false, false]],
 | 
			
		||||
            new \stdClass(),
 | 
			
		||||
            'SOMETHING',
 | 
			
		||||
            false,
 | 
			
		||||
            false,
 | 
			
		||||
            VoterInterface::ACCESS_ABSTAIN,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
 | 
			
		||||
            $so,
 | 
			
		||||
            // we try with an unsupported attribute, the voter must abstain
 | 
			
		||||
            [[false, false, false]],
 | 
			
		||||
            new StoredObject(),
 | 
			
		||||
            'SOMETHING',
 | 
			
		||||
            false,
 | 
			
		||||
            false,
 | 
			
		||||
            VoterInterface::ACCESS_ABSTAIN,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
 | 
			
		||||
            $so,
 | 
			
		||||
            StoredObjectRoleEnum::SEE->value,
 | 
			
		||||
            VoterInterface::ACCESS_GRANTED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
 | 
			
		||||
            $so,
 | 
			
		||||
            StoredObjectRoleEnum::EDIT->value,
 | 
			
		||||
            VoterInterface::ACCESS_GRANTED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
 | 
			
		||||
            $so,
 | 
			
		||||
            StoredObjectRoleEnum::EDIT->value,
 | 
			
		||||
            VoterInterface::ACCESS_DENIED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
 | 
			
		||||
            $so,
 | 
			
		||||
            StoredObjectRoleEnum::SEE->value,
 | 
			
		||||
            VoterInterface::ACCESS_GRANTED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(null, null),
 | 
			
		||||
            // happy scenario: there is a role voter
 | 
			
		||||
            [[true, true, true]],
 | 
			
		||||
            new StoredObject(),
 | 
			
		||||
            StoredObjectRoleEnum::SEE->value,
 | 
			
		||||
            VoterInterface::ACCESS_DENIED,
 | 
			
		||||
            false,
 | 
			
		||||
            false,
 | 
			
		||||
            VoterInterface::ACCESS_GRANTED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(null, null),
 | 
			
		||||
            // there is a role voter, but not allowed to see the stored object
 | 
			
		||||
            [[true, true, false]],
 | 
			
		||||
            new StoredObject(),
 | 
			
		||||
            StoredObjectRoleEnum::SEE->value,
 | 
			
		||||
            false,
 | 
			
		||||
            false,
 | 
			
		||||
            VoterInterface::ACCESS_DENIED,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildToken(?StoredObjectRoleEnum $storedObjectRoleEnum = null, ?StoredObject $storedObject = null): TokenInterface
 | 
			
		||||
    {
 | 
			
		||||
        $token = $this->prophesize(TokenInterface::class);
 | 
			
		||||
 | 
			
		||||
        if (null !== $storedObjectRoleEnum) {
 | 
			
		||||
            $token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(true);
 | 
			
		||||
            $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn($storedObjectRoleEnum);
 | 
			
		||||
        } else {
 | 
			
		||||
            $token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(false);
 | 
			
		||||
            $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willThrow(new \InvalidArgumentException());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null !== $storedObject) {
 | 
			
		||||
            $token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(true);
 | 
			
		||||
            $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn($storedObject->getUuid()->toString());
 | 
			
		||||
        } else {
 | 
			
		||||
            $token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(false);
 | 
			
		||||
            $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willThrow(new \InvalidArgumentException());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $token->reveal();
 | 
			
		||||
        yield [
 | 
			
		||||
            // there is no role voter, fallback to security, which does not grant access
 | 
			
		||||
            [[true, false, false]],
 | 
			
		||||
            new StoredObject(),
 | 
			
		||||
            StoredObjectRoleEnum::SEE->value,
 | 
			
		||||
            true,
 | 
			
		||||
            false,
 | 
			
		||||
            VoterInterface::ACCESS_DENIED,
 | 
			
		||||
        ];
 | 
			
		||||
        yield [
 | 
			
		||||
            // there is no role voter, fallback to security, which does grant access
 | 
			
		||||
            [[true, false, false]],
 | 
			
		||||
            new StoredObject(),
 | 
			
		||||
            StoredObjectRoleEnum::SEE->value,
 | 
			
		||||
            true,
 | 
			
		||||
            true,
 | 
			
		||||
            VoterInterface::ACCESS_GRANTED,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,8 @@ class SignedUrlNormalizerTest extends KernelTestCase
 | 
			
		||||
        $signedUrl = new SignedUrl(
 | 
			
		||||
            'GET',
 | 
			
		||||
            'https://object.store.example/container/object',
 | 
			
		||||
            \DateTimeImmutable::createFromFormat('U', '1700000')
 | 
			
		||||
            \DateTimeImmutable::createFromFormat('U', '1700000'),
 | 
			
		||||
            'object'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
 | 
			
		||||
@@ -48,6 +49,7 @@ class SignedUrlNormalizerTest extends KernelTestCase
 | 
			
		||||
                'method' => 'GET',
 | 
			
		||||
                'expires' => 1_700_000,
 | 
			
		||||
                'url' => 'https://object.store.example/container/object',
 | 
			
		||||
                'object_name' => 'object',
 | 
			
		||||
            ],
 | 
			
		||||
            $actual
 | 
			
		||||
        );
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,7 @@ class SignedUrlPostNormalizerTest extends KernelTestCase
 | 
			
		||||
        $signedUrl = new SignedUrlPost(
 | 
			
		||||
            'https://object.store.example/container/object',
 | 
			
		||||
            \DateTimeImmutable::createFromFormat('U', '1700000'),
 | 
			
		||||
            'abc',
 | 
			
		||||
            15000,
 | 
			
		||||
            1,
 | 
			
		||||
            180,
 | 
			
		||||
@@ -59,6 +60,7 @@ class SignedUrlPostNormalizerTest extends KernelTestCase
 | 
			
		||||
                'method' => 'POST',
 | 
			
		||||
                'expires' => 1_700_000,
 | 
			
		||||
                'url' => 'https://object.store.example/container/object',
 | 
			
		||||
                'object_name' => 'abc',
 | 
			
		||||
            ],
 | 
			
		||||
            $actual
 | 
			
		||||
        );
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,99 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessageHandler;
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Psr\Log\NullLogger;
 | 
			
		||||
use Symfony\Component\Clock\MockClock;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class PdfSignedMessageHandlerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    public function testThatObjectIsWrittenInStoredObjectManagerHappyScenario(): void
 | 
			
		||||
    {
 | 
			
		||||
        // a dummy stored object
 | 
			
		||||
        $storedObject = new StoredObject();
 | 
			
		||||
        // build the associated EntityWorkflow, with one step with a person signature
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futurePersonSignatures[] = new Person();
 | 
			
		||||
        $entityWorkflow->setStep('new_step', $dto, 'new_transition', new \DateTimeImmutable(), new User());
 | 
			
		||||
        $step = $entityWorkflow->getCurrentStep();
 | 
			
		||||
        $signature = $step->getSignatures()->first();
 | 
			
		||||
 | 
			
		||||
        $handler = new PdfSignedMessageHandler(
 | 
			
		||||
            new NullLogger(),
 | 
			
		||||
            $this->buildEntityWorkflowManager($storedObject),
 | 
			
		||||
            $this->buildStoredObjectManager($storedObject, $expectedContent = '1234'),
 | 
			
		||||
            $this->buildSignatureRepository($signature),
 | 
			
		||||
            $this->buildEntityManager(true),
 | 
			
		||||
            new MockClock('now'),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // we simply call the handler. The mocked StoredObjectManager will check that the "write" method is invoked once
 | 
			
		||||
        // with the content "1234"
 | 
			
		||||
        $handler(new PdfSignedMessage(10, $expectedContent));
 | 
			
		||||
 | 
			
		||||
        self::assertEquals('signed', $signature->getState()->value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildSignatureRepository(EntityWorkflowStepSignature $signature): EntityWorkflowStepSignatureRepository
 | 
			
		||||
    {
 | 
			
		||||
        $entityWorkflowStepSignatureRepository = $this->createMock(EntityWorkflowStepSignatureRepository::class);
 | 
			
		||||
        $entityWorkflowStepSignatureRepository->method('find')->with($this->isType('int'))->willReturn($signature);
 | 
			
		||||
 | 
			
		||||
        return $entityWorkflowStepSignatureRepository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildEntityWorkflowManager(?StoredObject $associatedStoredObject): EntityWorkflowManager
 | 
			
		||||
    {
 | 
			
		||||
        $entityWorkflowManager = $this->createMock(EntityWorkflowManager::class);
 | 
			
		||||
        $entityWorkflowManager->method('getAssociatedStoredObject')->willReturn($associatedStoredObject);
 | 
			
		||||
 | 
			
		||||
        return $entityWorkflowManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildStoredObjectManager(StoredObject $expectedStoredObject, string $expectedContent): StoredObjectManagerInterface
 | 
			
		||||
    {
 | 
			
		||||
        $storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);
 | 
			
		||||
        $storedObjectManager->expects($this->once())
 | 
			
		||||
            ->method('write')
 | 
			
		||||
            ->with($this->identicalTo($expectedStoredObject), $expectedContent);
 | 
			
		||||
 | 
			
		||||
        return $storedObjectManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildEntityManager(bool $willFlush): EntityManagerInterface
 | 
			
		||||
    {
 | 
			
		||||
        $em = $this->createMock(EntityManagerInterface::class);
 | 
			
		||||
        $em->expects($willFlush ? $this->once() : $this->never())->method('flush');
 | 
			
		||||
        $em->expects($willFlush ? $this->once() : $this->never())->method('clear');
 | 
			
		||||
 | 
			
		||||
        return $em;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessageSerializer;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Component\Messenger\Envelope;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class PdfSignedMessageSerializerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    public function testDecode(): void
 | 
			
		||||
    {
 | 
			
		||||
        $asString = <<<'JSON'
 | 
			
		||||
            {"signatureId": 0, "content": "dGVzdAo="}
 | 
			
		||||
            JSON;
 | 
			
		||||
 | 
			
		||||
        $actual = $this->buildSerializer()->decode(['body' => $asString]);
 | 
			
		||||
 | 
			
		||||
        self::assertInstanceOf(Envelope::class, $actual);
 | 
			
		||||
        $message = $actual->getMessage();
 | 
			
		||||
        self::assertInstanceOf(PdfSignedMessage::class, $message);
 | 
			
		||||
        self::assertEquals("test\n", $message->content);
 | 
			
		||||
        self::assertEquals(0, $message->signatureId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testEncode(): void
 | 
			
		||||
    {
 | 
			
		||||
        $envelope = new Envelope(
 | 
			
		||||
            new PdfSignedMessage(0, "test\n")
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $actual = $this->buildSerializer()->encode($envelope);
 | 
			
		||||
 | 
			
		||||
        self::assertIsArray($actual);
 | 
			
		||||
        self::assertArrayHasKey('body', $actual);
 | 
			
		||||
        self::assertArrayHasKey('headers', $actual);
 | 
			
		||||
        self::assertEquals([], $actual['headers']);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(<<<'JSON'
 | 
			
		||||
                {"signatureId":0,"content":"dGVzdAo="}
 | 
			
		||||
                JSON, $actual['body']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildSerializer(): PdfSignedMessageSerializer
 | 
			
		||||
    {
 | 
			
		||||
        return new PdfSignedMessageSerializer();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,137 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessageSerializer;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Component\Messenger\Envelope;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Serializer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class RequestPdfSignMessageSerializerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    public function testEncode(): void
 | 
			
		||||
    {
 | 
			
		||||
        $serializer = $this->buildSerializer();
 | 
			
		||||
 | 
			
		||||
        $envelope = new Envelope(
 | 
			
		||||
            $request = new RequestPdfSignMessage(
 | 
			
		||||
                0,
 | 
			
		||||
                new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
 | 
			
		||||
                0,
 | 
			
		||||
                'metadata to add to the signature',
 | 
			
		||||
                'Mme Caroline Diallo',
 | 
			
		||||
                'abc'
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $actual = $serializer->encode($envelope);
 | 
			
		||||
        $expectedBody = json_encode([
 | 
			
		||||
            'signatureId' => $request->signatureId,
 | 
			
		||||
            'signatureZoneIndex' => $request->signatureZoneIndex,
 | 
			
		||||
            'signatureZone' => ['x' => 10.0],
 | 
			
		||||
            'reason' => $request->reason,
 | 
			
		||||
            'signerText' => $request->signerText,
 | 
			
		||||
            'content' => base64_encode($request->content),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        self::assertIsArray($actual);
 | 
			
		||||
        self::assertArrayHasKey('body', $actual);
 | 
			
		||||
        self::assertArrayHasKey('headers', $actual);
 | 
			
		||||
        self::assertEquals($expectedBody, $actual['body']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testDecode(): void
 | 
			
		||||
    {
 | 
			
		||||
        $serializer = $this->buildSerializer();
 | 
			
		||||
 | 
			
		||||
        $request = new RequestPdfSignMessage(
 | 
			
		||||
            0,
 | 
			
		||||
            new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
 | 
			
		||||
            0,
 | 
			
		||||
            'metadata to add to the signature',
 | 
			
		||||
            'Mme Caroline Diallo',
 | 
			
		||||
            'abc'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $bodyAsString = json_encode([
 | 
			
		||||
            'signatureId' => $request->signatureId,
 | 
			
		||||
            'signatureZoneIndex' => $request->signatureZoneIndex,
 | 
			
		||||
            'signatureZone' => ['x' => 10.0],
 | 
			
		||||
            'reason' => $request->reason,
 | 
			
		||||
            'signerText' => $request->signerText,
 | 
			
		||||
            'content' => base64_encode($request->content),
 | 
			
		||||
        ], JSON_THROW_ON_ERROR);
 | 
			
		||||
 | 
			
		||||
        $actual = $serializer->decode([
 | 
			
		||||
            'body' => $bodyAsString,
 | 
			
		||||
            'headers' => [
 | 
			
		||||
                'Message' => RequestPdfSignMessage::class,
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        self::assertInstanceOf(RequestPdfSignMessage::class, $actual->getMessage());
 | 
			
		||||
        self::assertEquals($request->signatureId, $actual->getMessage()->signatureId);
 | 
			
		||||
        self::assertEquals($request->signatureZoneIndex, $actual->getMessage()->signatureZoneIndex);
 | 
			
		||||
        self::assertEquals($request->reason, $actual->getMessage()->reason);
 | 
			
		||||
        self::assertEquals($request->signerText, $actual->getMessage()->signerText);
 | 
			
		||||
        self::assertEquals($request->content, $actual->getMessage()->content);
 | 
			
		||||
        self::assertNotNull($actual->getMessage()->PDFSignatureZone);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildSerializer(): RequestPdfSignMessageSerializer
 | 
			
		||||
    {
 | 
			
		||||
        $normalizer =
 | 
			
		||||
            new class () implements NormalizerInterface {
 | 
			
		||||
                public function normalize($object, ?string $format = null, array $context = []): array
 | 
			
		||||
                {
 | 
			
		||||
                    if (!$object instanceof PDFSignatureZone) {
 | 
			
		||||
                        throw new UnexpectedValueException('expected RequestPdfSignMessage');
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return [
 | 
			
		||||
                        'x' => $object->x,
 | 
			
		||||
                    ];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                public function supportsNormalization($data, ?string $format = null): bool
 | 
			
		||||
                {
 | 
			
		||||
                    return $data instanceof PDFSignatureZone;
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        $denormalizer = new class () implements DenormalizerInterface {
 | 
			
		||||
            public function denormalize($data, string $type, ?string $format = null, array $context = [])
 | 
			
		||||
            {
 | 
			
		||||
                return new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public function supportsDenormalization($data, string $type, ?string $format = null)
 | 
			
		||||
            {
 | 
			
		||||
                return PDFSignatureZone::class === $type;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        $serializer = new Serializer([$normalizer, $denormalizer]);
 | 
			
		||||
 | 
			
		||||
        return new RequestPdfSignMessageSerializer($serializer, $serializer);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,79 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Tests\Service\Signature;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class PDFSignatureZoneParserTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    private static PDFSignatureZoneParser $parser;
 | 
			
		||||
 | 
			
		||||
    public static function setUpBeforeClass(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::$parser = new PDFSignatureZoneParser();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider provideFiles
 | 
			
		||||
     *
 | 
			
		||||
     * @param list<PDFSignatureZone> $expected
 | 
			
		||||
     */
 | 
			
		||||
    public function testFindSignatureZones(string $filePath, array $expected): void
 | 
			
		||||
    {
 | 
			
		||||
        $content = file_get_contents($filePath);
 | 
			
		||||
 | 
			
		||||
        if (false === $content) {
 | 
			
		||||
            throw new \LogicException("Unable to read file {$filePath}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $actual = self::$parser->findSignatureZones($content);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(count($expected), count($actual));
 | 
			
		||||
 | 
			
		||||
        foreach ($actual as $index => $signatureZone) {
 | 
			
		||||
            self::assertObjectEquals($expected[$index], $signatureZone);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function provideFiles(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        yield [
 | 
			
		||||
            __DIR__.'/data/signature_2_signature_page_1.pdf',
 | 
			
		||||
            [
 | 
			
		||||
                new PDFSignatureZone(
 | 
			
		||||
                    0,
 | 
			
		||||
                    127.7,
 | 
			
		||||
                    95.289,
 | 
			
		||||
                    90.0,
 | 
			
		||||
                    180.0,
 | 
			
		||||
                    $page = new PDFPage(0, 595.30393, 841.8897)
 | 
			
		||||
                ),
 | 
			
		||||
                new PDFSignatureZone(
 | 
			
		||||
                    1,
 | 
			
		||||
                    269.5,
 | 
			
		||||
                    95.289,
 | 
			
		||||
                    90.0,
 | 
			
		||||
                    180.0,
 | 
			
		||||
                    $page,
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@@ -202,7 +202,8 @@ final class StoredObjectManagerTest extends TestCase
 | 
			
		||||
        $response = new SignedUrl(
 | 
			
		||||
            'PUT',
 | 
			
		||||
            'https://example.com/'.$storedObject->getFilename(),
 | 
			
		||||
            new \DateTimeImmutable('1 hours')
 | 
			
		||||
            new \DateTimeImmutable('1 hours'),
 | 
			
		||||
            $storedObject->getFilename()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ class AsyncFileExistsValidatorTest extends ConstraintValidatorTestCase
 | 
			
		||||
 | 
			
		||||
        $generator = $this->prophesize(TempUrlGeneratorInterface::class);
 | 
			
		||||
        $generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any())
 | 
			
		||||
            ->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
 | 
			
		||||
            ->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'), $args[1]));
 | 
			
		||||
 | 
			
		||||
        return new AsyncFileExistsValidator($generator->reveal(), $client);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -12,27 +12,25 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\DocStoreBundle\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
 | 
			
		||||
class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface
 | 
			
		||||
/**
 | 
			
		||||
 * @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingCourseDocument>
 | 
			
		||||
 */
 | 
			
		||||
readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
 | 
			
		||||
{
 | 
			
		||||
    private readonly EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * TODO: injecter le repository directement.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        EntityManagerInterface $em,
 | 
			
		||||
        private readonly TranslatorInterface $translator
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->repository = $em->getRepository(AccompanyingCourseDocument::class);
 | 
			
		||||
    }
 | 
			
		||||
        private TranslatorInterface $translator,
 | 
			
		||||
        private EntityWorkflowRepository $workflowRepository,
 | 
			
		||||
        private AccompanyingCourseDocumentRepository $repository
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function getDeletionRoles(): array
 | 
			
		||||
    {
 | 
			
		||||
@@ -73,8 +71,6 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param AccompanyingCourseDocument $object
 | 
			
		||||
     *
 | 
			
		||||
     * @return array[]
 | 
			
		||||
     */
 | 
			
		||||
    public function getRelatedObjects(object $object): array
 | 
			
		||||
@@ -122,8 +118,22 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
 | 
			
		||||
        return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject
 | 
			
		||||
    {
 | 
			
		||||
        return $this->getRelatedEntity($entityWorkflow)?->getObject();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findByRelatedEntity(object $object): array
 | 
			
		||||
    {
 | 
			
		||||
        if (!$object instanceof AccompanyingCourseDocument) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,4 +5,5 @@ module.exports = function(encore)
 | 
			
		||||
    });
 | 
			
		||||
    encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.ts');
 | 
			
		||||
    encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index');
 | 
			
		||||
    encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index.ts');
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								src/Bundle/ChillDocStoreBundle/config/services/security.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/Bundle/ChillDocStoreBundle/config/services/security.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
services:
 | 
			
		||||
    _defaults:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
    Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $storedObjectVoters: !tagged_iterator stored_object_voter
 | 
			
		||||
        tags:
 | 
			
		||||
            - { name: security.voter }
 | 
			
		||||
 | 
			
		||||
    Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter:
 | 
			
		||||
        tags:
 | 
			
		||||
            - { name: security.voter }
 | 
			
		||||
@@ -31,7 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
/**
 | 
			
		||||
 * Class Event.
 | 
			
		||||
 */
 | 
			
		||||
#[ORM\Entity(repositoryClass: \Chill\EventBundle\Repository\EventRepository::class)]
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\HasLifecycleCallbacks]
 | 
			
		||||
#[ORM\Table(name: 'chill_event_event')]
 | 
			
		||||
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
@@ -62,9 +62,9 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
 | 
			
		||||
    private ?string $name = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<Participation>
 | 
			
		||||
     * @var Collection<int, Participation>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: Participation::class, mappedBy: 'event')]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'event', targetEntity: Participation::class)]
 | 
			
		||||
    private Collection $participations;
 | 
			
		||||
 | 
			
		||||
    #[Assert\NotNull]
 | 
			
		||||
@@ -79,7 +79,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
 | 
			
		||||
    private ?Location $location = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<StoredObject>
 | 
			
		||||
     * @var Collection<int, StoredObject>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist', 'refresh'])]
 | 
			
		||||
    #[ORM\JoinTable('chill_event_event_documents')]
 | 
			
		||||
@@ -192,7 +192,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
 | 
			
		||||
    {
 | 
			
		||||
        $iterator = iterator_to_array($this->participations->getIterator());
 | 
			
		||||
 | 
			
		||||
        uasort($iterator, static fn ($first, $second) => strnatcasecmp((string) $first->getPerson()->getFirstName(), (string) $second->getPerson()->getFirstName()));
 | 
			
		||||
        uasort($iterator, static fn ($first, $second) => strnatcasecmp($first->getPerson()->getFirstName(), $second->getPerson()->getFirstName()));
 | 
			
		||||
 | 
			
		||||
        return new \ArrayIterator($iterator);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -38,13 +38,13 @@ class EventType
 | 
			
		||||
    private $name;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<Role>
 | 
			
		||||
     * @var Collection<int, Role>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: Role::class, mappedBy: 'type')]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'type', targetEntity: Role::class)]
 | 
			
		||||
    private Collection $roles;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<Status>
 | 
			
		||||
     * @var Collection<int, Status>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: Status::class, mappedBy: 'type')]
 | 
			
		||||
    private Collection $statuses;
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\EventBundle\Form\ChoiceLoader;
 | 
			
		||||
 | 
			
		||||
use Chill\EventBundle\Entity\Event;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Chill\EventBundle\Repository\EventRepository;
 | 
			
		||||
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
 | 
			
		||||
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
 | 
			
		||||
 | 
			
		||||
@@ -26,9 +26,6 @@ class EventChoiceLoader implements ChoiceLoaderInterface
 | 
			
		||||
     */
 | 
			
		||||
    protected $centers = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var EntityRepository
 | 
			
		||||
     */
 | 
			
		||||
    protected $eventRepository;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -40,7 +37,7 @@ class EventChoiceLoader implements ChoiceLoaderInterface
 | 
			
		||||
     * EventChoiceLoader constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        EntityRepository $eventRepository,
 | 
			
		||||
        EventRepository $eventRepository,
 | 
			
		||||
        ?array $centers = null
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->eventRepository = $eventRepository;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,17 +11,65 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
 | 
			
		||||
use Chill\EventBundle\Entity\Event;
 | 
			
		||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 | 
			
		||||
use Doctrine\Persistence\ManagerRegistry;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class EventRepository.
 | 
			
		||||
 */
 | 
			
		||||
class EventRepository extends ServiceEntityRepository
 | 
			
		||||
class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(ManagerRegistry $registry)
 | 
			
		||||
    private readonly EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(EntityManagerInterface $entityManager)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($registry, Event::class);
 | 
			
		||||
        $this->repository = $entityManager->getRepository(Event::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->createQueryBuilder($alias, $indexBy);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->createQueryBuilder('e');
 | 
			
		||||
        $query = $qb
 | 
			
		||||
            ->join('e.documents', 'ed')
 | 
			
		||||
            ->where('ed.id = :storedObjectId')
 | 
			
		||||
            ->setParameter('storedObjectId', $storedObject->getId())
 | 
			
		||||
            ->getQuery();
 | 
			
		||||
 | 
			
		||||
        return $query->getOneOrNullResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function find($id)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->find($id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAll(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return Event::class;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
 | 
			
		||||
use Chill\EventBundle\Entity\Event;
 | 
			
		||||
use Chill\EventBundle\Repository\EventRepository;
 | 
			
		||||
use Chill\EventBundle\Security\EventVoter;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
class EventStoredObjectVoter extends AbstractStoredObjectVoter
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly EventRepository $repository,
 | 
			
		||||
        Security $security,
 | 
			
		||||
        WorkflowStoredObjectPermissionHelper $workflowDocumentService
 | 
			
		||||
    ) {
 | 
			
		||||
        parent::__construct($security, $workflowDocumentService);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getRepository(): AssociatedEntityToStoredObjectInterface
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getClass(): string
 | 
			
		||||
    {
 | 
			
		||||
        return Event::class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function attributeToRole(StoredObjectRoleEnum $attribute): string
 | 
			
		||||
    {
 | 
			
		||||
        return match ($attribute) {
 | 
			
		||||
            StoredObjectRoleEnum::EDIT => EventVoter::UPDATE,
 | 
			
		||||
            StoredObjectRoleEnum::SEE => EventVoter::SEE_DETAILS,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function canBeAssociatedWithWorkflow(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -96,7 +96,7 @@ class SearchController extends AbstractController
 | 
			
		||||
        return $this->render('@ChillMain/Search/choose_list.html.twig');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json'], defaults: ['_format' => 'html'])]
 | 
			
		||||
    #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json', '_locale' => '[a-z]{1,3}'], defaults: ['_format' => 'html'])]
 | 
			
		||||
    public function searchAction(Request $request, mixed $_format)
 | 
			
		||||
    {
 | 
			
		||||
        $pattern = trim((string) $request->query->get('q', ''));
 | 
			
		||||
 
 | 
			
		||||
@@ -11,25 +11,30 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
 | 
			
		||||
use Chill\MainBundle\Form\EntityWorkflowCommentType;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
 | 
			
		||||
use Chill\MainBundle\Form\WorkflowSignatureMetadataType;
 | 
			
		||||
use Chill\MainBundle\Form\WorkflowStepType;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
 | 
			
		||||
use Chill\MainBundle\Security\ChillSecurity;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
 | 
			
		||||
use Symfony\Component\Workflow\Registry;
 | 
			
		||||
@@ -38,7 +43,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
 | 
			
		||||
class WorkflowController extends AbstractController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly ValidatorInterface $validator, private readonly PaginatorFactory $paginatorFactory, private readonly Registry $registry, private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, private readonly ChillSecurity $security, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry) {}
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly EntityWorkflowManager $entityWorkflowManager,
 | 
			
		||||
        private readonly EntityWorkflowRepository $entityWorkflowRepository,
 | 
			
		||||
        private readonly ValidatorInterface $validator,
 | 
			
		||||
        private readonly StoredObjectManagerInterface $storedObjectManagerInterface,
 | 
			
		||||
        private readonly PaginatorFactory $paginatorFactory,
 | 
			
		||||
        private readonly Registry $registry,
 | 
			
		||||
        private readonly EntityManagerInterface $entityManager,
 | 
			
		||||
        private readonly TranslatorInterface $translator,
 | 
			
		||||
        private readonly ChillSecurity $security,
 | 
			
		||||
        private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
 | 
			
		||||
        private readonly ClockInterface $clock,
 | 
			
		||||
        private readonly PDFSignatureZoneParser $PDFSignatureZoneParser,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
 | 
			
		||||
    public function create(Request $request): Response
 | 
			
		||||
@@ -276,10 +294,11 @@ class WorkflowController extends AbstractController
 | 
			
		||||
        $handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
 | 
			
		||||
        $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
 | 
			
		||||
        $errors = [];
 | 
			
		||||
        $signatures = $entityWorkflow->getCurrentStep()->getSignatures();
 | 
			
		||||
 | 
			
		||||
        if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
 | 
			
		||||
            // possible transition
 | 
			
		||||
 | 
			
		||||
            $stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
            $usersInvolved = $entityWorkflow->getUsersInvolved();
 | 
			
		||||
            $currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
 | 
			
		||||
 | 
			
		||||
@@ -289,9 +308,8 @@ class WorkflowController extends AbstractController
 | 
			
		||||
 | 
			
		||||
            $transitionForm = $this->createForm(
 | 
			
		||||
                WorkflowStepType::class,
 | 
			
		||||
                $entityWorkflow->getCurrentStep(),
 | 
			
		||||
                $stepDTO,
 | 
			
		||||
                [
 | 
			
		||||
                    'transition' => true,
 | 
			
		||||
                    'entity_workflow' => $entityWorkflow,
 | 
			
		||||
                    'suggested_users' => $usersInvolved,
 | 
			
		||||
                ]
 | 
			
		||||
@@ -310,12 +328,14 @@ class WorkflowController extends AbstractController
 | 
			
		||||
                    throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
 | 
			
		||||
                $entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
 | 
			
		||||
                $entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
 | 
			
		||||
                $entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
 | 
			
		||||
                $byUser = $this->security->getUser();
 | 
			
		||||
 | 
			
		||||
                $workflow->apply($entityWorkflow, $transition);
 | 
			
		||||
                $workflow->apply($entityWorkflow, $transition, [
 | 
			
		||||
                    'context' => $stepDTO,
 | 
			
		||||
                    'byUser' => $byUser,
 | 
			
		||||
                    'transition' => $transition,
 | 
			
		||||
                    'transitionAt' => $this->clock->now(),
 | 
			
		||||
                ]);
 | 
			
		||||
 | 
			
		||||
                $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
@@ -327,22 +347,6 @@ class WorkflowController extends AbstractController
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /*
 | 
			
		||||
        $commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment());
 | 
			
		||||
        $commentForm->handleRequest($request);
 | 
			
		||||
 | 
			
		||||
        if ($commentForm->isSubmitted() && $commentForm->isValid()) {
 | 
			
		||||
            $this->entityManager->persist($newComment);
 | 
			
		||||
            $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
            $this->addFlash('success', $this->translator->trans('workflow.Comment added'));
 | 
			
		||||
 | 
			
		||||
            return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
 | 
			
		||||
        } elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) {
 | 
			
		||||
            $this->addFlash('error', $this->translator->trans('This form contains errors'));
 | 
			
		||||
        }
 | 
			
		||||
         */
 | 
			
		||||
 | 
			
		||||
        return $this->render(
 | 
			
		||||
            '@ChillMain/Workflow/index.html.twig',
 | 
			
		||||
            [
 | 
			
		||||
@@ -352,7 +356,7 @@ class WorkflowController extends AbstractController
 | 
			
		||||
                'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
 | 
			
		||||
                'entity_workflow' => $entityWorkflow,
 | 
			
		||||
                'transition_form_errors' => $errors,
 | 
			
		||||
                // 'comment_form' => $commentForm->createView(),
 | 
			
		||||
                'signatures' => $signatures,
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
@@ -371,4 +375,78 @@ class WorkflowController extends AbstractController
 | 
			
		||||
 | 
			
		||||
        return $lines;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/metadata', name: 'chill_main_workflow_signature_metadata')]
 | 
			
		||||
    public function addSignatureMetadata(int $signature_id, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        $signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
 | 
			
		||||
 | 
			
		||||
        if ($signature->getSigner() instanceof User) {
 | 
			
		||||
            return $this->redirectToRoute('chill_main_workflow_signature', ['signature_id' => $signature_id]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
 | 
			
		||||
        $metadataForm->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
 | 
			
		||||
 | 
			
		||||
        $metadataForm->handleRequest($request);
 | 
			
		||||
 | 
			
		||||
        if ($metadataForm->isSubmitted() && $metadataForm->isValid()) {
 | 
			
		||||
            $data = $metadataForm->getData();
 | 
			
		||||
 | 
			
		||||
            $signature->setSignatureMetadata(
 | 
			
		||||
                [
 | 
			
		||||
                    'base_signer' => [
 | 
			
		||||
                        'document_type' => $data['documentType'],
 | 
			
		||||
                        'document_number' => $data['documentNumber'],
 | 
			
		||||
                        'expiration_date' => $data['expirationDate'],
 | 
			
		||||
                    ],
 | 
			
		||||
                ]
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            $this->entityManager->persist($signature);
 | 
			
		||||
            $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
            return $this->redirectToRoute('chill_main_workflow_signature', ['signature_id' => $signature_id]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->render(
 | 
			
		||||
            '@ChillMain/Workflow/_signature_metadata.html.twig',
 | 
			
		||||
            [
 | 
			
		||||
                'metadata_form' => $metadataForm->createView(),
 | 
			
		||||
                'person' => $signature->getSigner(),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/sign', name: 'chill_main_workflow_signature', methods: 'GET')]
 | 
			
		||||
    public function addSignature(int $signature_id, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        $signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
 | 
			
		||||
        $entityWorkflow = $signature->getStep()->getEntityWorkflow();
 | 
			
		||||
 | 
			
		||||
        $storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
 | 
			
		||||
        if (null === $storedObject) {
 | 
			
		||||
            throw new NotFoundHttpException('No stored object found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $zones = [];
 | 
			
		||||
        $content = $this->storedObjectManagerInterface->read($storedObject);
 | 
			
		||||
        if (null != $content) {
 | 
			
		||||
            $zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $signatureClient = [];
 | 
			
		||||
        $signatureClient['id'] = $signature->getId();
 | 
			
		||||
        $signatureClient['storedObject'] = [
 | 
			
		||||
            'filename' => $storedObject->getFilename(),
 | 
			
		||||
            'iv' => $storedObject->getIv(),
 | 
			
		||||
            'keyInfos' => $storedObject->getKeyInfos(),
 | 
			
		||||
        ];
 | 
			
		||||
        $signatureClient['zones'] = $zones;
 | 
			
		||||
 | 
			
		||||
        return $this->render(
 | 
			
		||||
            '@ChillMain/Workflow/_signature_sign.html.twig',
 | 
			
		||||
            ['signature' => $signatureClient]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -85,6 +85,29 @@ class Configuration implements ConfigurationInterface
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end() // end of notifications
 | 
			
		||||
            ->arrayNode('workflow_signature')
 | 
			
		||||
            ->children()
 | 
			
		||||
            ->arrayNode('base_signer')
 | 
			
		||||
            ->children()
 | 
			
		||||
            ->arrayNode('document_kinds')
 | 
			
		||||
            ->arrayPrototype()
 | 
			
		||||
            ->children()
 | 
			
		||||
            ->scalarNode('key')->cannotBeEmpty()->end()
 | 
			
		||||
            ->arrayNode('labels')
 | 
			
		||||
            ->arrayPrototype()
 | 
			
		||||
            ->children()
 | 
			
		||||
            ->scalarNode('lang')->cannotBeEmpty()->end()
 | 
			
		||||
            ->scalarNode('label')->cannotBeEmpty()->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end() // end of workflow signature document types
 | 
			
		||||
            ->arrayNode('phone_helper')
 | 
			
		||||
            ->canBeUnset()
 | 
			
		||||
            ->children()
 | 
			
		||||
 
 | 
			
		||||
@@ -92,7 +92,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
     * This list is computed by a materialized view. It won't be populated until a refresh is done
 | 
			
		||||
     * on the materialized view.
 | 
			
		||||
     *
 | 
			
		||||
     * @var Collection<GeographicalUnit>
 | 
			
		||||
     * @var Collection<int, GeographicalUnit>
 | 
			
		||||
     *
 | 
			
		||||
     * @readonly
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -21,9 +21,9 @@ use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
class Center implements HasCenterInterface, \Stringable
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<GroupCenter>
 | 
			
		||||
     * @var Collection<int, GroupCenter>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: GroupCenter::class, mappedBy: 'center')]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'center', targetEntity: GroupCenter::class)]
 | 
			
		||||
    private Collection $groupCenters;
 | 
			
		||||
 | 
			
		||||
    #[Serializer\Groups(['docgen:read'])]
 | 
			
		||||
@@ -40,7 +40,7 @@ class Center implements HasCenterInterface, \Stringable
 | 
			
		||||
    private bool $isActive = true;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<Regroupment>
 | 
			
		||||
     * @var Collection<int, Regroupment>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: Regroupment::class, mappedBy: 'centers')]
 | 
			
		||||
    private Collection $regroupments;
 | 
			
		||||
 
 | 
			
		||||
@@ -36,9 +36,9 @@ class GeographicalUnitLayer
 | 
			
		||||
    private string $refId = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<GeographicalUnit>
 | 
			
		||||
     * @var Collection<int, GeographicalUnit>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: GeographicalUnit::class, mappedBy: 'layer')]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'layer', targetEntity: GeographicalUnit::class)]
 | 
			
		||||
    private Collection $units;
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ class GroupCenter
 | 
			
		||||
    private ?PermissionsGroup $permissionsGroup = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User::class>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'groupCenters')]
 | 
			
		||||
    private Collection $users;
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
    private array $addedAddresses = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
 | 
			
		||||
@@ -54,9 +54,9 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
    private ?ArrayCollection $addressesOnLoad = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<NotificationComment>
 | 
			
		||||
     * @var Collection<int, NotificationComment>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: NotificationComment::class, mappedBy: 'notification', orphanRemoval: true)]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'notification', targetEntity: NotificationComment::class, orphanRemoval: true)]
 | 
			
		||||
    #[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
 | 
			
		||||
    private Collection $comments;
 | 
			
		||||
 | 
			
		||||
@@ -88,7 +88,7 @@ class Notification implements TrackUpdateInterface
 | 
			
		||||
    private string $title = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
 | 
			
		||||
 
 | 
			
		||||
@@ -28,9 +28,9 @@ class PermissionsGroup
 | 
			
		||||
    private array $flags = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<GroupCenter>
 | 
			
		||||
     * @var Collection<int, GroupCenter>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: GroupCenter::class, mappedBy: 'permissionsGroup')]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'permissionsGroup', targetEntity: GroupCenter::class)]
 | 
			
		||||
    private Collection $groupCenters;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
@@ -42,7 +42,7 @@ class PermissionsGroup
 | 
			
		||||
    private string $name = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<RoleScope>
 | 
			
		||||
     * @var Collection<int, RoleScope>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: RoleScope::class, inversedBy: 'permissionsGroups', cascade: ['persist'])]
 | 
			
		||||
    #[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
class Regroupment
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<Center>
 | 
			
		||||
     * @var Collection<int, Center>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: Center::class, inversedBy: 'regroupments')]
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ class RoleScope
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<PermissionsGroup>
 | 
			
		||||
     * @var Collection<int, PermissionsGroup>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: PermissionsGroup::class, mappedBy: 'roleScopes')]
 | 
			
		||||
    private Collection $permissionsGroups;
 | 
			
		||||
 
 | 
			
		||||
@@ -42,9 +42,9 @@ class Scope
 | 
			
		||||
    private array $name = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<RoleScope>
 | 
			
		||||
     * @var Collection<int, RoleScope>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: RoleScope::class, mappedBy: 'scope')]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'scope', targetEntity: RoleScope::class)]
 | 
			
		||||
    #[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
 | 
			
		||||
    private Collection $roleScopes;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
 | 
			
		||||
    private bool $enabled = true;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<GroupCenter>
 | 
			
		||||
     * @var Collection<int, \Chill\MainBundle\Entity\GroupCenter>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: GroupCenter::class, inversedBy: 'users')]
 | 
			
		||||
    #[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
 | 
			
		||||
@@ -83,9 +83,9 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
 | 
			
		||||
    private ?Location $mainLocation = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection&Selectable<int, UserScopeHistory>
 | 
			
		||||
     * @var \Doctrine\Common\Collections\Collection<int, \Chill\MainBundle\Entity\User\UserScopeHistory>&Selectable
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: UserScopeHistory::class, mappedBy: 'user', cascade: ['persist', 'remove'], orphanRemoval: true)]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserScopeHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
 | 
			
		||||
    private Collection&Selectable $scopeHistories;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
 | 
			
		||||
@@ -98,9 +98,9 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
 | 
			
		||||
    private ?string $salt = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection&Selectable<int, UserJobHistory>
 | 
			
		||||
     * @var \Doctrine\Common\Collections\Collection<int, \Chill\MainBundle\Entity\User\UserJobHistory>&Selectable
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: UserJobHistory::class, mappedBy: 'user', cascade: ['persist', 'remove'], orphanRemoval: true)]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserJobHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
 | 
			
		||||
    private Collection&Selectable $jobHistories;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 80)]
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,9 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
 | 
			
		||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
			
		||||
use Doctrine\Common\Collections\ArrayCollection;
 | 
			
		||||
use Doctrine\Common\Collections\Collection;
 | 
			
		||||
use Doctrine\Common\Collections\Order;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
@@ -35,36 +35,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * a list of future cc users for the next steps.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array|User[]
 | 
			
		||||
     */
 | 
			
		||||
    public array $futureCcUsers = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * a list of future dest emails for the next steps.
 | 
			
		||||
     *
 | 
			
		||||
     * This is in used in order to let controller inform who will be the future emails which will validate
 | 
			
		||||
     * the next step. This is necessary to perform some computation about the next emails, before they are
 | 
			
		||||
     * associated to the entity EntityWorkflowStep.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array|string[]
 | 
			
		||||
     */
 | 
			
		||||
    public array $futureDestEmails = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * a list of future dest users for the next steps.
 | 
			
		||||
     *
 | 
			
		||||
     * This is in used in order to let controller inform who will be the future users which will validate
 | 
			
		||||
     * the next step. This is necessary to perform some computation about the next users, before they are
 | 
			
		||||
     * associated to the entity EntityWorkflowStep.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array|User[]
 | 
			
		||||
     */
 | 
			
		||||
    public array $futureDestUsers = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<EntityWorkflowComment>
 | 
			
		||||
     * @var Collection<int, \Chill\MainBundle\Entity\Workflow\EntityWorkflowComment>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: EntityWorkflowComment::class, mappedBy: 'entityWorkflow', orphanRemoval: true)]
 | 
			
		||||
    private Collection $comments;
 | 
			
		||||
@@ -81,10 +52,10 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
    private int $relatedEntityId;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<EntityWorkflowStep>
 | 
			
		||||
     * @var Collection<int, EntityWorkflowStep>
 | 
			
		||||
     */
 | 
			
		||||
    #[Assert\Valid(traverse: true)]
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: EntityWorkflowStep::class, mappedBy: 'entityWorkflow', orphanRemoval: true, cascade: ['persist'])]
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowStep::class, cascade: ['persist'], orphanRemoval: true)]
 | 
			
		||||
    #[ORM\OrderBy(['transitionAt' => \Doctrine\Common\Collections\Criteria::ASC, 'id' => 'ASC'])]
 | 
			
		||||
    private Collection $steps;
 | 
			
		||||
 | 
			
		||||
@@ -94,14 +65,14 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
    private ?array $stepsChainedCache = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_final')]
 | 
			
		||||
    private Collection $subscriberToFinal;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_step')]
 | 
			
		||||
@@ -276,12 +247,16 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
        return $this->steps;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function getStepsChained(): array
 | 
			
		||||
    {
 | 
			
		||||
        if (\is_array($this->stepsChainedCache)) {
 | 
			
		||||
            return $this->stepsChainedCache;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /** @var \ArrayIterator $iterator */
 | 
			
		||||
        $iterator = $this->steps->getIterator();
 | 
			
		||||
        $current = null;
 | 
			
		||||
        $steps = [];
 | 
			
		||||
@@ -442,11 +417,43 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
     *
 | 
			
		||||
     * @return $this
 | 
			
		||||
     */
 | 
			
		||||
    public function setStep(string $step): self
 | 
			
		||||
    {
 | 
			
		||||
    public function setStep(
 | 
			
		||||
        string $step,
 | 
			
		||||
        WorkflowTransitionContextDTO $transitionContextDTO,
 | 
			
		||||
        string $transition,
 | 
			
		||||
        \DateTimeImmutable $transitionAt,
 | 
			
		||||
        ?User $byUser = null
 | 
			
		||||
    ): self {
 | 
			
		||||
        $previousStep = $this->getCurrentStep();
 | 
			
		||||
 | 
			
		||||
        $previousStep
 | 
			
		||||
            ->setTransitionAfter($transition)
 | 
			
		||||
            ->setTransitionAt($transitionAt)
 | 
			
		||||
            ->setTransitionBy($byUser);
 | 
			
		||||
 | 
			
		||||
        $newStep = new EntityWorkflowStep();
 | 
			
		||||
        $newStep->setCurrentStep($step);
 | 
			
		||||
 | 
			
		||||
        foreach ($transitionContextDTO->futureCcUsers as $user) {
 | 
			
		||||
            $newStep->addCcUser($user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($transitionContextDTO->futureDestUsers as $user) {
 | 
			
		||||
            $newStep->addDestUser($user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($transitionContextDTO->futureDestEmails as $email) {
 | 
			
		||||
            $newStep->addDestEmail($email);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null !== $transitionContextDTO->futureUserSignature) {
 | 
			
		||||
            new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature);
 | 
			
		||||
        } else {
 | 
			
		||||
            foreach ($transitionContextDTO->futurePersonSignatures as $personSignature) {
 | 
			
		||||
                new EntityWorkflowStepSignature($newStep, $personSignature);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // copy the freeze
 | 
			
		||||
        if ($this->isFreeze()) {
 | 
			
		||||
            $newStep->setFreezeAfter(true);
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
enum EntityWorkflowSignatureStateEnum: string
 | 
			
		||||
{
 | 
			
		||||
    case PENDING = 'pending';
 | 
			
		||||
    case SIGNED = 'signed';
 | 
			
		||||
    case REJECTED = 'rejected';
 | 
			
		||||
    case CANCELED = 'canceled';
 | 
			
		||||
}
 | 
			
		||||
@@ -26,7 +26,7 @@ class EntityWorkflowStep
 | 
			
		||||
    private string $accessKey;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_cc_user')]
 | 
			
		||||
@@ -42,19 +42,25 @@ class EntityWorkflowStep
 | 
			
		||||
    private array $destEmail = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
 | 
			
		||||
    private Collection $destUser;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<User>
 | 
			
		||||
     * @var Collection<int, User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')]
 | 
			
		||||
    private Collection $destUserByAccessKey;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection <int, EntityWorkflowStepSignature>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)]
 | 
			
		||||
    private Collection $signatures;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
 | 
			
		||||
    private ?EntityWorkflow $entityWorkflow = null;
 | 
			
		||||
 | 
			
		||||
@@ -97,6 +103,7 @@ class EntityWorkflowStep
 | 
			
		||||
        $this->ccUser = new ArrayCollection();
 | 
			
		||||
        $this->destUser = new ArrayCollection();
 | 
			
		||||
        $this->destUserByAccessKey = new ArrayCollection();
 | 
			
		||||
        $this->signatures = new ArrayCollection();
 | 
			
		||||
        $this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -136,6 +143,27 @@ class EntityWorkflowStep
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @internal use @see{EntityWorkflowStepSignature}'s constructor instead
 | 
			
		||||
     */
 | 
			
		||||
    public function addSignature(EntityWorkflowStepSignature $signature): self
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->signatures->contains($signature)) {
 | 
			
		||||
            $this->signatures[] = $signature;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function removeSignature(EntityWorkflowStepSignature $signature): self
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->signatures->contains($signature)) {
 | 
			
		||||
            $this->signatures->removeElement($signature);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAccessKey(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->accessKey;
 | 
			
		||||
@@ -198,6 +226,14 @@ class EntityWorkflowStep
 | 
			
		||||
        return $this->entityWorkflow;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return Collection<int, EntityWorkflowStepSignature>
 | 
			
		||||
     */
 | 
			
		||||
    public function getSignatures(): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->signatures;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,138 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\Table(name: 'chill_main_workflow_entity_step_signature')]
 | 
			
		||||
class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, unique: true)]
 | 
			
		||||
    #[ORM\GeneratedValue(strategy: 'AUTO')]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    private ?User $userSigner = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: Person::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    private ?Person $personSigner = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, nullable: false, enumType: EntityWorkflowSignatureStateEnum::class)]
 | 
			
		||||
    private EntityWorkflowSignatureStateEnum $state = EntityWorkflowSignatureStateEnum::PENDING;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIMETZ_IMMUTABLE, nullable: true, options: ['default' => null])]
 | 
			
		||||
    private ?\DateTimeImmutable $stateDate = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
 | 
			
		||||
    private array $signatureMetadata = [];
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
 | 
			
		||||
    private ?int $zoneSignatureIndex = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
 | 
			
		||||
        private EntityWorkflowStep $step,
 | 
			
		||||
        User|Person $signer,
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->step->addSignature($this);
 | 
			
		||||
        $this->setSigner($signer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function setSigner(User|Person $signer): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($signer instanceof User) {
 | 
			
		||||
            $this->userSigner = $signer;
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->personSigner = $signer;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStep(): EntityWorkflowStep
 | 
			
		||||
    {
 | 
			
		||||
        return $this->step;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getSigner(): User|Person
 | 
			
		||||
    {
 | 
			
		||||
        if (null !== $this->userSigner) {
 | 
			
		||||
            return $this->userSigner;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->personSigner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getSignatureMetadata(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->signatureMetadata;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        $this->signatureMetadata = $signatureMetadata;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getState(): EntityWorkflowSignatureStateEnum
 | 
			
		||||
    {
 | 
			
		||||
        return $this->state;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        $this->state = $state;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStateDate(): ?\DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->stateDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        $this->stateDate = $stateDate;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getZoneSignatureIndex(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->zoneSignatureIndex;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        $this->zoneSignatureIndex = $zoneSignatureIndex;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,62 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Form;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillDateType;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class WorkflowSignatureMetadataType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly ParameterBagInterface $parameterBag, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder, array $options): void
 | 
			
		||||
    {
 | 
			
		||||
        $documentTypeChoices = $this->parameterBag->get('chill_main')['workflow_signature']['base_signer']['document_kinds'];
 | 
			
		||||
 | 
			
		||||
        $choices = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($documentTypeChoices as $documentType) {
 | 
			
		||||
            $labels = [];
 | 
			
		||||
 | 
			
		||||
            foreach ($documentType['labels'] as $label) {
 | 
			
		||||
                $labels[$label['lang']] = $label['label'];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $localizedLabel = $this->translatableStringHelper->localize($labels);
 | 
			
		||||
            if (null !== $localizedLabel) {
 | 
			
		||||
                $choices[$localizedLabel] = $documentType['key'];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('documentType', ChoiceType::class, [
 | 
			
		||||
                'label' => 'workflow.signature_zone.metadata.docType',
 | 
			
		||||
                'expanded' => false,
 | 
			
		||||
                'required' => true,
 | 
			
		||||
                'choices' => $choices,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('documentNumber', TextType::class, [
 | 
			
		||||
                'required' => true,
 | 
			
		||||
                'label' => 'workflow.signature_zone.metadata.docNumber',
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('expirationDate', ChillDateType::class, [
 | 
			
		||||
                'required' => true,
 | 
			
		||||
                'input' => 'datetime_immutable',
 | 
			
		||||
                'label' => 'workflow.signature_zone.metadata.docExpiration',
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,14 +12,13 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\MainBundle\Form;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
			
		||||
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
@@ -34,169 +33,179 @@ use Symfony\Component\Workflow\Transition;
 | 
			
		||||
 | 
			
		||||
class WorkflowStepType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly Registry $registry,
 | 
			
		||||
        private readonly TranslatableStringHelperInterface $translatableStringHelper
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder, array $options)
 | 
			
		||||
    {
 | 
			
		||||
        /** @var EntityWorkflow $entityWorkflow */
 | 
			
		||||
        $entityWorkflow = $options['entity_workflow'];
 | 
			
		||||
        $handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
 | 
			
		||||
        $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
 | 
			
		||||
        $place = $workflow->getMarking($entityWorkflow);
 | 
			
		||||
        $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]);
 | 
			
		||||
 | 
			
		||||
        if (true === $options['transition']) {
 | 
			
		||||
            if (null === $options['entity_workflow']) {
 | 
			
		||||
                throw new \LogicException('if transition is true, entity_workflow should be defined');
 | 
			
		||||
            }
 | 
			
		||||
        if (null === $options['entity_workflow']) {
 | 
			
		||||
            throw new \LogicException('if transition is true, entity_workflow should be defined');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            $transitions = $this->registry
 | 
			
		||||
                ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName())
 | 
			
		||||
                ->getEnabledTransitions($entityWorkflow);
 | 
			
		||||
        $transitions = $this->registry
 | 
			
		||||
            ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName())
 | 
			
		||||
            ->getEnabledTransitions($entityWorkflow);
 | 
			
		||||
 | 
			
		||||
            $choices = array_combine(
 | 
			
		||||
                array_map(
 | 
			
		||||
                    static fn (Transition $transition) => $transition->getName(),
 | 
			
		||||
                    $transitions
 | 
			
		||||
                ),
 | 
			
		||||
        $choices = array_combine(
 | 
			
		||||
            array_map(
 | 
			
		||||
                static fn (Transition $transition) => $transition->getName(),
 | 
			
		||||
                $transitions
 | 
			
		||||
            );
 | 
			
		||||
            ),
 | 
			
		||||
            $transitions
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
            if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) {
 | 
			
		||||
                $inputLabels = $placeMetadata['validationFilterInputLabels'];
 | 
			
		||||
        if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) {
 | 
			
		||||
            $inputLabels = $placeMetadata['validationFilterInputLabels'];
 | 
			
		||||
 | 
			
		||||
                $builder->add('transitionFilter', ChoiceType::class, [
 | 
			
		||||
                    'multiple' => false,
 | 
			
		||||
                    'label' => 'workflow.My decision',
 | 
			
		||||
                    'choices' => [
 | 
			
		||||
                        'forward' => 'forward',
 | 
			
		||||
                        'backward' => 'backward',
 | 
			
		||||
                        'neutral' => 'neutral',
 | 
			
		||||
                    ],
 | 
			
		||||
                    'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]),
 | 
			
		||||
                    'choice_attr' => static fn (string $key) => [
 | 
			
		||||
                        $key => $key,
 | 
			
		||||
                    ],
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                    'expanded' => true,
 | 
			
		||||
                    'data' => 'forward',
 | 
			
		||||
                ]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $builder
 | 
			
		||||
                ->add('transition', ChoiceType::class, [
 | 
			
		||||
                    'label' => 'workflow.Next step',
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                    'multiple' => false,
 | 
			
		||||
                    'expanded' => true,
 | 
			
		||||
                    'choices' => $choices,
 | 
			
		||||
                    'constraints' => [new NotNull()],
 | 
			
		||||
                    'choice_label' => function (Transition $transition) use ($workflow) {
 | 
			
		||||
                        $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
 | 
			
		||||
 | 
			
		||||
                        if (\array_key_exists('label', $meta)) {
 | 
			
		||||
                            return $this->translatableStringHelper->localize($meta['label']);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return $transition->getName();
 | 
			
		||||
                    },
 | 
			
		||||
                    'choice_attr' => static function (Transition $transition) use ($workflow) {
 | 
			
		||||
                        $toFinal = true;
 | 
			
		||||
                        $isForward = 'neutral';
 | 
			
		||||
 | 
			
		||||
                        $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
 | 
			
		||||
 | 
			
		||||
                        if (\array_key_exists('isForward', $metadata)) {
 | 
			
		||||
                            if ($metadata['isForward']) {
 | 
			
		||||
                                $isForward = 'forward';
 | 
			
		||||
                            } else {
 | 
			
		||||
                                $isForward = 'backward';
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        foreach ($transition->getTos() as $to) {
 | 
			
		||||
                            $meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
 | 
			
		||||
 | 
			
		||||
                            if (
 | 
			
		||||
                                !\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
 | 
			
		||||
                            ) {
 | 
			
		||||
                                $toFinal = false;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return [
 | 
			
		||||
                            'data-is-transition' => 'data-is-transition',
 | 
			
		||||
                            'data-to-final' => $toFinal ? '1' : '0',
 | 
			
		||||
                            'data-is-forward' => $isForward,
 | 
			
		||||
                        ];
 | 
			
		||||
                    },
 | 
			
		||||
                ])
 | 
			
		||||
                ->add('future_dest_users', PickUserDynamicType::class, [
 | 
			
		||||
                    'label' => 'workflow.dest for next steps',
 | 
			
		||||
                    'multiple' => true,
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                    'suggested' => $options['suggested_users'],
 | 
			
		||||
                ])
 | 
			
		||||
                ->add('future_cc_users', PickUserDynamicType::class, [
 | 
			
		||||
                    'label' => 'workflow.cc for next steps',
 | 
			
		||||
                    'multiple' => true,
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                    'required' => false,
 | 
			
		||||
                    'suggested' => $options['suggested_users'],
 | 
			
		||||
                ])
 | 
			
		||||
                ->add('future_dest_emails', ChillCollectionType::class, [
 | 
			
		||||
                    'label' => 'workflow.dest by email',
 | 
			
		||||
                    'help' => 'workflow.dest by email help',
 | 
			
		||||
                    'mapped' => false,
 | 
			
		||||
                    'allow_add' => true,
 | 
			
		||||
                    'entry_type' => EmailType::class,
 | 
			
		||||
                    'button_add_label' => 'workflow.Add an email',
 | 
			
		||||
                    'button_remove_label' => 'workflow.Remove an email',
 | 
			
		||||
                    'empty_collection_explain' => 'workflow.Any email',
 | 
			
		||||
                    'entry_options' => [
 | 
			
		||||
                        'constraints' => [
 | 
			
		||||
                            new NotNull(), new NotBlank(), new Email(),
 | 
			
		||||
                        ],
 | 
			
		||||
                        'label' => 'Email',
 | 
			
		||||
                    ],
 | 
			
		||||
                ]);
 | 
			
		||||
            $builder->add('transitionFilter', ChoiceType::class, [
 | 
			
		||||
                'multiple' => false,
 | 
			
		||||
                'label' => 'workflow.My decision',
 | 
			
		||||
                'choices' => [
 | 
			
		||||
                    'forward' => 'forward',
 | 
			
		||||
                    'backward' => 'backward',
 | 
			
		||||
                    'neutral' => 'neutral',
 | 
			
		||||
                ],
 | 
			
		||||
                'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]),
 | 
			
		||||
                'choice_attr' => static fn (string $key) => [
 | 
			
		||||
                    $key => $key,
 | 
			
		||||
                ],
 | 
			
		||||
                'mapped' => false,
 | 
			
		||||
                'expanded' => true,
 | 
			
		||||
                'data' => 'forward',
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            $handler->supportsFreeze($entityWorkflow)
 | 
			
		||||
            && !$entityWorkflow->isFreeze()
 | 
			
		||||
        ) {
 | 
			
		||||
            $builder
 | 
			
		||||
                ->add('freezeAfter', CheckboxType::class, [
 | 
			
		||||
                    'required' => false,
 | 
			
		||||
                    'label' => 'workflow.Freeze',
 | 
			
		||||
                    'help' => 'workflow.The associated element will be freezed',
 | 
			
		||||
                ]);
 | 
			
		||||
        }
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('transition', ChoiceType::class, [
 | 
			
		||||
                'label' => 'workflow.Next step',
 | 
			
		||||
                'mapped' => false,
 | 
			
		||||
                'multiple' => false,
 | 
			
		||||
                'expanded' => true,
 | 
			
		||||
                'choices' => $choices,
 | 
			
		||||
                'constraints' => [new NotNull()],
 | 
			
		||||
                'choice_label' => function (Transition $transition) use ($workflow) {
 | 
			
		||||
                    $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
 | 
			
		||||
 | 
			
		||||
                    if (\array_key_exists('label', $meta)) {
 | 
			
		||||
                        return $this->translatableStringHelper->localize($meta['label']);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return $transition->getName();
 | 
			
		||||
                },
 | 
			
		||||
                'choice_attr' => static function (Transition $transition) use ($workflow) {
 | 
			
		||||
                    $toFinal = true;
 | 
			
		||||
                    $isForward = 'neutral';
 | 
			
		||||
                    $isSignature = [];
 | 
			
		||||
 | 
			
		||||
                    $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
 | 
			
		||||
 | 
			
		||||
                    if (\array_key_exists('isForward', $metadata)) {
 | 
			
		||||
                        if ($metadata['isForward']) {
 | 
			
		||||
                            $isForward = 'forward';
 | 
			
		||||
                        } else {
 | 
			
		||||
                            $isForward = 'backward';
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    foreach ($transition->getTos() as $to) {
 | 
			
		||||
                        $meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
 | 
			
		||||
 | 
			
		||||
                        if (
 | 
			
		||||
                            !\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
 | 
			
		||||
                        ) {
 | 
			
		||||
                            $toFinal = false;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (\array_key_exists('isSignature', $meta)) {
 | 
			
		||||
                            $isSignature = $meta['isSignature'];
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return [
 | 
			
		||||
                        'data-is-transition' => 'data-is-transition',
 | 
			
		||||
                        'data-to-final' => $toFinal ? '1' : '0',
 | 
			
		||||
                        'data-is-forward' => $isForward,
 | 
			
		||||
                        'data-is-signature' => json_encode($isSignature),
 | 
			
		||||
                    ];
 | 
			
		||||
                },
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('isPersonOrUserSignature', ChoiceType::class, [
 | 
			
		||||
                'mapped' => false,
 | 
			
		||||
                'multiple' => false,
 | 
			
		||||
                'expanded' => true,
 | 
			
		||||
                'label' => 'workflow.signature_zone.type of signature',
 | 
			
		||||
                'choices' => [
 | 
			
		||||
                    'workflow.signature_zone.persons' => 'person',
 | 
			
		||||
                    'workflow.signature_zone.user' => 'user',
 | 
			
		||||
                ],
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('futurePersonSignatures', PickPersonDynamicType::class, [
 | 
			
		||||
                'label' => 'workflow.signature_zone.person signatures',
 | 
			
		||||
                'multiple' => true,
 | 
			
		||||
                'empty_data' => '[]',
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('futureUserSignature', PickUserDynamicType::class, [
 | 
			
		||||
                'label' => 'workflow.signature_zone.user signature',
 | 
			
		||||
                'multiple' => false,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('futureDestUsers', PickUserDynamicType::class, [
 | 
			
		||||
                'label' => 'workflow.dest for next steps',
 | 
			
		||||
                'multiple' => true,
 | 
			
		||||
                'empty_data' => '[]',
 | 
			
		||||
                'suggested' => $options['suggested_users'],
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('futureCcUsers', PickUserDynamicType::class, [
 | 
			
		||||
                'label' => 'workflow.cc for next steps',
 | 
			
		||||
                'multiple' => true,
 | 
			
		||||
                'required' => false,
 | 
			
		||||
                'suggested' => $options['suggested_users'],
 | 
			
		||||
                'empty_data' => '[]',
 | 
			
		||||
                'attr' => ['class' => 'future-cc-users'],
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('futureDestEmails', ChillCollectionType::class, [
 | 
			
		||||
                'label' => 'workflow.dest by email',
 | 
			
		||||
                'help' => 'workflow.dest by email help',
 | 
			
		||||
                'allow_add' => true,
 | 
			
		||||
                'entry_type' => EmailType::class,
 | 
			
		||||
                'button_add_label' => 'workflow.Add an email',
 | 
			
		||||
                'button_remove_label' => 'workflow.Remove an email',
 | 
			
		||||
                'empty_collection_explain' => 'workflow.Any email',
 | 
			
		||||
                'entry_options' => [
 | 
			
		||||
                    'constraints' => [
 | 
			
		||||
                        new NotNull(), new NotBlank(), new Email(),
 | 
			
		||||
                    ],
 | 
			
		||||
                    'label' => 'Email',
 | 
			
		||||
                ],
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('comment', ChillTextareaType::class, [
 | 
			
		||||
                'required' => false,
 | 
			
		||||
                'label' => 'Comment',
 | 
			
		||||
                'empty_data' => '',
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function configureOptions(OptionsResolver $resolver)
 | 
			
		||||
    {
 | 
			
		||||
        $resolver
 | 
			
		||||
            ->setDefined('class')
 | 
			
		||||
            ->setRequired('transition')
 | 
			
		||||
            ->setAllowedTypes('transition', 'bool')
 | 
			
		||||
            ->setDefault('data_class', WorkflowTransitionContextDTO::class)
 | 
			
		||||
            ->setRequired('entity_workflow')
 | 
			
		||||
            ->setAllowedTypes('entity_workflow', EntityWorkflow::class)
 | 
			
		||||
            ->setDefault('suggested_users', [])
 | 
			
		||||
            ->setDefault('constraints', [
 | 
			
		||||
                new Callback(
 | 
			
		||||
                    function ($step, ExecutionContextInterface $context, $payload) {
 | 
			
		||||
                        /** @var EntityWorkflowStep $step */
 | 
			
		||||
                        $form = $context->getObject();
 | 
			
		||||
                        $workflow = $this->registry->get($step->getEntityWorkflow(), $step->getEntityWorkflow()->getWorkflowName());
 | 
			
		||||
                        $transition = $form['transition']->getData();
 | 
			
		||||
                    function (WorkflowTransitionContextDTO $step, ExecutionContextInterface $context, $payload) {
 | 
			
		||||
                        $workflow = $this->registry->get($step->entityWorkflow, $step->entityWorkflow->getWorkflowName());
 | 
			
		||||
                        $transition = $step->transition;
 | 
			
		||||
                        $toFinal = true;
 | 
			
		||||
 | 
			
		||||
                        if (null === $transition) {
 | 
			
		||||
@@ -212,8 +221,8 @@ class WorkflowStepType extends AbstractType
 | 
			
		||||
                                    $toFinal = false;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            $destUsers = $form['future_dest_users']->getData();
 | 
			
		||||
                            $destEmails = $form['future_dest_emails']->getData();
 | 
			
		||||
                            $destUsers = $step->futureDestUsers;
 | 
			
		||||
                            $destEmails = $step->futureDestEmails;
 | 
			
		||||
 | 
			
		||||
                            if (!$toFinal && [] === $destUsers && [] === $destEmails) {
 | 
			
		||||
                                $context
 | 
			
		||||
@@ -224,20 +233,6 @@ class WorkflowStepType extends AbstractType
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
                new Callback(
 | 
			
		||||
                    function ($step, ExecutionContextInterface $context, $payload) {
 | 
			
		||||
                        $form = $context->getObject();
 | 
			
		||||
 | 
			
		||||
                        foreach ($form->get('future_dest_users')->getData() as $u) {
 | 
			
		||||
                            if (in_array($u, $form->get('future_cc_users')->getData(), true)) {
 | 
			
		||||
                                $context
 | 
			
		||||
                                    ->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
 | 
			
		||||
                                    ->atPath('ccUsers')
 | 
			
		||||
                                    ->addViolation();
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -99,6 +99,24 @@ class EntityWorkflowRepository implements ObjectRepository
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return list<EntityWorkflow>
 | 
			
		||||
     */
 | 
			
		||||
    public function findByRelatedEntity($entityClass, $relatedEntityId): array
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->repository->createQueryBuilder('w');
 | 
			
		||||
 | 
			
		||||
        $query = $qb->where(
 | 
			
		||||
            $qb->expr()->andX(
 | 
			
		||||
                $qb->expr()->eq('w.relatedEntityClass', ':entity_class'),
 | 
			
		||||
                $qb->expr()->eq('w.relatedEntityId', ':entity_id'),
 | 
			
		||||
            )
 | 
			
		||||
        )->setParameter('entity_class', $entityClass)
 | 
			
		||||
            ->setParameter('entity_id', $relatedEntityId);
 | 
			
		||||
 | 
			
		||||
        return $query->getQuery()->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param mixed|null $limit
 | 
			
		||||
     * @param mixed|null $offset
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Repository\Workflow;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @template-implements ObjectRepository<EntityWorkflowStepSignature>
 | 
			
		||||
 */
 | 
			
		||||
class EntityWorkflowStepSignatureRepository implements ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
    private readonly \Doctrine\ORM\EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(EntityManagerInterface $entityManager)
 | 
			
		||||
    {
 | 
			
		||||
        $this->repository = $entityManager->getRepository(EntityWorkflowStepSignature::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function find($id): ?EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->find($id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAll(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->findBy($criteria, $orderBy, $limit, $offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria): ?EntityWorkflowStepSignature
 | 
			
		||||
    {
 | 
			
		||||
        return $this->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return EntityWorkflowStepSignature::class;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -53,14 +53,17 @@ export const handleAdd = (button: any): void => {
 | 
			
		||||
    let
 | 
			
		||||
        empty_explain: HTMLLIElement | null = collection.querySelector('li[data-collection-empty-explain]'),
 | 
			
		||||
        entry = document.createElement('li'),
 | 
			
		||||
        counter = collection.childNodes.length + 1,
 | 
			
		||||
        content = prototype.replace(new RegExp('__name__', 'g'), counter.toString()),
 | 
			
		||||
        counter = collection.querySelectorAll('li.entry').length, // Updated counter logic
 | 
			
		||||
        content = prototype.replace(/__name__/g, counter.toString()),
 | 
			
		||||
        event = new CustomEvent('collection-add-entry', {detail: new CollectionEventPayload(collection, entry)});
 | 
			
		||||
 | 
			
		||||
    console.log(counter)
 | 
			
		||||
    console.log(content)
 | 
			
		||||
 | 
			
		||||
    entry.innerHTML = content;
 | 
			
		||||
    entry.classList.add('entry');
 | 
			
		||||
 | 
			
		||||
    if ("dataCollectionRegular" in collection.dataset) {
 | 
			
		||||
    if ("collectionRegular" in collection.dataset) {
 | 
			
		||||
        initializeRemove(collection, entry);
 | 
			
		||||
        if (empty_explain !== null) {
 | 
			
		||||
            empty_explain.remove();
 | 
			
		||||
 
 | 
			
		||||
@@ -159,3 +159,5 @@ document.addEventListener('DOMContentLoaded', function(e) {
 | 
			
		||||
    loadDynamicPicker(document)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
window.loadDynamicPicker = loadDynamicPicker;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,38 +4,124 @@ window.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
    let
 | 
			
		||||
        divTransitions = document.querySelector('#transitions'),
 | 
			
		||||
        futureDestUsersContainer = document.querySelector('#futureDests')
 | 
			
		||||
    ;
 | 
			
		||||
 | 
			
		||||
    if (null !== divTransitions) {
 | 
			
		||||
        new ShowHide({
 | 
			
		||||
            load_event: null,
 | 
			
		||||
            froms: [divTransitions],
 | 
			
		||||
            container: [futureDestUsersContainer],
 | 
			
		||||
            test: function(divs, arg2, arg3) {
 | 
			
		||||
                for (let div of divs) {
 | 
			
		||||
                    for (let input of div.querySelectorAll('input')) {
 | 
			
		||||
                        if (input.checked) {
 | 
			
		||||
                            if (input.dataset.toFinal === "1") {
 | 
			
		||||
                                return false;
 | 
			
		||||
                            } else {
 | 
			
		||||
                                return true;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
        personSignatureField = document.querySelector('#person-signature-field');
 | 
			
		||||
        userSignatureField = document.querySelector('#user-signature-field');
 | 
			
		||||
        signatureTypeChoices = document.querySelector('#signature-type-choice');
 | 
			
		||||
        personChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_0');
 | 
			
		||||
        userChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_1');
 | 
			
		||||
        signatureZone = document.querySelector('#signature-zone');
 | 
			
		||||
  ;
 | 
			
		||||
 | 
			
		||||
    let
 | 
			
		||||
        transitionFilterContainer = document.querySelector('#transitionFilter'),
 | 
			
		||||
        transitions = document.querySelector('#transitions')
 | 
			
		||||
      transitionFilterContainer = document.querySelector('#transitionFilter'),
 | 
			
		||||
      transitionsContainer = document.querySelector('#transitions')
 | 
			
		||||
    ;
 | 
			
		||||
 | 
			
		||||
    // ShowHide instance for signatureTypeChoices. This should always be present in the DOM and we toggle visibility.
 | 
			
		||||
    // The field is not mapped and so not submitted with the form. Without it's presence upon DOM loading other show hides do not function well.
 | 
			
		||||
    signatureTypeChoices.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // ShowHide instance for future dest users
 | 
			
		||||
    new ShowHide({
 | 
			
		||||
      debug: false,
 | 
			
		||||
      load_event: null,
 | 
			
		||||
      froms: [divTransitions],
 | 
			
		||||
      container: [futureDestUsersContainer],
 | 
			
		||||
      test: function(froms, event) {
 | 
			
		||||
        for (let transition of froms) {
 | 
			
		||||
          for (let input of transition.querySelectorAll('input')) {
 | 
			
		||||
            if (input.checked) {
 | 
			
		||||
              const inputData = JSON.parse(input.getAttribute('data-is-signature'))
 | 
			
		||||
              if (inputData.includes('person') || inputData.includes('user')) {
 | 
			
		||||
                signatureTypeChoices.style.display = '';
 | 
			
		||||
                return false;
 | 
			
		||||
              } else {
 | 
			
		||||
                personChoice.checked = false
 | 
			
		||||
                userChoice.checked = false
 | 
			
		||||
 | 
			
		||||
                signatureTypeChoices.style.display = 'none';
 | 
			
		||||
                return true;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // ShowHide signature zone
 | 
			
		||||
    new ShowHide({
 | 
			
		||||
      debug: false,
 | 
			
		||||
      load_event: null,
 | 
			
		||||
      froms: [divTransitions],
 | 
			
		||||
      container: [signatureZone],
 | 
			
		||||
      test: function(froms, event) {
 | 
			
		||||
        for (let transition of froms) {
 | 
			
		||||
          for (let input of transition.querySelectorAll('input')) {
 | 
			
		||||
            if (input.checked) {
 | 
			
		||||
              const inputData = JSON.parse(input.getAttribute('data-is-signature'))
 | 
			
		||||
              if (inputData.includes('person') || inputData.includes('user')) {
 | 
			
		||||
                signatureTypeChoices.style.display = '';
 | 
			
		||||
                return true;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // ShowHides for personSignatureField or userSignatureField within signature zone
 | 
			
		||||
    new ShowHide({
 | 
			
		||||
      debug: false,
 | 
			
		||||
      froms: [signatureTypeChoices],
 | 
			
		||||
      container: [personSignatureField],
 | 
			
		||||
      test: function(froms, event) {
 | 
			
		||||
        for (let container of froms) {
 | 
			
		||||
          return personChoice.checked;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    new ShowHide({
 | 
			
		||||
      debug: false,
 | 
			
		||||
      froms: [signatureTypeChoices],
 | 
			
		||||
      container: [userSignatureField],
 | 
			
		||||
      test: function(froms, event) {
 | 
			
		||||
        for (let container of froms) {
 | 
			
		||||
          return userChoice.checked;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (null !== divTransitions) {
 | 
			
		||||
      new ShowHide({
 | 
			
		||||
          load_event: null,
 | 
			
		||||
          froms: [divTransitions],
 | 
			
		||||
          container: [futureDestUsersContainer],
 | 
			
		||||
          test: function(divs, arg2, arg3) {
 | 
			
		||||
            for (let div of divs) {
 | 
			
		||||
                  for (let input of div.querySelectorAll('input')) {
 | 
			
		||||
                      if (input.checked) {
 | 
			
		||||
                          if (input.dataset.toFinal === "1") {
 | 
			
		||||
                              return false;
 | 
			
		||||
                          } else {
 | 
			
		||||
                              return true;
 | 
			
		||||
                          }
 | 
			
		||||
                      }
 | 
			
		||||
                  }
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              return true;
 | 
			
		||||
          },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (null !== transitionFilterContainer) {
 | 
			
		||||
        transitions.querySelectorAll('.form-check').forEach(function(row) {
 | 
			
		||||
        transitionsContainer.querySelectorAll('.form-check').forEach(function(row) {
 | 
			
		||||
 | 
			
		||||
            const isForward = row.querySelector('input').dataset.isForward;
 | 
			
		||||
 | 
			
		||||
@@ -66,5 +152,4 @@ window.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -58,17 +58,34 @@
 | 
			
		||||
        {{ form_row(transition_form.transition) }}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {% if transition_form.freezeAfter is defined %}
 | 
			
		||||
        {{ form_row(transition_form.freezeAfter) }}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    <div id="signature-zone">
 | 
			
		||||
        <div id="signature-type-choice">
 | 
			
		||||
            {{ form_row(transition_form.isPersonOrUserSignature) }}
 | 
			
		||||
            {{ form_errors(transition_form.isPersonOrUserSignature) }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="user-signature-field">
 | 
			
		||||
            {{ form_row(transition_form.futureUserSignature) }}
 | 
			
		||||
            {{ form_errors(transition_form.futureUserSignature) }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="person-signature-field">
 | 
			
		||||
            {{ form_row(transition_form.futurePersonSignatures) }}
 | 
			
		||||
            {{ form_errors(transition_form.futurePersonSignatures) }}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div id="futureDests">
 | 
			
		||||
        {{ form_row(transition_form.future_dest_users) }}
 | 
			
		||||
 | 
			
		||||
        {{ form_row(transition_form.future_cc_users) }}
 | 
			
		||||
 | 
			
		||||
        {{ form_row(transition_form.future_dest_emails) }}
 | 
			
		||||
        {{ form_errors(transition_form.future_dest_users) }}
 | 
			
		||||
        <div id="future-dest-users">
 | 
			
		||||
        {{ form_row(transition_form.futureDestUsers) }}
 | 
			
		||||
        {{ form_errors(transition_form.futureDestUsers) }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="future-cc-users">
 | 
			
		||||
        {{ form_row(transition_form.futureCcUsers) }}
 | 
			
		||||
        {{ form_errors(transition_form.futureCcUsers) }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="future-dest-emails">
 | 
			
		||||
        {{ form_row(transition_form.futureDestEmails) }}
 | 
			
		||||
        {{ form_errors(transition_form.futureDestEmails) }}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <p>{{ form_label(transition_form.comment) }}</p>
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user