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