Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.92% covered (danger)
4.92%
16 / 325
10.00% covered (danger)
10.00%
1 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractGenerator
4.92% covered (danger)
4.92%
16 / 325
10.00% covered (danger)
10.00%
1 / 10
4042.14
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 setCollectivity
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initializeDocument
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
20
 addHomepage
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 1
30
 addTableOfContent
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 createContentSection
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
132
 generateResponse
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 addLinkedData
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
42
 addTable
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
1332
 getDate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
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
22declare(strict_types=1);
23
24namespace App\Domain\Reporting\Generator\Word;
25
26use App\Application\Symfony\Security\UserProvider;
27use App\Domain\Registry\Model\Contractor;
28use App\Domain\Registry\Model\Mesurement;
29use App\Domain\Registry\Model\Proof;
30use App\Domain\Registry\Model\Request;
31use App\Domain\Registry\Model\Tool;
32use App\Domain\Registry\Model\Treatment;
33use App\Domain\Registry\Model\Violation;
34use App\Domain\User\Dictionary\ContactCivilityDictionary;
35use App\Domain\User\Model\Collectivity;
36use PhpOffice\PhpWord\Element\Section;
37use PhpOffice\PhpWord\IOFactory;
38use PhpOffice\PhpWord\PhpWord;
39use PhpOffice\PhpWord\Settings;
40use PhpOffice\PhpWord\SimpleType\Jc;
41use PhpOffice\PhpWord\SimpleType\TblWidth;
42use PhpOffice\PhpWord\Style;
43use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
44use Symfony\Component\HttpFoundation\BinaryFileResponse;
45use Symfony\Component\HttpFoundation\ResponseHeaderBag;
46
47abstract 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}