1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Latte\Runtime;
9:
10: use Latte;
11: use Latte\Engine;
12:
13:
14: 15: 16: 17:
18: class Filters
19: {
20:
21: public static $dateFormat = '%x';
22:
23:
24: public static $xhtml = false;
25:
26:
27: 28: 29: 30: 31:
32: public static function escapeHtml($s)
33: {
34: return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
35: }
36:
37:
38: 39: 40: 41: 42:
43: public static function escapeHtmlText($s)
44: {
45: return $s instanceof IHtmlString || $s instanceof \Nette\Utils\IHtmlString
46: ? $s->__toString(true)
47: : htmlspecialchars((string) $s, ENT_NOQUOTES, 'UTF-8');
48: }
49:
50:
51: 52: 53: 54: 55:
56: public static function escapeHtmlAttr($s, $double = true)
57: {
58: $double = $double && $s instanceof IHtmlString ? false : $double;
59: $s = (string) $s;
60: if (strpos($s, '`') !== false && strpbrk($s, ' <>"\'') === false) {
61: $s .= ' ';
62: }
63: return htmlspecialchars($s, ENT_QUOTES, 'UTF-8', $double);
64: }
65:
66:
67: 68: 69: 70: 71:
72: public static function escapeHtmlAttrConv($s)
73: {
74: return self::escapeHtmlAttr($s, false);
75: }
76:
77:
78: 79: 80: 81: 82:
83: public static function escapeHtmlAttrUnquoted($s)
84: {
85: $s = (string) $s;
86: return preg_match('#^[a-z0-9:-]+$#i', $s)
87: ? $s
88: : '"' . self::escapeHtmlAttr($s) . '"';
89: }
90:
91:
92: 93: 94: 95: 96:
97: public static function ($s)
98: {
99: $s = (string) $s;
100: if ($s && ($s[0] === '-' || $s[0] === '>' || $s[0] === '!')) {
101: $s = ' ' . $s;
102: }
103: $s = str_replace('--', '- - ', $s);
104: if (substr($s, -1) === '-') {
105: $s .= ' ';
106: }
107: return $s;
108: }
109:
110:
111: 112: 113: 114: 115:
116: public static function escapeXml($s)
117: {
118:
119:
120:
121: return htmlspecialchars(preg_replace('#[\x00-\x08\x0B\x0C\x0E-\x1F]+#', '', (string) $s), ENT_QUOTES, 'UTF-8');
122: }
123:
124:
125: 126: 127: 128: 129:
130: public static function escapeXmlAttrUnquoted($s)
131: {
132: $s = (string) $s;
133: return preg_match('#^[a-z0-9:-]+$#i', $s)
134: ? $s
135: : '"' . self::escapeXml($s) . '"';
136: }
137:
138:
139: 140: 141: 142: 143:
144: public static function escapeCss($s)
145: {
146:
147: return addcslashes((string) $s, "\x00..\x1F!\"#$%&'()*+,./:;<=>?@[\\]^`{|}~");
148: }
149:
150:
151: 152: 153: 154: 155:
156: public static function escapeJs($s)
157: {
158: if ($s instanceof IHtmlString || $s instanceof \Nette\Utils\IHtmlString) {
159: $s = $s->__toString(true);
160: }
161:
162: $json = json_encode($s, JSON_UNESCAPED_UNICODE);
163: if ($error = json_last_error()) {
164: throw new \RuntimeException(PHP_VERSION_ID >= 50500 ? json_last_error_msg() : 'JSON encode error', $error);
165: }
166:
167: return str_replace(["\xe2\x80\xa8", "\xe2\x80\xa9", ']]>', '<!'], ['\u2028', '\u2029', ']]\x3E', '\x3C!'], $json);
168: }
169:
170:
171: 172: 173: 174: 175:
176: public static function escapeICal($s)
177: {
178:
179: return addcslashes(preg_replace('#[\x00-\x08\x0B\x0C-\x1F]+#', '', (string) $s), "\";\\,:\n");
180: }
181:
182:
183: 184: 185: 186: 187:
188: public static function escapeHtmlRawText($s)
189: {
190: return preg_replace('#</(script|style)#i', '<\\/$1', (string) $s);
191: }
192:
193:
194: 195: 196: 197: 198: 199:
200: public static function stripHtml(FilterInfo $info, $s)
201: {
202: if (!in_array($info->contentType, [null, 'html', 'xhtml', 'htmlAttr', 'xhtmlAttr', 'xml', 'xmlAttr'], true)) {
203: trigger_error('Filter |stripHtml used with incompatible type ' . strtoupper($info->contentType), E_USER_WARNING);
204: }
205: $info->contentType = Engine::CONTENT_TEXT;
206: return html_entity_decode(strip_tags((string) $s), ENT_QUOTES, 'UTF-8');
207: }
208:
209:
210: 211: 212: 213: 214: 215:
216: public static function stripTags(FilterInfo $info, $s)
217: {
218: if (!in_array($info->contentType, [null, 'html', 'xhtml', 'htmlAttr', 'xhtmlAttr', 'xml', 'xmlAttr'], true)) {
219: trigger_error('Filter |stripTags used with incompatible type ' . strtoupper($info->contentType), E_USER_WARNING);
220: }
221: return strip_tags((string) $s);
222: }
223:
224:
225: 226: 227: 228:
229: public static function convertTo(FilterInfo $info, $dest, $s)
230: {
231: $source = $info->contentType ?: Engine::CONTENT_TEXT;
232: if ($source === $dest) {
233: return $s;
234: } elseif ($conv = self::getConvertor($source, $dest)) {
235: $info->contentType = $dest;
236: return $conv($s);
237: } else {
238: trigger_error('Filters: unable to convert content type ' . strtoupper($source) . ' to ' . strtoupper($dest), E_USER_WARNING);
239: return $s;
240: }
241: }
242:
243:
244: 245: 246:
247: public static function getConvertor($source, $dest)
248: {
249: static $table = [
250: Engine::CONTENT_TEXT => [
251: 'html' => 'escapeHtmlText', 'xhtml' => 'escapeHtmlText',
252: 'htmlAttr' => 'escapeHtmlAttr', 'xhtmlAttr' => 'escapeHtmlAttr',
253: 'htmlAttrJs' => 'escapeHtmlAttr', 'xhtmlAttrJs' => 'escapeHtmlAttr',
254: 'htmlAttrCss' => 'escapeHtmlAttr', 'xhtmlAttrCss' => 'escapeHtmlAttr',
255: 'htmlAttrUrl' => 'escapeHtmlAttr', 'xhtmlAttrUrl' => 'escapeHtmlAttr',
256: 'htmlComment' => 'escapeHtmlComment', 'xhtmlComment' => 'escapeHtmlComment',
257: 'xml' => 'escapeXml', 'xmlAttr' => 'escapeXml',
258: ],
259: Engine::CONTENT_JS => [
260: 'html' => 'escapeHtmlText', 'xhtml' => 'escapeHtmlText',
261: 'htmlAttr' => 'escapeHtmlAttr', 'xhtmlAttr' => 'escapeHtmlAttr',
262: 'htmlAttrJs' => 'escapeHtmlAttr', 'xhtmlAttrJs' => 'escapeHtmlAttr',
263: 'htmlJs' => 'escapeHtmlRawText', 'xhtmlJs' => 'escapeHtmlRawText',
264: 'htmlComment' => 'escapeHtmlComment', 'xhtmlComment' => 'escapeHtmlComment',
265: ],
266: Engine::CONTENT_CSS => [
267: 'html' => 'escapeHtmlText', 'xhtml' => 'escapeHtmlText',
268: 'htmlAttr' => 'escapeHtmlAttr', 'xhtmlAttr' => 'escapeHtmlAttr',
269: 'htmlAttrCss' => 'escapeHtmlAttr', 'xhtmlAttrCss' => 'escapeHtmlAttr',
270: 'htmlCss' => 'escapeHtmlRawText', 'xhtmlCss' => 'escapeHtmlRawText',
271: 'htmlComment' => 'escapeHtmlComment', 'xhtmlComment' => 'escapeHtmlComment',
272: ],
273: Engine::CONTENT_HTML => [
274: 'htmlAttr' => 'escapeHtmlAttrConv',
275: 'htmlAttrJs' => 'escapeHtmlAttrConv',
276: 'htmlAttrCss' => 'escapeHtmlAttrConv',
277: 'htmlAttrUrl' => 'escapeHtmlAttrConv',
278: 'htmlComment' => 'escapeHtmlComment',
279: ],
280: Engine::CONTENT_XHTML => [
281: 'xhtmlAttr' => 'escapeHtmlAttrConv',
282: 'xhtmlAttrJs' => 'escapeHtmlAttrConv',
283: 'xhtmlAttrCss' => 'escapeHtmlAttrConv',
284: 'xhtmlAttrUrl' => 'escapeHtmlAttrConv',
285: 'xhtmlComment' => 'escapeHtmlComment',
286: ],
287: ];
288: return isset($table[$source][$dest]) ? [__CLASS__, $table[$source][$dest]] : null;
289: }
290:
291:
292: 293: 294: 295: 296:
297: public static function safeUrl($s)
298: {
299: $s = (string) $s;
300: return preg_match('~^(?:(?:https?|ftp)://[^@]+(?:/.*)?|mailto:.+|[/?#].*|[^:]+)\z~i', $s) ? $s : '';
301: }
302:
303:
304: 305: 306: 307: 308: 309:
310: public static function strip(FilterInfo $info, $s)
311: {
312: return in_array($info->contentType, [Engine::CONTENT_HTML, Engine::CONTENT_XHTML], true)
313: ? trim(self::spacelessHtml($s))
314: : trim(self::spacelessText($s));
315: }
316:
317:
318: 319: 320: 321: 322: 323: 324:
325: public static function spacelessHtml($s, $phase = null, &$strip = true)
326: {
327: if ($phase & PHP_OUTPUT_HANDLER_START) {
328: $s = ltrim($s);
329: }
330: if ($phase & PHP_OUTPUT_HANDLER_FINAL) {
331: $s = rtrim($s);
332: }
333: return preg_replace_callback(
334: '#[ \t\r\n]+|<(/)?(textarea|pre|script)(?=\W)#si',
335: function ($m) use (&$strip) {
336: if (empty($m[2])) {
337: return $strip ? ' ' : $m[0];
338: } else {
339: $strip = !empty($m[1]);
340: return $m[0];
341: }
342: },
343: $s
344: );
345: }
346:
347:
348: 349: 350: 351: 352:
353: public static function spacelessText($s)
354: {
355: return preg_replace('#[ \t\r\n]+#', ' ', $s);
356: }
357:
358:
359: 360: 361: 362: 363: 364: 365: 366:
367: public static function indent(FilterInfo $info, $s, $level = 1, $chars = "\t")
368: {
369: if ($level < 1) {
370:
371: } elseif (in_array($info->contentType, [Engine::CONTENT_HTML, Engine::CONTENT_XHTML], true)) {
372: $s = preg_replace_callback('#<(textarea|pre).*?</\\1#si', function ($m) {
373: return strtr($m[0], " \t\r\n", "\x1F\x1E\x1D\x1A");
374: }, $s);
375: if (preg_last_error()) {
376: throw new Latte\RegexpException(null, preg_last_error());
377: }
378: $s = preg_replace('#(?:^|[\r\n]+)(?=[^\r\n])#', '$0' . str_repeat($chars, $level), $s);
379: $s = strtr($s, "\x1F\x1E\x1D\x1A", " \t\r\n");
380: } else {
381: $s = preg_replace('#(?:^|[\r\n]+)(?=[^\r\n])#', '$0' . str_repeat($chars, $level), $s);
382: }
383: return $s;
384: }
385:
386:
387: 388: 389: 390: 391: 392: 393:
394: public static function repeat(FilterInfo $info, $s, $count)
395: {
396: return str_repeat((string) $s, $count);
397: }
398:
399:
400: 401: 402: 403: 404: 405:
406: public static function date($time, $format = null)
407: {
408: if ($time == null) {
409: return null;
410: }
411:
412: if (!isset($format)) {
413: $format = self::$dateFormat;
414: }
415:
416: if ($time instanceof \DateInterval) {
417: return $time->format($format);
418:
419: } elseif (is_numeric($time)) {
420: $time = new \DateTime('@' . $time);
421: $time->setTimeZone(new \DateTimeZone(date_default_timezone_get()));
422:
423: } elseif (!$time instanceof \DateTime && !$time instanceof \DateTimeInterface) {
424: $time = new \DateTime($time);
425: }
426: return strpos($format, '%') === false
427: ? $time->format($format)
428: : strftime($format, $time->format('U') + 0);
429: }
430:
431:
432: 433: 434: 435: 436: 437:
438: public static function bytes($bytes, $precision = 2)
439: {
440: $bytes = round($bytes);
441: $units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
442: foreach ($units as $unit) {
443: if (abs($bytes) < 1024 || $unit === end($units)) {
444: break;
445: }
446: $bytes = $bytes / 1024;
447: }
448: return round($bytes, $precision) . ' ' . $unit;
449: }
450:
451:
452: 453: 454: 455: 456: 457: 458: 459:
460: public static function replace(FilterInfo $info, $subject, $search, $replacement = '')
461: {
462: return str_replace($search, $replacement, (string) $subject);
463: }
464:
465:
466: 467: 468: 469: 470: 471:
472: public static function replaceRe($subject, $pattern, $replacement = '')
473: {
474: $res = preg_replace($pattern, $replacement, $subject);
475: if (preg_last_error()) {
476: throw new Latte\RegexpException(null, preg_last_error());
477: }
478: return $res;
479: }
480:
481:
482: 483: 484: 485: 486: 487:
488: public static function dataStream($data, $type = null)
489: {
490: if ($type === null) {
491: $type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
492: }
493: return 'data:' . ($type ? "$type;" : '') . 'base64,' . base64_encode($data);
494: }
495:
496:
497: 498: 499: 500:
501: public static function nl2br($value)
502: {
503: trigger_error('Filter |nl2br is deprecated, use |breaklines which correctly handles escaping.', E_USER_DEPRECATED);
504: return nl2br($value, self::$xhtml);
505: }
506:
507:
508: 509: 510: 511:
512: public static function breaklines($s)
513: {
514: return new Html(nl2br(htmlspecialchars((string) $s, ENT_NOQUOTES, 'UTF-8'), self::$xhtml));
515: }
516:
517:
518: 519: 520: 521: 522: 523: 524:
525: public static function substring($s, $start, $length = null)
526: {
527: $s = (string) $s;
528: if ($length === null) {
529: $length = self::strLength($s);
530: }
531: if (function_exists('mb_substr')) {
532: return mb_substr($s, $start, $length, 'UTF-8');
533: }
534: return iconv_substr($s, $start, $length, 'UTF-8');
535: }
536:
537:
538: 539: 540: 541: 542: 543: 544:
545: public static function truncate($s, $maxLen, $append = "\xE2\x80\xA6")
546: {
547: $s = (string) $s;
548: if (self::strLength($s) > $maxLen) {
549: $maxLen = $maxLen - self::strLength($append);
550: if ($maxLen < 1) {
551: return $append;
552:
553: } elseif (preg_match('#^.{1,' . $maxLen . '}(?=[\s\x00-/:-@\[-`{-~])#us', $s, $matches)) {
554: return $matches[0] . $append;
555:
556: } else {
557: return self::substring($s, 0, $maxLen) . $append;
558: }
559: }
560: return $s;
561: }
562:
563:
564: 565: 566: 567: 568:
569: public static function lower($s)
570: {
571: return mb_strtolower((string) $s, 'UTF-8');
572: }
573:
574:
575: 576: 577: 578: 579:
580: public static function upper($s)
581: {
582: return mb_strtoupper((string) $s, 'UTF-8');
583: }
584:
585:
586: 587: 588: 589: 590:
591: public static function firstUpper($s)
592: {
593: $s = (string) $s;
594: return self::upper(self::substring($s, 0, 1)) . self::substring($s, 1);
595: }
596:
597:
598: 599: 600: 601: 602:
603: public static function capitalize($s)
604: {
605: return mb_convert_case((string) $s, MB_CASE_TITLE, 'UTF-8');
606: }
607:
608:
609: 610: 611: 612: 613:
614: public static function length($val)
615: {
616: if (is_array($val) || $val instanceof \Countable) {
617: return count($val);
618: } elseif ($val instanceof \Traversable) {
619: return iterator_count($val);
620: } else {
621: return self::strLength($val);
622: }
623: }
624:
625:
626: 627: 628: 629:
630: private static function strLength($s)
631: {
632: return function_exists('mb_strlen') ? mb_strlen($s, 'UTF-8') : strlen(utf8_decode($s));
633: }
634:
635:
636: 637: 638: 639: 640: 641:
642: public static function trim(FilterInfo $info, $s, $charlist = " \t\n\r\0\x0B\xC2\xA0")
643: {
644: $charlist = preg_quote($charlist, '#');
645: $s = preg_replace('#^[' . $charlist . ']+|[' . $charlist . ']+\z#u', '', (string) $s);
646: if (preg_last_error()) {
647: throw new Latte\RegexpException(null, preg_last_error());
648: }
649: return $s;
650: }
651:
652:
653: 654: 655: 656: 657: 658: 659:
660: public static function padLeft($s, $length, $pad = ' ')
661: {
662: $s = (string) $s;
663: $length = max(0, $length - self::strLength($s));
664: $padLen = self::strLength($pad);
665: return str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen) . $s;
666: }
667:
668:
669: 670: 671: 672: 673: 674: 675:
676: public static function padRight($s, $length, $pad = ' ')
677: {
678: $s = (string) $s;
679: $length = max(0, $length - self::strLength($s));
680: $padLen = self::strLength($pad);
681: return $s . str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen);
682: }
683:
684:
685: 686: 687: 688:
689: public static function reverse($val, $preserveKeys = false)
690: {
691: if (is_array($val)) {
692: return array_reverse($val, $preserveKeys);
693: } elseif ($val instanceof \Traversable) {
694: return array_reverse(iterator_to_array($val), $preserveKeys);
695: } else {
696: return iconv('UTF-32LE', 'UTF-8', strrev(iconv('UTF-8', 'UTF-32BE', (string) $val)));
697: }
698: }
699:
700:
701: 702: 703: 704:
705: public static function htmlAttributes($attrs)
706: {
707: if (!is_array($attrs)) {
708: return '';
709: }
710:
711: $s = '';
712: foreach ($attrs as $key => $value) {
713: if ($value === null || $value === false) {
714: continue;
715:
716: } elseif ($value === true) {
717: if (static::$xhtml) {
718: $s .= ' ' . $key . '="' . $key . '"';
719: } else {
720: $s .= ' ' . $key;
721: }
722: continue;
723:
724: } elseif (is_array($value)) {
725: $tmp = null;
726: foreach ($value as $k => $v) {
727: if ($v != null) {
728:
729: $tmp[] = $v === true ? $k : (is_string($k) ? $k . ':' . $v : $v);
730: }
731: }
732: if ($tmp === null) {
733: continue;
734: }
735:
736: $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
737:
738: } else {
739: $value = (string) $value;
740: }
741:
742: $q = strpos($value, '"') === false ? '"' : "'";
743: $s .= ' ' . $key . '=' . $q
744: . str_replace(
745: ['&', $q, '<'],
746: ['&', $q === '"' ? '"' : ''', self::$xhtml ? '<' : '<'],
747: $value
748: )
749: . (strpos($value, '`') !== false && strpbrk($value, ' <>"\'') === false ? ' ' : '')
750: . $q;
751: }
752: return $s;
753: }
754: }
755: