Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.07% covered (success)
91.07%
153 / 168
81.25% covered (warning)
81.25%
13 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Messaging
91.07% covered (success)
91.07%
153 / 168
81.25% covered (warning)
81.25%
13 / 16
47.51
0.00% covered (danger)
0.00%
0 / 1
 isIcsRequired
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 isEmptyProcessListAllowed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 twigView
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dbTwigView
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 createTwigEnvironment
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 filesystemLoader
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
4.20
 getMailContentPreview
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getMailContent
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 generateMailParameters
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
7
 getScopeAdminProcessListContent
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getTemplate
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getMailSubject
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 getMailIcs
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 generateIcsContent
81.48% covered (warning)
81.48%
22 / 27
0.00% covered (danger)
0.00%
0 / 1
5.16
 getPlainText
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 getTextWithFoldedLines
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3/**
4 *
5 * @package Zmsentities
6 * @copyright BerlinOnline Stadtportal GmbH & Co. KG
7 *
8 */
9
10namespace BO\Zmsentities\Helper;
11
12use BO\Zmsentities\Client;
13use BO\Zmsentities\Process;
14use BO\Zmsentities\Collection\ProcessList;
15use BO\Zmsentities\Config;
16use Twig\Loader\FilesystemLoader;
17use Twig\Environment;
18use Symfony\Bridge\Twig\Extension\TranslationExtension;
19use Twig\Extra\Intl\IntlExtension;
20
21/**
22 * @SuppressWarnings(Coupling)
23 *
24 */
25class Messaging
26{
27    public static $icsRequiredForStatus = [
28        'confirmed',
29        'appointment'
30    ];
31
32    public static $allowEmptyProcesses = [
33        'overview'
34    ];
35
36    public static function isIcsRequired(
37        \BO\Zmsentities\Config $config,
38        \BO\Zmsentities\Process $process,
39        $status
40    ): bool {
41        $client = $process->getFirstClient();
42        $noAttachmentDomains = $config->toProperty()->notifications->noAttachmentDomains->get();
43        $noAttachmentDomains = explode(',', (string)$noAttachmentDomains);
44        foreach ($noAttachmentDomains as $matching) {
45            if (trim($matching) && strpos($client->email, '@' . trim($matching))) {
46                return false;
47            }
48        }
49        return (in_array($status, self::$icsRequiredForStatus));
50    }
51
52    public static function isEmptyProcessListAllowed($status): bool
53    {
54        return (in_array($status, self::$allowEmptyProcesses));
55    }
56
57    protected static $templates = array(
58        'mail' => array(
59            'queued' => 'mail_queued.twig',
60            'appointment' => 'mail_confirmation.twig',
61            'reminder' => 'mail_reminder.twig',
62            'deleted' => 'mail_delete.twig',
63            'blocked' => 'mail_delete.twig',
64            'survey' => 'mail_survey.twig',
65            'overview' => 'mail_processlist_overview.twig',
66            'preconfirmed' => 'mail_preconfirmed.twig'
67        ),
68        'ics' => array(
69            'appointment' => 'icsappointment.twig',
70            'deleted' => 'icsappointment_delete.twig'
71        ),
72        'admin' => array(
73            'deleted' => 'mail_admin_delete.twig',
74            'blocked' => 'mail_admin_delete.twig',
75            'updated' => 'mail_admin_update.twig'
76        )
77    );
78
79    protected static function twigView(): Environment
80    {
81        return self::createTwigEnvironment(self::filesystemLoader());
82    }
83
84    protected static function dbTwigView($templateProvider): Environment
85    {
86        $loader = new \Twig\Loader\ChainLoader([
87            new \Twig\Loader\ArrayLoader($templateProvider->getTemplates()),
88            self::filesystemLoader(),
89        ]);
90        return self::createTwigEnvironment($loader);
91    }
92
93    protected static function createTwigEnvironment(\Twig\Loader\LoaderInterface $loader): Environment
94    {
95        $twig = new Environment($loader, array(//'cache' => '/cache/',
96        ));
97        $twig->addExtension(new TranslationExtension());
98        $twig->addExtension(new IntlExtension());
99        return $twig;
100    }
101
102    protected static function filesystemLoader(): FilesystemLoader
103    {
104        $templatePath = TemplateFinder::getTemplatePath();
105        $customTemplatesPath = 'custom_templates/';
106
107        if (getenv("ZMS_CUSTOM_TEMPLATES_PATH")) {
108            $customTemplatesPath = getenv("ZMS_CUSTOM_TEMPLATES_PATH");
109        }
110
111        $initialTemplatePaths = [];
112
113        if (is_dir($customTemplatesPath)) {
114            $initialTemplatePaths[] = $customTemplatesPath;
115        }
116
117        $initialTemplatePaths[] = $templatePath;
118
119        $loader = new FilesystemLoader($initialTemplatePaths);
120
121        if (is_dir($customTemplatesPath)) {
122            $loader->addPath($customTemplatesPath, 'zmsentities');
123        }
124
125        $loader->addPath($templatePath, 'zmsentities');
126        return $loader;
127    }
128
129    public static function getMailContentPreview($templateContent, $process): string
130    {
131        $parameters = self::generateMailParameters(
132            $process,
133            new Config(),
134            null,
135            'appointment'
136        );
137
138        return self::twigView()->createTemplate($templateContent)->render($parameters);
139    }
140
141    public static function getMailContent(
142        Process|ProcessList $processList,
143        Config $config,
144        $initiator = null,
145        $status = 'appointment',
146        $templateProvider = false
147    ) {
148        $parameters = self::generateMailParameters($processList, $config, $initiator, $status);
149
150        (new ProcessList())->testProcessListLength($processList, self::isEmptyProcessListAllowed($status));
151        $template = self::getTemplate('mail', $status);
152        if ($initiator) {
153            $template = self::getTemplate('admin', $status);
154        }
155        if (!$template) {
156            $exception = new \BO\Zmsentities\Exception\TemplateNotFound("Template for status $status not found");
157            $exception->data = $status;
158            throw $exception;
159        }
160
161        if ($templateProvider) {
162            $message = self::dbTwigView($templateProvider)->render($template, $parameters);
163        } else {
164            $message = self::twigView()->render('messaging/' . $template, $parameters);
165        }
166
167        return $message;
168    }
169
170    /**
171     *
172     * @return ((int|mixed)[][]|Client|Process|\DateTimeImmutable|mixed|null|string)[]
173     *
174     */
175    public static function generateMailParameters(Process|ProcessList $processList, Config $config, $initiator, string $status): array
176    {
177        $collection = (new ProcessList())
178            ->testProcessListLength($processList, self::isEmptyProcessListAllowed($status));
179        $mainProcess = $collection->getFirst();
180        $date = (new \DateTimeImmutable())->setTimestamp(0);
181        $client = (new Client());
182        if ($mainProcess) {
183            $collection = $collection->withoutProcessByStatus($mainProcess, $status);
184            $date = $mainProcess->getFirstAppointment()->toDateTime()->format('U');
185            $client = $mainProcess->getFirstClient();
186        }
187
188        $requestGroups = [];
189        if ($mainProcess) {
190            foreach ($mainProcess->requests as $request) {
191                if (! isset($requestGroups[$request->id])) {
192                    $requestGroups[$request->id] = [
193                        'request' => $request,
194                        'count' => 0
195                    ];
196                }
197                $requestGroups[$request->id]['count']++;
198            }
199        }
200
201        return [
202            'date' => $date,
203            'client' => $client,
204            'process' => $mainProcess,
205            'requestGroups' => $requestGroups,
206            'processList' => $collection->sortByAppointmentDate(),
207            'config' => $config,
208            'initiator' => $initiator,
209            'appointmentLink' => base64_encode(json_encode([
210                'id' => $mainProcess ? $mainProcess->id : '',
211                'authKey' => $mainProcess ? $mainProcess->authKey : ''
212            ]))
213        ];
214    }
215
216    public static function getScopeAdminProcessListContent(
217        \BO\Zmsentities\Collection\ProcessList $processList,
218        \BO\Zmsentities\Scope $scope,
219        \DateTimeInterface $dateTime
220    ): string {
221        $message = self::twigView()->render(
222            'messaging/mail_scopeadmin_processlist.twig',
223            array(
224                'dateTime' => $dateTime,
225                'processList' => $processList,
226                'scope' => $scope
227            )
228        );
229        return $message;
230    }
231
232    protected static function getTemplate(string $type, $status)
233    {
234        $template = null;
235        if (Property::__keyExists($type, self::$templates)) {
236            if (Property::__keyExists($status, self::$templates[$type])) {
237                $template = self::$templates[$type][$status];
238            }
239        }
240        return $template;
241    }
242
243    public static function getMailSubject(
244        Process $process,
245        Config $config,
246        $initiator = null,
247        $status = 'appointment',
248        $templateProvider = null
249    ): string {
250        $appointment = $process->getFirstAppointment();
251        $parameters = [
252            'date' => $appointment ? $appointment->toDateTime()->format('U') : null,
253            'client' => $process->getFirstClient(),
254            'process' => $process,
255            'config' => $config,
256            'initiator' => $initiator,
257            'status' => $status
258        ];
259
260        $template = 'subjects.twig';
261
262        if ($templateProvider) {
263            $subject = self::dbTwigView($templateProvider)->render($template, $parameters);
264        } else {
265            $subject = self::twigView()->render('messaging/' . $template, $parameters);
266        }
267
268        return trim($subject);
269    }
270
271    public static function getMailIcs(
272        Process $process,
273        Config $config,
274        $status = 'appointment',
275        $initiator = null,
276        $now = false,
277        $templateProvider = false
278    ): \BO\Zmsentities\Ics {
279        $ics = new \BO\Zmsentities\Ics();
280        $message = self::getMailContent($process, $config, $initiator, $status, $templateProvider);
281        $ics->content = self::generateIcsContent($process, $config, $status, $now, $templateProvider, $message);
282
283        return $ics;
284    }
285
286    protected static function generateIcsContent(
287        Process $process,
288        Config $config,
289        $status = 'appointment',
290        $now = false,
291        $templateProvider = false,
292        $message = '' // Pass $message from getMailIcs, or query if not set
293    ) {
294        // If $message is not provided, retrieve it from the getMailContent query
295        if (empty($message)) {
296            $message = self::getMailContent($process, $config, null, $status, $templateProvider);
297        }
298
299        // Convert the email message to plain text for the ICS description
300        $plainTextDescription = self::getPlainText($message);
301
302        // Get the ICS template for the process status dynamically
303        $template = self::getTemplate('ics', $status);
304        if (!$template) {
305            $exception = new \BO\Zmsentities\Exception\TemplateNotFound("ICS template for status $status not found");
306            $exception->data = $status;
307            throw $exception;
308        }
309
310        $baseParameters = self::generateMailParameters(new ProcessList([$process]), $config, null, $status);
311
312        // Extract the first appointment details
313        $appointment = $process->getFirstAppointment();
314        $currentYear = $appointment->getStartTime()->format('Y');
315
316        // Prepare parameters for ICS rendering, including the plain text description
317        $additionalParameters = [
318            'date' => $appointment->toDateTime()->format('U'),
319            'startTime' => $appointment->getStartTime()->format('U'),
320            'endTime' => $appointment->getEndTime()->format('U'),
321            'startSummerTime' => \BO\Zmsentities\Helper\DateTime::getSummerTimeStartDateTime($currentYear)->format('U'),
322            'endSummerTime' => \BO\Zmsentities\Helper\DateTime::getSummerTimeEndDateTime($currentYear)->format('U'),
323            'process' => $process,
324            'timestamp' => (!$now) ? time() : $now,
325            'message' => $plainTextDescription // Pass the plain text email content to the ICS template
326        ];
327
328        $parameters = array_merge($baseParameters, $additionalParameters);
329
330        // Render the ICS content using Twig and the fetched template
331        if ($templateProvider) {
332            $icsString = self::dbTwigView($templateProvider)->render($template, $parameters);
333        } else {
334            $icsString = self::twigView()->render('messaging/' . $template, $parameters);
335        }
336
337        // Decode HTML entities to plain text and ensure lines follow ICS standards
338        $icsString = html_entity_decode($icsString);
339        return self::getTextWithFoldedLines($icsString);
340    }
341
342    public static function getPlainText($content, $lineBreak = "\n"): string
343    {
344        $converter = new \League\HTMLToMarkdown\HtmlConverter();
345        $converter->getConfig()->setOption('remove_nodes', 'script');
346        $converter->getConfig()->setOption('strip_tags', true);
347        $converter->getConfig()->setOption('hard_break', true);
348        $converter->getConfig()->setOption('use_autolinks', false);
349        $text = $converter->convert($content);
350        $text = str_replace(',', '\,', $text);
351        $text = str_replace(';', '\;', $text);
352        $text = str_replace("\n", $lineBreak, $text);
353        return trim($text);
354    }
355
356    public static function getTextWithFoldedLines(string $content): string
357    {
358        $newLines = [];
359        $lines = explode("\n", $content);
360        foreach ($lines as $text) {
361            $subline = '';
362            while (strlen($text) > 75) {
363                $line = mb_substr($text, 0, 72);
364                $llength = mb_strlen($line);
365                $subline .= $line . chr(13) . chr(10) . chr(32);
366                $text = mb_substr($text, $llength);
367            }
368            if (!empty($text) && 0 < strlen($subline)) {
369                $subline .= $text;
370            }
371            if (0 < strlen($subline)) {
372                $newLines[] = $subline;
373            }
374            if (!empty($text) && '' == $subline) {
375                $newLines[] = $text;
376            }
377        }
378        return implode(chr(13) . chr(10), $newLines);
379    }
380}