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