root/tags/omega-1.0.0-rc3/gui/include/idna.php

Revision 738, 37.5 KB (checked in by kilburn, 18 months 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 */
54class 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 == $test)) {
459                $encoded .= chr($decoded[$i]);
460                $codecount++;
461            }
462        }
463        if ($codecount == $deco_len) return $encoded; // All codepoints were basic ones
464
465        // Start with the prefix; copy it to output
466        $encoded = $this->_punycode_prefix.$encoded;
467
468        // If we have basic code points in output, add an hyphen to the end
469        if ($codecount) $encoded .= '-';
470
471        // Now find and encode all non-basic code points
472        $is_first  = true;
473        $cur_code  = $this->_initial_n;
474        $bias      = $this->_initial_bias;
475        $delta     = 0;
476        while ($codecount < $deco_len) {
477            // Find the smallest code point >= the current code point and
478            // remember the last ouccrence of it in the input
479            for ($i = 0, $next_code = $this->_max_ucs; $i < $deco_len; $i++) {
480                if ($decoded[$i] >= $cur_code && $decoded[$i] <= $next_code) {
481                    $next_code = $decoded[$i];
482                }
483            }
484
485            $delta += ($next_code - $cur_code) * ($codecount + 1);
486            $cur_code = $next_code;
487
488            // Scan input again and encode all characters whose code point is $cur_code
489            for ($i = 0; $i < $deco_len; $i++) {
490                if ($decoded[$i] < $cur_code) {
491                    $delta++;
492                } elseif ($decoded[$i] == $cur_code) {
493                    for ($q = $delta, $k = $this->_base; 1; $k += $this->_base) {
494                        $t = ($k <= $bias) ? $this->_tmin :
495                                (($k >= $bias + $this->_tmax) ? $this->_tmax : $k - $bias);
496                        if ($q < $t) break;
497