Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
39.38% covered (danger)
39.38%
89 / 226
36.67% covered (danger)
36.67%
11 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
TwigExtension
39.38% covered (danger)
39.38%
89 / 226
36.67% covered (danger)
36.67%
11 / 30
1894.35
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFunctions
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 isImageAllowed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getLanguageDescriptor
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isNumeric
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNow
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getSystemStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toTextFormat
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 formatDateTime
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
3.01
 currentRoute
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
8.06
 currentLang
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 currentLocale
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 currentVersion
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 urlGet
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 csvProperty
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 azPrefixList
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 azPrefixListCollator
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 isValueInArray
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 remoteInclude
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 getEsiFromPath
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 getClientHost
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 toSortableString
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 sortByName
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 sortFirstChar
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 dumpAppProfiler
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 kindOfPayment
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 isMultiLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDebug
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEsiEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * @package   BO Slim
5 * @copyright BerlinOnline Stadtportal GmbH & Co. KG
6 **/
7
8namespace BO\Slim;
9
10use Psr\Container\ContainerInterface;
11use Psr\Http\Message\ServerRequestInterface;
12use Twig\Extension\AbstractExtension;
13
14/**
15  * Extension for Twig and Slim
16  *
17  *  @SuppressWarnings(PublicMethod)
18  *  @SuppressWarnings(TooManyMethods)
19  *  @SuppressWarnings(Coupling)
20  *  @SuppressWarnings(Complexity)
21  */
22class TwigExtension extends AbstractExtension
23{
24    private ContainerInterface $container;
25
26    public function __construct(ContainerInterface $container)
27    {
28        $this->container = $container;
29    }
30
31    /** @psalm-api Called by Twig when the extension is registered. */
32    public function getName(): string
33    {
34        return 'boslimExtension';
35    }
36
37    #[\Override]
38    public function getFunctions(): array
39    {
40        $safe = array('is_safe' => array('html'));
41        return array(
42            new \Twig\TwigFunction('urlGet', array($this, 'urlGet')),
43            new \Twig\TwigFunction('csvProperty', array($this, 'csvProperty')),
44            new \Twig\TwigFunction('azPrefixList', array($this, 'azPrefixList')),
45            new \Twig\TwigFunction('azPrefixListCollator', array($this, 'azPrefixListCollator')),
46            new \Twig\TwigFunction('isValueInArray', array($this, 'isValueInArray')),
47            new \Twig\TwigFunction('remoteInclude', array($this, 'remoteInclude'), $safe),
48            new \Twig\TwigFunction('getEsiFromPath', array($this, 'getEsiFromPath')),
49            new \Twig\TwigFunction('getLanguageDescriptor', array($this, 'getLanguageDescriptor')),
50            new \Twig\TwigFunction('currentLang', array($this, 'currentLang')),
51            new \Twig\TwigFunction('currentRoute', array($this, 'currentRoute')),
52            new \Twig\TwigFunction('currentLocale', array($this, 'currentLocale')),
53            new \Twig\TwigFunction('currentVersion', array($this, 'currentVersion')),
54            new \Twig\TwigFunction('formatDateTime', array($this, 'formatDateTime')),
55            new \Twig\TwigFunction('toTextFormat', array($this, 'toTextFormat')),
56            new \Twig\TwigFunction('getNow', array($this, 'getNow')),
57            new \Twig\TwigFunction('isNumeric', array($this, 'isNumeric')),
58            new \Twig\TwigFunction('dumpAppProfiler', array($this, 'dumpAppProfiler'), $safe),
59            new \Twig\TwigFunction('getSystemStatus', array($this, 'getSystemStatus'), $safe),
60            new \Twig\TwigFunction('getClientHost', array($this, 'getClientHost')),
61            new \Twig\TwigFunction('kindOfPayment', array($this, 'kindOfPayment')),
62            new \Twig\TwigFunction('isImageAllowed', array($this, 'isImageAllowed')),
63        );
64    }
65
66
67    public static function isImageAllowed(): bool
68    {
69        return (isset(\App::$isImageAllowed)) ? \App::$isImageAllowed : true;
70    }
71
72    public function getLanguageDescriptor(string $locale = 'de'): ?string
73    {
74        $language = \App::$supportedLanguages[$locale] ?? [];
75        return $language['name'] ?? null;
76    }
77
78    public static function isNumeric(mixed $var): bool
79    {
80        return is_numeric($var);
81    }
82
83    public static function getNow(): \DateTimeInterface
84    {
85        if (\App::$now instanceof \DateTimeInterface) {
86            return \App::$now;
87        }
88        return new \DateTimeImmutable();
89    }
90
91    /**
92     * @return false|string
93     */
94    public static function getSystemStatus(string $env): string|false
95    {
96        return getenv($env);
97    }
98
99    public function toTextFormat(string $string): string
100    {
101        $string = \strip_tags($string, '<br />');
102        $temp = str_replace(array("<br />"), "\n", $string);
103        $lines = explode("\n", $temp);
104        $new_lines = array();
105        foreach ($lines as $line) {
106            if (!empty($line)) {
107                $new_lines[] = trim($line);
108            }
109        }
110        $result = implode("\n", $new_lines);
111        return addSlashes($result);
112    }
113
114    /**
115     * @return array<string, mixed>
116     */
117    public function formatDateTime(object $dateString): array
118    {
119        $dateTime = new \DateTimeImmutable(
120            $dateString->year . '-' . $dateString->month . '-' . $dateString->day,
121            new \DateTimeZone('Europe/Berlin')
122        );
123        $formatDate = [];
124        $formatDate['date']     = Helper::getFormatedDates($dateTime, "EE, dd. MMMM yyyy");
125        $formatDate['fulldate'] = Helper::getFormatedDates($dateTime, "EEEE, 'den' dd. MMMM yyyy");
126        $formatDate['weekday']  = (date('w', $dateTime->getTimestamp()) == 0) ?
127            date('w', $dateTime->getTimestamp()) + 6 :
128            date('w', $dateTime->getTimestamp()) - 1;
129        $formatDate['ym']       = $dateTime->format('Y-m');
130        $formatDate['ymd']       = $dateTime->format('Y-m-d');
131        $formatDate['ts']       = $dateTime->getTimestamp();
132        $formatDate['time']     = ($dateTime->format('H:i') != '00:00') ?
133            Helper::getFormatedDates($dateTime, 'HH:mm Uhr') :
134            false;
135        return $formatDate;
136    }
137
138    /**
139     * @return array{name: mixed, params: array<array-key, mixed>}
140     */
141    public function currentRoute(?string $lang = null): array
142    {
143        $route = array(
144            'name' => 'noroute',
145            'params' => []
146        );
147        if ($this->container->has('currentRoute')) {
148            $routeParams = $this->container->get('currentRouteParams');
149            if (!is_array($routeParams)) {
150                $routeParams = [];
151            }
152            if (null !== $lang && 'de' == $lang) {
153                unset($routeParams['lang']);
154            } elseif (self::isMultiLanguage()) {
155                $routeParams['lang'] = ($lang !== null) ? $lang : (
156                    \App::$language instanceof Language ? \App::$language->getCurrentLanguage() : 'de'
157                );
158            }
159
160            $routeName = $this->container->get('currentRoute');
161            $route = array(
162                'name' => $routeName,
163                'params' => $routeParams
164            );
165        }
166        return $route;
167    }
168
169    public function currentLang(): string
170    {
171        if (self::isMultiLanguage() && \App::$language instanceof Language) {
172            return \App::$language->getCurrentLanguage();
173        }
174        return 'de';
175    }
176
177    public function currentLocale(): string
178    {
179        $locale = 'de_DE';
180        if (self::isMultiLanguage() && \App::$language instanceof Language) {
181            $parts = explode('.', \App::$language->getCurrentLocale());
182            $locale = $parts[0] !== '' ? $parts[0] : 'de_DE';
183        }
184        return $locale;
185    }
186
187    public function currentVersion(): array|string|null
188    {
189        $version = Version::getString();
190        return ($version != Version::UNKNOWN) ? $version : Git::readCurrentVersion();
191    }
192
193    public function urlGet(string $routeName, array $params = [], array $getparams = []): string
194    {
195        $url = \App::$slim->urlFor($routeName, $params);
196        $url = preg_replace('#^.*?(https?://)#', '\1', $url) ?? $url; // allow http:// routes
197        if ($getparams !== []) {
198            $url .= '?' . http_build_query($getparams);
199        }
200        $sanitized = Helper::proxySanitizeUri($url);
201        return is_string($sanitized) ? $sanitized : $url;
202    }
203
204    public function csvProperty(iterable $list, string $property): string
205    {
206        $propertylist = array();
207        foreach ($list as $item) {
208            if (is_array($item) && array_key_exists($property, $item)) {
209                $propertylist[] = $item[$property];
210            }
211        }
212        return implode(',', array_unique($propertylist));
213    }
214
215    /**
216     * @return array<string, array{prefix: string, sublist: list<mixed>}>
217     */
218    public function azPrefixList(iterable $list, string $property): array
219    {
220        $azList = array();
221        foreach ($list as $item) {
222            if (is_array($item) && array_key_exists($property, $item)) {
223                $currentPrefix = self::sortFirstChar((string) $item[$property]);
224                if (!array_key_exists($currentPrefix, $azList)) {
225                    $azList[$currentPrefix] = array(
226                        'prefix' => $currentPrefix,
227                        'sublist' => array(),
228                    );
229                }
230                $azList[$currentPrefix]['sublist'][] = $item;
231                uasort(
232                    $azList[$currentPrefix]['sublist'],
233                    static fn ($left, $right) => self::sortByName($left, $right)
234                );
235                ksort($azList);
236            }
237        }
238        return $azList;
239    }
240
241    /**
242     * @return array<string, array{prefix: string, sublist: list<mixed>}>
243     */
244    public function azPrefixListCollator(mixed $list, string $property, string $locale): array
245    {
246        $collator = collator_create($locale);
247        if ($collator === null) {
248            return [];
249        }
250        $collator->setAttribute(\Collator::QUATERNARY, \Collator::ON);
251        $collator->setAttribute(\Collator::CASE_FIRST, \Collator::ON);
252        $collator->setAttribute(\Collator::NUMERIC_COLLATION, \Collator::ON);
253
254        if (is_array($list)) {
255            uasort($list, function ($itemA, $itemB) use ($collator, $property) {
256                $compared = collator_compare($collator, $itemA[$property], $itemB[$property]);
257                return (int) $compared;
258            });
259        } elseif (is_object($list) && method_exists($list, 'sortWithCollator')) {
260            $list = $list->sortWithCollator($property, $locale);
261        }
262
263        $azList = array();
264
265        foreach ($list as $item) {
266            if (!is_array($item) || !array_key_exists($property, $item)) {
267                continue;
268            }
269            $currentPrefix = self::sortFirstChar((string) $item[$property]);
270            if (!array_key_exists($currentPrefix, $azList)) {
271                $azList[$currentPrefix] = array(
272                    'prefix' => $currentPrefix,
273                    'sublist' => array(),
274                );
275            }
276            $azList[$currentPrefix]['sublist'][] = $item;
277        }
278        return $azList;
279    }
280
281    public function isValueInArray(mixed $value, string $params): bool
282    {
283        $paramsArr = explode(',', $params);
284        if (in_array($value, $paramsArr)) {
285            return true;
286        }
287        return false;
288    }
289
290    public static function remoteInclude(string $uri): string
291    {
292        $prepend = '';
293        $append = '';
294        if (self::isDebug()) {
295            $prepend = "<!-- include($uri) -->\n";
296            $append = "\n<!-- /include($uri) -->";
297        }
298        if (self::isEsiEnabled()) {
299            // Varnish does not support https
300            $httpUri = preg_replace('#^(https?:)?//#', 'http://', $uri);
301            $uri = is_string($httpUri) ? $httpUri : $uri;
302            if (self::isDebug()) {
303                $prepend = "<!-- replaced uri=$uri --> " . $prepend;
304            }
305            return $prepend . '<esi:include src="' . $uri . '" />' . $append;
306        }
307        $useragent = 'Client-' . (defined("\App::IDENTIFIER") ? constant("\App::IDENTIFIER") : 'ZMS');
308        $options = array(
309            'http' => array(
310              'method' => "GET",
311              'header' => "Accept-language: de\r\n" .
312                        "Cookie: zms=development\r\n" .
313                        "user-agent: $useragent \r\n"
314            )
315          );
316        $context = stream_context_create($options);
317        $contents = file_get_contents($uri, false, $context);
318        return $prepend . ($contents !== false ? $contents : '') . $append;
319    }
320
321    public function getEsiFromPath(string $path, string|false $locale = false): string
322    {
323        $localePath = ($locale !== false && 'de' != $locale) ? '/' . $locale : '';
324        $base = \App::$esiBaseUrl ?? '';
325        $pathValue = \App::$$path ?? '';
326        return $base . $localePath . (is_string($pathValue) ? $pathValue : '');
327    }
328
329    public function getClientHost(): string
330    {
331        if (!$this->container->has('request')) {
332            return '';
333        }
334        $request = $this->container->get('request');
335        if (!$request instanceof ServerRequestInterface) {
336            return '';
337        }
338        $hostname = '';
339        $headerList = ['host', 'x-forwarded-host'];
340        foreach ($headerList as $headername) {
341            if ($request->hasHeader($headername)) {
342                $hostname = $request->getHeaderLine($headername);
343            }
344        }
345        return $hostname;
346    }
347
348    protected static function toSortableString(string $string): string
349    {
350        $string = strtr($string, array(
351            'Ä' => 'Ae',
352            'Ö' => 'Oe',
353            'Ü' => 'Ue',
354            'ä' => 'ae',
355            'ö' => 'oe',
356            'ü' => 'ue',
357            'ß' => 'ss',
358            '€' => 'E',
359        ));
360        return $string;
361    }
362
363    protected static function sortByName(mixed $left, mixed $right): int
364    {
365        $leftName = is_array($left) && isset($left['name']) ? (string) $left['name'] : '';
366        $rightName = is_array($right) && isset($right['name']) ? (string) $right['name'] : '';
367        return strcmp(
368            self::toSortableString(strtolower($leftName)),
369            strtolower(self::toSortableString($rightName))
370        );
371    }
372
373    protected static function sortFirstChar(string $string): string
374    {
375        $firstChar = mb_substr($string, 0, 1);
376        $firstChar = mb_strtoupper($firstChar);
377        $firstChar = strtr($firstChar, array('Ä' => 'A', 'Ö' => 'O', 'Ü' => 'U'));
378        return $firstChar;
379    }
380
381    public function dumpAppProfiler(): string
382    {
383        $output = '<h2>App Profiles</h2>'
384            . ' <p>For debugging: This log contains runtime information.
385            <strong>DISABLE FOR PRODUCTION!</strong></p><ul>';
386        foreach (Profiler::$profileList as $entry) {
387            $output .= "<li>$entry</li>";
388        }
389        return $output . '</ul>';
390    }
391
392    public function kindOfPayment(mixed $code): string
393    {
394        $result = '';
395        if ($code == 0) {
396            $result = 'eccash';
397        } elseif ($code == 1) {
398            $result = 'nocash';
399        } elseif ($code == 2) {
400            $result = 'ec';
401        } elseif ($code == 3) {
402            $result = 'cash';
403        } elseif ($code == 4) {
404            $result = 'subscribecash';
405        }
406        return $result;
407    }
408
409    private static function isMultiLanguage(): bool
410    {
411        /** @psalm-suppress RedundantCondition Module App subclasses may set MULTILANGUAGE to false. */
412        return \App::MULTILANGUAGE;
413    }
414
415    private static function isDebug(): bool
416    {
417        /** @psalm-suppress RedundantCondition */
418        /** @psalm-suppress TypeDoesNotContainType Module App subclasses may set DEBUG to true. */
419        return \App::DEBUG;
420    }
421
422    private static function isEsiEnabled(): bool
423    {
424        /** @psalm-suppress RedundantCondition Module App subclasses may set ESI_ENABLED to false. */
425        return \App::ESI_ENABLED;
426    }
427}