Кодирование IDN Punycode в php

Кодирование IDN Punycode в php

С введением IDN вебмастера стали ускоренно знакомиться с таким понятием как punycode. Конечно же, их интересовало не столько понятие, сколько способы конвертации доменных имен в этот код и обратно.

А так как стандартом "де-факто" веб-разработки мелких и средних проектов стал интерпретируемый язык программирования php, то поиски решения задачи искали и ищут в первую очередь для него.

До данного момента сия проблема решалась двумя способами:

  • Использование класса от phlylabs.
  • Использование функций php - idn_to_ascii и idn_to_utf8.

Все бы хорошо - конвертация стала поддерживаться языком, НО как оказалось, это возможно только с версии 5.3 и то с оговорками:

  1. Для версии 5.3. нужно установить расширение PECL idn
  2. но для php>=5.4 idn не соберется. Этой версии нужно расширение intl.
  3. Для PECL расширений idn или intl нужна библиотека libidn
  4. Не всякий хостинг предоставляет нужные расширения php.
  5. php до 5.3 не поддерживает IDN

Использование же класса - это проигрыш в скорости примерно в 20 раз.

Я написал более скоростные функции:

  • EncodePunycodeIDN() - Кодирование utf-8 строки в punycode. Для её работы требуется функция ordUTF8 (функция быстрее аналогичной из класса в 2 раза)
  • DecodePunycodeIDN() - Декодирование punycode в строку utf-8. (функция быстрее аналогичной из класса в 1,5 раза)

Обновление 16 12 2015

  • Исправление ошибки, о которой мне написал на почту Михаил

    Здравствуйте
    Сегодня заметили проблему с вашей функцией EncodePunycodeIDN():
    Домен "симферополь-мп.рф" преобразовался в "симьферопол-мп.рф"

  • Исправлена проблема с китайским, японским и корейским языками в DecodePunycodeIDN()

