Chris@0: Chris@0: Chris@0: Permission is hereby granted, free of charge, to any person obtaining a Chris@0: copy of this software and associated documentation files (the Chris@0: "Software"), to deal in the Software without restriction, including Chris@0: without limitation the rights to use, copy, modify, merge, publish, Chris@0: distribute, sublicense, and/or sell copies of the Software, and to Chris@0: permit persons to whom the Software is furnished to do so, subject to Chris@0: the following conditions: Chris@0: Chris@0: The above copyright notice and this permission notice shall be included Chris@0: in all copies or substantial portions of the Software. Chris@0: Chris@0: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS Chris@0: OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF Chris@0: MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. Chris@0: IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY Chris@0: CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, Chris@0: TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE Chris@0: SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Chris@0: */ Chris@17: Chris@17: use Masterminds\HTML5\Exception; Chris@17: Chris@0: class UTF8Utils Chris@0: { Chris@0: /** Chris@18: * The Unicode replacement character. Chris@0: */ Chris@0: const FFFD = "\xEF\xBF\xBD"; Chris@0: Chris@0: /** Chris@0: * Count the number of characters in a string. Chris@18: * UTF-8 aware. This will try (in order) iconv, MB, libxml, and finally a custom counter. Chris@17: * Chris@17: * @param string $string Chris@17: * Chris@17: * @return int Chris@0: */ Chris@0: public static function countChars($string) Chris@0: { Chris@0: // Get the length for the string we need. Chris@0: if (function_exists('mb_strlen')) { Chris@0: return mb_strlen($string, 'utf-8'); Chris@18: } Chris@18: Chris@18: if (function_exists('iconv_strlen')) { Chris@0: return iconv_strlen($string, 'utf-8'); Chris@18: } Chris@18: Chris@18: if (function_exists('utf8_decode')) { Chris@0: // MPB: Will this work? Won't certain decodes lead to two chars Chris@0: // extrapolated out of 2-byte chars? Chris@0: return strlen(utf8_decode($string)); Chris@0: } Chris@18: Chris@0: $count = count_chars($string); Chris@18: Chris@0: // 0x80 = 0x7F - 0 + 1 (one added to get inclusive range) Chris@0: // 0x33 = 0xF4 - 0x2C + 1 (one added to get inclusive range) Chris@0: return array_sum(array_slice($count, 0, 0x80)) + array_sum(array_slice($count, 0xC2, 0x33)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Convert data from the given encoding to UTF-8. Chris@0: * Chris@0: * This has not yet been tested with charactersets other than UTF-8. Chris@0: * It should work with ISO-8859-1/-13 and standard Latin Win charsets. Chris@0: * Chris@17: * @param string $data The data to convert Chris@17: * @param string $encoding A valid encoding. Examples: http://www.php.net/manual/en/mbstring.supported-encodings.php Chris@17: * Chris@17: * @return string Chris@0: */ Chris@0: public static function convertToUTF8($data, $encoding = 'UTF-8') Chris@0: { Chris@0: /* Chris@18: * From the HTML5 spec: Given an encoding, the bytes in the input stream must be converted Chris@18: * to Unicode characters for the tokeniser, as described by the rules for that encoding, Chris@18: * except that the leading U+FEFF BYTE ORDER MARK character, if any, must not be stripped Chris@18: * by the encoding layer (it is stripped by the rule below). Bytes or sequences of bytes Chris@18: * in the original byte stream that could not be converted to Unicode characters must be Chris@18: * converted to U+FFFD REPLACEMENT CHARACTER code points. Chris@0: */ Chris@0: Chris@0: // mb_convert_encoding is chosen over iconv because of a bug. The best Chris@0: // details for the bug are on http://us1.php.net/manual/en/function.iconv.php#108643 Chris@0: // which contains links to the actual but reports as well as work around Chris@0: // details. Chris@0: if (function_exists('mb_convert_encoding')) { Chris@0: // mb library has the following behaviors: Chris@0: // - UTF-16 surrogates result in false. Chris@0: // - Overlongs and outside Plane 16 result in empty strings. Chris@0: Chris@0: // Before we run mb_convert_encoding we need to tell it what to do with Chris@0: // characters it does not know. This could be different than the parent Chris@0: // application executing this library so we store the value, change it Chris@0: // to our needs, and then change it back when we are done. This feels Chris@0: // a little excessive and it would be great if there was a better way. Chris@0: $save = mb_substitute_character(); Chris@0: mb_substitute_character('none'); Chris@0: $data = mb_convert_encoding($data, 'UTF-8', $encoding); Chris@0: mb_substitute_character($save); Chris@18: } Chris@18: // @todo Get iconv running in at least some environments if that is possible. Chris@17: elseif (function_exists('iconv') && 'auto' !== $encoding) { Chris@0: // fprintf(STDOUT, "iconv found\n"); Chris@0: // iconv has the following behaviors: Chris@0: // - Overlong representations are ignored. Chris@0: // - Beyond Plane 16 is replaced with a lower char. Chris@0: // - Incomplete sequences generate a warning. Chris@0: $data = @iconv($encoding, 'UTF-8//IGNORE', $data); Chris@0: } else { Chris@0: throw new Exception('Not implemented, please install mbstring or iconv'); Chris@0: } Chris@0: Chris@0: /* Chris@0: * One leading U+FEFF BYTE ORDER MARK character must be ignored if any are present. Chris@0: */ Chris@17: if ("\xEF\xBB\xBF" === substr($data, 0, 3)) { Chris@0: $data = substr($data, 3); Chris@0: } Chris@0: Chris@0: return $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks for Unicode code points that are not valid in a document. Chris@0: * Chris@17: * @param string $data A string to analyze Chris@17: * Chris@17: * @return array An array of (string) error messages produced by the scanning Chris@0: */ Chris@0: public static function checkForIllegalCodepoints($data) Chris@0: { Chris@0: // Vestigal error handling. Chris@0: $errors = array(); Chris@0: Chris@0: /* Chris@18: * All U+0000 null characters in the input must be replaced by U+FFFD REPLACEMENT CHARACTERs. Chris@18: * Any occurrences of such characters is a parse error. Chris@0: */ Chris@17: for ($i = 0, $count = substr_count($data, "\0"); $i < $count; ++$i) { Chris@0: $errors[] = 'null-character'; Chris@0: } Chris@0: Chris@0: /* Chris@18: * Any occurrences of any characters in the ranges U+0001 to U+0008, U+000B, U+000E to U+001F, U+007F Chris@18: * to U+009F, U+D800 to U+DFFF , U+FDD0 to U+FDEF, and characters U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, Chris@18: * U+2FFFE, U+2FFFF, U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE, U+6FFFF, U+7FFFE, Chris@18: * U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF, U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, Chris@18: * U+DFFFE, U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, and U+10FFFF are parse errors. Chris@18: * (These are all control characters or permanently undefined Unicode characters.) Chris@0: */ Chris@0: // Check PCRE is loaded. Chris@0: $count = preg_match_all( Chris@0: '/(?: Chris@0: [\x01-\x08\x0B\x0E-\x1F\x7F] # U+0001 to U+0008, U+000B, U+000E to U+001F and U+007F Chris@0: | Chris@0: \xC2[\x80-\x9F] # U+0080 to U+009F Chris@0: | Chris@0: \xED(?:\xA0[\x80-\xFF]|[\xA1-\xBE][\x00-\xFF]|\xBF[\x00-\xBF]) # U+D800 to U+DFFFF Chris@0: | Chris@0: \xEF\xB7[\x90-\xAF] # U+FDD0 to U+FDEF Chris@0: | Chris@0: \xEF\xBF[\xBE\xBF] # U+FFFE and U+FFFF Chris@0: | Chris@0: [\xF0-\xF4][\x8F-\xBF]\xBF[\xBE\xBF] # U+nFFFE and U+nFFFF (1 <= n <= 10_{16}) Chris@0: )/x', $data, $matches); Chris@17: for ($i = 0; $i < $count; ++$i) { Chris@0: $errors[] = 'invalid-codepoint'; Chris@0: } Chris@0: Chris@0: return $errors; Chris@0: } Chris@0: }