1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Core\Utility;
17
18/**
19 * Class with helper functions for string handling
20 */
21class StringUtility
22{
23    /**
24     * Returns TRUE if $haystack begins with $needle.
25     * The input string is not trimmed before and search is done case sensitive.
26     *
27     * @param string $haystack Full string to check
28     * @param string $needle Reference string which must be found as the "first part" of the full string
29     * @throws \InvalidArgumentException
30     * @return bool TRUE if $needle was found to be equal to the first part of $haystack
31     * @deprecated will be removed in TYPO3 v12.0. Use PHP's native str_starts_with() function instead.
32     */
33    public static function beginsWith($haystack, $needle)
34    {
35        trigger_error('StringUtility::beginsWith() will be removed in TYPO3 v12.0. Use PHPs str_starts_with() function instead.', E_USER_DEPRECATED);
36        // Sanitize $haystack and $needle
37        if (is_array($haystack) || is_object($haystack) || $haystack === null || (string)$haystack != $haystack) {
38            throw new \InvalidArgumentException(
39                '$haystack can not be interpreted as string',
40                1347135546
41            );
42        }
43        if (is_array($needle) || is_object($needle) || (string)$needle != $needle || strlen($needle) < 1) {
44            throw new \InvalidArgumentException(
45                '$needle can not be interpreted as string or has zero length',
46                1347135547
47            );
48        }
49        $haystack = (string)$haystack;
50        $needle = (string)$needle;
51        return $needle !== '' && strpos($haystack, $needle) === 0;
52    }
53
54    /**
55     * Returns TRUE if $haystack ends with $needle.
56     * The input string is not trimmed before and search is done case sensitive.
57     *
58     * @param string $haystack Full string to check
59     * @param string $needle Reference string which must be found as the "last part" of the full string
60     * @throws \InvalidArgumentException
61     * @return bool TRUE if $needle was found to be equal to the last part of $haystack
62     * @deprecated will be removed in TYPO3 v12.0. Use PHP's native str_ends_with() function instead.
63     */
64    public static function endsWith($haystack, $needle)
65    {
66        trigger_error('StringUtility::endsWith() will be removed in TYPO3 v12.0. Use PHPs str_ends_with() function instead.', E_USER_DEPRECATED);
67        // Sanitize $haystack and $needle
68        if (is_array($haystack) || is_object($haystack) || $haystack === null || (string)$haystack != $haystack) {
69            throw new \InvalidArgumentException(
70                '$haystack can not be interpreted as string',
71                1347135544
72            );
73        }
74        if (is_array($needle) || is_object($needle) || (string)$needle != $needle || strlen($needle) < 1) {
75            throw new \InvalidArgumentException(
76                '$needle can not be interpreted as string or has no length',
77                1347135545
78            );
79        }
80        $haystackLength = strlen($haystack);
81        $needleLength = strlen($needle);
82        if (!$haystackLength || $needleLength > $haystackLength) {
83            return false;
84        }
85        $position = strrpos((string)$haystack, (string)$needle);
86        return $position !== false && $position === $haystackLength - $needleLength;
87    }
88
89    /**
90     * This function generates a unique id by using the more entropy parameter.
91     * Furthermore the dots are removed so the id can be used inside HTML attributes e.g. id.
92     *
93     * @param string $prefix
94     * @return string
95     */
96    public static function getUniqueId($prefix = '')
97    {
98        $uniqueId = uniqid($prefix, true);
99        return str_replace('.', '', $uniqueId);
100    }
101
102    /**
103     * Escape a CSS selector to be used for DOM queries
104     *
105     * This method takes care to escape any CSS selector meta character.
106     * The result may be used to query the DOM like $('#' + escapedSelector)
107     *
108     * @param string $selector
109     * @return string
110     */
111    public static function escapeCssSelector(string $selector): string
112    {
113        return preg_replace('/([#:.\\[\\],=@])/', '\\\\$1', $selector);
114    }
115
116    /**
117     * Removes the Byte Order Mark (BOM) from the input string. This method supports UTF-8 encoded strings only!
118     *
119     * @param string $input
120     * @return string
121     */
122    public static function removeByteOrderMark(string $input): string
123    {
124        if (strpos($input, "\xef\xbb\xbf") === 0) {
125            $input = substr($input, 3);
126        }
127
128        return $input;
129    }
130
131    /**
132     * Matching two strings against each other, supporting a "*" wildcard (match many) or a "?" wildcard (match one= or (if wrapped in "/") PCRE regular expressions
133     *
134     * @param string $haystack The string in which to find $needle.
135     * @param string $needle The string to find in $haystack
136     * @return bool Returns TRUE if $needle matches or is found in (according to wildcards) $haystack. E.g. if $haystack is "Netscape 6.5" and $needle is "Net*" or "Net*ape" then it returns TRUE.
137     */
138    public static function searchStringWildcard($haystack, $needle): bool
139    {
140        $result = false;
141        if ($haystack === $needle) {
142            $result = true;
143        } elseif ($needle) {
144            if (preg_match('/^\\/.+\\/$/', $needle)) {
145                // Regular expression, only "//" is allowed as delimiter
146                $regex = $needle;
147            } else {
148                $needle = str_replace(['*', '?'], ['%%%MANY%%%', '%%%ONE%%%'], $needle);
149                $regex = '/^' . preg_quote($needle, '/') . '$/';
150                // Replace the marker with .* to match anything (wildcard)
151                $regex = str_replace(['%%%MANY%%%', '%%%ONE%%%'], ['.*', '.'], $regex);
152            }
153            $result = (bool)preg_match($regex, $haystack);
154        }
155        return $result;
156    }
157
158    /**
159     * Takes a comma-separated list and removes all duplicates.
160     * If a value in the list is trim(empty), the value is ignored.
161     *
162     * @param string $list Accept a comma-separated list of values.
163     * @return string Returns the list without any duplicates of values, space around values are trimmed.
164     */
165    public static function uniqueList(string $list): string
166    {
167        return implode(',', array_unique(GeneralUtility::trimExplode(',', $list, true)));
168    }
169
170    /**
171     * Works the same as str_pad() except that it correctly handles strings with multibyte characters
172     * and takes an additional optional argument $encoding.
173     *
174     * @param string $string
175     * @param int $length
176     * @param string $pad_string
177     * @param int $pad_type
178     * @param string $encoding
179     * @return string
180     */
181    public static function multibyteStringPad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, string $encoding = 'UTF-8'): string
182    {
183        $len = mb_strlen($string, $encoding);
184        $pad_string_len = mb_strlen($pad_string, $encoding);
185        if ($len >= $length || $pad_string_len === 0) {
186            return $string;
187        }
188
189        switch ($pad_type) {
190            case STR_PAD_RIGHT:
191                $string .= str_repeat($pad_string, (int)(($length - $len)/$pad_string_len));
192                $string .= mb_substr($pad_string, 0, ($length - $len) % $pad_string_len);
193                return $string;
194
195            case STR_PAD_LEFT:
196                $leftPad = str_repeat($pad_string, (int)(($length - $len)/$pad_string_len));
197                $leftPad .= mb_substr($pad_string, 0, ($length - $len) % $pad_string_len);
198                return $leftPad . $string;
199
200            case STR_PAD_BOTH:
201                $leftPadCount = (int)(($length - $len)/2);
202                $len += $leftPadCount;
203                $padded = ((int)($leftPadCount / $pad_string_len)) * $pad_string_len;
204                $leftPad = str_repeat($pad_string, (int)($leftPadCount / $pad_string_len));
205                $leftPad .= mb_substr($pad_string, 0, $leftPadCount - $padded);
206                $string = $leftPad . $string . str_repeat($pad_string, (int)(($length - $len)/$pad_string_len));
207                $string .= mb_substr($pad_string, 0, ($length - $len) % $pad_string_len);
208                return $string;
209        }
210        return $string;
211    }
212}
213