Вот и сами функции:

  1. /**
  2.  * Finds the character code for a UTF-8 character: like ord() but for UTF-8.
  3.  *
  4.  * @author Nicolas Thouvenin <nthouvenin@gmail.com>
  5.  * @copyright 2008 Nicolas Thouvenin
  6.  * @license http://opensource.org/licenses/LGPL-2.1 LGPL v2.1
  7.  */
  8. function ordUTF8($c, $index = 0, &$bytes = null)
  9.   {
  10.     $len = strlen($c);
  11.     $bytes = 0;
  12.     if ($index >= $len)
  13.     return false;
  14.     $h = ord($c{$index});
  15.     if ($h <= 0x7F) {
  16.     $bytes = 1;
  17.     return $h;
  18.     }
  19.     else if ($h < 0xC2)
  20.     return false;
  21.     else if ($h <= 0xDF && $index < $len - 1) {
  22.     $bytes = 2;
  23.     return ($h & 0x1F) << 6 | (ord($c{$index + 1}) & 0x3F);
  24.     }
  25.     else if ($h <= 0xEF && $index < $len - 2) {
  26.     $bytes = 3;
  27.     return ($h & 0x0F) << 12 | (ord($c{$index + 1}) & 0x3F) << 6
  28.     | (ord($c{$index + 2}) & 0x3F);
  29.     }
  30.     else if ($h <= 0xF4 && $index < $len - 3) {
  31.     $bytes = 4;
  32.     return ($h & 0x0F) << 18 | (ord($c{$index + 1}) & 0x3F) << 12
  33.     | (ord($c{$index + 2}) & 0x3F) << 6
  34.     | (ord($c{$index + 3}) & 0x3F);
  35.     }
  36.     else
  37.     return false;
  38.   }
  39.  
  40. /**
  41.  * Encode UTF-8 domain name to IDN Punycode
  42.  *
  43.  * @param string $value Domain name
  44.  * @return string Encoded Domain name
  45.  *
  46.  * @author Igor V Belousov <igor@belousovv.ru>
  47.  * @copyright 2013, 2015 Igor V Belousov
  48.  * @license http://opensource.org/licenses/LGPL-2.1 LGPL v2.1
  49.  * @link http://belousovv.ru/myscript/phpIDN
  50.  */
  51. function EncodePunycodeIDN( $value )
  52.   {
  53.     /* search subdomains */
  54.     $sub_domain = explode( '.', $value );
  55.     if ( count( $sub_domain ) > 1 ) {
  56.       $sub_result = '';
  57.       foreach ( $sub_domain as $sub_value ) {
  58.         $sub_result .= '.' . EncodePunycodeIDN( $sub_value );
  59.       }
  60.       return substr( $sub_result, 1 );
  61.     }
  62.  
  63.     /* http://tools.ietf.org/html/rfc3492#section-6.3 */
  64.     $n      = 0x80;
  65.     $delta  = 0;
  66.     $bias   = 72;
  67.     $output = array();
  68.  
  69.     $input  = array();
  70.     $str    = $value;
  71.     while ( mb_strlen( $str , 'UTF-8' ) > 0 )
  72.       {
  73.         array_push( $input, mb_substr( $str, 0, 1, 'UTF-8' ) );
  74.         $str = mb_substr( $str, 1, null, 'UTF-8' );
  75.       }
  76.  
  77.     /* basic symbols */
  78.     $basic = preg_grep( '/[\x00-\x7f]/', $input );
  79.     $b = $basic;
  80.  
  81.     if ( $b == $input )
  82.       {
  83.         return $value;
  84.       }
  85.     $b = count( $b );
  86.     if ( $b > 0 ) {
  87.       $output = $basic;
  88.       /* add delimeter */
  89.       $output[] = '-';
  90.     }
  91.     unset($basic);
  92.     /* add prefix */
  93.     array_unshift( $output, 'xn--' );
  94.  
  95.     $input_len = count( $input );
  96.     $h = $b;
  97.  
  98.     $ord_input = array();
  99.  
  100.     while ( $h < $input_len ) {
  101.       $m = 0x10FFFF;
  102.       for ( $i = 0; $i < $input_len; ++$i )
  103.         {
  104.           $ord_input[ $i ] = ordUtf8( $input[ $i ] );
  105.           if ( ( $ord_input[ $i ] >= $n ) && ( $ord_input[ $i ] < $m ) )
  106.             {
  107.               $m = $ord_input[ $i ];
  108.             }
  109.         }
  110.       if ( ( $m - $n ) > ( 0x10FFFF / ( $h + 1 ) ) )
  111.         {
  112.           return $value;
  113.         }
  114.       $delta += ( $m - $n ) * ( $h + 1 );
  115.       $n = $m;
  116.  
  117.       for ( $i = 0; $i < $input_len; ++$i )
  118.         {
  119.           $c = $ord_input[ $i ];
  120.           if ( $c < $n )
  121.             {
  122.               ++$delta;
  123.               if ( $delta == 0 )
  124.                 {
  125.                   return $value;
  126.                 }
  127.             }
  128.           if ( $c == $n )
  129.             {
  130.               $q = $delta;
  131.               for ( $k = 36;; $k += 36 )
  132.                 {
  133.                   if ( $k <= $bias )
  134.                     {
  135.                       $t = 1;
  136.                     }
  137.                   elseif ( $k >= ( $bias + 26 ) )
  138.                     {
  139.                       $t = 26;
  140.                     }
  141.                   else
  142.                     {
  143.                       $t = $k - $bias;
  144.                     }
  145.                   if ( $q < $t )
  146.                     {
  147.                       break;
  148.                     }
  149.                     $tmp_int = $t + ( $q - $t ) % ( 36 - $t );
  150.                   $output[] = chr( ( $tmp_int + 22 + 75 * ( $tmp_int < 26 ) ) );
  151.                   $q = ( $q - $t ) / ( 36 - $t );
  152.                 }
  153.  
  154.               $output[] = chr( ( $q + 22 + 75 * ( $q < 26 ) ) );
  155.               /* http://tools.ietf.org/html/rfc3492#section-6.1 */
  156.               $delta = ( $h == $b ) ? $delta / 700 : $delta>>1;
  157.  
  158.               $delta += intval( $delta / ( $h + 1 ) );
  159.  
  160.               $k2 = 0;
  161.               while ( $delta > 455 )
  162.                 {
  163.                   $delta /= 35;
  164.                   $k2 += 36;
  165.                 }
  166.               $bias = intval( $k2 + 36 * $delta / ( $delta + 38 ) );
  167.               /* end section-6.1 */              
  168.               $delta = 0;
  169.               ++$h;
  170.             }
  171.         }
  172.       ++$delta;
  173.       ++$n;
  174.     }
  175.     return implode( '', $output );
  176.   }
  177.  
  178. /**
  179.  * Decode IDN Punycode to UTF-8 domain name
  180.  *
  181.  * @param string $value Punycode
  182.  * @return string Domain name in UTF-8 charset
  183.  *
  184.  * @author Igor V Belousov <igor@belousovv.ru>
  185.  * @copyright 2013, 2015 Igor V Belousov
  186.  * @license http://opensource.org/licenses/LGPL-2.1 LGPL v2.1
  187.  * @link http://belousovv.ru/myscript/phpIDN
  188.  */
  189. function DecodePunycodeIDN( $value )
  190.   {
  191.     /* search subdomains */
  192.     $sub_domain = explode( '.', $value );
  193.     if ( count( $sub_domain ) > 1 ) {
  194.       $sub_result = '';
  195.       foreach ( $sub_domain as $sub_value ) {
  196.         $sub_result .= '.' . DecodePunycodeIDN( $sub_value );
  197.       }
  198.       return substr( $sub_result, 1 );
  199.     }
  200.  
  201.     /* search prefix */
  202.     if ( substr( $value, 0, 4 ) != 'xn--' )
  203.       {
  204.         return $value;
  205.       }
  206.     else
  207.       {
  208.         $bad_input = $value;
  209.         $value = substr( $value, 4 );
  210.       }
  211.  
  212.     $n      = 0x80;
  213.     $i      = 0;
  214.     $bias   = 72;
  215.     $output = array();
  216.  
  217.     /* search delimeter */
  218.     $d = strrpos( $value, '-' );
  219.  
  220.     if ( $d > 0 ) {
  221.       for ( $j = 0; $j < $d; ++$j) {
  222.         $c = $value[ $j ];
  223.         $output[] = $c;
  224.         if ( $c > 0x7F )
  225.           {
  226.             return $bad_input;
  227.           }
  228.       }
  229.       ++$d;
  230.     } else {
  231.       $d = 0;
  232.     }
  233.  
  234.     while ($d < strlen( $value ) )
  235.       {
  236.         $old_i = $i;
  237.         $w = 1;
  238.  
  239.         for ($k = 36;; $k += 36)
  240.           {
  241.             if ( $d == strlen( $value ) )
  242.               {
  243.                 return $bad_input;
  244.               }
  245.             $c = $value[ $d++ ];
  246.             $c = ord( $c );
  247.  
  248.             $digit = ( $c - 48 < 10 ) ? $c - 22 :
  249.               (
  250.                 ( $c - 65 < 26 ) ? $c - 65 :
  251.                   (
  252.                     ( $c - 97 < 26 ) ? $c - 97 : 36
  253.                   )
  254.               );
  255.             if ( $digit > ( 0x10FFFF - $i ) / $w )
  256.               {
  257.                 return $bad_input;
  258.               }
  259.             $i += $digit * $w;
  260.  
  261.             if ( $k <= $bias )
  262.               {
  263.                 $t = 1;
  264.               }
  265.             elseif ( $k >= $bias + 26 )
  266.               {
  267.                 $t = 26;
  268.               }
  269.             else
  270.               {
  271.                 $t = $k - $bias;
  272.               }
  273.             if ( $digit < $t ) {
  274.                 break;
  275.               }
  276.  
  277.             $w *= 36 - $t;
  278.  
  279.           }
  280.  
  281.         $delta = $i - $old_i;
  282.  
  283.         /* http://tools.ietf.org/html/rfc3492#section-6.1 */
  284.         $delta = ( $old_i == 0 ) ? $delta/700 : $delta>>1;
  285.  
  286.         $count_output_plus_one = count( $output ) + 1;
  287.         $delta += intval( $delta / $count_output_plus_one );
  288.  
  289.         $k2 = 0;
  290.         while ( $delta > 455 )
  291.           {
  292.             $delta /= 35;
  293.             $k2 += 36;
  294.           }
  295.         $bias = intval( $k2 + 36  * $delta / ( $delta + 38 ) );
  296.         /* end section-6.1 */
  297.         if ( $i / $count_output_plus_one > 0x10FFFF - $n )
  298.           {
  299.             return $bad_input;
  300.           }
  301.         $n += intval( $i / $count_output_plus_one );
  302.         $i %= $count_output_plus_one;
  303.         array_splice( $output, $i, 0,
  304.             html_entity_decode( '&#' . $n . ';', ENT_NOQUOTES, 'UTF-8' )
  305.          );
  306.         ++$i;
  307.       }
  308.  
  309.     return implode( '', $output );
  310.   }