Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.75% covered (warning)
86.75%
144 / 166
57.14% covered (warning)
57.14%
8 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Messaging
86.75% covered (warning)
86.75%
144 / 166
57.14% covered (warning)
57.14%
8 / 14
48.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
82.35% covered (warning)
82.35%
14 / 17
0.00% covered (danger)
0.00%
0 / 1
4.09
 dbTwigView
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getMailContentPreview
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getMailContent
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
4.01
 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
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
3.00
 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    ) {
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)
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        $templatePath = TemplateFinder::getTemplatePath();
82        $customTemplatesPath = 'custom_templates/';
83
84        if (getenv("ZMS_CUSTOM_TEMPLATES_PATH")) {
85            $customTemplatesPath = getenv("ZMS_CUSTOM_TEMPLATES_PATH");
86        }
87
88        $initialTemplatePaths = [];
89
90        if (is_dir($customTemplatesPath)) {
91            $initialTemplatePaths[] = $customTemplatesPath;
92        }
93
94        $initialTemplatePaths[] = $templatePath;
95
96        $loader = new FilesystemLoader($initialTemplatePaths);
97
98        if (is_dir($customTemplatesPath)) {
99            $loader->addPath($customTemplatesPath, 'zmsentities');
100        }
101
102        $loader->addPath($templatePath, 'zmsentities');
103        $twig = new Environment($loader, array(//'cache' => '/cache/',
104        ));
105        $twig->addExtension(new TranslationExtension());
106        $twig->addExtension(new IntlExtension());
107        return $twig;
108    }
109
110    protected static function dbTwigView($templateProvider)
111    {
112        $loader = new \Twig\Loader\ArrayLoader($templateProvider->getTemplates());
113        $twig = new \Twig\Environment($loader);
114        $twig->addExtension(new TranslationExtension());
115        $twig->addExtension(new IntlExtension());
116        return $twig;
117    }
118
119    public static function getMailContentPreview($templateContent, $process)
120    {
121        $parameters = self::generateMailParameters(
122            $process,
123            new Config(),
124            null,
125            'appointment'
126        );
127
128        return self::twigView()->createTemplate($templateContent)->render($parameters);
129    }
130
131    public static function getMailContent(
132        $processList,
133        Config $config,
134        $initiator = null,
135        $status = 'appointment',
136        $templateProvider = false
137    ) {
138        $parameters = self::generateMailParameters($processList, $config, $initiator, $status);
139
140        (new ProcessList())->testProcessListLength($processList, self::isEmptyProcessListAllowed($status));
141        $template = self::getTemplate('mail', $status);
142        if ($initiator) {
143            $template = self::getTemplate('admin', $status);
144        }
145        if (!$template) {
146            $exception = new \BO\Zmsentities\Exception\TemplateNotFound("Template for status $status not found");
147            $exception->data = $status;
148            throw $exception;
149        }
150
151        if ($templateProvider) {
152            $message = self::dbTwigView($templateProvider)->render($template, $parameters);
153        } else {
154            $message = self::twigView()->render('messaging/' . $template, $parameters);
155        }
156
157        return $message;
158    }
159
160    public static function generateMailParameters($processList, $config, $initiator, $status)
161    {
162        $collection = (new ProcessList())
163            ->testProcessListLength($processList, self::isEmptyProcessListAllowed($status));
164        $mainProcess = $collection->getFirst();
165        $date = (new \DateTimeImmutable())->setTimestamp(0);
166        $client = (new Client());
167        if ($mainProcess) {
168            $collection = $collection->withoutProcessByStatus($mainProcess, $status);
169            $date = $mainProcess->getFirstAppointment()->toDateTime()->format('U');
170            $client = $mainProcess->getFirstClient();
171        }
172
173        $requestGroups = [];
174        if ($mainProcess) {
175            foreach ($mainProcess->requests as $request) {
176                if (! isset($requestGroups[$request->id])) {
177                    $requestGroups[$request->id] = [
178                        'request' => $request,
179                        'count' => 0
180                    ];
181                }
182                $requestGroups[$request->id]['count']++;
183            }
184        }
185
186        return [
187            'date' => $date,
188            'client' => $client,
189            'process' => $mainProcess,
190            'requestGroups' => $requestGroups,
191            'processList' => $collection->sortByAppointmentDate(),
192            'config' => $config,
193            'initiator' => $initiator,
194            'appointmentLink' => base64_encode(json_encode([
195                'id' => $mainProcess ? $mainProcess->id : '',
196                'authKey' => $mainProcess ? $mainProcess->authKey : ''
197            ]))
198        ];
199    }
200
201    public static function getScopeAdminProcessListContent(
202        \BO\Zmsentities\Collection\ProcessList $processList,
203        \BO\Zmsentities\Scope $scope,
204        \DateTimeInterface $dateTime
205    ) {
206        $message = self::twigView()->render(
207            'messaging/mail_scopeadmin_processlist.twig',
208            array(
209                'dateTime' => $dateTime,
210                'processList' => $processList,
211                'scope' => $scope
212            )
213        );
214        return $message;
215    }
216
217    protected static function getTemplate($type, $status)
218    {
219        $template = null;
220        if (Property::__keyExists($type, self::$templates)) {
221            if (Property::__keyExists($status, self::$templates[$type])) {
222                $template = self::$templates[$type][$status];
223            }
224        }
225        return $template;
226    }
227
228    public static function getMailSubject(
229        Process $process,
230        Config $config,
231        $initiator = null,
232        $status = 'appointment',
233        $templateProvider = null
234    ) {
235        $appointment = $process->getFirstAppointment();
236        $parameters = [
237            'date' => $appointment ? $appointment->toDateTime()->format('U') : null,
238            'client' => $process->getFirstClient(),
239            'process' => $process,
240            'config' => $config,
241            'initiator' => $initiator,
242            'status' => $status
243        ];
244
245        $template = 'subjects.twig';
246
247        if ($templateProvider) {
248            $subject = self::dbTwigView($templateProvider)->render($template, $parameters);
249        } else {
250            $subject = self::twigView()->render('messaging/' . $template, $parameters);
251        }
252
253        return trim($subject);
254    }
255
256    public static function getMailIcs(
257        Process $process,
258        Config $config,
259        $status = 'appointment',
260        $initiator = null,
261        $now = false,
262        $templateProvider = false
263    ) {
264        $ics = new \BO\Zmsentities\Ics();
265        $message = self::getMailContent($process, $config, $initiator, $status, $templateProvider);
266        $ics->content = self::generateIcsContent($process, $config, $status, $now, $templateProvider, $message);
267
268        return $ics;
269    }
270
271    protected static function generateIcsContent(
272        Process $process,
273        Config $config,
274        $status = 'appointment',
275        $now = false,
276        $templateProvider = false,
277        $message = '' // Pass $message from getMailIcs, or query if not set
278    ) {
279        // If $message is not provided, retrieve it from the getMailContent query
280        if (empty($message)) {
281            $message = self::getMailContent($process, $config, null, $status, $templateProvider);
282        }
283
284        // Convert the email message to plain text for the ICS description
285        $plainTextDescription = self::getPlainText($message);
286
287        // Get the ICS template for the process status dynamically
288        $template = self::getTemplate('ics', $status);
289        if (!$template) {
290            $exception = new \BO\Zmsentities\Exception\TemplateNotFound("ICS template for status $status not found");
291            $exception->data = $status;
292            throw $exception;
293        }
294
295        $baseParameters = self::generateMailParameters(new ProcessList([$process]), $config, null, $status);
296
297        // Extract the first appointment details
298        $appointment = $process->getFirstAppointment();
299        $currentYear = $appointment->getStartTime()->format('Y');
300
301        // Prepare parameters for ICS rendering, including the plain text description
302        $additionalParameters = [
303            'date' => $appointment->toDateTime()->format('U'),
304            'startTime' => $appointment->getStartTime()->format('U'),
305            'endTime' => $appointment->getEndTime()->format('U'),
306            'startSummerTime' => \BO\Zmsentities\Helper\DateTime::getSummerTimeStartDateTime($currentYear)->format('U'),
307            'endSummerTime' => \BO\Zmsentities\Helper\DateTime::getSummerTimeEndDateTime($currentYear)->format('U'),
308            'process' => $process,
309            'timestamp' => (!$now) ? time() : $now,
310            'message' => $plainTextDescription // Pass the plain text email content to the ICS template
311        ];
312
313        $parameters = array_merge($baseParameters, $additionalParameters);
314
315        // Render the ICS content using Twig and the fetched template
316        if ($templateProvider) {
317            $icsString = self::dbTwigView($templateProvider)->render($template, $parameters);
318        } else {
319            $icsString = self::twigView()->render('messaging/' . $template, $parameters);
320        }
321
322        // Decode HTML entities to plain text and ensure lines follow ICS standards
323        $icsString = html_entity_decode($icsString);
324        return self::getTextWithFoldedLines($icsString);
325    }
326
327    public static function getPlainText($content, $lineBreak = "\n")
328    {
329        $converter = new \League\HTMLToMarkdown\HtmlConverter();
330        $converter->getConfig()->setOption('remove_nodes', 'script');
331        $converter->getConfig()->setOption('strip_tags', true);
332        $converter->getConfig()->setOption('hard_break', true);
333        $converter->getConfig()->setOption('use_autolinks', false);
334        $text = $converter->convert($content);
335        $text = str_replace(',', '\,', $text);
336        $text = str_replace(';', '\;', $text);
337        $text = str_replace("\n", $lineBreak, $text);
338        return trim($text);
339    }
340
341    public static function getTextWithFoldedLines($content)
342    {
343        $newLines = [];
344        $lines = explode("\n", $content);
345        foreach ($lines as $text) {
346            $subline = '';
347            while (strlen($text) > 75) {
348                $line = mb_substr($text, 0, 72);
349                $llength = mb_strlen($line);
350                $subline .= $line . chr(13) . chr(10) . chr(32);
351                $text = mb_substr($text, $llength);
352            }
353            if (!empty($text) && 0 < strlen($subline)) {
354                $subline .= $text;
355            }
356            if (0 < strlen($subline)) {
357                $newLines[] = $subline;
358            }
359            if (!empty($text) && '' == $subline) {
360                $newLines[] = $text;
361            }
362        }
363        return implode(chr(13) . chr(10), $newLines);
364    }
365}