Lines 98.84% 171 / 173
Methods 83.33% 10 / 12
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 isLanguage 100.00% 1 / 1 100.00% 1 / 1 1
 isLikeOnPMFDate 100.00% 6 / 6 100.00% 1 / 1 3
 makeShorterText 100.00% 6 / 6 100.00% 1 / 1 3
 resolveMarkers 100.00% 4 / 4 100.00% 1 / 1 1
 chopString 100.00% 8 / 8 100.00% 1 / 1 3
 setHighlightedString 100.00% 106 / 106 100.00% 1 / 1 2
 highlightNoLinks 83.33% 5 / 6 0.00% 0 / 1 3.04
 isForbiddenElement 90.00% 9 / 10 0.00% 0 / 1 3.01
 parseUrl 100.00% 12 / 12 100.00% 1 / 1 1
 getHostFromUrl 100.00% 4 / 4 100.00% 1 / 1 3
 moveToTop 100.00% 4 / 4 100.00% 1 / 1 2
 formatBytes 100.00% 6 / 6 100.00% 1 / 1 2
28class Utils
29{
30    /**
31     * Check if a given string could be a language.
32     *
33     * @param string $language Language
34     */
35    public static function isLanguage(string $language): bool
36    {
37        return (bool) preg_match(pattern: '/^[a-zA-Z\-]+$/', subject: $language);
38    }
39
40    /**
41     * Checks if a date is a phpMyFAQ valid date.
42     *
43     * @param string $date Date
44     */
45    public static function isLikeOnPMFDate(string $date): bool
46    {
47        // Test if the passed string is in the format: %YYYYMMDDhhmmss%
48        $dateToTest = $date;
49        // Suppress first occurrences of '%'
50        if (str_starts_with($dateToTest, '%')) {
51            $dateToTest = substr($dateToTest, offset: 1);
52        }
53
54        // Suppress last occurrences of '%'
55        if (str_ends_with($dateToTest, needle: '%')) {
56            $dateToTest = substr(string: $dateToTest, offset: 0, length: strlen($dateToTest) - 1);
57        }
58
59        // PMF date consists of numbers only: YYYYMMDDhhmmss
60        return is_numeric($dateToTest);
61    }
62
63    /**
64     * Shortens a string for a given number of words.
65     *
66     * @param string $string String
67     * @param int    $characters Characters
68     * @todo This function doesn't work with Chinese, Japanese, Korean and Thai because they don't have spaces as word
69     *       delimiters
70     */
71    public static function makeShorterText(string $string, int $characters): string
72    {
73        $condensed = Strings::preg_replace(pattern: '/\s+/u', replacement: ' ', subject: $string);
74        $string = is_string($condensed) ? $condensed : $string;
75        $arrStr = explode(separator: ' ', string: $string);
76
77        if (count($arrStr) > $characters) {
78            return implode(separator: ' ', array: array_slice(array: $arrStr, offset: 0, length: $characters)) . ' ...';
79        }
80
81        return $string;
82    }
83
84    /**
85     * Resolves the phpMyFAQ markers like e.g. %sitename%.
86     *
87     * @param string $text Text contains phpMyFAQ markers
88     */
89    public static function resolveMarkers(string $text, Configuration $configuration): string
90    {
91        // Available markers: key and resolving value
92        $markers = [
93            '%sitename%' => $configuration->getTitle(),
94        ];
95
96        // Resolve any known pattern
97        return str_replace(array_keys($markers), array_values($markers), $text);
98    }
99
100    /**
101     * This method chops a string.
102     *
103     * @param string $string String to chop
104     * @param int    $words Number of words
105     */
106    public static function chopString(string $string, int $words): string
107    {
108        $str = '';
109        $pieces = explode(separator: ' ', string: $string);
110        $num = count($pieces);
111        if ($words > $num) {
112            $words = $num;
113        }
114
115        for ($i = 0; $i < $words; ++$i) {
116            $str .= $pieces[$i] . ' ';
117        }
118
119        return $str;
120    }
121
122    /**
123     * Adds a highlighted word to a string.
124     *
125     * @param string $string String
126     * @param string $highlight Given word for highlighting
127     */
128    public static function setHighlightedString(string $string, string $highlight): string
129    {
130        $attributes = [
131            'href',
132            'src',
133            'title',
134            'alt',
135            'class',
136            'style',
137            'id',
138            'name',
139            'face',
140            'size',
141            'dir',
142            'rel',
143            'rev',
144            'role',
145            'onmouseenter',
146            'onmouseleave',
147            'onafterprint',
148            'onbeforeprint',
149            'onbeforeunload',
150            'onhashchange',
151            'onmessage',
152            'onoffline',
153            'ononline',
154            'onpopstate',
155            'onpagehide',
156            'onpageshow',
157            'onresize',
158            'onunload',
159            'ondevicemotion',
160            'ondeviceorientation',
161            'onabort',
162            'onblur',
163            'oncanplay',
164            'oncanplaythrough',
165            'onchange',
166            'onclick',
167            'oncontextmenu',
168            'ondblclick',
169            'ondrag',
170            'ondragend',
171            'ondragenter',
172            'ondragleave',
173            'ondragover',
174            'ondragstart',
175            'ondrop',
176            'ondurationchange',
177            'onemptied',
178            'onended',
179            'onerror',
180            'onfocus',
181            'oninput',
182            'oninvalid',
183            'onkeydown',
184            'onkeypress',
185            'onkeyup',
186            'onload',
187            'onloadeddata',
188            'onloadedmetadata',
189            'onloadstart',
190            'onmousedown',
191            'onmousemove',
192            'onmouseout',
193            'onmouseover',
194            'onmouseup',
195            'onmozfullscreenchange',
196            'onmozfullscreenerror',
197            'onpause',
198            'onplay',
199            'onplaying',
200            'onprogress',
201            'onratechange',
202            'onreset',
203            'onscroll',
204            'onseeked',
205            'onseeking',
206            'onselect',
207            'onshow',
208            'onstalled',
209            'onsubmit',
210            'onsuspend',
211            'ontimeupdate',
212            'onvolumechange',
213            'onwaiting',
214            'oncopy',
215            'oncut',
216            'onpaste',
217            'onbeforescriptexecute',
218            'onafterscriptexecute',
219        ];
220
221        $highlighted = Strings::preg_replace_callback(
222            '/('
223            . $highlight
224            . '="[^"]*")|'
225            . '(('
226            . implode(separator: '|', array: $attributes)
227            . ')="[^"]*'
228            . $highlight
229            . '[^"]*")|'
230            . '('
231            . $highlight
232            . ')/mis',
233            ['phpMyFAQ\Utils', 'highlightNoLinks'],
234            $string,
235        );
236
237        return is_string($highlighted) ? $highlighted : $string;
238    }
239
240    /**
241     * Callback function for filtering HTML from URLs and images.
242     *
243     * @param array<int, string> $matches Array of matches from a regex pattern
244     */
245    public static function highlightNoLinks(array $matches): string
246    {
247        $prefix = $matches[3] ?? '';
248        $item = $matches[4] ?? '';
249        $postfix = $matches[5] ?? '';
250
251        if ($item !== '' && !self::isForbiddenElement($item)) {
252            return '<mark class="pmf-highlighted-string">' . $prefix . $item . $postfix . '</mark>';
253        }
254
255        // Fallback: the original matched string
256        return $matches[0];
257    }
258
259    /**
260     * Tries to detect if a string could be an HTML element
261     */
262    public static function isForbiddenElement(string $string): bool
263    {
264        $forbiddenElements = [
265            'img',
266            'picture',
267            'mark',
268        ];
269
270        foreach ($forbiddenElements as $forbiddenElement) {
271            if (!str_starts_with($forbiddenElement, $string)) {
272                continue;
273            }
274
275            return true;
276        }
277
278        return false;
279    }
280
281    /**
282     * Parses a given string and convert all the URLs into links.
283     */
284    public static function parseUrl(string $string): string
285    {
286        $protocols = ['http://', 'https://'];
287
288        $string = str_replace(search: $protocols, replace: '', subject: $string);
289        $string = str_replace(search: 'www.', replace: 'https://www.', subject: $string);
290
291        $pattern = '/(https?:\/\/[^\s]+)/i';
292
293        return preg_replace_callback(
294            $pattern,
295            static function (array $matches): string {
296                $url = htmlspecialchars($matches[1], ENT_QUOTES, encoding: 'UTF-8');
297                return '<a href="' . $url . '">' . $url . '</a>';
298            },
299            $string,
300        ) ?? $string;
301    }
302
303    /**
304     * Extracts the hostname from a given URL.
305     *
306     * @param string $url The URL from which to extract the hostname.
307     * @return string|null The hostname or null if the URL is invalid.
308     */
309    public static function getHostFromUrl(string $url): ?string
310    {
311        $parsedUrl = parse_url($url);
312
313        if (is_array($parsedUrl) && array_key_exists('host', $parsedUrl)) {
314            return $parsedUrl['host'];
315        }
316
317        return null;
318    }
319
320    /**
321     * Moves the given key of an array to the top
322     *
323     * @param array<string, array<string, string>> $array
324     */
325    public static function moveToTop(array &$array, string $key): void
326    {
327        if (array_key_exists($key, $array)) {
328            $temp = [$key => $array[$key]];
329            unset($array[$key]);
330            $array = $temp + $array;
331        }
332    }
333
334    /**
335     * Formats a given number of Bytes to kB, MB, GB, and so on.
336     */
337    public static function formatBytes(float|int $bytes, int $precision = 2): string
338    {
339        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
340
341        $bytes = max($bytes, 0);
342        $pow = floor(($bytes !== 0 ? log($bytes) : 0) / log(num: 1024));
343        $pow = min($pow, count($units) - 1);
344
345        $bytes /= 1 << (10 * $pow);
346
347        return round($bytes, $precision) . ' ' . $units[$pow];
348    }
349}