Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
4.92% |
16 / 325 |
|
10.00% |
1 / 10 |
CRAP | |
0.00% |
0 / 1 |
AbstractGenerator | |
4.92% |
16 / 325 |
|
10.00% |
1 / 10 |
4042.14 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 | |||
setCollectivity | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initializeDocument | |
0.00% |
0 / 61 |
|
0.00% |
0 / 1 |
20 | |||
addHomepage | |
0.00% |
0 / 70 |
|
0.00% |
0 / 1 |
30 | |||
addTableOfContent | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
createContentSection | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
132 | |||
generateResponse | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
addLinkedData | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
42 | |||
addTable | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
1332 | |||
getDate | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * This file is part of the MADIS - RGPD Management application. |
5 | * |
6 | * @copyright Copyright (c) 2018-2019 Soluris - Solutions Numériques Territoriales Innovantes |
7 | * |
8 | * This program is free software: you can redistribute it and/or modify |
9 | * it under the terms of the GNU Affero General Public License as published by |
10 | * the Free Software Foundation, either version 3 of the License, or |
11 | * (at your option) any later version. |
12 | * |
13 | * This program is distributed in the hope that it will be useful, |
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | * GNU Affero General Public License for more details. |
17 | * |
18 | * You should have received a copy of the GNU Affero General Public License |
19 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
20 | */ |
21 | |
22 | declare(strict_types=1); |
23 | |
24 | namespace App\Domain\Reporting\Generator\Word; |
25 | |
26 | use App\Application\Symfony\Security\UserProvider; |
27 | use App\Domain\Registry\Model\Contractor; |
28 | use App\Domain\Registry\Model\Mesurement; |
29 | use App\Domain\Registry\Model\Proof; |
30 | use App\Domain\Registry\Model\Request; |
31 | use App\Domain\Registry\Model\Tool; |
32 | use App\Domain\Registry\Model\Treatment; |
33 | use App\Domain\Registry\Model\Violation; |
34 | use App\Domain\User\Dictionary\ContactCivilityDictionary; |
35 | use App\Domain\User\Model\Collectivity; |
36 | use PhpOffice\PhpWord\Element\Section; |
37 | use PhpOffice\PhpWord\IOFactory; |
38 | use PhpOffice\PhpWord\PhpWord; |
39 | use PhpOffice\PhpWord\Settings; |
40 | use PhpOffice\PhpWord\SimpleType\Jc; |
41 | use PhpOffice\PhpWord\SimpleType\TblWidth; |
42 | use PhpOffice\PhpWord\Style; |
43 | use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; |
44 | use Symfony\Component\HttpFoundation\BinaryFileResponse; |
45 | use Symfony\Component\HttpFoundation\ResponseHeaderBag; |
46 | |
47 | abstract class AbstractGenerator implements GeneratorInterface |
48 | { |
49 | /** |
50 | * @var UserProvider |
51 | */ |
52 | protected $userProvider; |
53 | |
54 | /** |
55 | * @var ParameterBagInterface |
56 | */ |
57 | protected $parameterBag; |
58 | |
59 | /** |
60 | * @var array |
61 | */ |
62 | protected $tableStyle; |
63 | |
64 | /** |
65 | * @var array |
66 | */ |
67 | protected $cellHeadStyle; |
68 | |
69 | /** |
70 | * @var array |
71 | */ |
72 | protected $textHeadStyle; |
73 | |
74 | /** |
75 | * @var Collectivity |
76 | */ |
77 | protected $collectivity; |
78 | |
79 | /** |
80 | * AbstractGenerator constructor. |
81 | */ |
82 | public function __construct( |
83 | UserProvider $userProvider, |
84 | ParameterBagInterface $parameterBag, |
85 | ) { |
86 | $this->userProvider = $userProvider; |
87 | $this->parameterBag = $parameterBag; |
88 | |
89 | $this->tableStyle = [ |
90 | 'borderColor' => '006699', |
91 | 'borderSize' => 6, |
92 | 'cellMargin' => 100, |
93 | 'unit' => TblWidth::PERCENT, |
94 | 'width' => 100 * 50, |
95 | ]; |
96 | |
97 | $this->cellHeadStyle = [ |
98 | 'bgColor' => '3c8dbc', |
99 | ]; |
100 | |
101 | $this->textHeadStyle = [ |
102 | 'bold' => true, |
103 | 'color' => 'ffffff', |
104 | ]; |
105 | } |
106 | |
107 | public function setCollectivity(Collectivity $collectivity) |
108 | { |
109 | $this->collectivity = $collectivity; |
110 | } |
111 | |
112 | /** |
113 | * Initialize PHPWord document variables & default values. |
114 | */ |
115 | public function initializeDocument(PhpWord $document): void |
116 | { |
117 | if ($this->collectivity) { |
118 | $collectivity = $this->collectivity; |
119 | } else { |
120 | $collectivity = $this->userProvider->getAuthenticatedUser()->getCollectivity(); |
121 | } |
122 | // CONFIGURATION |
123 | $document->getSettings()->setThemeFontLang(new Style\Language(Style\Language::FR_FR)); |
124 | $document->getSettings()->setUpdateFields(true); |
125 | |
126 | $document->setDefaultFontName('Verdana'); |
127 | $document->setDefaultFontSize(10); |
128 | $document->setDefaultParagraphStyle(['spaceAfter' => 200]); |
129 | |
130 | $properties = $document->getDocInfo(); |
131 | if ($collectivity->getReviewData() && $collectivity->getReviewData()->getDocumentName()) { |
132 | $properties->setTitle($collectivity->getReviewData()->getDocumentName()); |
133 | } else { |
134 | $properties->setTitle('Bilan de gestion des données à caractère personnel'); |
135 | } |
136 | |
137 | // STYLE |
138 | // Numbered heading |
139 | $document->addNumberingStyle( |
140 | 'headingNumbering', |
141 | ['type' => 'multilevel', |
142 | 'levels' => [ |
143 | ['pStyle' => 'Heading1', 'format' => 'decimal', 'text' => '%1.'], |
144 | ['pStyle' => 'Heading2', 'format' => 'decimal', 'text' => '%1.%2.'], |
145 | ], |
146 | ] |
147 | ); |
148 | |
149 | // Title style |
150 | $document->addTitleStyle( |
151 | 1, |
152 | [ |
153 | 'allCaps' => true, |
154 | 'bold' => true, |
155 | 'size' => 16, |
156 | ], |
157 | [ |
158 | 'pageBreakBefore' => false, |
159 | 'numLevel' => 0, |
160 | 'numStyle' => 'headingNumbering', |
161 | 'spaceBefore' => 500, |
162 | 'spaceAfter' => 250, |
163 | ] |
164 | ); |
165 | $document->addTitleStyle( |
166 | 2, |
167 | [ |
168 | 'bold' => true, |
169 | 'size' => 12, |
170 | ], |
171 | [ |
172 | 'pageBreakBefore' => false, |
173 | 'numLevel' => 1, |
174 | 'numStyle' => 'headingNumbering', |
175 | 'spaceBefore' => 500, |
176 | 'spaceAfter' => 250, |
177 | ] |
178 | ); |
179 | $document->addTitleStyle( |
180 | 3, |
181 | [ |
182 | 'bold' => true, |
183 | ], |
184 | [ |
185 | 'pageBreakBefore' => false, |
186 | 'spaceBefore' => 500, |
187 | 'spaceAfter' => 250, |
188 | ] |
189 | ); |
190 | } |
191 | |
192 | /** |
193 | * Add PhpWord homepage. |
194 | */ |
195 | public function addHomepage(PhpWord $document, string $title): void |
196 | { |
197 | if ($this->collectivity) { |
198 | $collectivity = $this->collectivity; |
199 | } else { |
200 | $collectivity = $this->userProvider->getAuthenticatedUser()->getCollectivity(); |
201 | } |
202 | |
203 | $legalManager = $collectivity->getLegalManager(); |
204 | $section = $document->addSection(); |
205 | $section->addText( |
206 | $title, |
207 | [ |
208 | 'bold' => true, |
209 | 'color' => '3c8dbc', |
210 | 'size' => 36, |
211 | ], |
212 | [ |
213 | 'alignment' => Jc::CENTER, |
214 | 'spaceBefore' => 1000, |
215 | ] |
216 | ); |
217 | $section->addText( |
218 | \utf8_decode((string) $collectivity), |
219 | [ |
220 | 'bold' => true, |
221 | 'italic' => true, |
222 | 'size' => 20, |
223 | ], |
224 | [ |
225 | 'alignment' => Jc::CENTER, |
226 | 'spaceBefore' => 1000, |
227 | ] |
228 | ); |
229 | |
230 | // TREATMENT RESPONSABLE |
231 | $section->addText( |
232 | 'Responsable du traitement', |
233 | ['bold' => true, 'size' => 15], |
234 | ['spaceBefore' => 1000] |
235 | ); |
236 | $section->addText( |
237 | ContactCivilityDictionary::getCivilities()[$legalManager->getCivility()] . ' ' . $legalManager->getFullName(), |
238 | ['size' => 12] |
239 | ); |
240 | |
241 | // DPO |
242 | $section->addText( |
243 | 'Délégué à la Protection des Données', |
244 | ['bold' => true, 'size' => 15], |
245 | ['spaceBefore' => 1000] |
246 | ); |
247 | |
248 | $hasDpo = $collectivity->isDifferentDpo(); |
249 | $dpoDefaultFullName = $this->parameterBag->get('APP_DPO_FIRST_NAME') . ' ' . $this->parameterBag->get('APP_DPO_LAST_NAME'); |
250 | $dpoDefaultStreetAddress = $this->parameterBag->get('APP_DPO_ADDRESS_STREET'); |
251 | $dpoDefaultZipCodeCity = $this->parameterBag->get('APP_DPO_ADDRESS_ZIP_CODE') . ' ' . \strtoupper($this->parameterBag->get('APP_DPO_ADDRESS_CITY')); |
252 | |
253 | $section->addText( |
254 | $hasDpo ? $collectivity->getDpo()->getFullName() : $dpoDefaultFullName, |
255 | ['size' => 12] |
256 | ); |
257 | $section->addText( |
258 | $hasDpo ? $collectivity->getAddress()->getLineOne() : $dpoDefaultStreetAddress, |
259 | ['size' => 12] |
260 | ); |
261 | $section->addText( |
262 | $hasDpo ? "{$collectivity->getAddress()->getZipCode()} {$collectivity->getAddress()->getCity()}" : $dpoDefaultZipCodeCity, |
263 | ['size' => 12] |
264 | ); |
265 | |
266 | // CNIL |
267 | $section->addText( |
268 | 'Déclaration CNIL', |
269 | ['bold' => true, 'size' => 15], |
270 | ['spaceBefore' => 1000] |
271 | ); |
272 | |
273 | $section->addText( |
274 | 'Numéro de désignation : ' . $collectivity->getNbrCnil(), |
275 | ['size' => 12] |
276 | ); |
277 | |
278 | $section->addTextBreak(3); |
279 | $section->addText(date('d/m/Y'), ['italic' => true], ['alignment' => Jc::CENTER]); |
280 | } |
281 | |
282 | /** |
283 | * Create a table of content. |
284 | * |
285 | * @param Section $section The section on which to add content |
286 | * @param int $maxDepth The max depth for TOC generation |
287 | */ |
288 | public function addTableOfContent(Section $section, int $maxDepth = 1) |
289 | { |
290 | $section->addText( |
291 | 'Table des matières', |
292 | [ |
293 | 'name' => 'Verdana', |
294 | 'size' => 16, |
295 | 'bold' => true, |
296 | ], |
297 | [ |
298 | 'alignment' => Jc::CENTER, |
299 | 'spaceAfter' => 500, |
300 | ] |
301 | ); |
302 | |
303 | $section->addTOC(['bold' => true], null, 1, $maxDepth); |
304 | $section->addPageBreak(); |
305 | } |
306 | |
307 | /** |
308 | * Create the section used to generate document. |
309 | * |
310 | * @param PhpWord $document The word document to use |
311 | * @param string $title The title to add in header |
312 | * |
313 | * @return Section The created section |
314 | */ |
315 | public function createContentSection(PhpWord $document, string $title): Section |
316 | { |
317 | // Create section |
318 | $section = $document->addSection(); |
319 | |
320 | // add page header |
321 | $header = $section->addHeader(); |
322 | $table = $header->addTable([ |
323 | 'borderColor' => '000000', |
324 | 'borderSize' => 3, |
325 | 'cellMargin' => 100, |
326 | 'unit' => TblWidth::PERCENT, |
327 | 'width' => 100 * 50, |
328 | ]); |
329 | $row = $table->addRow(10, ['tblHeader' => true, 'cantsplit' => true]); |
330 | $cell = $row->addCell(70 * 50, ['alignment' => Jc::CENTER]); |
331 | $cell->addText($title, ['alignment' => Jc::CENTER, 'bold' => true]); |
332 | $cell = $row->addCell(15 * 50, ['alignment' => Jc::CENTER]); |
333 | $cell->addPreserveText('Page {PAGE}/{NUMPAGES}', ['alignment' => Jc::CENTER]); |
334 | |
335 | $header->addTextBreak(); |
336 | |
337 | // Add page footer |
338 | $footer = $section->addFooter(); |
339 | $table = $footer->addTable([ |
340 | 'borderColor' => 'FFFFFF', |
341 | 'borderTopColor' => '000000', |
342 | 'borderSize' => 6, |
343 | 'unit' => TblWidth::PERCENT, |
344 | 'width' => 100 * 50, |
345 | ]); |
346 | $r = $table->addRow(); |
347 | $cell = $r->addCell(); |
348 | if (isset($this->logoDir) && $this->collectivity && $this->collectivity->getReviewData() && $this->collectivity->getReviewData()->isShowCollectivityLogoFooter() && $this->collectivity->getReviewData()->getLogo()) { |
349 | $cell->addImage(file_get_contents($this->logoDir . $this->collectivity->getReviewData()->getLogo()), [ |
350 | 'width' => 100, |
351 | 'alignment' => Jc::LEFT, |
352 | ]); |
353 | } |
354 | if (isset($this->logoDir) && isset($this->dpdLogo) && $this->collectivity && $this->collectivity->getReviewData() && $this->collectivity->getReviewData()->isShowDPDLogoFooter()) { |
355 | $cell = $r->addCell(); |
356 | $cell->addImage(file_get_contents($this->logoDir . '/' . $this->dpdLogo), [ |
357 | 'width' => 100, |
358 | 'alignment' => Jc::RIGHT, |
359 | ]); |
360 | } |
361 | |
362 | return $section; |
363 | } |
364 | |
365 | /** |
366 | * Generate the response |
367 | * - This response parse PhpWord to a Word document |
368 | * - Prepare it in a BinaryFileResponse. |
369 | * |
370 | * @param string $documentName The document name to use |
371 | * |
372 | * @throws \PhpOffice\PhpWord\Exception\Exception |
373 | * @throws \Exception |
374 | * |
375 | * @return BinaryFileResponse The response |
376 | */ |
377 | public function generateResponse(PhpWord $document, string $documentName): BinaryFileResponse |
378 | { |
379 | Settings::setOutputEscapingEnabled(true); |
380 | $objWriter = IOFactory::createWriter($document, 'Word2007'); |
381 | |
382 | $currentDate = (new \DateTimeImmutable())->format('Ymd'); |
383 | $fileName = "{$currentDate}-{$documentName}.doc"; |
384 | $tempFile = \tempnam(\sys_get_temp_dir(), $fileName); |
385 | |
386 | $objWriter->save($tempFile); |
387 | |
388 | // Create response and return it |
389 | $response = new BinaryFileResponse($tempFile); |
390 | $response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); |
391 | $response->setContentDisposition( |
392 | ResponseHeaderBag::DISPOSITION_ATTACHMENT, |
393 | $fileName |
394 | ); |
395 | |
396 | return $response; |
397 | } |
398 | |
399 | protected function addLinkedData(Section $section, Treatment|Violation|Tool|Request|Proof|Mesurement|Contractor $object) |
400 | { |
401 | $treatments = array_map(function (Treatment $t) { return $t->__toString(); }, $object->getTreatments()->toArray()); |
402 | $requests = array_map(function (Request $t) { return $t->__toString(); }, (array) $object->getRequests()->toArray()); |
403 | $violations = array_map(function (Violation $t) { return $t->__toString(); }, (array) $object->getViolations()->toArray()); |
404 | $tools = array_map(function (Tool $t) { return $t->__toString(); }, (array) $object->getTools()->toArray()); |
405 | $proofs = array_map(function (Proof $t) { return $t->__toString(); }, (array) $object->getProofs()->toArray()); |
406 | $mesurements = array_map(function (Mesurement $t) { return $t->__toString(); }, (array) $object->getMesurements()->toArray()); |
407 | $contractors = array_map(function (Contractor $t) { return $t->__toString(); }, (array) $object->getContractors()->toArray()); |
408 | |
409 | $linkedData = [ |
410 | [ |
411 | 'Traitements', |
412 | [['list' => $treatments]], |
413 | ], |
414 | [ |
415 | 'Sous-traitants', |
416 | [['list' => $contractors]], |
417 | ], |
418 | [ |
419 | 'Logiciels et supports', |
420 | [['list' => $tools]], |
421 | ], |
422 | [ |
423 | 'Demandes', |
424 | [['list' => $requests]], |
425 | ], |
426 | [ |
427 | 'Violations', |
428 | [['list' => $violations]], |
429 | ], |
430 | [ |
431 | 'Preuves', |
432 | [['list' => $proofs]], |
433 | ], |
434 | [ |
435 | 'Actions de protection', |
436 | [['list' => $mesurements]], |
437 | ], |
438 | ]; |
439 | if ($object instanceof Treatment) { |
440 | unset($linkedData[1]); |
441 | unset($linkedData[2]); |
442 | $linkedData = array_values($linkedData); |
443 | } |
444 | if (!($object instanceof Treatment) && $object->getCollectivity() && !$object->getCollectivity()->isHasModuleTools()) { |
445 | unset($linkedData[2]); |
446 | $linkedData = array_values($linkedData); |
447 | } |
448 | |
449 | if ($object instanceof Tool) { |
450 | unset($linkedData[1]); |
451 | $linkedData = array_values($linkedData); |
452 | } |
453 | |
454 | $this->addTable($section, $linkedData, false, self::TABLE_ORIENTATION_VERTICAL); |
455 | } |
456 | |
457 | protected function addTable(Section $section, array $data = [], bool $header = false, string $orientation = self::TABLE_ORIENTATION_HORIZONTAL): void |
458 | { |
459 | $table = $section->addTable($this->tableStyle); |
460 | if ($header) { |
461 | $headersTable = $data[0]; |
462 | if (array_key_exists('data', $headersTable)) { |
463 | $headersTable = $headersTable['data']; |
464 | } |
465 | $table->addRow(null, ['tblHeader' => true, 'cantsplit' => true]); |
466 | foreach ($headersTable as $element) { |
467 | $cell = $table->addCell(2500, $this->cellHeadStyle); |
468 | if (is_array($element) && array_key_exists('text', $element)) { |
469 | $cell->addText($element['text'], $element['style']); |
470 | } else { |
471 | $cell->addText($element, $this->textHeadStyle); |
472 | } |
473 | } |
474 | unset($data[0]); |
475 | } |
476 | |
477 | foreach ($data as $nbLine => $line) { |
478 | $table->addRow(null, ['cantsplit' => true]); |
479 | $lineData = $line['data'] ?? $line; |
480 | $lineStyle = $line['style'] ?? null; |
481 | foreach ($lineData as $nbCol => $col) { |
482 | $col = \is_array($col) ? $col : [$col]; |
483 | |
484 | if ($header && self::TABLE_ORIENTATION_HORIZONTAL === $orientation && 0 === $nbLine |
485 | || $header && self::TABLE_ORIENTATION_VERTICAL === $orientation && 0 === $nbCol) { |
486 | $cell = $table->addCell(2500, $this->cellHeadStyle); |
487 | foreach ($col as $item) { |
488 | $cell->addText($item, $this->textHeadStyle); |
489 | } |
490 | } else { |
491 | if (0 === $nbCol && !$header) { |
492 | $cell = $table->addCell(2500, $this->cellHeadStyle); |
493 | $cell->addText($col[$nbCol], $this->textHeadStyle); |
494 | } else { |
495 | /* If a style for the cell is specified, it bypass the line style */ |
496 | $cell = $table->addCell(5000 / \count($lineData), $col['style'] ?? $lineStyle); |
497 | $columns = $col['content'] ?? $col; |
498 | if (isset($columns[0]) && (!\is_array($columns[0]) || isset($columns[0]['array']))) { |
499 | $textrun = $cell->addTextRun(); |
500 | } |
501 | foreach ($columns as $key => $item) { |
502 | // If item is simple text, there is no other configuration |
503 | if (!\is_array($item)) { |
504 | if (!isset($textrun)) { |
505 | $textrun = $cell->addTextRun(); |
506 | } |
507 | |
508 | if (0 != $key) { |
509 | $textrun->addTextBreak(); |
510 | } |
511 | |
512 | $textrun->addText($item); |
513 | continue; |
514 | } |
515 | /* If item is array, there is 2 possibility : |
516 | - this is an array of item to display in a single cell |
517 | - there is additionnal configuration */ |
518 | if (isset($item['array']) && null !== $item['array']) { |
519 | if (!isset($textrun)) { |
520 | $textrun = $cell->addTextRun(); |
521 | } |
522 | $textrun->addTextBreak(); |
523 | foreach ($item['array'] as $subItemKey => $subItem) { |
524 | $textrun->addText($subItem); |
525 | if ($subItemKey !== count($item['array']) - 1) { |
526 | $textrun->addTextBreak(2); |
527 | } |
528 | } |
529 | } elseif (isset($item['list']) && is_array($item['list'])) { |
530 | foreach ($item['list'] as $subItem) { |
531 | if ($subItem) { |
532 | $cell->addListItem($subItem); |
533 | } |
534 | } |
535 | } else { |
536 | if (!isset($textrun)) { |
537 | $textrun = $cell->addTextRun(); |
538 | } |
539 | if (0 != $key) { |
540 | $textrun->addTextBreak(); |
541 | } |
542 | $textrun->addText( |
543 | $item['text'] ?? '', |
544 | $item['style'] ?? [] |
545 | ); |
546 | } |
547 | } |
548 | } |
549 | } |
550 | } |
551 | } |
552 | } |
553 | |
554 | /** |
555 | * Format a date for Word document. |
556 | * |
557 | * @param \DateTimeInterface|null $dateTime The date to parse |
558 | * |
559 | * @throws \Exception |
560 | * |
561 | * @return string The parsed date with the good timezone |
562 | */ |
563 | protected function getDate(?\DateTimeInterface $dateTime, ?string $format = null): string |
564 | { |
565 | if (\is_null($dateTime)) { |
566 | return ''; |
567 | } |
568 | |
569 | $format = $format ?? self::DATE_TIME_FORMAT; |
570 | |
571 | $parsedDateTime = new \DateTimeImmutable(); |
572 | $parsedDateTime = $parsedDateTime->setTimestamp($dateTime->getTimestamp()); |
573 | $parsedDateTime = $parsedDateTime->setTimezone(new \DateTimeZone(self::DATE_TIME_ZONE)); |
574 | |
575 | return $parsedDateTime->format($format); |
576 | } |
577 | } |