mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-10 08:44:58 +00:00
Compare commits
298 Commits
v3.0.0-RC4
...
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
|
|||
a46e987f81 | |||
81220b5b22 | |||
5b0019cde7 | |||
b19dd4fc11 | |||
44226d6f7f
|
|||
d75607a1d2 | |||
bf66af0f25 | |||
15f3e474a0 | |||
5623cf946e | |||
0a6f3a99da | |||
b42473b01d | |||
be19d09bad | |||
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 | |||
c82991674e | |||
3fc3f32c5f | |||
20af766cdf | |||
681f637d13 | |||
fb8a6d960e | |||
a2310a662f | |||
dd7d126bec | |||
29f6a43288 | |||
74be6460d4 | |||
c8e87ced35 | |||
b8002d56ec | |||
a80b36bb31 | |||
116fe35ad2 | |||
5b95336bac | |||
f9d5ba7778 | |||
f76379551c | |||
15094d5a91 | |||
db73dcffc7 | |||
8aec69f0f9
|
|||
9f88eef249
|
|||
d689ce9aef
|
|||
d5e4991982
|
|||
1079c7e394 | |||
ca68b58246
|
|||
747a1de321
|
|||
9e92ede16f
|
|||
31f842471a
|
|||
7d0f9175be
|
|||
e83307ca6d
|
|||
bc2dfd159c | |||
b100792a34 | |||
00ceee1fd5 | |||
215eba41b7 | |||
52a3d1be1b | |||
8d543be5cc | |||
0474b25859 | |||
db94af0958
|
|||
3e8805bdda | |||
a887602f4f
|
|||
c1cf27c42d
|
|||
fe6b4848e6
|
|||
b5af9f7b63
|
|||
7f3de62b2c
|
|||
cfa51cd659 | |||
facc4affed | |||
f9122341d1 | |||
7dd5f542a6 | |||
3b80d9a93b | |||
790576863f | |||
25e89571f7 | |||
435836c7d1 | |||
af4db22184 | |||
724b98e8c5 | |||
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 | |||
e38d47ec5e | |||
36f2275a56 | |||
9a34064b23 | |||
1812e84c92 | |||
dfa7de4f38 | |||
610239930b
|
|||
b65e2c62c4 | |||
89f5231649
|
|||
73797b98f6 | |||
3d40db7493 | |||
760d65b972 | |||
d26fa6bde6 | |||
427f232ab8 | |||
99818c211d
|
|||
a9f0059743
|
|||
5bc542a567
|
|||
482f279dc5 | |||
e0828b1f0f | |||
e015f71bb0 | |||
04a48f22ad | |||
ad4fe80240 | |||
4b82e67952 | |||
c8ccce83fd
|
|||
e9a9262fae | |||
d9e37d0958 | |||
65c41e6fa9 | |||
7923b5a1ef | |||
4a229ebf6b | |||
635b1ee537 | |||
3fd6e52e9d | |||
f60a595ab6 | |||
436ef33dbc | |||
405aad7333 | |||
a5c2576124 | |||
33a6f9996e | |||
b96cbc5594 | |||
854d72fa42 | |||
cd6fd091dc | |||
cb5ade3d14 | |||
dddb6d66bc | |||
d34f9450b8 | |||
9ce1788a14 | |||
2895638f3b | |||
2708bafb1f | |||
7d309136b1 | |||
82d3ec4d6f | |||
cad2dea148 | |||
bff14aa700 | |||
66570cd430 | |||
53df2ec9ba | |||
068503a830 | |||
c07a728f1d | |||
b7e61c6747 | |||
97846a5877 | |||
4ed9d3d8e2 | |||
d82d534a4c | |||
684f28291a | |||
526882a5b6 | |||
422b6b99eb | |||
02b150b0a5 | |||
20c27c100c | |||
12a22bcc13 | |||
cba8a342d5 | |||
6f55ba15d6 | |||
454ab73303 | |||
800942bc92 | |||
c4e7683e48 | |||
28c986fddf | |||
adca4f0d6a | |||
7b25c8e390 | |||
0b5be0419b | |||
650e85c481 | |||
56d5d08ed3 | |||
d3390ca334 | |||
511c0af5fa | |||
4c354c47c9 | |||
d8b6cef7b4 | |||
e312929d86 | |||
20b38af812 | |||
2f07be0843 | |||
11c069a2ff | |||
3929602f59 | |||
38fcccfd83 | |||
deaab80270 | |||
a71573136a | |||
b1082f6a55 | |||
dcc285e976 | |||
ed3e0f889e | |||
63fe8070c4 | |||
8e3322f578 | |||
00756a3bde | |||
2c68224e9c | |||
166d7fe0b0
|
|||
ab9d5439c1
|
|||
0737838dd6
|
|||
88bac5b5d8
|
|||
cf1df462dc
|
|||
a0328b9d68
|
|||
813a80d6f9
|
|||
ab95bb157e
|
|||
18fd1dbc4a
|
|||
a35f7656cb
|
|||
ff05f9f48a
|
|||
482c494034
|
@@ -1,5 +0,0 @@
|
||||
kind: Feature
|
||||
body: '[DX] move async-upload-bundle features into chill-bundles'
|
||||
time: 2023-12-12T15:48:41.954970271+01:00
|
||||
custom:
|
||||
Issue: "221"
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: |
|
||||
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||
time: 2024-05-30T16:00:03.440767606+02:00
|
||||
custom:
|
||||
Issue: ""
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: |
|
||||
Upgrade CKEditor and refactor configuration with use of typescript
|
||||
time: 2024-05-31T19:02:42.776662753+02:00
|
||||
custom:
|
||||
Issue: ""
|
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,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: Fix resolving of centers for an household, which will fix in turn the access
|
||||
control
|
||||
time: 2024-04-10T10:37:36.462484988+02:00
|
||||
custom:
|
||||
Issue: ""
|
11
.changes/v2.23.0.md
Normal file
11
.changes/v2.23.0.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## 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
|
||||
* Add job bundle (module emploi)
|
||||
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||
|
||||
* Upgrade CKEditor and refactor configuration with use of typescript
|
||||
|
||||
### Fixed
|
||||
* Fix resolving of centers for an household, which will fix in turn the access control
|
||||
* Resolved type hinting error in activity list export
|
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"
|
||||
|
18
CHANGELOG.md
18
CHANGELOG.md
@@ -6,6 +6,24 @@ 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
|
||||
* Add job bundle (module emploi)
|
||||
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||
|
||||
* Upgrade CKEditor and refactor configuration with use of typescript
|
||||
|
||||
### Fixed
|
||||
* Fix resolving of centers for an household, which will fix in turn the access control
|
||||
* Resolved type hinting error in activity list export
|
||||
|
||||
## v2.22.2 - 2024-07-03
|
||||
### Fixed
|
||||
* Remove scope required for event participation stats
|
||||
|
@@ -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",
|
||||
@@ -114,6 +115,8 @@
|
||||
"Chill\\DocGeneratorBundle\\": "src/Bundle/ChillDocGeneratorBundle",
|
||||
"Chill\\DocStoreBundle\\": "src/Bundle/ChillDocStoreBundle",
|
||||
"Chill\\EventBundle\\": "src/Bundle/ChillEventBundle",
|
||||
"Chill\\FranceTravailApiBundle\\": "src/Bundle/ChillFranceTravailApiBundle/src",
|
||||
"Chill\\JobBundle\\": "src/Bundle/ChillJobBundle/src",
|
||||
"Chill\\MainBundle\\": "src/Bundle/ChillMainBundle",
|
||||
"Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle",
|
||||
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",
|
||||
|
@@ -21,7 +21,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
class BirthdateFilter implements ExportElementValidatedInterface, FilterInterface
|
||||
{
|
||||
// add specific role for this filter
|
||||
public function addRole()
|
||||
public function addRole(): ?string
|
||||
{
|
||||
// we do not need any new role for this filter, so we return null
|
||||
return null;
|
||||
|
@@ -27,7 +27,7 @@
|
||||
"popper.js": "^1.16.1",
|
||||
"postcss-loader": "^7.0.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass-loader": "^13.0.0",
|
||||
"sass-loader": "^14.0.0",
|
||||
"select2": "^4.0.13",
|
||||
"select2-bootstrap-theme": "0.1.0-beta.10",
|
||||
"style-loader": "^3.3.1",
|
||||
@@ -53,12 +53,13 @@
|
||||
"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",
|
||||
"vue-i18n": "^9.1.6",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-toast-notification": "^2.0",
|
||||
"vue-toast-notification": "^3.1.2",
|
||||
"vuex": "^4.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
|
@@ -1,34 +1,29 @@
|
||||
parameters:
|
||||
ignoreErrors:
|
||||
-
|
||||
message: "#^Foreach overwrites \\$key with its key variable\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillCustomFieldsBundle/Controller/CustomFieldsGroupController.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomField.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
message: "#^Property Chill\\\\CustomFieldsBundle\\\\Entity\\\\CustomField\\:\\:\\$required \\(false\\) does not accept bool\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillPersonBundle/Form/PersonType.php
|
||||
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomField.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillMainBundle/Templating/ChillTwigRoutingHelper.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomFieldsGroup.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
message: "#^Parameter \\#1 \\$user of method Chill\\\\DocStoreBundle\\\\Entity\\\\Document\\:\\:setUser\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User\\|null, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#"
|
||||
count: 2
|
||||
path: src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
|
||||
path: src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php
|
||||
|
||||
-
|
||||
message: "#^Foreach overwrites \\$key with its key variable\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillCustomFieldsBundle/Controller/CustomFieldsGroupController.php
|
||||
message: "#^Parameter \\#1 \\$user of method Chill\\\\DocStoreBundle\\\\Entity\\\\Document\\:\\:setUser\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User\\|null, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#"
|
||||
count: 2
|
||||
path: src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php
|
||||
|
||||
-
|
||||
message: "#^Variable \\$participation might not be defined\\.$#"
|
||||
@@ -40,6 +35,106 @@ parameters:
|
||||
count: 1
|
||||
path: src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Comparison operation \"\\>\" between \\(bool\\|int\\|Redis\\) and 0 results in an error\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/ApiWrapper.php
|
||||
|
||||
-
|
||||
message: "#^Variable \\$response might not be defined\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/ApiWrapper.php
|
||||
|
||||
-
|
||||
message: "#^Function GuzzleHttp\\\\Psr7\\\\get not found\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
|
||||
|
||||
-
|
||||
message: "#^Function GuzzleHttp\\\\Psr7\\\\str not found\\.$#"
|
||||
count: 2
|
||||
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$seconds of function sleep expects int, string given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
|
||||
|
||||
-
|
||||
message: "#^Unreachable statement \\- code above always terminates\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Controller/CSPersonController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$interval of method DateTimeImmutable\\:\\:add\\(\\) expects DateInterval, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Entity/Immersion.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$object of static method DateTimeImmutable\\:\\:createFromMutable\\(\\) expects DateTime, DateTimeInterface given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Entity/Immersion.php
|
||||
|
||||
-
|
||||
message: "#^Property Chill\\\\JobBundle\\\\Entity\\\\Rome\\\\Metier\\:\\:\\$appellations is never read, only written\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Entity/Rome/Metier.php
|
||||
|
||||
-
|
||||
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListCSPerson\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Export/ListCSPerson.php
|
||||
|
||||
-
|
||||
message: "#^Variable \\$f might not be defined\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Export/ListCSPerson.php
|
||||
|
||||
-
|
||||
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListFrein\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Export/ListFrein.php
|
||||
|
||||
-
|
||||
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListProjetProfessionnel\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Export/ListProjetProfessionnel.php
|
||||
|
||||
-
|
||||
message: "#^Property Chill\\\\JobBundle\\\\Form\\\\ChoiceLoader\\\\RomeAppellationChoiceLoader\\:\\:\\$appellationRepository \\(Chill\\\\JobBundle\\\\Repository\\\\Rome\\\\AppellationRepository\\) does not accept Doctrine\\\\ORM\\\\EntityRepository\\<Chill\\\\JobBundle\\\\Entity\\\\Rome\\\\Appellation\\>\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Result of && is always false\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Strict comparison using \\=\\=\\= between array\\{\\} and Symfony\\\\Component\\\\Validator\\\\ConstraintViolationListInterface will always evaluate to false\\.$#"
|
||||
count: 2
|
||||
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Strict comparison using \\=\\=\\= between null and string will always evaluate to false\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Variable \\$metier might not be defined\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$interval of method DateTimeImmutable\\:\\:add\\(\\) expects DateInterval, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Security/Authorization/CSConnectesVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$object of static method DateTimeImmutable\\:\\:createFromMutable\\(\\) expects DateTime, DateTimeInterface given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Security/Authorization/CSConnectesVoter.php
|
||||
|
||||
-
|
||||
message: "#^Cannot unset offset '_token' on array\\{formatter\\: mixed, export\\: mixed, centers\\: mixed, alias\\: string\\}\\.$#"
|
||||
count: 1
|
||||
@@ -65,11 +160,31 @@ parameters:
|
||||
count: 1
|
||||
path: src/Bundle/ChillMainBundle/Form/ChoiceLoader/PostalCodeChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 2
|
||||
path: src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$user of method Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:\\:userHasAccessForCenter\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillMainBundle/Templating/ChillTwigRoutingHelper.php
|
||||
|
||||
-
|
||||
message: "#^Foreach overwrites \\$value with its value variable\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillPersonBundle/Form/ChoiceLoader/PersonChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillPersonBundle/Form/PersonType.php
|
||||
|
||||
-
|
||||
message: "#^Foreach overwrites \\$value with its value variable\\.$#"
|
||||
count: 1
|
||||
|
22
rector.php
22
rector.php
@@ -28,6 +28,9 @@ return static function (RectorConfig $rectorConfig): void {
|
||||
|
||||
// register a single rule
|
||||
$rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
|
||||
$rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromPropertyTypeRector::class);
|
||||
$rectorConfig->rule(Rector\TypeDeclaration\Rector\Class_\MergeDateTimePropertyTypeDeclarationRector::class);
|
||||
$rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector::class);
|
||||
|
||||
// part of the symfony 54 rules
|
||||
$rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class);
|
||||
@@ -36,14 +39,14 @@ return static function (RectorConfig $rectorConfig): void {
|
||||
|
||||
//define sets of rules
|
||||
$rectorConfig->sets([
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_50,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_50_TYPES,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_51,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_52,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_53,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_54,
|
||||
LevelSetList::UP_TO_PHP_82,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_40,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_41,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_42,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_43,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_44,
|
||||
\Rector\Doctrine\Set\DoctrineSetList::DOCTRINE_CODE_QUALITY,
|
||||
\Rector\Doctrine\Set\DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
|
||||
\Rector\PHPUnit\Set\PHPUnitSetList::PHPUNIT_90,
|
||||
]);
|
||||
|
||||
$rectorConfig->ruleWithConfiguration(\Rector\Php80\Rector\Class_\AnnotationToAttributeRector::class, [
|
||||
@@ -66,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, [
|
||||
|
@@ -99,10 +99,10 @@ final class ActivityController extends AbstractController
|
||||
|
||||
$form = $this->createDeleteForm($activity->getId(), $person, $accompanyingPeriod);
|
||||
|
||||
if (Request::METHOD_DELETE === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->logger->notice('An activity has been removed', [
|
||||
'by_user' => $this->getUser()->getUsername(),
|
||||
'activity_id' => $activity->getId(),
|
||||
@@ -640,7 +640,6 @@ final class ActivityController extends AbstractController
|
||||
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl('chill_activity_activity_delete', $params))
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
}
|
||||
|
@@ -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)]
|
||||
|
@@ -79,11 +79,9 @@ class ActivityReason
|
||||
/**
|
||||
* Set active.
|
||||
*
|
||||
* @param bool $active
|
||||
*
|
||||
* @return ActivityReason
|
||||
*/
|
||||
public function setActive($active)
|
||||
public function setActive(bool $active)
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
@@ -110,11 +108,9 @@ class ActivityReason
|
||||
/**
|
||||
* Set name.
|
||||
*
|
||||
* @param array $name
|
||||
*
|
||||
* @return ActivityReason
|
||||
*/
|
||||
public function setName($name)
|
||||
public function setName(array $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
/**
|
||||
|
@@ -152,7 +152,7 @@ class ListActivityHelper
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->translator->trans($value);
|
||||
return $this->translator->trans((string) $value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@@ -73,7 +73,7 @@ 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 = acp"
|
||||
'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = activity.accompanyingPeriod"
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -15,9 +15,9 @@ use Chill\ActivityBundle\Entity\ActivityType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
final class ActivityTypeRepository implements ActivityTypeRepositoryInterface
|
||||
final readonly class ActivityTypeRepository implements ActivityTypeRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
|
@@ -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]
|
||||
|
@@ -54,7 +54,7 @@ abstract class AbstractElementController extends AbstractController
|
||||
$indexPage = 'chill_budget_elements_household_index';
|
||||
}
|
||||
|
||||
if (Request::METHOD_DELETE === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
@@ -198,10 +198,9 @@ abstract class AbstractElementController extends AbstractController
|
||||
/**
|
||||
* Creates a form to delete a help request entity by id.
|
||||
*/
|
||||
private function createDeleteForm(): Form
|
||||
private function createDeleteForm(): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
return $this->createFormBuilder()
|
||||
->setMethod(Request::METHOD_DELETE)
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
}
|
||||
|
@@ -100,7 +100,7 @@ class Charge extends AbstractElement implements HasCentersInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setHelp($help)
|
||||
public function setHelp(?string $help)
|
||||
{
|
||||
$this->help = $help;
|
||||
|
||||
|
@@ -15,9 +15,9 @@ use Chill\BudgetBundle\Entity\ChargeKind;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
final class ChargeKindRepository implements ChargeKindRepositoryInterface
|
||||
final readonly class ChargeKindRepository implements ChargeKindRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
|
@@ -15,9 +15,9 @@ use Chill\BudgetBundle\Entity\ResourceKind;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
final class ResourceKindRepository implements ResourceKindRepositoryInterface
|
||||
final readonly class ResourceKindRepository implements ResourceKindRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
|
@@ -84,7 +84,7 @@ class CalendarController extends AbstractController
|
||||
|
||||
$form = $this->createDeleteForm($entity);
|
||||
|
||||
if (Request::METHOD_DELETE === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
@@ -512,7 +512,6 @@ class CalendarController extends AbstractController
|
||||
{
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl('chill_calendar_calendar_delete', ['id' => $calendar->getId()]))
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -172,11 +172,9 @@ class CustomField
|
||||
/**
|
||||
* Set active.
|
||||
*
|
||||
* @param bool $active
|
||||
*
|
||||
* @return CustomField
|
||||
*/
|
||||
public function setActive($active)
|
||||
public function setActive(bool $active)
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
@@ -224,18 +222,16 @@ class CustomField
|
||||
/**
|
||||
* Set order.
|
||||
*
|
||||
* @param float $order
|
||||
*
|
||||
* @return CustomField
|
||||
*/
|
||||
public function setOrdering($order)
|
||||
public function setOrdering(?float $order)
|
||||
{
|
||||
$this->ordering = $order;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRequired($required)
|
||||
public function setRequired(bool $required)
|
||||
{
|
||||
$this->required = $required;
|
||||
|
||||
@@ -245,7 +241,7 @@ class CustomField
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSlug($slug)
|
||||
public function setSlug(?string $slug)
|
||||
{
|
||||
$this->slug = $slug;
|
||||
|
||||
@@ -255,11 +251,9 @@ class CustomField
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return CustomField
|
||||
*/
|
||||
public function setType($type)
|
||||
public function setType(?string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
|
@@ -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]
|
||||
@@ -129,7 +129,7 @@ class Option
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setActive($active)
|
||||
public function setActive(bool $active)
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
@@ -139,7 +139,7 @@ class Option
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setInternalKey($internal_key)
|
||||
public function setInternalKey(string $internal_key)
|
||||
{
|
||||
$this->internalKey = $internal_key;
|
||||
|
||||
@@ -149,7 +149,7 @@ class Option
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setKey($key)
|
||||
public function setKey(?string $key)
|
||||
{
|
||||
$this->key = $key;
|
||||
|
||||
|
@@ -69,7 +69,7 @@ class CustomFieldsDefaultGroup
|
||||
*
|
||||
* @return CustomFieldsDefaultGroup
|
||||
*/
|
||||
public function setCustomFieldsGroup($customFieldsGroup)
|
||||
public function setCustomFieldsGroup(?CustomFieldsGroup $customFieldsGroup)
|
||||
{
|
||||
$this->customFieldsGroup = $customFieldsGroup;
|
||||
|
||||
@@ -79,11 +79,9 @@ class CustomFieldsDefaultGroup
|
||||
/**
|
||||
* Set entity.
|
||||
*
|
||||
* @param string $entity
|
||||
*
|
||||
* @return CustomFieldsDefaultGroup
|
||||
*/
|
||||
public function setEntity($entity)
|
||||
public function setEntity(?string $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
@@ -165,11 +165,9 @@ class CustomFieldsGroup
|
||||
/**
|
||||
* Set entity.
|
||||
*
|
||||
* @param string $entity
|
||||
*
|
||||
* @return CustomFieldsGroup
|
||||
*/
|
||||
public function setEntity($entity)
|
||||
public function setEntity(?string $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
|
||||
|
@@ -21,14 +21,14 @@ use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final class RelatorioDriver implements DriverInterface
|
||||
final readonly class RelatorioDriver implements DriverInterface
|
||||
{
|
||||
private readonly string $url;
|
||||
private string $url;
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $client,
|
||||
private HttpClientInterface $client,
|
||||
ParameterBagInterface $parameterBag,
|
||||
private readonly LoggerInterface $logger
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
$this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url'];
|
||||
}
|
||||
|
@@ -16,11 +16,11 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
|
||||
final readonly class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, private readonly RequestStack $requestStack)
|
||||
public function __construct(EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(DocGeneratorTemplate::class);
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -129,7 +129,7 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUser($user): self
|
||||
public function setUser(?\Chill\MainBundle\Entity\User $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
|
@@ -86,7 +86,7 @@ class DocumentCategory
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDocumentClass($documentClass): self
|
||||
public function setDocumentClass(?string $documentClass): self
|
||||
{
|
||||
$this->documentClass = $documentClass;
|
||||
|
||||
|
@@ -55,14 +55,14 @@ class PersonDocument extends Document implements HasCenterInterface, HasScopeInt
|
||||
return $this->scope;
|
||||
}
|
||||
|
||||
public function setPerson($person): self
|
||||
public function setPerson(Person $person): self
|
||||
{
|
||||
$this->person = $person;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setScope($scope): self
|
||||
public function setScope(?Scope $scope): self
|
||||
{
|
||||
$this->scope = $scope;
|
||||
|
||||
|
@@ -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 }
|
@@ -61,7 +61,7 @@ final class EventController extends AbstractController
|
||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
|
||||
) {}
|
||||
|
||||
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'DELETE'])]
|
||||
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'POST', 'DELETE'])]
|
||||
public function deleteAction($event_id, Request $request): \Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
{
|
||||
$em = $this->managerRegistry->getManager();
|
||||
@@ -78,10 +78,10 @@ final class EventController extends AbstractController
|
||||
|
||||
$form = $this->createDeleteForm($event_id);
|
||||
|
||||
if (Request::METHOD_DELETE === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
foreach ($participations as $participation) {
|
||||
$em->remove($participation);
|
||||
}
|
||||
@@ -108,28 +108,6 @@ final class EventController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a form to edit an existing Event entity.
|
||||
*/
|
||||
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/edit', name: 'chill_event__event_edit')]
|
||||
public function editAction($event_id): Response
|
||||
{
|
||||
$em = $this->managerRegistry->getManager();
|
||||
|
||||
$entity = $em->getRepository(Event::class)->find($event_id);
|
||||
|
||||
if (!$entity) {
|
||||
throw $this->createNotFoundException('Unable to find Event entity.');
|
||||
}
|
||||
|
||||
$editForm = $this->createEditForm($entity);
|
||||
|
||||
return $this->render('@ChillEvent/Event/edit.html.twig', [
|
||||
'entity' => $entity,
|
||||
'edit_form' => $editForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List events subscriptions for a person.
|
||||
*
|
||||
@@ -313,7 +291,7 @@ final class EventController extends AbstractController
|
||||
/**
|
||||
* Edits an existing Event entity.
|
||||
*/
|
||||
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/update', name: 'chill_event__event_update', methods: ['POST', 'PUT'])]
|
||||
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/edit', name: 'chill_event__event_edit', methods: ['GET', 'POST', 'PUT'])]
|
||||
public function updateAction(Request $request, $event_id): \Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
{
|
||||
$em = $this->managerRegistry->getManager();
|
||||
@@ -324,14 +302,20 @@ final class EventController extends AbstractController
|
||||
throw $this->createNotFoundException('Unable to find Event entity.');
|
||||
}
|
||||
|
||||
$editForm = $this->createEditForm($entity);
|
||||
$editForm = $this->createForm(EventType::class, $entity, [
|
||||
'center' => $entity->getCenter(),
|
||||
'role' => EventVoter::UPDATE,
|
||||
]);
|
||||
|
||||
$editForm->add('submit', SubmitType::class, ['label' => 'Update']);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isValid()) {
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$em->persist($entity);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator
|
||||
->trans('The event was updated'));
|
||||
$this->addFlash('success', $this->translator->trans('The event was updated'));
|
||||
|
||||
return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]);
|
||||
}
|
||||
@@ -599,29 +583,7 @@ final class EventController extends AbstractController
|
||||
->setAction($this->generateUrl('chill_event__event_delete', [
|
||||
'event_id' => $event_id,
|
||||
]))
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form to edit a Event entity.
|
||||
*
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(Event $entity)
|
||||
{
|
||||
$form = $this->createForm(EventType::class, $entity, [
|
||||
'action' => $this->generateUrl('chill_event__event_update', ['event_id' => $entity->getId()]),
|
||||
'method' => 'PUT',
|
||||
'center' => $entity->getCenter(),
|
||||
'role' => 'CHILL_EVENT_CREATE',
|
||||
]);
|
||||
|
||||
$form->remove('center');
|
||||
|
||||
$form->add('submit', SubmitType::class, ['label' => 'Update']);
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
@@ -201,7 +201,7 @@ class EventTypeController extends AbstractController
|
||||
/**
|
||||
* Creates a form to delete a EventType entity by id.
|
||||
*
|
||||
* @return \Symfony\Component\Form\Form The form
|
||||
* @return \Symfony\Component\Form\FormInterface The form
|
||||
*/
|
||||
private function createDeleteForm(mixed $id)
|
||||
{
|
||||
@@ -210,7 +210,6 @@ class EventTypeController extends AbstractController
|
||||
'chill_eventtype_admin_delete',
|
||||
['id' => $id]
|
||||
))
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
}
|
||||
|
@@ -259,10 +259,10 @@ final class ParticipationController extends AbstractController
|
||||
|
||||
$form = $this->createDeleteForm($participation_id);
|
||||
|
||||
if (Request::METHOD_DELETE === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em->remove($participation);
|
||||
$em->flush();
|
||||
|
||||
@@ -753,7 +753,6 @@ final class ParticipationController extends AbstractController
|
||||
->setAction($this->generateUrl('chill_event_participation_delete', [
|
||||
'participation_id' => $participation_id,
|
||||
]))
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
}
|
||||
|
@@ -201,7 +201,7 @@ class RoleController extends AbstractController
|
||||
/**
|
||||
* Creates a form to delete a Role entity by id.
|
||||
*
|
||||
* @return \Symfony\Component\Form\Form The form
|
||||
* @return \Symfony\Component\Form\FormInterface The form
|
||||
*/
|
||||
private function createDeleteForm(mixed $id)
|
||||
{
|
||||
|
@@ -201,13 +201,12 @@ class StatusController extends AbstractController
|
||||
/**
|
||||
* Creates a form to delete a Status entity by id.
|
||||
*
|
||||
* @return \Symfony\Component\Form\Form The form
|
||||
* @return \Symfony\Component\Form\FormInterface The form
|
||||
*/
|
||||
private function createDeleteForm(mixed $id)
|
||||
{
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl('chill_event_admin_status_delete', ['id' => $id]))
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -47,7 +47,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
|
||||
private ?Scope $circle = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE)]
|
||||
private ?\DateTime $date;
|
||||
private ?\DateTime $date = null;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
@@ -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);
|
||||
}
|
||||
@@ -265,11 +265,9 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
|
||||
/**
|
||||
* Set label.
|
||||
*
|
||||
* @param string $label
|
||||
*
|
||||
* @return Event
|
||||
*/
|
||||
public function setName($label)
|
||||
public function setName(?string $label)
|
||||
{
|
||||
$this->name = $label;
|
||||
|
||||
|
@@ -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;
|
||||
@@ -146,11 +146,9 @@ class EventType
|
||||
/**
|
||||
* Set active.
|
||||
*
|
||||
* @param bool $active
|
||||
*
|
||||
* @return EventType
|
||||
*/
|
||||
public function setActive($active)
|
||||
public function setActive(bool $active)
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
|
@@ -81,11 +81,9 @@ class Role
|
||||
/**
|
||||
* Set active.
|
||||
*
|
||||
* @param bool $active
|
||||
*
|
||||
* @return Role
|
||||
*/
|
||||
public function setActive($active)
|
||||
public function setActive(bool $active)
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user