Source for file Encoder.php

Documentation is available at Encoder.php

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

Documentation generated on Thu, 19 Jun 2008 18:49:08 -0400 by phpDocumentor 1.4.2