Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
5.56% covered (danger)
5.56%
7 / 126
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
HtmlUtils
5.56% covered (danger)
5.56%
7 / 126
0.00% covered (danger)
0.00%
0 / 8
290.94
0.00% covered (danger)
0.00%
0 / 1
 renderMarkdown
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 postprocess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 replaceEmailAdresses
0.00% covered (danger)
0.00%
0 / 85
0.00% covered (danger)
0.00%
0 / 1
90
 getImageSrcHtml
41.18% covered (danger)
41.18%
7 / 17
0.00% covered (danger)
0.00%
0 / 1
4.83
 getPrefix
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSubject
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSuffix
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 escapeDollar
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        preg_match_all(
52            "/{$this->prefix_regex}{$this->olz_email_regex}{$this->subject_regex}{$this->suffix_regex}/",
53            $html,
54            $matches,
55        );
56        for ($i = 0; $i < count($matches[0]); $i++) {
57            $username = $matches[2][$i];
58            // TODO: Only active roles!
59            $role = $role_repo->findOneBy(['username' => $username]);
60            if ($role) {
61                $prefix = $this->getPrefix($matches[1][$i]);
62                $username = preg_quote($username);
63                $email = "{$username}@{$host}";
64                $subject = $this->getSubject($matches[3][$i]);
65                $suffix = $this->getSuffix($matches[4][$i], $matches[5][$i], $matches[6][$i]);
66                $html = preg_replace(
67                    "/{$prefix}{$email}{$subject}{$suffix}/",
68                    $this->escapeDollar(OlzRoleInfoModal::render([
69                        'role' => $role,
70                        'text' => $matches[5][$i] ?: null,
71                    ])),
72                    $html
73                );
74                $this->generalUtils()->checkNotNull($html, "String replacement failed");
75            }
76        }
77
78        preg_match_all(
79            "/{$this->prefix_regex}{$this->olz_email_regex}{$this->suffix_regex}/",
80            $html,
81            $matches,
82        );
83        for ($i = 0; $i < count($matches[0]); $i++) {
84            $username = $matches[2][$i];
85            // TODO: Only active roles!
86            $role = $role_repo->findOneBy(['username' => $username]);
87            if ($role) {
88                $prefix = $this->getPrefix($matches[1][$i]);
89                $username = preg_quote($username);
90                $email = "{$username}@{$host}";
91                $suffix = $this->getSuffix($matches[3][$i], $matches[4][$i], $matches[5][$i]);
92                $html = preg_replace(
93                    "/{$prefix}{$email}{$suffix}/",
94                    $this->escapeDollar(OlzRoleInfoModal::render([
95                        'role' => $role,
96                        'text' => $matches[4][$i] ?: null,
97                    ])),
98                    $html
99                );
100                $this->generalUtils()->checkNotNull($html, "String replacement failed");
101            }
102        }
103
104        preg_match_all(
105            "/(\\s|^){$this->olz_email_regex}([\\s,\\.!\\?]|$)/",
106            $html,
107            $matches,
108        );
109        for ($i = 0; $i < count($matches[0]); $i++) {
110            $username = $matches[2][$i];
111            // TODO: Only active roles!
112            $role = $role_repo->findOneBy(['username' => $username]);
113            if ($role) {
114                $username = preg_quote($username);
115                $email = "{$username}@{$host}";
116                $html = preg_replace(
117                    "/{$email}/",
118                    $this->escapeDollar(OlzRoleInfoModal::render(['role' => $role])),
119                    $html
120                );
121                $this->generalUtils()->checkNotNull($html, "String replacement failed");
122            }
123        }
124
125        $html = preg_replace(
126            "/{$this->prefix_regex}{$this->email_regex}{$this->subject_regex}{$this->suffix_regex}/",
127            "<script>olz.MailTo(\"\$2\", \"\$3\", \"\$6\" + \"\$7\", \"\$4\")</script>",
128            $html
129        );
130        $this->generalUtils()->checkNotNull($html, "String replacement failed");
131        $html = preg_replace(
132            "/{$this->prefix_regex}{$this->email_regex}{$this->suffix_regex}/",
133            "<script>olz.MailTo(\"\$2\", \"\$3\", \"\$5\" + \"\$6\")</script>",
134            $html
135        );
136        $this->generalUtils()->checkNotNull($html, "String replacement failed");
137        $html = preg_replace(
138            "/(\\s|^){$this->email_regex}([\\s,\\.!\\?]|$)/",
139            "\$1<script>olz.MailTo(\"\$2\", \"\$3\", \"E-Mail\")</script>\$4",
140            $html
141        );
142        $this->generalUtils()->checkNotNull($html, "String replacement failed");
143        return $html;
144    }
145
146    /** @param array<string, string> $image_hrefs */
147    public function getImageSrcHtml(array $image_hrefs): string {
148        $keys = array_keys($image_hrefs);
149        if (count($keys) < 1) {
150            return '';
151        }
152        $default_src = $image_hrefs['1x'] ?? $image_hrefs[$keys[0]];
153        if (count($keys) < 2) {
154            return <<<ZZZZZZZZZZ
155                src='{$default_src}'
156                ZZZZZZZZZZ;
157        }
158        $srcset = implode(",\n    ", array_map(function ($key) use ($image_hrefs) {
159            $value = $image_hrefs[$key];
160            return "{$value} {$key}";
161        }, $keys));
162        return <<<ZZZZZZZZZZ
163            srcset='
164                {$srcset}
165            '
166            src='{$default_src}'
167            ZZZZZZZZZZ;
168    }
169
170    protected function getPrefix(string $match): string {
171        $esc_match = preg_quote($match);
172        return "<a {$esc_match}href=['\"]mailto:";
173    }
174
175    protected function getSubject(string $match): string {
176        $esc_match = preg_quote($match);
177        return "\\?subject={$esc_match}";
178    }
179
180    protected function getSuffix(string $match1, string $match2, string $match3): string {
181        $esc_match1 = preg_quote($match1);
182        $esc_match2 = preg_quote($match2);
183        $esc_match3 = preg_quote($match3);
184        return "['\"]{$esc_match1}>{$esc_match2}{$esc_match3}<\\/a>";
185    }
186
187    protected function escapeDollar(string $replacement): string {
188        return str_replace('$', '\$', $replacement);
189    }
190}