1: <?php
2: /**
3: * Smarty plugin
4: * @package Smarty
5: * @subpackage plugins
6: */
7:
8:
9: /**
10: * Smarty truncateHtml modifier plugin
11: *
12: * Type: modifier
13: * Name: truncateHtml
14: * Purpose: Truncate an HTML string to a certain number of words, while ensuring that
15: * valid markup is maintained.
16: * Example: <{$body|truncateHtml:30:'...'}>
17: *
18: * @param string $string HTML to be truncated
19: * @param integer $count truncate to $count words
20: * @param string $etc ellipsis
21: *
22: * @return string
23: */
24: function smarty_modifier_truncateHtml($string, $count = 80, $etc = '…')
25: {
26: if($count <= 0) {
27: return '';
28: }
29: return BaseStringHelper::truncateWords($string, $count, $etc, true);
30: }
31:
32: if (!class_exists('\HTMLPurifier_Bootstrap', false)) {
33: require_once XOOPS_PATH . '/modules/protector/library/HTMLPurifier/Bootstrap.php';
34: HTMLPurifier_Bootstrap::registerAutoload();
35: }
36:
37: if (!class_exists('\BaseStringHelper', false)) {
38: /**
39: * The Yii framework is free software. It is released under the terms of the following BSD License.
40: *
41: * Copyright © 2008-2018 by Yii Software LLC, All rights reserved.
42: *
43: * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
44: * following conditions are met:
45: *
46: * - Redistributions of source code must retain the above copyright notice, this list of
47: * conditions and the following disclaimer.
48: * - Redistributions in binary form must reproduce the above copyright notice, this list of
49: * conditions and the following disclaimer in the documentation and/or other materials provided
50: * with the distribution.
51: * - Neither the name of Yii Software LLC nor the names of its contributors may be used to endorse
52: * or promote products derived from this software without specific prior written permission.
53: *
54: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
55: * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
56: * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
57: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
58: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
59: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
60: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
61: * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
62: */
63: class BaseStringHelper
64: {
65: /**
66: * Returns the number of bytes in the given string.
67: * This method ensures the string is treated as a byte array by using `mb_strlen()`.
68: *
69: * @param string $string the string being measured for length
70: *
71: * @return int the number of bytes in the given string.
72: */
73: public static function byteLength($string)
74: {
75: return mb_strlen($string, '8bit');
76: }
77:
78: /**
79: * Returns the portion of string specified by the start and length parameters.
80: * This method ensures the string is treated as a byte array by using `mb_substr()`.
81: *
82: * @param string $string the input string. Must be one character or longer.
83: * @param int $start the starting position
84: * @param int $length the desired portion length. If not specified or `null`, there will be
85: * no limit on length i.e. the output will be until the end of the string.
86: *
87: * @return string the extracted part of string, or FALSE on failure or an empty string.
88: * @see http://www.php.net/manual/en/function.substr.php
89: */
90: public static function byteSubstr($string, $start, $length = null)
91: {
92: return mb_substr($string, $start, $length === null ? mb_strlen($string, '8bit') : $length, '8bit');
93: }
94:
95: /**
96: * Returns the trailing name component of a path.
97: * This method is similar to the php function `basename()` except that it will
98: * treat both \ and / as directory separators, independent of the operating system.
99: * This method was mainly created to work on php namespaces. When working with real
100: * file paths, php's `basename()` should work fine for you.
101: * Note: this method is not aware of the actual filesystem, or path components such as "..".
102: *
103: * @param string $path A path string.
104: * @param string $suffix If the name component ends in suffix this will also be cut off.
105: *
106: * @return string the trailing name component of the given path.
107: * @see http://www.php.net/manual/en/function.basename.php
108: */
109: public static function basename($path, $suffix = '')
110: {
111: if (($len = mb_strlen($suffix)) > 0 && mb_substr($path, -$len) === $suffix) {
112: $path = mb_substr($path, 0, -$len);
113: }
114: $path = rtrim(str_replace('\\', '/', $path), '/\\');
115: if (($pos = mb_strrpos($path, '/')) !== false) {
116: return mb_substr($path, $pos + 1);
117: }
118:
119: return $path;
120: }
121:
122: /**
123: * Returns parent directory's path.
124: * This method is similar to `dirname()` except that it will treat
125: * both \ and / as directory separators, independent of the operating system.
126: *
127: * @param string $path A path string.
128: *
129: * @return string the parent directory's path.
130: * @see http://www.php.net/manual/en/function.basename.php
131: */
132: public static function dirname($path)
133: {
134: $pos = mb_strrpos(str_replace('\\', '/', $path), '/');
135: if ($pos !== false) {
136: return mb_substr($path, 0, $pos);
137: }
138:
139: return '';
140: }
141:
142: /**
143: * Truncates a string to the number of characters specified.
144: *
145: * @param string $string The string to truncate.
146: * @param int $length How many characters from original string to include into truncated string.
147: * @param string $suffix String to append to the end of truncated string.
148: * @param string $encoding The charset to use, defaults to charset currently used by application.
149: * @param bool $asHtml Whether to treat the string being truncated as HTML and preserve proper HTML tags.
150: * This parameter is available since version 2.0.1.
151: *
152: * @return string the truncated string.
153: */
154: public static function truncate($string, $length, $suffix = '...', $encoding = null, $asHtml = false)
155: {
156: if ($encoding === null) {
157: $encoding = 'UTF-8';
158: }
159: if ($asHtml) {
160: return static::truncateHtml($string, $length, $suffix, $encoding);
161: }
162:
163: if (mb_strlen($string, $encoding) > $length) {
164: return rtrim(mb_substr($string, 0, $length, $encoding)) . $suffix;
165: }
166:
167: return $string;
168: }
169:
170: /**
171: * Truncates a string to the number of words specified.
172: *
173: * @param string $string The string to truncate.
174: * @param int $count How many words from original string to include into truncated string.
175: * @param string $suffix String to append to the end of truncated string.
176: * @param bool $asHtml Whether to treat the string being truncated as HTML and preserve proper HTML tags.
177: * This parameter is available since version 2.0.1.
178: *
179: * @return string the truncated string.
180: */
181: public static function truncateWords($string, $count, $suffix = '...', $asHtml = false)
182: {
183: if ($asHtml) {
184: return static::truncateHtml($string, $count, $suffix);
185: }
186:
187: $words = preg_split('/(\s+)/u', trim($string), -1, PREG_SPLIT_DELIM_CAPTURE);
188: if (count($words) / 2 > $count) {
189: return implode('', array_slice($words, 0, ($count * 2) - 1)) . $suffix;
190: }
191:
192: return $string;
193: }
194:
195: /**
196: * Truncate a string while preserving the HTML.
197: *
198: * @param string $string The string to truncate
199: * @param int $count
200: * @param string $suffix String to append to the end of the truncated string.
201: * @param string|bool $encoding
202: *
203: * @return string
204: * @since 2.0.1
205: */
206: protected static function truncateHtml($string, $count, $suffix, $encoding = false)
207: {
208: $config = \HTMLPurifier_Config::create(null);
209: $lexer = \HTMLPurifier_Lexer::create($config);
210: $tokens = $lexer->tokenizeHTML($string, $config, new \HTMLPurifier_Context());
211: $openTokens = array();
212: $totalCount = 0;
213: $depth = 0;
214: $truncated = array();
215: foreach ($tokens as $token) {
216: if ($token instanceof \HTMLPurifier_Token_Start) { //Tag begins
217: $openTokens[$depth] = $token->name;
218: $truncated[] = $token;
219: ++$depth;
220: } elseif ($token instanceof \HTMLPurifier_Token_Text && $totalCount <= $count) { //Text
221: if (false === $encoding) {
222: preg_match('/^(\s*)/um', $token->data, $prefixSpace) ?: $prefixSpace = array('', '');
223: $token->data = $prefixSpace[1] . self::truncateWords(ltrim($token->data), $count - $totalCount, '');
224: $currentCount = self::countWords($token->data);
225: } else {
226: $token->data = self::truncate($token->data, $count - $totalCount, '', $encoding);
227: $currentCount = mb_strlen($token->data, $encoding);
228: }
229: $totalCount += $currentCount;
230: $truncated[] = $token;
231: } elseif ($token instanceof \HTMLPurifier_Token_End) { //Tag ends
232: if ($token->name === $openTokens[$depth - 1]) {
233: --$depth;
234: unset($openTokens[$depth]);
235: $truncated[] = $token;
236: }
237: } elseif ($token instanceof \HTMLPurifier_Token_Empty) { //Self contained tags, i.e. <img/> etc.
238: $truncated[] = $token;
239: }
240: if ($totalCount >= $count) {
241: if (0 < count($openTokens)) {
242: krsort($openTokens);
243: foreach ($openTokens as $name) {
244: $truncated[] = new \HTMLPurifier_Token_End($name);
245: }
246: }
247: break;
248: }
249: }
250: $context = new \HTMLPurifier_Context();
251: $generator = new \HTMLPurifier_Generator($config, $context);
252: return $generator->generateFromTokens($truncated) . ($totalCount >= $count ? $suffix : '');
253: }
254:
255: /**
256: * Check if given string starts with specified substring.
257: * Binary and multibyte safe.
258: *
259: * @param string $string Input string
260: * @param string $with Part to search inside the $string
261: * @param bool $caseSensitive Case-sensitive search. Default is true. When case-sensitive is enabled, $with must exactly match the starting of the string in order to get a true value.
262: *
263: * @return bool Returns true if first input starts with second input, false otherwise
264: */
265: public static function startsWith($string, $with, $caseSensitive = true)
266: {
267: if (!$bytes = static::byteLength($with)) {
268: return true;
269: }
270: if ($caseSensitive) {
271: return strncmp($string, $with, $bytes) === 0;
272: }
273: $encoding = 'UTF-8';
274: return mb_strtolower(mb_substr($string, 0, $bytes, '8bit'), $encoding) === mb_strtolower($with, $encoding);
275: }
276:
277: /**
278: * Check if given string ends with specified substring.
279: * Binary and multibyte safe.
280: *
281: * @param string $string Input string to check
282: * @param string $with Part to search inside the $string.
283: * @param bool $caseSensitive Case-sensitive search. Default is true. When case-sensitive is enabled, $with must exactly match the ending of the string in order to get a true value.
284: *
285: * @return bool Returns true if first input ends with second input, false otherwise
286: */
287: public static function endsWith($string, $with, $caseSensitive = true)
288: {
289: if (!$bytes = static::byteLength($with)) {
290: return true;
291: }
292: if ($caseSensitive) {
293: // Warning check, see http://php.net/manual/en/function.substr-compare.php#refsect1-function.substr-compare-returnvalues
294: if (static::byteLength($string) < $bytes) {
295: return false;
296: }
297:
298: return substr_compare($string, $with, -$bytes, $bytes) === 0;
299: }
300:
301: $encoding = 'UTF-8';
302: return mb_strtolower(mb_substr($string, -$bytes, mb_strlen($string, '8bit'), '8bit'), $encoding) === mb_strtolower($with, $encoding);
303: }
304:
305: /**
306: * Explodes string into array, optionally trims values and skips empty ones.
307: *
308: * @param string $string String to be exploded.
309: * @param string $delimiter Delimiter. Default is ','.
310: * @param mixed $trim Whether to trim each element. Can be:
311: * - boolean - to trim normally;
312: * - string - custom characters to trim. Will be passed as a second argument to `trim()` function.
313: * - callable - will be called for each value instead of trim. Takes the only argument - value.
314: * @param bool $skipEmpty Whether to skip empty strings between delimiters. Default is false.
315: *
316: * @return array
317: * @since 2.0.4
318: */
319: public static function explode($string, $delimiter = ',', $trim = true, $skipEmpty = false)
320: {
321: $result = explode($delimiter, $string);
322: if ($trim) {
323: if ($trim === true) {
324: $trim = 'trim';
325: } elseif (!is_callable($trim)) {
326: $trim = function ($v) use ($trim) {
327: return trim($v, $trim);
328: };
329: }
330: $result = array_map($trim, $result);
331: }
332: if ($skipEmpty) {
333: // Wrapped with array_values to make array keys sequential after empty values removing
334: $result = array_values(array_filter($result, function ($value) {
335: return $value !== '';
336: }));
337: }
338:
339: return $result;
340: }
341:
342: /**
343: * Counts words in a string.
344: *
345: * @since 2.0.8
346: *
347: * @param string $string
348: *
349: * @return int
350: */
351: public static function countWords($string)
352: {
353: return count(preg_split('/\s+/u', $string, -1, PREG_SPLIT_NO_EMPTY));
354: }
355:
356: /**
357: * Returns string representation of number value with replaced commas to dots, if decimal point
358: * of current locale is comma.
359: *
360: * @param int|float|string $value
361: *
362: * @return string
363: * @since 2.0.11
364: */
365: public static function normalizeNumber($value)
366: {
367: $value = (string)$value;
368:
369: $localeInfo = localeconv();
370: $decimalSeparator = isset($localeInfo['decimal_point']) ? $localeInfo['decimal_point'] : null;
371:
372: if ($decimalSeparator !== null && $decimalSeparator !== '.') {
373: $value = str_replace($decimalSeparator, '.', $value);
374: }
375:
376: return $value;
377: }
378:
379: /**
380: * Encodes string into "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
381: *
382: * > Note: Base 64 padding `=` may be at the end of the returned string.
383: * > `=` is not transparent to URL encoding.
384: *
385: * @see https://tools.ietf.org/html/rfc4648#page-7
386: *
387: * @param string $input the string to encode.
388: *
389: * @return string encoded string.
390: * @since 2.0.12
391: */
392: public static function base64UrlEncode($input)
393: {
394: return strtr(base64_encode($input), '+/', '-_');
395: }
396:
397: /**
398: * Decodes "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
399: *
400: * @see https://tools.ietf.org/html/rfc4648#page-7
401: *
402: * @param string $input encoded string.
403: *
404: * @return string decoded string.
405: * @since 2.0.12
406: */
407: public static function base64UrlDecode($input)
408: {
409: return base64_decode(strtr($input, '-_', '+/'));
410: }
411:
412: /**
413: * Safely casts a float to string independent of the current locale.
414: *
415: * The decimal separator will always be `.`.
416: *
417: * @param float|int $number a floating point number or integer.
418: *
419: * @return string the string representation of the number.
420: * @since 2.0.13
421: */
422: public static function floatToString($number)
423: {
424: // . and , are the only decimal separators known in ICU data,
425: // so it's safe to call str_replace here
426: return str_replace(',', '.', (string)$number);
427: }
428:
429: /**
430: * Checks if the passed string would match the given shell wildcard pattern.
431: * This function emulates [[fnmatch()]], which may be unavailable at certain environment, using PCRE.
432: *
433: * @param string $pattern the shell wildcard pattern.
434: * @param string $string the tested string.
435: * @param array $options options for matching. Valid options are:
436: *
437: * - caseSensitive: bool, whether pattern should be case-sensitive. Defaults to `true`.
438: * - escape: bool, whether backslash escaping is enabled. Defaults to `true`.
439: * - filePath: bool, whether slashes in string only matches slashes in the given pattern. Defaults to `false`.
440: *
441: * @return bool whether the string matches pattern or not.
442: * @since 2.0.14
443: */
444: public static function matchWildcard($pattern, $string, $options = array())
445: {
446: if ($pattern === '*' && empty($options['filePath'])) {
447: return true;
448: }
449:
450: $replacements = array(
451: '\\\\\\\\' => '\\\\',
452: '\\\\\\*' => '[*]',
453: '\\\\\\?' => '[?]',
454: '\*' => '.*',
455: '\?' => '.',
456: '\[\!' => '[^',
457: '\[' => '[',
458: '\]' => ']',
459: '\-' => '-',
460: );
461:
462: if (isset($options['escape']) && !$options['escape']) {
463: unset($replacements['\\\\\\\\']);
464: unset($replacements['\\\\\\*']);
465: unset($replacements['\\\\\\?']);
466: }
467:
468: if (!empty($options['filePath'])) {
469: $replacements['\*'] = '[^/\\\\]*';
470: $replacements['\?'] = '[^/\\\\]';
471: }
472:
473: $pattern = strtr(preg_quote($pattern, '#'), $replacements);
474: $pattern = '#^' . $pattern . '$#us';
475:
476: if (isset($options['caseSensitive']) && !$options['caseSensitive']) {
477: $pattern .= 'i';
478: }
479:
480: return preg_match($pattern, $string) === 1;
481: }
482: }
483: }
484: