1: <?php
2:
3: /**
4: * A UTF-8 specific character encoder that handles cleaning and transforming.
5: * @note All functions in this class should be static.
6: */
7: class HTMLPurifier_Encoder
8: {
9:
10: /**
11: * Constructor throws fatal error if you attempt to instantiate class
12: */
13: private function __construct() {
14: trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR);
15: }
16:
17: /**
18: * Error-handler that mutes errors, alternative to shut-up operator.
19: */
20: public static function muteErrorHandler() {}
21:
22: /**
23: * Cleans a UTF-8 string for well-formedness and SGML validity
24: *
25: * It will parse according to UTF-8 and return a valid UTF8 string, with
26: * non-SGML codepoints excluded.
27: *
28: * @note Just for reference, the non-SGML code points are 0 to 31 and
29: * 127 to 159, inclusive. However, we allow code points 9, 10
30: * and 13, which are the tab, line feed and carriage return
31: * respectively. 128 and above the code points map to multibyte
32: * UTF-8 representations.
33: *
34: * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and
35: * hsivonen@iki.fi at <http://iki.fi/hsivonen/php-utf8/> under the
36: * LGPL license. Notes on what changed are inside, but in general,
37: * the original code transformed UTF-8 text into an array of integer
38: * Unicode codepoints. Understandably, transforming that back to
39: * a string would be somewhat expensive, so the function was modded to
40: * directly operate on the string. However, this discourages code
41: * reuse, and the logic enumerated here would be useful for any
42: * function that needs to be able to understand UTF-8 characters.
43: * As of right now, only smart lossless character encoding converters
44: * would need that, and I'm probably not going to implement them.
45: * Once again, PHP 6 should solve all our problems.
46: */
47: public static function cleanUTF8($str, $force_php = false) {
48:
49: // UTF-8 validity is checked since PHP 4.3.5
50: // This is an optimization: if the string is already valid UTF-8, no
51: // need to do PHP stuff. 99% of the time, this will be the case.
52: // The regexp matches the XML char production, as well as well as excluding
53: // non-SGML codepoints U+007F to U+009F
54: if (preg_match('/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', $str)) {
55: return $str;
56: }
57:
58: $mState = 0; // cached expected number of octets after the current octet
59: // until the beginning of the next UTF8 character sequence
60: $mUcs4 = 0; // cached Unicode character
61: $mBytes = 1; // cached expected number of octets in the current sequence
62:
63: // original code involved an $out that was an array of Unicode
64: // codepoints. Instead of having to convert back into UTF-8, we've
65: // decided to directly append valid UTF-8 characters onto a string
66: // $out once they're done. $char accumulates raw bytes, while $mUcs4
67: // turns into the Unicode code point, so there's some redundancy.
68:
69: $out = '';
70: $char = '';
71:
72: $len = strlen($str);
73: for($i = 0; $i < $len; $i++) {
74: $in = ord($str{$i});
75: $char .= $str[$i]; // append byte to char
76: if (0 == $mState) {
77: // When mState is zero we expect either a US-ASCII character
78: // or a multi-octet sequence.
79: if (0 == (0x80 & ($in))) {
80: // US-ASCII, pass straight through.
81: if (($in <= 31 || $in == 127) &&
82: !($in == 9 || $in == 13 || $in == 10) // save \r\t\n
83: ) {
84: // control characters, remove
85: } else {
86: $out .= $char;
87: }
88: // reset
89: $char = '';
90: $mBytes = 1;
91: } elseif (0xC0 == (0xE0 & ($in))) {
92: // First octet of 2 octet sequence
93: $mUcs4 = ($in);
94: $mUcs4 = ($mUcs4 & 0x1F) << 6;
95: $mState = 1;
96: $mBytes = 2;
97: } elseif (0xE0 == (0xF0 & ($in))) {
98: // First octet of 3 octet sequence
99: $mUcs4 = ($in);
100: $mUcs4 = ($mUcs4 & 0x0F) << 12;
101: $mState = 2;
102: $mBytes = 3;
103: } elseif (0xF0 == (0xF8 & ($in))) {
104: // First octet of 4 octet sequence
105: $mUcs4 = ($in);
106: $mUcs4 = ($mUcs4 & 0x07) << 18;
107: $mState = 3;
108: $mBytes = 4;
109: } elseif (0xF8 == (0xFC & ($in))) {
110: // First octet of 5 octet sequence.
111: //
112: // This is illegal because the encoded codepoint must be
113: // either:
114: // (a) not the shortest form or
115: // (b) outside the Unicode range of 0-0x10FFFF.
116: // Rather than trying to resynchronize, we will carry on
117: // until the end of the sequence and let the later error
118: // handling code catch it.
119: $mUcs4 = ($in);
120: $mUcs4 = ($mUcs4 & 0x03) << 24;
121: $mState = 4;
122: $mBytes = 5;
123: } elseif (0xFC == (0xFE & ($in))) {
124: // First octet of 6 octet sequence, see comments for 5
125: // octet sequence.
126: $mUcs4 = ($in);
127: $mUcs4 = ($mUcs4 & 1) << 30;
128: $mState = 5;
129: $mBytes = 6;
130: } else {
131: // Current octet is neither in the US-ASCII range nor a
132: // legal first octet of a multi-octet sequence.
133: $mState = 0;
134: $mUcs4 = 0;
135: $mBytes = 1;
136: $char = '';
137: }
138: } else {
139: // When mState is non-zero, we expect a continuation of the
140: // multi-octet sequence
141: if (0x80 == (0xC0 & ($in))) {
142: // Legal continuation.
143: $shift = ($mState - 1) * 6;
144: $tmp = $in;
145: $tmp = ($tmp & 0x0000003F) << $shift;
146: $mUcs4 |= $tmp;
147:
148: if (0 == --$mState) {
149: // End of the multi-octet sequence. mUcs4 now contains
150: // the final Unicode codepoint to be output
151:
152: // Check for illegal sequences and codepoints.
153:
154: // From Unicode 3.1, non-shortest form is illegal
155: if (((2 == $mBytes) && ($mUcs4 < 0x0080)) ||
156: ((3 == $mBytes) && ($mUcs4 < 0x0800)) ||
157: ((4 == $mBytes) && ($mUcs4 < 0x10000)) ||
158: (4 < $mBytes) ||
159: // From Unicode 3.2, surrogate characters = illegal
160: (($mUcs4 & 0xFFFFF800) == 0xD800) ||
161: // Codepoints outside the Unicode range are illegal
162: ($mUcs4 > 0x10FFFF)
163: ) {
164:
165: } elseif (0xFEFF != $mUcs4 && // omit BOM
166: // check for valid Char unicode codepoints
167: (
168: 0x9 == $mUcs4 ||
169: 0xA == $mUcs4 ||
170: 0xD == $mUcs4 ||
171: (0x20 <= $mUcs4 && 0x7E >= $mUcs4) ||
172: // 7F-9F is not strictly prohibited by XML,
173: // but it is non-SGML, and thus we don't allow it
174: (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) ||
175: (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4)
176: )
177: ) {
178: $out .= $char;
179: }
180: // initialize UTF8 cache (reset)
181: $mState = 0;
182: $mUcs4 = 0;
183: $mBytes = 1;
184: $char = '';
185: }
186: } else {
187: // ((0xC0 & (*in) != 0x80) && (mState != 0))
188: // Incomplete multi-octet sequence.
189: // used to result in complete fail, but we'll reset
190: $mState = 0;
191: $mUcs4 = 0;
192: $mBytes = 1;
193: $char ='';
194: }
195: }
196: }
197: return $out;
198: }
199:
200: /**
201: * Translates a Unicode codepoint into its corresponding UTF-8 character.
202: * @note Based on Feyd's function at
203: * <http://forums.devnetwork.net/viewtopic.php?p=191404#191404>,
204: * which is in public domain.
205: * @note While we're going to do code point parsing anyway, a good
206: * optimization would be to refuse to translate code points that
207: * are non-SGML characters. However, this could lead to duplication.
208: * @note This is very similar to the unichr function in
209: * maintenance/generate-entity-file.php (although this is superior,
210: * due to its sanity checks).
211: */
212:
213: // +----------+----------+----------+----------+
214: // | 33222222 | 22221111 | 111111 | |
215: // | 10987654 | 32109876 | 54321098 | 76543210 | bit
216: // +----------+----------+----------+----------+
217: // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F
218: // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF
219: // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF
220: // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF
221: // +----------+----------+----------+----------+
222: // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF)
223: // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes
224: // +----------+----------+----------+----------+
225:
226: public static function unichr($code) {
227: if($code > 1114111 or $code < 0 or
228: ($code >= 55296 and $code <= 57343) ) {
229: // bits are set outside the "valid" range as defined
230: // by UNICODE 4.1.0
231: return '';
232: }
233:
234: $x = $y = $z = $w = 0;
235: if ($code < 128) {
236: // regular ASCII character
237: $x = $code;
238: } else {
239: // set up bits for UTF-8
240: $x = ($code & 63) | 128;
241: if ($code < 2048) {
242: $y = (($code & 2047) >> 6) | 192;
243: } else {
244: $y = (($code & 4032) >> 6) | 128;
245: if($code < 65536) {
246: $z = (($code >> 12) & 15) | 224;
247: } else {
248: $z = (($code >> 12) & 63) | 128;
249: $w = (($code >> 18) & 7) | 240;
250: }
251: }
252: }
253: // set up the actual character
254: $ret = '';
255: if($w) $ret .= chr($w);
256: if($z) $ret .= chr($z);
257: if($y) $ret .= chr($y);
258: $ret .= chr($x);
259:
260: return $ret;
261: }
262:
263: /**
264: * Converts a string to UTF-8 based on configuration.
265: */
266: public static function convertToUTF8($str, $config, $context) {
267: $encoding = $config->get('Core.Encoding');
268: if ($encoding === 'utf-8') return $str;
269: static $iconv = null;
270: if ($iconv === null) $iconv = function_exists('iconv');
271: set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
272: if ($iconv && !$config->get('Test.ForceNoIconv')) {
273: $str = iconv($encoding, 'utf-8//IGNORE', $str);
274: if ($str === false) {
275: // $encoding is not a valid encoding
276: restore_error_handler();
277: trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR);
278: return '';
279: }
280: // If the string is bjorked by Shift_JIS or a similar encoding
281: // that doesn't support all of ASCII, convert the naughty
282: // characters to their true byte-wise ASCII/UTF-8 equivalents.
283: $str = strtr($str, HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding));
284: restore_error_handler();
285: return $str;
286: } elseif ($encoding === 'iso-8859-1') {
287: $str = utf8_encode($str);
288: restore_error_handler();
289: return $str;
290: }
291: trigger_error('Encoding not supported, please install iconv', E_USER_ERROR);
292: }
293:
294: /**
295: * Converts a string from UTF-8 based on configuration.
296: * @note Currently, this is a lossy conversion, with unexpressable
297: * characters being omitted.
298: */
299: public static function convertFromUTF8($str, $config, $context) {
300: $encoding = $config->get('Core.Encoding');
301: if ($encoding === 'utf-8') return $str;
302: static $iconv = null;
303: if ($iconv === null) $iconv = function_exists('iconv');
304: if ($escape = $config->get('Core.EscapeNonASCIICharacters')) {
305: $str = HTMLPurifier_Encoder::convertToASCIIDumbLossless($str);
306: }
307: set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
308: if ($iconv && !$config->get('Test.ForceNoIconv')) {
309: // Undo our previous fix in convertToUTF8, otherwise iconv will barf
310: $ascii_fix = HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding);
311: if (!$escape && !empty($ascii_fix)) {
312: $clear_fix = array();
313: foreach ($ascii_fix as $utf8 => $native) $clear_fix[$utf8] = '';
314: $str = strtr($str, $clear_fix);
315: }
316: $str = strtr($str, array_flip($ascii_fix));
317: // Normal stuff
318: $str = iconv('utf-8', $encoding . '//IGNORE', $str);
319: restore_error_handler();
320: return $str;
321: } elseif ($encoding === 'iso-8859-1') {
322: $str = utf8_decode($str);
323: restore_error_handler();
324: return $str;
325: }
326: trigger_error('Encoding not supported', E_USER_ERROR);
327: }
328:
329: /**
330: * Lossless (character-wise) conversion of HTML to ASCII
331: * @param $str UTF-8 string to be converted to ASCII
332: * @returns ASCII encoded string with non-ASCII character entity-ized
333: * @warning Adapted from MediaWiki, claiming fair use: this is a common
334: * algorithm. If you disagree with this license fudgery,
335: * implement it yourself.
336: * @note Uses decimal numeric entities since they are best supported.
337: * @note This is a DUMB function: it has no concept of keeping
338: * character entities that the projected character encoding
339: * can allow. We could possibly implement a smart version
340: * but that would require it to also know which Unicode
341: * codepoints the charset supported (not an easy task).
342: * @note Sort of with cleanUTF8() but it assumes that $str is
343: * well-formed UTF-8
344: */
345: public static function convertToASCIIDumbLossless($str) {
346: $bytesleft = 0;
347: $result = '';
348: $working = 0;
349: $len = strlen($str);
350: for( $i = 0; $i < $len; $i++ ) {
351: $bytevalue = ord( $str[$i] );
352: if( $bytevalue <= 0x7F ) { //0xxx xxxx
353: $result .= chr( $bytevalue );
354: $bytesleft = 0;
355: } elseif( $bytevalue <= 0xBF ) { //10xx xxxx
356: $working = $working << 6;
357: $working += ($bytevalue & 0x3F);
358: $bytesleft--;
359: if( $bytesleft <= 0 ) {
360: $result .= "&#" . $working . ";";
361: }
362: } elseif( $bytevalue <= 0xDF ) { //110x xxxx
363: $working = $bytevalue & 0x1F;
364: $bytesleft = 1;
365: } elseif( $bytevalue <= 0xEF ) { //1110 xxxx
366: $working = $bytevalue & 0x0F;
367: $bytesleft = 2;
368: } else { //1111 0xxx
369: $working = $bytevalue & 0x07;
370: $bytesleft = 3;
371: }
372: }
373: return $result;
374: }
375:
376: /**
377: * This expensive function tests whether or not a given character
378: * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will
379: * fail this test, and require special processing. Variable width
380: * encodings shouldn't ever fail.
381: *
382: * @param string $encoding Encoding name to test, as per iconv format
383: * @param bool $bypass Whether or not to bypass the precompiled arrays.
384: * @return Array of UTF-8 characters to their corresponding ASCII,
385: * which can be used to "undo" any overzealous iconv action.
386: */
387: public static function testEncodingSupportsASCII($encoding, $bypass = false) {
388: static $encodings = array();
389: if (!$bypass) {
390: if (isset($encodings[$encoding])) return $encodings[$encoding];
391: $lenc = strtolower($encoding);
392: switch ($lenc) {
393: case 'shift_jis':
394: return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~');
395: case 'johab':
396: return array("\xE2\x82\xA9" => '\\');
397: }
398: if (strpos($lenc, 'iso-8859-') === 0) return array();
399: }
400: $ret = array();
401: set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
402: if (iconv('UTF-8', $encoding, 'a') === false) return false;
403: for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars
404: $c = chr($i); // UTF-8 char
405: $r = iconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion
406: if (
407: $r === '' ||
408: // This line is needed for iconv implementations that do not
409: // omit characters that do not exist in the target character set
410: ($r === $c && iconv($encoding, 'UTF-8//IGNORE', $r) !== $c)
411: ) {
412: // Reverse engineer: what's the UTF-8 equiv of this byte
413: // sequence? This assumes that there's no variable width
414: // encoding that doesn't support ASCII.
415: $ret[iconv($encoding, 'UTF-8//IGNORE', $c)] = $c;
416: }
417: }
418: restore_error_handler();
419: $encodings[$encoding] = $ret;
420: return $ret;
421: }
422:
423:
424: }
425:
426: // vim: et sw=4 sts=4
427: