Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.22% covered (success)
99.22%
127 / 128
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
HtmlUtils
99.22% covered (success)
99.22%
127 / 128
88.89% covered (warning)
88.89%
8 / 9
19
0.00% covered (danger)
0.00%
0 / 1
 renderMarkdown
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 postprocess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replaceEmailAdresses
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
1 / 1
9
 getImageSrcHtml
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 getPrefix
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSubject
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSuffix
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 escapeDollar
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromEnv
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Olz\Utils;
4
5use League\CommonMark\Environment\Environment;
6use League\CommonMark\Extension\Attributes\AttributesExtension;
7use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
8use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
9use League\CommonMark\MarkdownConverter;
10use Olz\Entity\Roles\Role;
11use Olz\Roles\Components\OlzRoleInfoModal\OlzRoleInfoModal;
12
13class HtmlUtils {
14    use WithUtilsTrait;
15
16    public string $prefix_regex = '<a ([^>]*)href=[\'"]mailto:';
17    public string $subject_regex = '\?subject=([^\'"]*)';
18    public string $suffix_regex = '[\'"]([^>]*)>([^<@]*)([^<]*)<\/a>';
19    public string $olz_email_regex = '';
20    public string $email_regex = '([A-Z0-9a-z._%+-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,64})';
21
22    /** @param array<string, mixed> $override_config */
23    public function renderMarkdown(string $markdown, array $override_config = []): string {
24        $default_config = [
25            'html_input' => 'escape',
26            'allow_unsafe_links' => false,
27            'max_nesting_level' => 100,
28        ];
29        $config = array_merge($default_config, $override_config);
30
31        $environment = new Environment($config);
32        $environment->addExtension(new CommonMarkCoreExtension());
33        $environment->addExtension(new GithubFlavoredMarkdownExtension());
34        $environment->addExtension(new AttributesExtension());
35        $converter = new MarkdownConverter($environment);
36        $rendered = $converter->convert($markdown);
37        $postprocessed = $this->postprocess(strval($rendered));
38        return "<div class='rendered-markdown'>{$postprocessed}</div>";
39    }
40
41    public function postprocess(string $html): string {
42        return $this->replaceEmailAdresses($html);
43    }
44
45    public function replaceEmailAdresses(string $html): string {
46        $role_repo = $this->entityManager()->getRepository(Role::class);
47        $host = $this->envUtils()->getEmailForwardingHost();
48        $esc_host = preg_quote($host);
49        $this->olz_email_regex = '([A-Z0-9a-z._%+-]+)@'.$esc_host;
50
51        $html = str_replace(['<p>', '<p ', '</p>'], ['<div>', '<div ', '</div>'], $html);
52
53        preg_match_all(
54            "/{$this->prefix_regex}{$this->olz_email_regex}{$this->subject_regex}{$this->suffix_regex}/",
55            $html,
56            $matches,
57        );
58        for ($i = 0; $i < count($matches[0]); $i++) {
59            $username = $matches[2][$i];
60            // TODO: Only active roles!
61            $role = $role_repo->findOneBy(['username' => $username]);
62            if ($role) {
63                $prefix = $this->getPrefix($matches[1][$i]);
64                $username = preg_quote($username);
65                $email = "{$username}@{$host}";
66                $subject = $this->getSubject($matches[3][$i]);
67                $suffix = $this->getSuffix($matches[4][$i], $matches[5][$i], $matches[6][$i]);
68                $html = preg_replace(
69                    "/{$prefix}{$email}{$subject}{$suffix}/",
70                    $this->escapeDollar(OlzRoleInfoModal::render([
71                        'role' => $role,
72                        'text' => $matches[5][$i] ?: null,
73                    ])),
74                    $html
75                );
76                $this->generalUtils()->checkNotNull($html, "String replacement failed");
77            }
78        }
79
80        preg_match_all(
81            "/{$this->prefix_regex}{$this->olz_email_regex}{$this->suffix_regex}/",
82            $html,
83            $matches,
84        );
85        for ($i = 0; $i < count($matches[0]); $i++) {
86            $username = $matches[2][$i];
87            // TODO: Only active roles!
88            $role = $role_repo->findOneBy(['username' => $username]);
89            if ($role) {
90                $prefix = $this->getPrefix($matches[1][$i]);
91                $username = preg_quote($username);
92                $email = "{$username}@{$host}";
93                $suffix = $this->getSuffix($matches[3][$i], $matches[4][$i], $matches[5][$i]);
94                $html = preg_replace(
95                    "/{$prefix}{$email}{$suffix}/",
96                    $this->escapeDollar(OlzRoleInfoModal::render([
97                        'role' => $role,
98                        'text' => $matches[4][$i] ?: null,
99                    ])),
100                    $html
101                );
102                $this->generalUtils()->checkNotNull($html, "String replacement failed");
103            }
104        }
105
106        preg_match_all(
107            "/(\\s|^){$this->olz_email_regex}([\\s,\\.!\\?]|$)/",
108            $html,
109            $matches,
110        );
111        for ($i = 0; $i < count($matches[0]); $i++) {
112            $username = $matches[2][$i];
113            // TODO: Only active roles!
114            $role = $role_repo->findOneBy(['username' => $username]);
115            if ($role) {
116                $username = preg_quote($username);
117                $email = "{$username}@{$host}";
118                $html = preg_replace(
119                    "/{$email}/",
120                    $this->escapeDollar(OlzRoleInfoModal::render(['role' => $role])),
121                    $html
122                );
123                $this->generalUtils()->checkNotNull($html, "String replacement failed");
124            }
125        }
126
127        $html = preg_replace(
128            "/{$this->prefix_regex}{$this->email_regex}{$this->subject_regex}{$this->suffix_regex}/",
129            "<script>olz.MailTo(\"\$2\", \"\$3\", \"\$6\" + \"\$7\", \"\$4\")</script>",
130            $html
131        );
132        $this->generalUtils()->checkNotNull($html, "String replacement failed");
133        $html = preg_replace(
134            "/{$this->prefix_regex}{$this->email_regex}{$this->suffix_regex}/",
135            "<script>olz.MailTo(\"\$2\", \"\$3\", \"\$5\" + \"\$6\")</script>",
136            $html
137        );
138        $this->generalUtils()->checkNotNull($html, "String replacement failed");
139        $html = preg_replace(
140            "/(\\s|^){$this->email_regex}([\\s,\\.!\\?]|$)/",
141            "\$1<script>olz.MailTo(\"\$2\", \"\$3\", \"E-Mail\")</script>\$4",
142            $html
143        );
144        $this->generalUtils()->checkNotNull($html, "String replacement failed");
145        return $html;
146    }
147
148    /** @param array<string, string> $image_hrefs */
149    public function getImageSrcHtml(array $image_hrefs): string {
150        $keys = array_keys($image_hrefs);
151        if (count($keys) < 1) {
152            return '';
153        }
154        $default_src = $image_hrefs['1x'] ?? $image_hrefs[$keys[0]];
155        if (count($keys) < 2) {
156            return <<<ZZZZZZZZZZ
157                src='{$default_src}'
158                ZZZZZZZZZZ;
159        }
160        $srcset = implode(",\n    ", array_map(function ($key) use ($image_hrefs) {
161            $value = $image_hrefs[$key];
162            return "{$value} {$key}";
163        }, $keys));
164        return <<<ZZZZZZZZZZ
165            srcset='
166                {$srcset}
167            '
168            src='{$default_src}'
169            ZZZZZZZZZZ;
170    }
171
172    protected function getPrefix(string $match): string {
173        $esc_match = preg_quote($match);
174        return "<a {$esc_match}href=['\"]mailto:";
175    }
176
177    protected function getSubject(string $match): string {
178        $esc_match = preg_quote($match);
179        return "\\?subject={$esc_match}";
180    }
181
182    protected function getSuffix(string $match1, string $match2, string $match3): string {
183        $esc_match1 = preg_quote($match1);
184        $esc_match2 = preg_quote($match2);
185        $esc_match3 = preg_quote($match3);
186        return "['\"]{$esc_match1}>{$esc_match2}{$esc_match3}<\\/a>";
187    }
188
189    protected function escapeDollar(string $replacement): string {
190        return str_replace('$', '\$', $replacement);
191    }
192
193    public static function fromEnv(): self {
194        return new self();
195    }
196}