Кодирование 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()

Обновление 24 11 2017

  • Фикс EncodePunycodeIDN для php < 5.4.8. Спасибо Константин.

Проверил работу на php 5.3.2. Выложил код на github

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