Subversion Repositories oidplus

Rev

Blame | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. namespace SpomkyLabs;
  4.  
  5. /**
  6.  * Punycode implementation as described in RFC 3492.
  7.  *
  8.  * @link http://tools.ietf.org/html/rfc3492
  9.  */
  10. final class Punycode
  11. {
  12.     /**
  13.      * Bootstring parameter values.
  14.      */
  15.     const BASE = 36;
  16.     const TMIN = 1;
  17.     const TMAX = 26;
  18.     const SKEW = 38;
  19.     const DAMP = 700;
  20.     const INITIAL_BIAS = 72;
  21.     const INITIAL_N = 128;
  22.     const PREFIX = 'xn--';
  23.     const DELIMITER = '-';
  24.  
  25.     /**
  26.      * Encode table.
  27.      *
  28.      * @param array
  29.      */
  30.     private static $encodeTable = [
  31.         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
  32.         'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
  33.         'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
  34.     ];
  35.  
  36.     /**
  37.      * Decode table.
  38.      *
  39.      * @param array
  40.      */
  41.     private static $decodeTable = [
  42.         'a' => 0, 'b' => 1, 'c' => 2, 'd' => 3, 'e' => 4, 'f' => 5,
  43.         'g' => 6, 'h' => 7, 'i' => 8, 'j' => 9, 'k' => 10, 'l' => 11,
  44.         'm' => 12, 'n' => 13, 'o' => 14, 'p' => 15, 'q' => 16, 'r' => 17,
  45.         's' => 18, 't' => 19, 'u' => 20, 'v' => 21, 'w' => 22, 'x' => 23,
  46.         'y' => 24, 'z' => 25, '0' => 26, '1' => 27, '2' => 28, '3' => 29,
  47.         '4' => 30, '5' => 31, '6' => 32, '7' => 33, '8' => 34, '9' => 35,
  48.     ];
  49.  
  50.     /**
  51.      * Encode a domain to its Punycode version.
  52.      *
  53.      * @param string $input    Domain name in Unicode to be encoded
  54.      * @param string $encoding Character encoding
  55.      *
  56.      * @return string Punycode representation in ASCII
  57.      */
  58.     public static function encode($input, $encoding = 'UTF-8')
  59.     {
  60.         $input = mb_strtolower($input, $encoding);
  61.         $parts = explode('.', $input);
  62.         foreach ($parts as &$part) {
  63.             $part = self::encodePart($part, $encoding);
  64.         }
  65.  
  66.         return implode('.', $parts);
  67.     }
  68.  
  69.     /**
  70.      * Encode a part of a domain name, such as tld, to its Punycode version.
  71.      *
  72.      * @param string $input    Part of a domain name
  73.      * @param string $encoding Character encoding
  74.      *
  75.      * @return string Punycode representation of a domain part
  76.      */
  77.     private static function encodePart($input, $encoding)
  78.     {
  79.         $codePoints = self::listCodePoints($input, $encoding);
  80.  
  81.         $n = static::INITIAL_N;
  82.         $bias = static::INITIAL_BIAS;
  83.         $delta = 0;
  84.         $h = $b = count($codePoints['basic']);
  85.  
  86.         $output = '';
  87.         foreach ($codePoints['basic'] as $code) {
  88.             $output .= self::codePointToChar($code);
  89.         }
  90.         if ($input === $output) {
  91.             return $output;
  92.         }
  93.         if ($b > 0) {
  94.             $output .= static::DELIMITER;
  95.         }
  96.  
  97.         $codePoints['nonBasic'] = array_unique($codePoints['nonBasic']);
  98.         sort($codePoints['nonBasic']);
  99.  
  100.         $i = 0;
  101.         $length = mb_strlen($input, $encoding);
  102.         while ($h < $length) {
  103.             $m = $codePoints['nonBasic'][$i++];
  104.             $delta = $delta + ($m - $n) * ($h + 1);
  105.             $n = $m;
  106.  
  107.             foreach ($codePoints['all'] as $c) {
  108.                 if ($c < $n || $c < static::INITIAL_N) {
  109.                     ++$delta;
  110.                 }
  111.                 if ($c === $n) {
  112.                     $q = $delta;
  113.                     for ($k = static::BASE; ; $k += static::BASE) {
  114.                         $t = self::calculateThreshold($k, $bias);
  115.                         if ($q < $t) {
  116.                             break;
  117.                         }
  118.  
  119.                         $code = $t + (($q - $t) % (static::BASE - $t));
  120.                         $output .= static::$encodeTable[$code];
  121.  
  122.                         $q = ($q - $t) / (static::BASE - $t);
  123.                     }
  124.  
  125.                     $output .= static::$encodeTable[$q];
  126.                     $bias = self::adapt($delta, $h + 1, ($h === $b));
  127.                     $delta = 0;
  128.                     ++$h;
  129.                 }
  130.             }
  131.  
  132.             ++$delta;
  133.             ++$n;
  134.         }
  135.  
  136.         return static::PREFIX.$output;
  137.     }
  138.  
  139.     /**
  140.      * Decode a Punycode domain name to its Unicode counterpart.
  141.      *
  142.      * @param string $input    Domain name in Punycode
  143.      * @param string $encoding Character encoding
  144.      *
  145.      * @return string Unicode domain name
  146.      */
  147.     public static function decode($input, $encoding = 'UTF-8')
  148.     {
  149.         $parts = explode('.', $input);
  150.         foreach ($parts as &$part) {
  151.             if (strpos($part, static::PREFIX) !== 0) {
  152.                 continue;
  153.             }
  154.  
  155.             $part = mb_substr($part, mb_strlen(static::PREFIX, $encoding), null, $encoding);
  156.             $part = self::decodePart($part, $encoding);
  157.         }
  158.  
  159.         return implode('.', $parts);
  160.     }
  161.  
  162.     /**
  163.      * Decode a part of domain name, such as tld.
  164.      *
  165.      * @param string $input    Part of a domain name
  166.      * @param string $encoding Character encoding
  167.      *
  168.      * @return string Unicode domain part
  169.      */
  170.     private static function decodePart($input, $encoding)
  171.     {
  172.         $n = static::INITIAL_N;
  173.         $i = 0;
  174.         $bias = static::INITIAL_BIAS;
  175.         $output = '';
  176.  
  177.         $pos = mb_strrpos($input, static::DELIMITER, null, $encoding);
  178.         if ($pos !== false) {
  179.             $output = mb_substr($input, 0, $pos++, $encoding);
  180.         } else {
  181.             $pos = 0;
  182.         }
  183.  
  184.         $outputLength = mb_strlen($output, $encoding);
  185.         $inputLength = mb_strlen($input, $encoding);
  186.         while ($pos < $inputLength) {
  187.             $oldi = $i;
  188.             $w = 1;
  189.  
  190.             for ($k = static::BASE; ; $k += static::BASE) {
  191.                 $digit = static::$decodeTable[$input[$pos++]];
  192.                 $i = $i + ($digit * $w);
  193.                 $t = self::calculateThreshold($k, $bias);
  194.  
  195.                 if ($digit < $t) {
  196.                     break;
  197.                 }
  198.  
  199.                 $w = $w * (static::BASE - $t);
  200.             }
  201.  
  202.             $bias = self::adapt($i - $oldi, ++$outputLength, ($oldi === 0));
  203.             $n = $n + (int) ($i / $outputLength);
  204.             $i = $i % ($outputLength);
  205.             $output = mb_substr($output, 0, $i, $encoding).self::codePointToChar($n).mb_substr($output, $i, $outputLength - 1, $encoding);
  206.  
  207.             ++$i;
  208.         }
  209.  
  210.         return $output;
  211.     }
  212.  
  213.     /**
  214.      * Calculate the bias threshold to fall between TMIN and TMAX.
  215.      *
  216.      * @param int $k
  217.      * @param int $bias
  218.      *
  219.      * @return int
  220.      */
  221.     private static function calculateThreshold($k, $bias)
  222.     {
  223.         if ($k <= $bias + static::TMIN) {
  224.             return static::TMIN;
  225.         } elseif ($k >= $bias + static::TMAX) {
  226.             return static::TMAX;
  227.         }
  228.  
  229.         return $k - $bias;
  230.     }
  231.  
  232.     /**
  233.      * Bias adaptation.
  234.      *
  235.      * @param int  $delta
  236.      * @param int  $numPoints
  237.      * @param bool $firstTime
  238.      *
  239.      * @return int
  240.      */
  241.     private static function adapt($delta, $numPoints, $firstTime)
  242.     {
  243.         $delta = (int) (
  244.             ($firstTime)
  245.                 ? $delta / static::DAMP
  246.                 : $delta / 2
  247.             );
  248.         $delta += (int) ($delta / $numPoints);
  249.  
  250.         $k = 0;
  251.         while ($delta > ((static::BASE - static::TMIN) * static::TMAX) / 2) {
  252.             $delta = (int) ($delta / (static::BASE - static::TMIN));
  253.             $k = $k + static::BASE;
  254.         }
  255.         $k = $k + (int) (((static::BASE - static::TMIN + 1) * $delta) / ($delta + static::SKEW));
  256.  
  257.         return $k;
  258.     }
  259.  
  260.     /**
  261.      * List code points for a given input.
  262.      *
  263.      * @param string $input
  264.      * @param string $encoding
  265.      *
  266.      * @return array Multi-dimension array with basic, non-basic and aggregated code points
  267.      */
  268.     private static function listCodePoints($input, $encoding)
  269.     {
  270.         $codePoints = [
  271.             'all'      => [],
  272.             'basic'    => [],
  273.             'nonBasic' => [],
  274.         ];
  275.  
  276.         $length = mb_strlen($input, $encoding);
  277.         for ($i = 0; $i < $length; ++$i) {
  278.             $char = mb_substr($input, $i, 1, $encoding);
  279.             $code = self::charToCodePoint($char);
  280.             if ($code < 128) {
  281.                 $codePoints['all'][] = $codePoints['basic'][] = $code;
  282.             } else {
  283.                 $codePoints['all'][] = $codePoints['nonBasic'][] = $code;
  284.             }
  285.         }
  286.  
  287.         return $codePoints;
  288.     }
  289.  
  290.     /**
  291.      * Convert a single or multi-byte character to its code point.
  292.      *
  293.      * @param string $char
  294.      *
  295.      * @return int
  296.      */
  297.     private static function charToCodePoint($char)
  298.     {
  299.         $code = ord($char[0]);
  300.         if ($code < 128) {
  301.             return $code;
  302.         } elseif ($code < 224) {
  303.             return (($code - 192) * 64) + (ord($char[1]) - 128);
  304.         } elseif ($code < 240) {
  305.             return (($code - 224) * 4096) + ((ord($char[1]) - 128) * 64) + (ord($char[2]) - 128);
  306.         } else {
  307.             return (($code - 240) * 262144) + ((ord($char[1]) - 128) * 4096) + ((ord($char[2]) - 128) * 64) + (ord($char[3]) - 128);
  308.         }
  309.     }
  310.  
  311.     /**
  312.      * Convert a code point to its single or multi-byte character.
  313.      *
  314.      * @param int $code
  315.      *
  316.      * @return string
  317.      */
  318.     private static function codePointToChar($code)
  319.     {
  320.         if ($code <= 0x7F) {
  321.             return chr($code);
  322.         } elseif ($code <= 0x7FF) {
  323.             return chr(($code >> 6) + 192).chr(($code & 63) + 128);
  324.         } elseif ($code <= 0xFFFF) {
  325.             return chr(($code >> 12) + 224).chr((($code >> 6) & 63) + 128).chr(($code & 63) + 128);
  326.         } else {
  327.             return chr(($code >> 18) + 240).chr((($code >> 12) & 63) + 128).chr((($code >> 6) & 63) + 128).chr(($code & 63) + 128);
  328.         }
  329.     }
  330. }
  331.