root/trunk/gui/include/idna.php

Revision 738, 37.5 kB (checked in by kilburn, 1 year ago)

GUI:

  • Fixed ticket #540 (Blank page after editing user)
  • Fixed system message display, wich sometimes failed to locate the "system-message.tpl" and then a blank page was returned instead of the actual error
  • Updated idna.php to the lastest available version: now it's possible to add domains with special characters "sömethïng.de" should work ie..
Line 
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 class idna_convert
55 {
56     /**
57      * Holds all relevant mapping tables, loaded from a seperate file on construct
58      * See RFC3454 for details
59      *
60      * @var array
61      * @access private
62      */
63     var $NP = array();
64
65     // Internal settings, do not mess with them
66     var $_punycode_prefix = 'xn--';
67     var $_invalid_ucs =     0x80000000;
68     var $_max_ucs =         0x10FFFF;
69     var $_base =            36;
70     var $_tmin =            1;
71     var $_tmax =            26;
72     var $_skew =            38;
73     var $_damp =            700;
74     var $_initial_bias =    72;
75     var $_initial_n =       0x80;
76     var $_sbase =           0xAC00;
77     var $_lbase =           0x1100;
78     var $_vbase =           0x1161;
79     var $_tbase =           0x11A7;
80     var $_lcount =          19;
81     var $_vcount =          21;
82     var $_tcount =          28;
83     var $_ncount =          588;   // _vcount * _tcount
84     var $_scount =          11172; // _lcount * _tcount * _vcount
85     var $_error =           false;
86
87     // See {@link set_paramter()} for details of how to change the following
88     // settings from within your script / application
89     var $_api_encoding   'utf8'; // Default input charset is UTF-8
90     var $_allow_overlong false// Overlong UTF-8 encodings are forbidden
91     var $_strict_mode    false// Behave strict or not
92
93     // The constructor
94     function idna_convert($options = false)
95     {
96         $this->slast = $this->_sbase + $this->_lcount * $this->_vcount * $this->_tcount;
97         if (function_exists('file_get_contents')) {
98             $this->NP = unserialize(file_get_contents(dirname(__FILE__).'/npdata.ser'));
99         } else {
100             $this->NP = unserialize(join('', file(dirname(__FILE__).'/npdata.ser')));
101         }
102         // If parameters are given, pass these to the respective method
103         if (is_array($options)) {
104             return $this->set_parameter($options);
105         }
106         return true;
107     }
108
109     /**
110      * Sets a new option value. Available options and values:
111      * [encoding - Use either UTF-8, UCS4 as array or UCS4 as string as input ('utf8' for UTF-8,
112      *         'ucs4_string' and 'ucs4_array' respectively for UCS4); The output is always UTF-8]
113      * [overlong - Unicode does not allow unnecessarily long encodings of chars,
114      *             to allow this, set this parameter to true, else to false;
115      *             default is false.]
116      * [strict - true: strict mode, good for registration purposes - Causes errors
117      *           on failures; false: loose mode, ideal for "wildlife" applications
118      *           by silently ignoring errors and returning the original input instead
119      *
120      * @param    mixed     Parameter to set (string: single parameter; array of Parameter => Value pairs)
121      * @param    string    Value to use (if parameter 1 is a string)
122      * @return   boolean   true on success, false otherwise
123      * @access   public
124      */
125     function set_parameter($option, $value = false)
126     {
127         if (!is_array($option)) {
128             $option = array($option => $value);
129         }
130         foreach ($option as $k => $v) {
131             switch ($k) {
132             case 'encoding':
133                 switch ($v) {
134                 case 'utf8':
135                 case 'ucs4_string':
136                 case 'ucs4_array':
137                     $this->_api_encoding = $v;
138                     break;
139                 default:
140                     $this->_error('Set Parameter: Unknown parameter '.$v.' for option '.$k);
141                     return false;
142                 }
143                 break;
144             case 'overlong':
145                 $this->_allow_overlong = ($v) ? true : false;
146                 break;
147             case 'strict':
148                 $this->_strict_mode = ($v) ? true : false;
149                 break;
150             default:
151                 $this->_error('Set Parameter: Unknown option '.$k);
152                 return false;
153             }
154         }
155         return true;
156     }
157
158     /**
159      * Decode a given ACE domain name
160      * @param    string   Domain name (ACE string)
161      * [@param    string   Desired output encoding, see {@link set_parameter}]
162      * @return   string   Decoded Domain name (UTF-8 or UCS-4)
163      * @access   public
164      */
165     function decode($input, $one_time_encoding = false)
166     {
167         // Optionally set
168         if ($one_time_encoding) {
169             switch ($one_time_encoding) {
170             case 'utf8':
171             case 'ucs4_string':
172             case 'ucs4_array':
173                 break;
174             default:
175                 $this->_error('Unknown encoding '.$one_time_encoding);
176                 return false;
177             }
178         }
179         // Make sure to drop any newline characters around
180         $input = trim($input);
181
182         // Negotiate input and try to determine, whether it is a plain string,
183         // an email address or something like a complete URL
184         if (strpos($input, '@')) { // Maybe it is an email address
185             // No no in strict mode
186             if ($this->_strict_mode) {
187                 $this->_error('Only simple domain name parts can be handled in strict mode');
188                 return false;
189             }
190             list ($email_pref, $input) = explode('@', $input, 2);
191             $arr = explode('.', $input);
192             foreach ($arr as $k => $v) {
193                 if (preg_match('!^'.preg_quote($this->_punycode_prefix, '!').'!', $v)) {
194                     $conv = $this->_decode($v);
195                     if ($conv) $arr[$k] = $conv;
196                 }
197             }
198             $input = join('.', $arr);
199             $arr = explode('.', $email_pref);
200             foreach ($arr as $k => $v) {
201                 if (preg_match('!^'.preg_quote($this->_punycode_prefix, '!').'!', $v)) {
202                     $conv = $this->_decode($v);
203                     if ($conv) $arr[$k] = $conv;
204                 }
205             }
206             $email_pref = join('.', $arr);
207             $return = $email_pref . '@' . $input;
208         } elseif (preg_match('![:\./]!', $input)) { // Or a complete domain name (with or without paths / parameters)
209             // No no in strict mode
210             if ($this->_strict_mode) {
211                 $this->_error('Only simple domain name parts can be handled in strict mode');
212                 return false;
213             }
214             $parsed = parse_url($input);
215             if (isset($parsed['host'])) {
216                 $arr = explode('.', $parsed['host']);
217                 foreach ($arr as $k => $v) {
218                     $conv = $this->_decode($v);
219                     if ($conv) $arr[$k] = $conv;
220                 }
221                 $parsed['host'] = join('.', $arr);
222                 $return =
223                         (empty($parsed['scheme']) ? '' : $parsed['scheme'].(strtolower($parsed['scheme']) == 'mailto' ? ':' : '://'))
224                         .(empty($parsed['user']) ? '' : $parsed['user'].(empty($parsed['pass']) ? '' : ':'.$parsed['pass']).'@')
225                         .$parsed['host']
226                         .(empty($parsed['port']) ? '' : ':'.$parsed['port'])
227                         .(empty($parsed['path']) ? '' : $parsed['path'])
228                         .(empty($parsed['query']) ? '' : '?'.$parsed['query'])
229                         .(empty($parsed['fragment']) ? '' : '#'.$parsed['fragment']);
230             } else { // parse_url seems to have failed, try without it
231                 $arr = explode('.', $input);
232                 foreach ($arr as $k => $v) {
233                     $conv = $this->_decode($v);
234                     $arr[$k] = ($conv) ? $conv : $v;
235                 }
236                 $return = join('.', $arr);
237             }
238         } else { // Otherwise we consider it being a pure domain name string
239             $return = $this->_decode($input);
240             if (!$return) $return = $input;
241         }
242         // The output is UTF-8 by default, other output formats need conversion here
243         // If one time encoding is given, use this, else the objects property
244         switch (($one_time_encoding) ? $one_time_encoding : $this->_api_encoding) {
245         case 'utf8':
246             return $return;
247             break;
248         case 'ucs4_string':
249            return $this->_ucs4_to_ucs4_string($this->_utf8_to_ucs4($return));
250            break;
251         case 'ucs4_array':
252             return $this->_utf8_to_ucs4($return);
253             break;
254         default:
255             $this->_error('Unsupported output format');
256             return false;
257         }
258     }
259
260     /**
261      * Encode a given UTF-8 domain name
262      * @param    string   Domain name (UTF-8 or UCS-4)
263      * [@param    string   Desired input encoding, see {@link set_parameter}]
264      * @return   string   Encoded Domain name (ACE string)
265      * @access   public
266      */
267     function encode($decoded, $one_time_encoding = false)
268     {
269         // Forcing conversion of input to UCS4 array
270         // If one time encoding is given, use this, else the objects property
271         switch ($one_time_encoding ? $one_time_encoding : $this->_api_encoding) {
272         case 'utf8':
273             $decoded = $this->_utf8_to_ucs4($decoded);
274             break;
275         case 'ucs4_string':
276            $decoded = $this->_ucs4_string_to_ucs4($decoded);
277         case 'ucs4_array':
278            break;
279         default:
280             $this->_error('Unsupported input format: '.($one_time_encoding ? $one_time_encoding : $this->_api_encoding));
281             return false;
282         }
283
284         // No input, no output, what else did you expect?
285         if (empty($decoded)) return '';
286
287         // Anchors for iteration
288         $last_begin = 0;
289         // Output string
290         $output = '';
291         foreach ($decoded as $k => $v) {
292             // Make sure to use just the plain dot
293             switch($v) {
294             case 0x3002:
295             case 0xFF0E:
296             case 0xFF61:
297                 $decoded[$k] = 0x2E;
298                 // Right, no break here, the above are converted to dots anyway
299             // Stumbling across an anchoring character
300             case 0x2E:
301             case 0x2F:
302             case 0x3A:
303             case 0x3F:
304             case 0x40:
305                 // Neither email addresses nor URLs allowed in strict mode
306                 if ($this->_strict_mode) {
307                    $this->_error('Neither email addresses nor URLs are allowed in strict mode.');
308                    return false;
309                 } else {
310                     // Skip first char
311                     if ($k) {
312                         $encoded = '';
313                         $encoded = $this->_encode(array_slice($decoded, $last_begin, (($k)-$last_begin)));
314                         if ($encoded) {
315                             $output .= $encoded;
316                         } else {
317                             $output .= $this->_ucs4_to_utf8(array_slice($decoded, $last_begin, (($k)-$last_begin)));
318                         }
319                         $output .= chr($decoded[$k]);
320                     }
321                     $last_begin = $k + 1;
322                 }
323             }
324         }
325         // Catch the rest of the string
326         if ($last_begin) {
327             $inp_len = sizeof($decoded);
328             $encoded = '';
329             $encoded = $this->_encode(array_slice($decoded, $last_begin, (($inp_len)-$last_begin)));
330             if ($encoded) {
331                 $output .= $encoded;
332             } else {
333                 $output .= $this->_ucs4_to_utf8(array_slice($decoded, $last_begin, (($inp_len)-$last_begin)));
334             }
335             return $output;
336         } else {
337             if ($output = $this->_encode($decoded)) {
338                 return $output;
339             } else {
340                 return $this->_ucs4_to_utf8($decoded);
341             }
342         }
343     }
344
345     /**
346      * Use this method to get the last error ocurred
347      * @param    void
348      * @return   string   The last error, that occured
349      * @access   public
350      */
351     function get_last_error()
352     {
353         return $this->_error;
354     }
355
356     /**
357      * The actual decoding algorithm
358      * @access   private
359      */
360     function _decode($encoded)
361     {
362         // We do need to find the Punycode prefix
363         if (!preg_match('!^'.preg_quote($this->_punycode_prefix, '!').'!', $encoded)) {
364             $this->_error('This is not a punycode string');
365             return false;
366         }
367         $encode_test = preg_replace('!^'.preg_quote($this->_punycode_prefix, '!').'!', '', $encoded);
368         // If nothing left after removing the prefix, it is hopeless
369         if (!$encode_test) {
370             $this->_error('The given encoded string was empty');
371             return false;
372         }
373         // Find last occurence of the delimiter
374         $delim_pos = strrpos($encoded, '-');
375         if ($delim_pos > strlen($this->_punycode_prefix)) {
376             for ($k = strlen($this->_punycode_prefix); $k < $delim_pos; ++$k) {
377                 $decoded[] = ord($encoded{$k});
378             }
379         } else {
380             $decoded = array();
381         }
382         $deco_len = count($decoded);
383         $enco_len = strlen($encoded);
384
385         // Wandering through the strings; init
386         $is_first = true;
387         $bias     = $this->_initial_bias;
388         $idx      = 0;
389         $char     = $this->_initial_n;
390
391         for ($enco_idx = ($delim_pos) ? ($delim_pos + 1) : 0; $enco_idx < $enco_len; ++$deco_len) {
392             for ($old_idx = $idx, $w = 1, $k = $this->_base; 1 ; $k += $this->_base) {
393                 $digit = $this->_decode_digit($encoded{$enco_idx++});
394                 $idx += $digit * $w;
395                 $t = ($k <= $bias) ? $this->_tmin :
396                         (($k >= $bias + $this->_tmax) ? $this->_tmax : ($k - $bias));
397                 if ($digit < $t) break;
398                 $w = (int) ($w * ($this->_base - $t));
399             }
400             $bias = $this->_adapt($idx - $old_idx, $deco_len + 1, $is_first);
401             $is_first = false;
402             $char += (int) ($idx / ($deco_len + 1));
403             $idx %= ($deco_len + 1);
404             if ($deco_len > 0) {
405                 // Make room for the decoded char
406                 for ($i = $deco_len; $i > $idx; $i--) {
407                     $decoded[$i] = $decoded[($i - 1)];
408                 }
409             }
410             $decoded[$idx++] = $char;
411         }
412         return $this->_ucs4_to_utf8($decoded);
413     }
414
415     /**
416      * The actual encoding algorithm
417      * @access   private
418      */
419     function _encode($decoded)
420     {
421         // We cannot encode a domain name containing the Punycode prefix
422         $extract = strlen($this->_punycode_prefix);
423         $check_pref = $this->_utf8_to_ucs4($this->_punycode_prefix);
424         $check_deco = array_slice($decoded, 0, $extract);
425
426         if ($check_pref == $check_deco) {
427             $this->_error('This is already a punycode string');
428             return false;
429         }
430         // We will not try to encode strings consisting of basic code points only
431         $encodable = false;
432         foreach ($decoded as $k => $v) {
433             if ($v > 0x7a) {
434                 $encodable = true;
435                 break;
436             }
437         }
438         if (!$encodable) {
439             $this->_error('The given string does not contain encodable chars');
440             return false;
441         }
442
443         // Do NAMEPREP
444         $decoded = $this->_nameprep($decoded);
445         if (!$decoded || !is_array($decoded)) return false; // NAMEPREP failed
446
447         $deco_len  = count($decoded);
448         if (!$deco_len) return false; // Empty array
449
450         $codecount = 0; // How many chars have been consumed
451
452         $encoded = '';
453         // Copy all basic code points to output
454         for ($i = 0; $i < $deco_len; ++$i) {
455             $test = $decoded[$i];
456             // Will match [-0-9a-zA-Z]
457             if ((0x2F < $test && $test < 0x40) || (0x40 < $test && $test < 0x5B)
458                     || (0x60 < $test && $test <= 0x7B) || (0x2D