Source for file idna_convert.class.php

Documentation is available at idna_convert.class.php

  1. <?php
  2. // {{{ license
  3.  
  4. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
  5. //
  6. // +----------------------------------------------------------------------+
  7. // | This library is free software; you can redistribute it and/or modify |
  8. // | it under the terms of the GNU Lesser General Public License as       |
  9. // | published by the Free Software Foundation; either version 2.1 of the |
  10. // | License, or (at your option) any later version.                      |
  11. // |                                                                      |
  12. // | This library is distributed in the hope that it will be useful, but  |
  13. // | WITHOUT ANY WARRANTY; without even the implied warranty of           |
  14. // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU    |
  15. // | Lesser General Public License for more details.                      |
  16. // |                                                                      |
  17. // | You should have received a copy of the GNU Lesser General Public     |
  18. // | License along with this library; if not, write to the Free Software  |
  19. // | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 |
  20. // | USA.                                                                 |
  21. // +----------------------------------------------------------------------+
  22. //
  23.  
  24. // }}}
  25.  
  26. /**
  27.  * Encode/decode Internationalized Domain Names.
  28.  *
  29.  * The class allows to convert internationalized domain names
  30.  * (see RFC 3490 for details) as they can be used with various registries worldwide
  31.  * to be translated between their original (localized) form and their encoded form
  32.  * as it will be used in the DNS (Domain Name System).
  33.  *
  34.  * The class provides two public methods, encode() and decode(), which do exactly
  35.  * what you would expect them to do. You are allowed to use complete domain names,
  36.  * simple strings and complete email addresses as well. That means, that you might
  37.  * use any of the following notations:
  38.  *
  39.  * - www.nörgler.com
  40.  * - xn--nrgler-wxa
  41.  * - xn--brse-5qa.xn--knrz-1ra.info
  42.  *
  43.  * Unicode input might be given as either UTF-8 string, UCS-4 string or UCS-4
  44.  * array. Unicode output is available in the same formats.
  45.  * You can select your preferred format via {@link set_paramter()}.
  46.  *
  47.  * ACE input and output is always expected to be ASCII.
  48.  *
  49.  * @author  Matthias Sommerfeld <mso@phlylabs.de>
  50.  * @copyright 2004-2007 phlyLabs Berlin, http://phlylabs.de
  51.  * @version 0.5.1
  52.  *
  53.  */
  54. {
  55.     /**
  56.      * Holds all relevant mapping tables, loaded from a seperate file on construct
  57.      * See RFC3454 for details
  58.      *
  59.      * @var array 
  60.      * @access private
  61.      */
  62.     var $NP = array();
  63.  
  64.     // Internal settings, do not mess with them
  65.     var $_punycode_prefix = 'xn--';
  66.     var $_invalid_ucs =     0x80000000;
  67.     var $_max_ucs =         0x10FFFF;
  68.     var $_base =            36;
  69.     var $_tmin =            1;
  70.     var $_tmax =            26;
  71.     var $_skew =            38;
  72.     var $_damp =            700;
  73.     var $_initial_bias =    72;
  74.     var $_initial_n =       0x80;
  75.     var $_sbase =           0xAC00;
  76.     var $_lbase =           0x1100;
  77.     var $_vbase =           0x1161;
  78.     var $_tbase =           0x11A7;
  79.     var $_lcount =          19;
  80.     var $_vcount =          21;
  81.     var $_tcount =          28;
  82.     var $_ncount =          588;   // _vcount * _tcount
  83.     var $_scount =          11172; // _lcount * _tcount * _vcount
  84.     var $_error =           false;
  85.  
  86.     // See {@link set_paramter()} for details of how to change the following
  87.     // settings from within your script / application
  88.     var $_api_encoding   =  'utf8'; // Default input charset is UTF-8
  89.     var $_allow_overlong =  false;  // Overlong UTF-8 encodings are forbidden
  90.     var $_strict_mode    =  false;  // Behave strict or not
  91.  
  92.     // The constructor
  93.     function idna_convert($options = false)
  94.     {
  95.         $this->slast = $this->_sbase + $this->_lcount * $this->_vcount * $this->_tcount;
  96.         if (function_exists('file_get_contents')) {
  97.             $this->NP = unserialize(file_get_contents(dirname(__FILE__).'/npdata.ser'));
  98.         } else {
  99.             $this->NP = unserialize(join('', file(dirname(__FILE__).'/npdata.ser')));
  100.         }
  101.         // If parameters are given, pass these to the respective method
  102.         if (is_array($options)) {
  103.             return $this->set_parameter($options);
  104.         }
  105.         return true;
  106.     }
  107.  
  108.     /**
  109.      * Sets a new option value. Available options and values:
  110.      * [encoding - Use either UTF-8, UCS4 as array or UCS4 as string as input ('utf8' for UTF-8,
  111.      *         'ucs4_string' and 'ucs4_array' respectively for UCS4); The output is always UTF-8]
  112.      * [overlong - Unicode does not allow unnecessarily long encodings of chars,
  113.      *             to allow this, set this parameter to true, else to false;
  114.      *             default is false.]
  115.      * [strict - true: strict mode, good for registration purposes - Causes errors
  116.      *           on failures; false: loose mode, ideal for "wildlife" applications
  117.      *           by silently ignoring errors and returning the original input instead
  118.      *
  119.      * @param    mixed     Parameter to set (string: single parameter; array of Parameter => Value pairs)
  120.      * @param    string    Value to use (if parameter 1 is a string)
  121.      * @return   boolean   true on success, false otherwise
  122.      * @access   public
  123.      */
  124.     function set_parameter($option, $value = false)
  125.     {
  126.         if (!is_array($option)) {
  127.             $option = array($option => $value);
  128.         }
  129.         foreach ($option as $k => $v) {
  130.             switch ($k) {
  131.             case 'encoding':
  132.                 switch ($v) {
  133.                 case 'utf8':
  134.                 case 'ucs4_string':
  135.                 case 'ucs4_array':
  136.                     $this->_api_encoding = $v;
  137.                     break;
  138.                 default:
  139.                     $this->_error('Set Parameter: Unknown parameter '.$v.' for option '.$k);
  140.                     return false;
  141.                 }
  142.                 break;
  143.             case 'overlong':
  144.                 $this->_allow_overlong = ($v) ? true : false;
  145.                 break;
  146.             case 'strict':
  147.                 $this->_strict_mode = ($v) ? true : false;
  148.                 break;
  149.             default:
  150.                 $this->_error('Set Parameter: Unknown option '.$k);
  151.                 return false;
  152.             }
  153.         }
  154.         return true;
  155.     }
  156.  
  157.     /**
  158.      * Decode a given ACE domain name
  159.      * @param    string   Domain name (ACE string)
  160.      *  [@param    string   Desired output encoding, see {@link set_parameter}]
  161.      * @return   string   Decoded Domain name (UTF-8 or UCS-4)
  162.      * @access   public
  163.      */
  164.     function decode($input, $one_time_encoding = false)
  165.     {
  166.         // Optionally set
  167.         if ($one_time_encoding) {
  168.             switch ($one_time_encoding) {
  169.             case 'utf8':
  170.             case 'ucs4_string':
  171.             case 'ucs4_array':
  172.                 break;
  173.             default:
  174.                 $this->_error('Unknown encoding '.$one_time_encoding);
  175.                 return false;
  176.             }
  177.         }
  178.         // Make sure to drop any newline characters around
  179.         $input = trim($input);
  180.  
  181.         // Negotiate input and try to determine, whether it is a plain string,
  182.         // an email address or something like a complete URL
  183.         if (strpos($input, '@')) { // Maybe it is an email address
  184.             // No no in strict mode
  185.             if ($this->_strict_mode) {
  186.                 $this->_error('Only simple domain name parts can be handled in strict mode');
  187.                 return false;
  188.             }
  189.             list ($email_pref, $input) = explode('@', $input, 2);
  190.             $arr = explode('.', $input);
  191.             foreach ($arr as $k => $v) {
  192.                 if (preg_match('!^'.preg_quote($this->_punycode_prefix, '!').'!', $v)) {
  193.                     $conv = $this->_decode($v);
  194.                     if ($conv) $arr[$k] = $conv;
  195.                 }
  196.             }
  197.             $input = join('.', $arr);
  198.             $arr = explode('.', $email_pref);
  199.             foreach ($arr as $k => $v) {
  200.                 if (preg_match('!^'.preg_quote($this->_punycode_prefix, '!').'!', $v)) {
  201.                     $conv = $this->_decode($v);
  202.                     if ($conv) $arr[$k] = $conv;
  203.                 }
  204.             }
  205.             $email_pref = join('.', $arr);
  206.             $return = $email_pref . '@' . $input;
  207.         } elseif (preg_match('![:\./]!', $input)) { // Or a complete domain name (with or without paths / parameters)
  208.             // No no in strict mode
  209.             if ($this->_strict_mode) {
  210.                 $this->_error('Only simple domain name parts can be handled in strict mode');
  211.                 return false;
  212.             }
  213.             $parsed = parse_url($input);
  214.             if (isset($parsed['host'])) {
  215.                 $arr = explode('.', $parsed['host']);
  216.                 foreach ($arr as $k => $v) {
  217.                     $conv = $this->_decode($v);
  218.                     if ($conv) $arr[$k] = $conv;
  219.                 }
  220.                 $parsed['host'] = join('.', $arr);
  221.                 $return =
  222.                         (empty($parsed['scheme']) ? '' : $parsed['scheme'].(strtolower($parsed['scheme']) == 'mailto' ? ':' : '://'))
  223.                         .(empty($parsed['user']) ? '' : $parsed['user'].(empty($parsed['pass']) ? '' : ':'.$parsed['pass']).'@')
  224.                         .$parsed['host']
  225.                         .(empty($parsed['port']) ? '' : ':'.$parsed['port'])
  226.                         .(empty($parsed['path']) ? '' : $parsed['path'])
  227.                         .(empty($parsed['query']) ? '' : '?'.$parsed['query'])
  228.                         .(empty($parsed['fragment']) ? '' : '#'.$parsed['fragment']);
  229.             } else { // parse_url seems to have failed, try without it
  230.                 $arr = explode('.', $input);
  231.                 foreach ($arr as $k => $v) {
  232.                     $conv = $this->_decode($v);
  233.                     $arr[$k] = ($conv) ? $conv : $v;
  234.                 }
  235.                 $return = join('.', $arr);
  236.             }
  237.         } else { // Otherwise we consider it being a pure domain name string
  238.             $return = $this->_decode($input);
  239.             if (!$return) $return = $input;
  240.         }
  241.         // The output is UTF-8 by default, other output formats need conversion here
  242.         // If one time encoding is given, use this, else the objects property
  243.         switch (($one_time_encoding) ? $one_time_encoding : $this->_api_encoding) {
  244.         case 'utf8':
  245.             return $return;
  246.             break;
  247.         case 'ucs4_string':
  248.            return $this->_ucs4_to_ucs4_string($this->_utf8_to_ucs4($return));
  249.            break;
  250.         case 'ucs4_array':
  251.             return $this->_utf8_to_ucs4($return);
  252.             break;
  253.         default:
  254.             $this->_error('Unsupported output format');
  255.             return false;
  256.         }
  257.     }
  258.  
  259.     /**
  260.      * Encode a given UTF-8 domain name
  261.      * @param    string   Domain name (UTF-8 or UCS-4)
  262.      *  [@param    string   Desired input encoding, see {@link set_parameter}]
  263.      * @return   string   Encoded Domain name (ACE string)
  264.      * @access   public
  265.      */
  266.     function encode($decoded, $one_time_encoding = false)
  267.     {
  268.         // Forcing conversion of input to UCS4 array
  269.         // If one time encoding is given, use this, else the objects property
  270.         switch ($one_time_encoding ? $one_time_encoding : $this->_api_encoding) {
  271.         case 'utf8':
  272.             $decoded = $this->_utf8_to_ucs4($decoded);
  273.             break;
  274.         case 'ucs4_string':
  275.            $decoded = $this->_ucs4_string_to_ucs4($decoded);
  276.         case 'ucs4_array':
  277.            break;
  278.         default:
  279.             $this->_error('Unsupported input format: '.($one_time_encoding ? $one_time_encoding : $this->_api_encoding));
  280.             return false;
  281.         }
  282.  
  283.         // No input, no output, what else did you expect?
  284.         if (empty($decoded)) return '';
  285.  
  286.         // Anchors for iteration
  287.         $last_begin = 0;
  288.         // Output string
  289.         $output = '';
  290.         foreach ($decoded as $k => $v) {
  291.             // Make sure to use just the plain dot
  292.             switch($v) {
  293.             case 0x3002:
  294.             case 0xFF0E:
  295.             case 0xFF61:
  296.                 $decoded[$k] = 0x2E;
  297.                 // Right, no break here, the above are converted to dots anyway
  298.             // Stumbling across an anchoring character
  299.             case 0x2E:
  300.             case 0x2F:
  301.             case 0x3A:
  302.             case 0x3F:
  303.             case 0x40:
  304.                 // Neither email addresses nor URLs allowed in strict mode
  305.                 if ($this->_strict_mode) {
  306.                    $this->_error('Neither email addresses nor URLs are allowed in strict mode.');
  307.                    return false;
  308.                 } else {
  309.                     // Skip first char
  310.                     if ($k) {
  311.                         $encoded = '';
  312.                         $encoded = $this->_encode(array_slice($decoded, $last_begin, (($k)-$last_begin)));
  313.                         if ($encoded) {
  314.                             $output .= $encoded;
  315.                         } else {
  316.                             $output .= $this->_ucs4_to_utf8(array_slice($decoded, $last_begin, (($k)-$last_begin)));
  317.                         }
  318.                         $output .= chr($decoded[$k]);
  319.                     }
  320.                     $last_begin = $k + 1;
  321.                 }
  322.             }
  323.         }
  324.         // Catch the rest of the string
  325.         if ($last_begin) {
  326.             $inp_len = sizeof($decoded);
  327.             $encoded = '';
  328.             $encoded = $this->_encode(array_slice($decoded, $last_begin, (($inp_len)-$last_begin)));
  329.             if ($encoded) {
  330.                 $output .= $encoded;
  331.             } else {
  332.                 $output .= $this->_ucs4_to_utf8(array_slice($decoded, $last_begin, (($inp_len)-$last_begin)));
  333.             }
  334.             return $output;
  335.         } else {
  336.             if ($output = $this->_encode($decoded)) {
  337.                 return $output;
  338.             } else {
  339.                 return $this->_ucs4_to_utf8($decoded);
  340.             }
  341.         }
  342.     }
  343.  
  344.     /**
  345.      * Use this method to get the last error ocurred
  346.      * @param    void 
  347.      * @return   string   The last error, that occured
  348.      * @access   public
  349.      */
  350.     function get_last_error()
  351.     {
  352.         return $this->_error;
  353.     }
  354.  
  355.     /**
  356.      * The actual decoding algorithm
  357.      * @access   private
  358.      */
  359.     function _decode($encoded)
  360.     {
  361.         // We do need to find the Punycode prefix
  362.         if (!preg_match('!^'.preg_quote($this->_punycode_prefix, '!').'!', $encoded)) {
  363.             $this->_error('This is not a punycode string');
  364.             return false;
  365.         }
  366.         $encode_test = preg_replace('!^'.preg_quote($this->_punycode_prefix, '!').'!', '', $encoded);
  367.         // If nothing left after removing the prefix, it is hopeless
  368.         if (!$encode_test) {
  369.             $this->_error('The given encoded string was empty');
  370.             return false;
  371.         }
  372.         // Find last occurence of the delimiter
  373.         $delim_pos = strrpos($encoded, '-');
  374.         if ($delim_pos > strlen($this->_punycode_prefix)) {
  375.             for ($k = strlen($this->_punycode_prefix); $k < $delim_pos; ++$k) {
  376.                 $decoded[] = ord($encoded{$k});
  377.             }
  378.         } else {
  379.             $decoded = array();
  380.         }
  381.         $deco_len = count($decoded);
  382.         $enco_len = strlen($encoded);
  383.  
  384.         // Wandering through the strings; init
  385.         $is_first = true;
  386.         $bias     = $this->_initial_bias;
  387.         $idx      = 0;
  388.         $char     = $this->_initial_n;
  389.  
  390.         for ($enco_idx = ($delim_pos) ? ($delim_pos + 1) : 0; $enco_idx < $enco_len; ++$deco_len) {
  391.             for ($old_idx = $idx, $w = 1, $k = $this->_base; 1 ; $k += $this->_base) {
  392.                 $digit = $this->_decode_digit($encoded{$enco_idx++});
  393.                 $idx += $digit * $w;
  394.                 $t = ($k <= $bias) ? $this->_tmin :
  395.                         (($k >= $bias + $this->_tmax) ? $this->_tmax : ($k - $bias));
  396.                 if ($digit < $t) break;
  397.                 $w = (int) ($w * ($this->_base - $t));
  398.             }
  399.             $bias = $this->_adapt($idx - $old_idx, $deco_len + 1, $is_first);
  400.             $is_first = false;
  401.             $char += (int) ($idx / ($deco_len + 1));
  402.             $idx %= ($deco_len + 1);
  403.             if ($deco_len > 0) {
  404.                 // Make room for the decoded char
  405.                 for ($i = $deco_len; $i > $idx; $i--) {
  406.                     $decoded[$i] = $decoded[($i - 1)];
  407.                 }
  408.             }
  409.             $decoded[$idx++] = $char;
  410.         }
  411.         return $this->_ucs4_to_utf8($decoded);
  412.     }
  413.  
  414.     /**
  415.      * The actual encoding algorithm
  416.      * @access   private
  417.      */
  418.     function _encode($decoded)
  419.     {
  420.         // We cannot encode a domain name containing the Punycode prefix
  421.         $extract = strlen($this->_punycode_prefix);
  422.         $check_pref = $this->_utf8_to_ucs4($this->_punycode_prefix);
  423.         $check_deco = array_slice($decoded, 0, $extract);
  424.  
  425.         if ($check_pref == $check_deco) {
  426.             $this->_error('This is already a punycode string');
  427.             return false;
  428.         }
  429.         // We will not try to encode strings consisting of basic code points only
  430.         $encodable = false;
  431.         foreach ($decoded as $k => $v) {
  432.             if ($v > 0x7a) {
  433.                 $encodable = true;
  434.                 break;
  435.             }
  436.         }
  437.         if (!$encodable) {
  438.             $this->_error('The given string does not contain encodable chars');
  439.             return false;
  440.         }
  441.  
  442.         // Do NAMEPREP
  443.         $decoded = $this->_nameprep($decoded);
  444.         if (!$decoded || !is_array($decoded)) return false; // NAMEPREP failed
  445.  
  446.         $deco_len  = count($decoded);
  447.         if (!$deco_len) return false; // Empty array
  448.  
  449.         $codecount = 0; // How many chars have been consumed
  450.  
  451.         $encoded = '';
  452.         // Copy all basic code points to output
  453.         for ($i = 0; $i < $deco_len; ++$i) {
  454.             $test = $decoded[$i];
  455.             // Will match [-0-9a-zA-Z]
  456.             if ((0x2F < $test && $test < 0x40) || (0x40 < $test && $test < 0x5B)
  457.                     || (0x60 < $test && $test <= 0x7B) || (0x2D == $test)) {
  458.                 $encoded .= chr($decoded[$i]);
  459.                 $codecount++;
  460.             }
  461.         }
  462.         if ($codecount == $deco_len) return $encoded; // All codepoints were basic ones
  463.  
  464.         // Start with the prefix; copy it to output
  465.         $encoded = $this->_punycode_prefix.$encoded;
  466.  
  467.         // If we have basic code points in output, add an hyphen to the end
  468.         if ($codecount) $encoded .= '-';
  469.  
  470.         // Now find and encode all non-basic code points
  471.         $is_first  = true;
  472.         $cur_code  = $this->_initial_n;
  473.         $bias      = $this->_initial_bias;
  474.         $delta     = 0;
  475.         while ($codecount < $deco_len) {
  476.             // Find the smallest code point >= the current code point and
  477.             // remember the last ouccrence of it in the input
  478.             for ($i = 0, $next_code = $this->_max_ucs; $i < $deco_len; $i++) {
  479.                 if ($decoded[$i] >= $cur_code && $decoded[$i] <= $next_code) {
  480.                     $next_code = $decoded[$i];
  481.                 }
  482.             }
  483.  
  484.             $delta += ($next_code - $cur_code) * ($codecount + 1);
  485.             $cur_code = $next_code;
  486.  
  487.             // Scan input again and encode all characters whose code point is $cur_code
  488.             for ($i = 0; $i < $deco_len; $i++) {
  489.                 if ($decoded[$i] < $cur_code) {
  490.                     $delta++;
  491.                 } elseif ($decoded[$i] == $cur_code) {
  492.                     for ($q = $delta, $k = $this->_base; 1; $k += $this->_base) {
  493.                         $t = ($k <= $bias) ? $this->_tmin :
  494.                                 (($k >= $bias + $this->_tmax) ? $this->_tmax : $k - $bias);
  495.                         if ($q < $t) break;
  496.                         $encoded .= $this->_encode_digit(intval($t + (($q - $t) % ($this->_base - $t)))); //v0.4.5 Changed from ceil() to intval()
  497.                         $q = (int) (($q - $t) / ($this->_base - $t));
  498.                     }
  499.                     $encoded .= $this->_encode_digit($q);
  500.                     $bias = $this->_adapt($delta, $codecount+1, $is_first);
  501.                     $codecount++;
  502.                     $delta = 0;
  503.                     $is_first = false;
  504.                 }
  505.             }
  506.             $delta++;
  507.             $cur_code++;
  508.         }
  509.         return $encoded;
  510.     }
  511.  
  512.     /**
  513.      * Adapt the bias according to the current code point and position
  514.      * @access   private
  515.      */
  516.     function _adapt($delta, $npoints, $is_first)
  517.     {
  518.         $delta = intval($is_first ? ($delta / $this->_damp) : ($delta / 2));
  519.         $delta += intval($delta / $npoints);
  520.         for ($k = 0; $delta > (($this->_base - $this->_tmin) * $this->_tmax) / 2; $k += $this->_base) {
  521.             $delta = intval($delta / ($this->_base - $this->_tmin));
  522.         }
  523.         return intval($k + ($this->_base - $this->_tmin + 1) * $delta / ($delta + $this->_skew));
  524.     }
  525.  
  526.     /**
  527.      * Encoding a certain digit
  528.      * @access   private
  529.      */
  530.     function _encode_digit($d)
  531.     {
  532.         return chr($d + 22 + 75 * ($d < 26));
  533.     }
  534.  
  535.     /**
  536.      * Decode a certain digit
  537.      * @access   private
  538.      */
  539.     function _decode_digit($cp)
  540.     {
  541.         $cp = ord($cp);
  542.         return ($cp - 48 < 10) ? $cp - 22 : (($cp - 65 < 26) ? $cp - 65 : (($cp - 97 < 26) ? $cp - 97 : $this->_base));
  543.     }
  544.  
  545.     /**
  546.      * Internal error handling method
  547.      * @access   private
  548.      */
  549.     function _error($error = '')
  550.     {
  551.         $this->_error = $error;
  552.     }
  553.  
  554.     /**
  555.      * Do Nameprep according to RFC3491 and RFC3454
  556.      * @param    array    Unicode Characters
  557.      * @return   string   Unicode Characters, Nameprep'd
  558.      * @access   private
  559.      */
  560.     function _nameprep($input)
  561.     {
  562.         $output = array();
  563.         $error = false;
  564.         //
  565.         // Mapping
  566.         // Walking through the input array, performing the required steps on each of
  567.         // the input chars and putting the result into the output array
  568.         // While mapping required chars we apply the cannonical ordering
  569.         foreach ($input as $v) {
  570.             // Map to nothing == skip that code point
  571.             if (in_array($v, $this->NP['map_nothing'])) continue;
  572.  
  573.             // Try to find prohibited input
  574.             if (in_array($v, $this->NP['prohibit']) || in_array($v, $this->NP['general_prohibited'])) {
  575.                 $this->_error('NAMEPREP: Prohibited input U+'.sprintf('%08X', $v));
  576.                 return false;
  577.             }
  578.             foreach ($this->NP['prohibit_ranges'] as $range) {
  579.                 if ($range[0] <= $v && $v <= $range[1]) {
  580.                     $this->_error('NAMEPREP: Prohibited input U+'.sprintf('%08X', $v));
  581.                     return false;
  582.                 }
  583.             }
  584.             //
  585.             // Hangul syllable decomposition
  586.             if (0xAC00 <= $v && $v <= 0xD7AF) {
  587.                 foreach ($this->_hangul_decompose($v) as $out) {
  588.                     $output[] = (int) $out;
  589.                 }
  590.             // There's a decomposition mapping for that code point
  591.             } elseif (isset($this->NP['replacemaps'][$v])) {
  592.                 foreach ($this->_apply_cannonical_ordering($this->NP['replacemaps'][$v]) as $out) {
  593.                     $output[] = (int) $out;
  594.                 }
  595.             } else {
  596.                 $output[] = (int) $v;
  597.             }
  598.         }
  599.         // Before applying any Combining, try to rearrange any Hangul syllables
  600.         $output = $this->_hangul_compose($output);
  601.         //
  602.         // Combine code points
  603.         //
  604.         $last_class   = 0;
  605.         $last_starter = 0;
  606.         $out_len      = count($output);
  607.         for ($i = 0; $i < $out_len; ++$i) {
  608.             $class = $this->_get_combining_class($output[$i]);
  609.             if ((!$last_class || $last_class > $class) && $class) {
  610.                 // Try to match
  611.                 $seq_len = $i - $last_starter;
  612.                 $out = $this->_combine(array_slice($output, $last_starter, $seq_len));
  613.                 // On match: Replace the last starter with the composed character and remove
  614.                 // the now redundant non-starter(s)
  615.                 if ($out) {
  616.                     $output[$last_starter] = $out;
  617.                     if (count($out) != $seq_len) {
  618.                         for ($j = $i+1; $j < $out_len; ++$j) {
  619.                             $output[$j-1] = $output[$j];
  620.                         }
  621.                         unset($output[$out_len]);
  622.                     }
  623.                     // Rewind the for loop by one, since there can be more possible compositions
  624.                     $i--;
  625.                     $out_len--;
  626.                     $last_class = ($i == $last_starter) ? 0 : $this->_get_combining_class($output[$i-1]);
  627.                     continue;
  628.                 }
  629.             }
  630.             // The current class is 0
  631.             if (!$class) $last_starter = $i;
  632.             $last_class = $class;
  633.         }
  634.         return $output;
  635.     }
  636.  
  637.     /**
  638.      * Decomposes a Hangul syllable
  639.      * (see http://www.unicode.org/unicode/reports/tr15/#Hangul
  640.      * @param    integer  32bit UCS4 code point
  641.      * @return   array    Either Hangul Syllable decomposed or original 32bit value as one value array
  642.      * @access   private
  643.      */
  644.     function _hangul_decompose($char)
  645.     {
  646.         $sindex = (int) $char - $this->_sbase;
  647.         if ($sindex < 0 || $sindex >= $this->_scount) {
  648.             return array($char);
  649.         }
  650.         $result = array();
  651.         $result[] = (int) $this->_lbase + $sindex / $this->_ncount;
  652.         $result[] = (int) $this->_vbase + ($sindex % $this->_ncount) / $this->_tcount;
  653.         $T = intval($this->_tbase + $sindex % $this->_tcount);
  654.         if ($T != $this->_tbase) $result[] = $T;
  655.         return $result;
  656.     }
  657.     /**
  658.      * Ccomposes a Hangul syllable
  659.      * (see http://www.unicode.org/unicode/reports/tr15/#Hangul
  660.      * @param    array    Decomposed UCS4 sequence
  661.      * @return   array    UCS4 sequence with syllables composed
  662.      * @access   private
  663.      */
  664.     function _hangul_compose($input)
  665.     {
  666.         $inp_len = count($input);
  667.         if (!$inp_len) return array();
  668.         $result = array();
  669.         $last = (int) $input[0];
  670.         $result[] = $last; // copy first char from input to output
  671.  
  672.         for ($i = 1; $i < $inp_len; ++$i) {
  673.             $char = (int) $input[$i];
  674.             $sindex = $last - $this->_sbase;
  675.             $lindex = $last - $this->_lbase;
  676.             $vindex = $char - $this->_vbase;
  677.             $tindex = $char - $this->_tbase;
  678.             // Find out, whether two current characters are LV and T
  679.             if (0 <= $sindex && $sindex < $this->_scount && ($sindex % $this->_tcount == 0)
  680.                     && 0 <= $tindex && $tindex <= $this->_tcount) {
  681.                 // create syllable of form LVT
  682.                 $last += $tindex;
  683.                 $result[(count($result) - 1)] = $last; // reset last
  684.                 continue; // discard char
  685.             }
  686.             // Find out, whether two current characters form L and V
  687.             if (0 <= $lindex && $lindex < $this->_lcount && 0 <= $vindex && $vindex < $this->_vcount) {
  688.                 // create syllable of form LV
  689.                 $last = (int) $this->_sbase + ($lindex * $this->_vcount + $vindex) * $this->_tcount;
  690.                 $result[(count($result) - 1)] = $last; // reset last
  691.                 continue; // discard char
  692.             }
  693.             // if neither case was true, just add the character
  694.             $last = $char;
  695.             $result[] = $char;
  696.         }
  697.         return $result;
  698.     }
  699.  
  700.     /**
  701.      * Returns the combining class of a certain wide char
  702.      * @param    integer    Wide char to check (32bit integer)
  703.      * @return   integer    Combining class if found, else 0
  704.      * @access   private
  705.      */
  706.     function _get_combining_class($char)
  707.     {
  708.         return isset($this->NP['norm_combcls'][$char]) ? $this->NP['norm_combcls'][$char] : 0;
  709.     }
  710.  
  711.     /**
  712.      * Apllies the cannonical ordering of a decomposed UCS4 sequence
  713.      * @param    array      Decomposed UCS4 sequence
  714.      * @return   array      Ordered USC4 sequence
  715.      * @access   private
  716.      */
  717.     function _apply_cannonical_ordering($input)
  718.     {
  719.         $swap = true;
  720.         $size = count($input);
  721.         while ($swap) {
  722.             $swap = false;
  723.             $last = $this->_get_combining_class(intval($input[0]));
  724.             for ($i = 0; $i < $size-1; ++$i) {
  725.                 $next = $this->_get_combining_class(intval($input[$i+1]));
  726.                 if ($next != 0 && $last > $next) {
  727.                     // Move item leftward until it fits
  728.                     for ($j = $i + 1; $j > 0; --$j) {
  729.                         if ($this->_get_combining_class(intval($input[$j-1])) <= $next) break;
  730.                         $t = intval($input[$j]);
  731.                         $input[$j] = intval($input[$j-1]);
  732.                         $input[$j-1] = $t;
  733.                         $swap = true;
  734.                     }
  735.                     // Reentering the loop looking at the old character again
  736.                     $next = $last;
  737.                 }
  738.                 $last = $next;
  739.             }
  740.         }
  741.         return $input;
  742.     }
  743.  
  744.     /**
  745.      * Do composition of a sequence of starter and non-starter
  746.      * @param    array      UCS4 Decomposed sequence
  747.      * @return   array      Ordered USC4 sequence
  748.      * @access   private
  749.      */
  750.     function _combine($input)
  751.     {
  752.         $inp_len = count($input);
  753.         foreach ($this->NP['replacemaps'] as $np_src => $np_target) {
  754.             if ($np_target[0] != $input[0]) continue;
  755.             if (count($np_target) != $inp_len) continue;
  756.             $hit = false;
  757.             foreach ($input as $k2 => $v2) {
  758.                 if ($v2 == $np_target[$k2]) {
  759.                     $hit = true;
  760.                 } else {
  761.                     $hit = false;
  762.                     break;
  763.                 }
  764.             }
  765.             if ($hit) return $np_src;
  766.         }
  767.         return false;
  768.     }
  769.  
  770.     /**
  771.      * This converts an UTF-8 encoded string to its UCS-4 representation
  772.      * By talking about UCS-4 "strings" we mean arrays of 32bit integers representing
  773.      * each of the "chars". This is due to PHP not being able to handle strings with
  774.      * bit depth different from 8. This apllies to the reverse method _ucs4_to_utf8(), too.
  775.      * The following UTF-8 encodings are supported:
  776.      * bytes bits  representation
  777.      * 1        7  0xxxxxxx
  778.      * 2       11  110xxxxx 10xxxxxx
  779.      * 3       16  1110xxxx 10xxxxxx 10xxxxxx
  780.      * 4       21  11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  781.      * 5       26  111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
  782.      * 6       31  1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
  783.      * Each x represents a bit that can be used to store character data.
  784.      * The five and six byte sequences are part of Annex D of ISO/IEC 10646-1:2000
  785.      * @access   private
  786.      */
  787.     function _utf8_to_ucs4($input)
  788.     {
  789.         $output = array();
  790.         $out_len = 0;
  791.         $inp_len = strlen($input);
  792.         $mode = 'next';
  793.         $test = 'none';
  794.         for ($k = 0; $k < $inp_len; ++$k) {
  795.             $v = ord($input{$k}); // Extract byte from input string
  796.  
  797.             if ($v < 128) { // We found an ASCII char - put into stirng as is
  798.                 $output[$out_len] = $v;
  799.                 ++$out_len;
  800.                 if ('add' == $mode) {
  801.                     $this->_error('Conversion from UTF-8 to UCS-4 failed: malformed input at byte '.$k);
  802.                     return false;
  803.                 }
  804.                 continue;
  805.             }
  806.             if ('next' == $mode) { // Try to find the next start byte; determine the width of the Unicode char
  807.                 $start_byte = $v;
  808.                 $mode = 'add';
  809.                 $test = 'range';
  810.                 if ($v >> 5 == 6) { // &110xxxxx 10xxxxx
  811.                     $next_byte = 0; // Tells, how many times subsequent bitmasks must rotate 6bits to the left
  812.                     $v = ($v - 192) << 6;
  813.                 } elseif ($v >> 4 == 14) { // &1110xxxx 10xxxxxx 10xxxxxx
  814.                     $next_byte = 1;
  815.                     $v = ($v - 224) << 12;
  816.                 } elseif ($v >> 3 == 30) { // &11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  817.                     $next_byte = 2;
  818.                     $v = ($v - 240) << 18;
  819.                 } elseif ($v >> 2 == 62) { // &111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
  820.                     $next_byte = 3;
  821.                     $v = ($v - 248) << 24;
  822.                 } elseif ($v >> 1 == 126) { // &1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
  823.                     $next_byte = 4;
  824.                     $v = ($v - 252) << 30;
  825.                 } else {
  826.                     $this->_error('This might be UTF-8, but I don\'t understand it at byte '.$k);
  827.                     return false;
  828.                 }
  829.                 if ('add' == $mode) {
  830.                     $output[$out_len] = (int) $v;
  831.                     ++$out_len;
  832.                     continue;
  833.                 }
  834.             }
  835.             if ('add' == $mode) {
  836.                 if (!$this->_allow_overlong && $test == 'range') {
  837.                     $test = 'none';
  838.                     if (($v < 0xA0 && $start_byte == 0xE0) || ($v < 0x90 && $start_byte == 0xF0) || ($v > 0x8F && $start_byte == 0xF4)) {
  839.                         $this->_error('Bogus UTF-8 character detected (out of legal range) at byte '.$k);
  840.                         return false;
  841.                     }
  842.                 }
  843.                 if ($v >> 6 == 2) { // Bit mask must be 10xxxxxx
  844.                     $v = ($v - 128) << ($next_byte * 6);
  845.                     $output[($out_len - 1)] += $v;
  846.                     --$next_byte;
  847.                 } else {
  848.                     $this->_error('Conversion from UTF-8 to UCS-4 failed: malformed input at byte '.$k);
  849.                     return false;
  850.                 }
  851.                 if ($next_byte < 0) {
  852.                     $mode = 'next';
  853.                 }
  854.             }
  855.         } // for
  856.         return $output;
  857.     }
  858.  
  859.     /**
  860.      * Convert UCS-4 string into UTF-8 string
  861.      * See _utf8_to_ucs4() for details
  862.      * @access   private
  863.      */
  864.     function _ucs4_to_utf8($input)
  865.     {
  866.         $output = '';
  867.         $k = 0;
  868.         foreach ($input as $v) {
  869.             ++$k;
  870.             // $v = ord($v);
  871.             if ($v < 128) { // 7bit are transferred literally
  872.                 $output .= chr($v);
  873.             } elseif ($v < (1 << 11)) { // 2 bytes
  874.                 $output .= chr(192 + ($v >> 6)) . chr(128 + ($v & 63));
  875.             } elseif ($v < (1 << 16)) { // 3 bytes
  876.                 $output .= chr(224 + ($v >> 12)) . chr(128 + (($v >> 6) & 63)) . chr(128 + ($v & 63));
  877.             } elseif ($v < (1 << 21)) { // 4 bytes
  878.                 $output .= chr(240 + ($v >> 18)) . chr(128 + (($v >> 12) & 63))
  879.                          . chr(128 + (($v >> 6) & 63)) . chr(128 + ($v & 63));
  880.             } elseif ($v < (1 << 26)) { // 5 bytes
  881.                 $output .= chr(248 + ($v >> 24)) . chr(128 + (($v >> 18) & 63))
  882.                          . chr(128 + (($v >> 12) & 63)) . chr(128 + (($v >> 6) & 63))
  883.                          . chr(128 + ($v & 63));
  884.             } elseif ($v < (1 << 31)) { // 6 bytes
  885.                 $output .= chr(252 + ($v >> 30)) . chr(128 + (($v >> 24) & 63))
  886.                          . chr(128 + (($v >> 18) & 63)) . chr(128 + (($v >> 12) & 63))
  887.                          . chr(128 + (($v >> 6) & 63)) . chr(128 + ($v & 63));
  888.             } else {
  889.                 $this->_error('Conversion from UCS-4 to UTF-8 failed: malformed input at byte '.$k);
  890.                 return false;
  891.             }
  892.         }
  893.         return $output;
  894.     }
  895.  
  896.     /**
  897.       * Convert UCS-4 array into UCS-4 string
  898.       *
  899.       * @access   private
  900.       */
  901.     function _ucs4_to_ucs4_string($input)
  902.     {
  903.         $output = '';
  904.         // Take array values and split output to 4 bytes per value
  905.         // The bit mask is 255, which reads &11111111
  906.         foreach ($input as $v) {
  907.             $output .= chr(($v >> 24) & 255).chr(($v >> 16) & 255).chr(($v >> 8) & 255).chr($v & 255);
  908.         }
  909.         return $output;
  910.     }
  911.  
  912.     /**
  913.       * Convert UCS-4 strin into UCS-4 garray
  914.       *
  915.       * @access   private
  916.       */
  917.     function _ucs4_string_to_ucs4($input)
  918.     {
  919.         $output = array();
  920.         $inp_len = strlen($input);
  921.         // Input length must be dividable by 4
  922.         if ($inp_len % 4) {
  923.             $this->_error('Input UCS4 string is broken');
  924.             return false;
  925.         }
  926.         // Empty input - return empty output
  927.         if (!$inp_len) return $output;
  928.         for ($i = 0, $out_len = -1; $i < $inp_len; ++$i) {
  929.             // Increment output position every 4 input bytes
  930.             if (!($i % 4)) {
  931.                 $out_len++;
  932.                 $output[$out_len] = 0;
  933.             }
  934.             $output[$out_len] += ord($input{$i}) << (8 * (3 - ($i % 4) ) );
  935.         }
  936.         return $output;
  937.     }
  938. }
  939.  
  940. /**
  941. * Adapter class for aligning the API of idna_convert with that of Net_IDNA
  942. * @author  Matthias Sommerfeld <mso@phlylabs.de>
  943. */
  944. class Net_IDNA_php4 extends idna_convert
  945. {
  946.     /**
  947.      * Sets a new option value. Available options and values:
  948.      * [encoding - Use either UTF-8, UCS4 as array or UCS4 as string as input ('utf8' for UTF-8,
  949.      *         'ucs4_string' and 'ucs4_array' respectively for UCS4); The output is always UTF-8]
  950.      * [overlong - Unicode does not allow unnecessarily long encodings of chars,
  951.      *             to allow this, set this parameter to true, else to false;
  952.      *             default is false.]
  953.      * [strict - true: strict mode, good for registration purposes - Causes errors
  954.      *           on failures; false: loose mode, ideal for "wildlife" applications
  955.      *           by silently ignoring errors and returning the original input instead
  956.      *
  957.      * @param    mixed     Parameter to set (string: single parameter; array of Parameter => Value pairs)
  958.      * @param    string    Value to use (if parameter 1 is a string)
  959.      * @return   boolean   true on success, false otherwise
  960.      * @access   public
  961.      */
  962.     function setParams($option, $param = false)
  963.     {
  964.         return $this->IC->set_parameters($option, $param);
  965.     }
  966. }
  967.  
  968. ?>

Documentation generated on Tue, 19 Nov 2013 15:05:13 +0100 by phpDocumentor 1.4.3